????彻底弄清 this call apply bind 以及原生实现
栏目: JavaScript · 发布时间: 6年前
内容简介:有关 JS 中的 this、call、apply 和 bind 的概念网络上已经有很多文章讲解了 这篇文章目的是梳理一下这几个概念的知识点以及阐述如何用原生 JS 去实现这几个功能this 的指向在严格模式和非严格模式下有所不同;this 究竟指向什么是,在绝大多数情况下取决于函数如何被调用非严格模式下,this 在全局执行环境中指向全局对象(window、global、self);严格模式下则为 undefined
:crystal_ball:彻底弄清 this call apply bind 以及原生实现
有关 JS 中的 this、call、apply 和 bind 的概念网络上已经有很多文章讲解了 这篇文章目的是梳理一下这几个概念的知识点以及阐述如何用原生 JS 去实现这几个功能
this 指向问题
this
this 的指向在严格模式和非严格模式下有所不同;this 究竟指向什么是,在绝大多数情况下取决于函数如何被调用
全局执行环境的情况:
非严格模式下,this 在全局执行环境中指向全局对象(window、global、self);严格模式下则为 undefined
作为对象方法的调用情况:
假设函数作为一个方法被定义在对象中,那么 this 指向最后调用他的这个对象
比如:
a = 10 obj = { a: 1, f() { console.log(this.a) // this -> obj } } obj.f() // 1 最后由 obj 调用
obj.f()
等同于 window.obj.f()
最后由 obj 对象调用,因此 this 指向这个 obj
即便是这个对象的方法被赋值给一个变量并执行也是如此:
const fn = obj.f fn() // 相当于 window.fn() 因此 this 仍然指向最后调用他的对象 window
call apply bind 的情况:
想要修改 this 指向的时候,我们通常使用上述方法改变 this 的指向
a = 10 obj = { a: 1 } function fn(...args) { console.log(this.a, 'args length: ', args) } fn.call(obj, 1, 2) fn.apply(obj, [1, 2]) fn.bind(obj, ...[1, 2])()
可以看到 this 全部被绑定在了 obj 对象上,打印的 this.a
也都为 1
new 操作符的情况:
new 操作符原理实际上就是创建了一个新的实例,被 new 的函数被称为构造函数,构造函数 new 出来的对象方法中的 this 永远指向这个新的对象:
a = 10 function fn(a) { this.a = a } b = new fn(1) b.a // 1
箭头函数的情况:
- 普通函数在运行时才会确定 this 的指向
- 箭头函数则是在函数定义的时候就确定了 this 的指向,此时的 this 指向外层的作用域
a = 10 fn = () => { console.log(this.a) } obj = { a: 20 } obj.fn = fn obj.fn() window.obj.fn() f = obj.fn f()
无论如何调用 fn 函数内的 this 永远被固定在了这个外层的作用域(上述例子中的 window 对象)
this 改变指向问题
如果需要改变 this 的指向,有以下几种方法:
- 箭头函数
- 内部缓存 this
- apply 方法
- call 方法
- bind 方法
- new 操作符
箭头函数
普通函数
a = 10 obj = { a: 1, f() { // this -> obj function g() { // this -> window console.log(this.a) } g() } } obj.f() // 10
在 f 函数体内 g 函数所在的作用域中 this 的指向是 obj:
在 g 函数体内,this 则变成了 window:
改为箭头函数
a = 10 obj = { a: 1, f() { // this -> obj const g = () => { // this -> obj console.log(this.a) } g() } } obj.f() // 1
在 f 函数体内 this 指向的是 obj:
在 g 函数体内 this 指向仍然是 obj:
内部缓存 this
这个方法曾经经常用,即手动缓存 this 给一个名为 _this
或 that
等其他变量,当需要使用时用后者代替
a = 10 obj = { a: 20, f() { const _this = this setTimeout(function() { console.log(_this.a, this.a) }, 0) } } obj.f() // _this.a 指向 20 this.a 则指向 10
查看一下 this 和 _this 的指向,前者指向 window 后者则指向 obj 对象:
call
call 方法第一个参数为指定需要绑定的 this 对象;其他参数则为传递的值:
需要注意的是,第一个参数如果是:
- null、undefined、不传,this 将会指向全局对象(非严格模式下)
- 原始值将被转为对应的包装对象,如
f.call(1)
this 将指向Number
,并且这个 Number 的[[PrimitiveValue]]
值为 1
obj = { name: 'obj name' } {(function() { console.log(this.name) }).call(obj)}
apply
与 call 类似但第二个参数必须为数组:
obj = { name: 'obj name' } {(function (...args){ console.log(this.name, [...args]) }).apply(obj, [1, 2, 3])}
bind
比如常见的函数内包含一个异步方法:
function foo() { let _this = this // _this -> obj setTimeout(function() { console.log(_this.a) // _this.a -> obj.a }, 0) } obj = { a: 1 } foo.call(obj) // this -> obj // 1
我们上面提到了可以使用缓存 this 的方法来固定 this 指向,那么使用 bind 代码看起来更加优雅:
function foo() { // this -> obj setTimeout(function () { // 如果不使用箭头函数,则需要用 bind 方法绑定 this console.log(this.a) // this.a -> obj.a }.bind(this), 100) } obj = { a: 1 } foo.call(obj) // this -> obj // 1
或者直接用箭头函数:
function foo() { // this -> obj setTimeout(() => { // 箭头函数没有 this 继承外部作用域的 this console.log(this.a) // this.a -> obj.a }, 100) } obj = { a: 1 } foo.call(obj) // this -> obj // 1
new 操作符
new 操作符实际上就是生成一个新的对象,这个对象就是原来对象的实例。因为箭头函数没有 this 所以函数不能作为构造函数,构造函数通过 new 操作符改变了 this 的指向。
function Person(name) { this.name = name // this -> new 生成的实例 } p = new Person('oli') console.table(p)
this.name
表明了新创建的实例拥有一个 name 属性;当调用 new 操作符的时候,构造函数中的 this 就绑定在了实例对象上
原生实现 call apply bind new
文章上半部分讲解了 this 的指向以及如何使用 call bind apply 方法修改 this 指向;文章下半部分我们用 JS 去自己实现这三种方法
myCall
Function.prototype
代码实现:
Function.prototype.myCall = function(ctx) { ctx.fn = this ctx.fn() delete ctx.fn }
最基本的 myCall 就实现了,ctx 代表的是需要绑定的对象,但这里有几个问题,如果 ctx 对象本身就拥有一个 fn 属性或方法就会导致冲突。为了解决这个问题,我们需要修改代码使用 Symbol 来避免属性的冲突:
Function.prototype.myCall = function(ctx) { const fn = Symbol('fn') // 使用 Symbol 避免属性名冲突 ctx[fn] = this ctx[fn]() delete ctx[fn] } obj = { fn: 'functionName' } function foo() { console.log(this.fn) } foo.myCall(obj)
同样的,我们还要解决参数传递的问题,上述代码中没有引入其他参数还要继续修改:
Function.prototype.myCall = function(ctx, ...argv) { const fn = Symbol('fn') ctx[fn] = this ctx[fn](...argv) // 传入参数 delete ctx[fn] } obj = { fn: 'functionName', a: 10 } function foo(name) { console.log(this[name]) } foo.myCall(obj, 'fn')
另外,我们还要检测传入的第一个值是否为对象:
Function.prototype.myCall = function(ctx, ...argv) { ctx = typeof ctx === 'object' ? ctx || window : {} // 当 ctx 是对象的时候默认设置为 ctx;如果为 null 则设置为 window 否则为空对象 const fn = Symbol('fn') ctx[fn] = this ctx[fn](...argv) delete ctx[fn] } obj = { fn: 'functionName', a: 10 } function foo(name) { console.log(this[name]) } foo.myCall(null, 'a')
如果 ctx 为对象,那么检查 ctx 是否为 null 是则返回默认的 window 否则返回这个 ctx 对象;如果 ctx 不为对象那么将 ctx 设置为空对象(按照语法规则,需要将原始类型转化,为了简单说明原理这里就不考虑了)
执行效果如下:
这么一来自定义的 myCall 也就完成了
myApply
apply 效果跟 call 类似,将传入的数组通过扩展操作符传入函数即可
Function.prototype.myCall = function(ctx, argv) { ctx = typeof ctx === 'object' ? ctx || window : {} // 或者可以鉴别一下 argv 是不是数组 const fn = Symbol('fn') ctx[fn] = this ctx[fn](...argv) delete ctx[fn] }
myBind
bind 与 call 和 apply 不同的是,他不会立即调用这个函数,而是返回一个新的 this 改变后的函数。根据这一特点我们写一个自定义的 myBind:
Function.prototype.myBind = function(ctx) { return () => { // 要用箭头函数,否则 this 指向错误 return this.call(ctx) } }
这里需要注意的是,this 的指向原因需要在返回一个箭头函数,箭头函数内部的 this 指向来自外部
然后考虑合并接收到的参数,因为 bind 可能有如下写法:
f.bind(obj, 2)(2) // or f.bind(obj)(2, 2)
修改代码:
Function.prototype.myBind = function(ctx, ...argv1) { return (...argv2) => { return this.call(ctx, ...argv1, ...argv2) } }
new 操作符
最后我们再来实现一个 new 操作符名为 myNew
new 操作符的原理是啥:
__proto__
代码实现:
function myNew(Constructor) { // 接收一个 Constructor 构造函数 let newObj = {} // 创建一个新的对象 newObj.__proto__ = Constructor.prototype // 绑定对象的 __proto__ 到构造函数的 prototype Constructor.call(newObj) // 修改 this 指向 return newObj // 返回这个对象 }
然后考虑传入参数问题,继续修改代码:
function myNew(Constructor, ...argv) { // 接收参数 let newObj = {} newObj.__proto__ = Constructor.prototype Constructor.call(newObj, ...argv) // 传入参数 return newObj }
小结
到此为止
- this 指向问题
- 如何修改 this
- 如何使用原生 JS 实现 call apply bind 和 new 方法
再遇到类似问题,基本常见的情况都能应付得来了
(完)
参考:
以上所述就是小编给大家介绍的《????彻底弄清 this call apply bind 以及原生实现》,希望对大家有所帮助,如果大家有任何疑问请给我留言,小编会及时回复大家的。在此也非常感谢大家对 码农网 的支持!
猜你喜欢:- 【Java转Go】弄清GOPATH
- 十个问题弄清 JVM & GC(二)
- 弄清Java虚拟机GC的运行过程
- 机器学习和深度学习中值得弄清楚的一些问题
- go 学习笔记之咬文嚼字带你弄清楚 defer 延迟函数
- java – 在过滤器中修改Jersey InputStream.无法弄清楚如何在Jersey资源中访问修改后的inputStream
本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们。
How to Solve It
Zbigniew Michalewicz、David B. Fogel / Springer / 2004-03-01 / USD 59.95
This book is the only source that provides comprehensive, current, and detailed information on problem solving using modern heuristics. It covers classic methods of optimization, including dynamic pro......一起来看看 《How to Solve It》 这本书的介绍吧!