手写call方法大致有以下几个步骤:
- 参数归一化:首先处理 call 方法的上下文参数 ctx。
- 收集参数:使用剩余参数语法(…args)来收集传递给 call 方法的所有参数,这些参数将被用于后续的函数调用。
- 确定调用函数。
- 绑定 this 并调用函数:将函数的 this 绑定到 ctx 上。
- 使用唯一的属性名
- 执行函数并返回函数执行结果
1 参数归一化
为了手写call方法,我们需要先看下call方法是怎么实现的,先写一个示例:
从打印可以看出this参数是由Object()方法包装过后的值,如果为null或者undefined,那么this为globalThis。
那么,在我们的myCall函数上,可以使用参数归一化的策略来处理传过来的context。
2 收集参数
处理完ctx之后,我们还需要处理传过来的参数,由于参数不固定,所以我们使用剩余参数语法收集参数。它用于表示函数的参数数量不确定,可以将多个参数收集到一个数组中。
现在我们处理好了ctx和参数,接下来我们还需要解决以下问题去实现myCall函数:
- 找到是哪个函数调用了myCall
- 怎么将函数的this指向ctx
3 找到是哪个函数调用了myCall
在myCall中的this就是调用myCall的函数。
4 绑定 this 并调用函数
我们直接用ctx来调用fn,来达成将fn的this指向ctx的目的。
看起来这样做就完事了,但是如果obj上有 fn 属性的话,myCall 方法会覆盖该属性的值,从而导致原有的 fn 属性值丢失。
5 使用唯一的属性名
为了避免这种情况,可以使用一个唯一的符号(Symbol)来作为属性名。
这样我们传入的ctx(即obj)就不会被修改了。我们手写的call方法就写好了。
番外
在控制台的打印中,我们可以看到 this 打印出来的值含有Symbol(fn)
,这是因为在调用 test 函数时,test 的 this 指向 obj ,obj 中的Symbol(fn)
属性还未被删去。
如果在 test 函数中使用了 this 用于枚举,那么Symbol(fn)
也会被枚举,由于这是意料之外属性,它不应该被枚举。那么我们可以使用[[属性描述符]]中的Object.defineProperty
方法来设置该属性不可枚举。
Object.defineProperty()
静态方法会直接在一个对象上定义一个新属性,或修改其现有属性,并返回此对象。