手写系列之 call、apply、bind 的实现
这三个货,都是用来绑定 this
的。区别如下:
call()
、apply()
都返回函数执行结果,区别在于参数不一样,前者接收参数列表,后者接收一个数组参数(也可以是类数组)。bind()
返回一个新函数,但注意对使用new
关键字调用时,绑定无效。其使用方法与call()
一致,接受一个参数列表。
一、call 实现
假设没有 Function.prototype.call()
方法,我们利用 this
默认绑定的特性处理即可。
Function.prototype.myCall() { // 获取执行函数 const fn = this // 获取绑定对象,及参数列表 const [context, ...args] = arguments // 避免与绑定对象属性冲突,采用 Symbol 值作为属性键,并将执行函数赋予该属性。 const key = Symbol() context[key] = fn // 执行函数并返回结果 const res = context[key](...args) // 移除临时属性 delete context[key] // 返回结果 return res }
还没完,请注意以下两种情况:
- 当
context
为undefined
或null
的情况。若处于非严格模式,context
应指向顶层对象globalThis
,在浏览器为window
对象。 - 若
context
为其他原始值(不是undefined
、null
),应该要将其转换为对应的引用值。
Function.prototype.myCall = function () { const fn = this const [ctx, ...args] = arguments const context = ctx == undefined ? window : Object(ctx) const key = Symbol() context[key] = fn const res = context[key](...args) delete context[key] return res }
再简化一下:
Function.prototype.myCall = function (ctx, ...args) { const key = Symbol() const context = ctx == undefined ? window : Object(ctx) context[key] = this const res = context[key](...args) delete context[key] return res }
二、apply 实现
我们知道,Function.prototype.apply()
和 Function.prototype.call()
的区别仅在接收参数的形式不同,前者接收一个数组。
基于上面的实现,简单修改下函数执行的参数即可。
Function.prototype.myApply = function (ctx, ...args) { const key = Symbol() const context = ctx == undefined ? window : Object(ctx) context[key] = this const res = context[key](args) // 与 call 方法的区别点 delete context[key] return res }
三、bind 实现
我们知道,Function.prototype.bind()
参数形式与 Function.prototype.call()
相同,区别在于 bind()
返回一个绑定了 this
的新函数。
其实,我们可以很快就写出以下方法:
Function.prototype.myBind = function (ctx, ...args) { const fn = this return function (...newArgs) { return fn.apply(ctx, [...args, ...newArgs]) } }
但是,这是不完全正确的...
需要注意的是,
bind()
方法返回的新函数,若通过new
关键字进行调用,那么this
绑定则不生效。
举个例子:
const person = { name: 'Frankie' } function Foo(name) { this.name = name } const Bar = Foo.bind(person) const bar = new Bar('Mandy') console.log(person.name) // "Frankie" console.log(bar.name) // "Mandy"
假设 Foo.bind(person)
生效的话,那么 new Bar('Mandy')
中 this.name
应该是 person.name = 'Mandy'
,修改的应该是 person
对象的 name
属性,但事实并非如此。
顺道总结一下
this
绑定的优先级:
- 只要使用
new
关键字调用,无论是否含有bind
绑定,this
总指向实例化对象。- 通过
call
、apply
或者bind
显式绑定,this
指向该绑定对象。若第一个参数缺省时,则根据是否为严格模式,来确定this
指向全局对象或者undefined
。- 函数通过上下文对象调用,
this
指向(最后)调用它的对象。- 如以上均没有,则会默认绑定。严格模式下,
this
指向undefined
,否则指向全局对象。
因此,我们来完善一下 myBind()
方法:
Function.prototype.myBind = function (ctx, ...args) { const fn = this return function newFn(...newArgs) { // 若通过 new 关键字调用,有几种方式可以判断: // 1. this instanceof newFn // 2. this.__proto__.constructor === newFn // 3. new.target 不为 undefined if (new.target) { return new newFn(...args, ...newArgs) } return fn.apply(ctx, [...args, ...newArgs]) } }
去掉注释,就长这样:
Function.prototype.myBind = function (ctx, ...args) { const fn = this return function newFn(...newArgs) { if (new.target) { return new newFn(...args, ...newArgs) } return fn.apply(ctx, [...args, ...newArgs]) } }
总的来说,其实并不能,真正了解 this 原理,要手写这几个常考的面试题,其实很简单哈。
插个话,我认为对于初学者来说,千万不要把作用域(链)和
this
混为一谈,其实它们完全就是两回事。作用域(链)与闭包相关,它在函数被定义时就已“确定”,不会再变了。而this
则与函数调用相关。
References
这里将此前写过关于 this
的文章列举出来:
如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。
绑定邮箱获取回复消息
由于您还没有绑定你的真实邮箱,如果其他用户或者作者回复了您的评论,将不能在第一时间通知您!
发布评论