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