实现 call 方法
基础知识
Function.prototype.call() 是 JavaScript 的一个内置核心方法,它允许我们以一个指定的 this 值和一个或多个参数来调用一个函数。它与 apply 方法的功能几乎完全相同,唯一的区别在于传递参数的方式。掌握 call 的实现原理,是深入理解 JavaScript 函数调用、this 绑定和参数处理机制的关键一步。
在实现我们自己的 call 方法之前,需要先掌握以下几个 JavaScript 核心概念:
this关键字: 和实现apply时一样,this的值由函数的调用方式决定。我们实现call的核心目的,就是手动改变函数执行时的this指向。函数作为对象方法调用:
call的实现原理与apply一致,都利用了“当函数作为对象的方法被调用时,其内部的this指向该对象”这一规则。rest 参数 (
...args): ES6 引入的 rest 参数语法允许我们将一个不确定数量的参数表示为一个数组。这对于实现call来说非常方便,因为call的参数是逐个列出的,rest 参数可以轻松地将context之后的所有参数收集到一个数组中。
核心思路
我们的目标是在 Function.prototype 上创建一个 myCall 方法,其功能要与原生的 call 方法保持一致。核心思路如下:
myCall方法需要挂载到Function.prototype上,以便所有函数实例都能调用。该方法接收的第一个参数是
context(要绑定的this对象),其后跟着的是一个不确定长度的参数列表arg1, arg2, ...。核心技巧:与
apply的实现完全相同,我们将调用myCall的函数(下文称fn)作为context对象的一个临时方法来调用。fn.myCall(obj, arg1, arg2)的执行过程,可以被转换为:- 把
fn挂载到obj上,如obj.tempFn = fn;。 - 通过
obj.tempFn(arg1, arg2)的方式调用它。 - 此时
tempFn内部的this就指向了obj。
- 把
调用结束后,必须从
context对象上删除这个临时添加的方法,以避免对原对象造成污染。处理边界情况,例如
context为null或undefined时,应指向全局对象。
关键点
根据上述思路,在具体实现时需要关注以下几点:
- 获取调用函数: 在
myCall内部,this指向调用myCall的那个函数本身。 - 处理
context: 与apply实现一样,需要处理null和undefined的情况,并确保context是一个对象。 - 唯一属性名: 为了避免与
context对象的原生属性冲突,最好使用Symbol来创建一个独一无二的属性名来挂载临时函数。 - 参数处理: 这是
call与apply实现上的唯一不同点。我们需要收集myCall中除了第一个context参数以外的所有其他参数,并将它们传递给在context上下文中执行的函数。ES6 的 rest 参数 (...) 在这里是完美的解决方案。 - 清理与返回: 调用后必须删除临时属性,并返回函数的执行结果。
代码实现
下面是 myCall 的一个完整实现,采用了现代的 ES6 语法。
/**
* 自定义 call 方法
*
* @param {Object} context - 新函数执行时绑定的 this 对象。
* @param {...*} args - 传递给函数的参数列表。
* @returns {*} - 返回调用函数的执行结果。
*/
Function.prototype.myCall = function (context, ...args) {
// 1. 获取调用 myCall 的函数本身
const fn = this;
// 2. 处理 context,如果为 null/undefined 则指向全局对象 window
// 使用 Object() 包装可以确保 context 是一个对象
context =
context === null || context === undefined ? window : Object(context);
// 3. 使用 Symbol 创建一个唯一的 key,防止与 context 上的原有属性冲突
const uniqueKey = Symbol('fn');
// 4. 将函数 fn 作为 context 的一个临时方法
context[uniqueKey] = fn;
// 5. 执行这个临时方法,并通过 ...args 将参数逐个传入
const result = context[uniqueKey](...args);
// 6. 从 context 上删除这个临时方法,避免污染
delete context[uniqueKey];
// 7. 返回函数的执行结果
return result;
};
代码解析
- function(context, ...args):
myCall的参数定义。- 第一个参数
context被明确接收。 ...args(rest 参数)会将后续传入的所有参数(arg1,arg2, ...)收集到一个名为args的数组中。例如,若调用为myCall(obj, 10, 20),则context是obj,args是[10, 20]。
- 第一个参数
- const fn = this;:
this在这里指向调用myCall的函数。 - context = ...:对
this上下文进行处理,确保它是一个对象,且处理了null和undefined的情况。 - const uniqueKey = Symbol('fn');:创建一个唯一的属性键,以确保在
context上挂载临时函数时不会发生属性名冲突。 - context[uniqueKey] = fn;:实现
this绑定的核心步骤,将被调用的函数挂载为context的一个方法。 - const result = contextuniqueKey;:
- 通过
context对象调用这个临时方法,此时函数fn内部的this就指向了context。 ...args(spread 语法)将之前收集的args数组[10, 20]展开为10, 20作为参数传递进去,完美匹配call的参数形式。
- 通过
- delete context[uniqueKey];:调用完成后,清理掉这个临时属性,保持
context对象的干净。 - return result;:
call会返回目标函数的执行结果,因此这里也将result返回。