实现 apply 方法
基础知识
Function.prototype.apply()
是 JavaScript 中用于调用函数的内置方法。它与 call()
方法非常相似,核心作用是允许我们指定函数在执行时的 this
上下文以及其参数。理解并手动实现一个 apply
方法,有助于深化对 JavaScript this
指向和函数调用机制的理解。
在实现我们自己的 apply
方法之前,需要先掌握以下几个 JavaScript 核心概念:
this
关键字:this
的值在 JavaScript 中由函数的调用方式决定。我们实现apply
的核心目的,就是去手动改变一个函数执行时的this
指向。函数上下文 (Function Context): 每个函数的调用都有一个与之关联的上下文,也就是
this
的值。当我们将函数作为对象的方法调用时(如obj.myFunc()
),this
就指向这个对象(obj
)。我们的myApply
方法就是要打破这种默认行为,临时将任何我们指定的对象设置为函数的上下文。arguments
对象: 这是一个在函数体内可用的类数组对象,包含了函数被调用时传入的所有参数。虽然现在更推荐使用 rest 参数 (...args
),但了解arguments
对象对于理解一些经典实现仍然有帮助。
核心思路
我们的目标是在 Function.prototype
上创建一个 myApply
方法,其功能要与原生的 apply
方法保持一致。核心思路如下:
myApply
方法需要挂载到Function.prototype
上,以便所有函数实例都能访问。该方法接收两个参数:
context
(要绑定的this
对象) 和一个可选的args
数组 (要传递给函数的参数列表)。核心技巧:要改变函数的
this
指向,最直接的方法就是将该函数作为context
对象的一个临时方法来调用。- 例如,要让
fn
函数在obj
的上下文中执行,我们可以先将fn
赋值给obj.tempFn
,然后执行obj.tempFn()
。在tempFn
的执行过程中,其内部的this
就自然指向了obj
。
- 例如,要让
执行完毕后,为了不污染原始的
context
对象,必须将我们添加的临时方法(obj.tempFn
)删除。需要处理一些边界情况,例如:
- 传入的
context
为null
或undefined
时,this
应指向全局对象(在浏览器中是window
,在非严格模式下)。 - 用户可能不传递第二个参数(参数数组)。
- 传入的
关键点
根据上述思路,在具体实现时需要关注以下几点:
- 获取调用函数: 在
myApply
内部,this
就指向调用myApply
的那个函数本身。我们需要将其保存下来。 - 处理
context
: 将传入的context
参数转换为对象类型,如果它是null
或undefined
,则替换为全局对象。 - 唯一属性名: 在向
context
添加临时方法时,属性名必须是唯一的,以避免与context
对象上已有的属性冲突。可以使用Symbol
来创建一个独一无二的属性名,这是一种更现代和健壮的做法。如果需要兼容旧版环境,也可以使用一个随机生成的字符串。 - 参数传递:
apply
的第二个参数是一个数组。在调用临时方法时,需要将这个数组展开作为参数传递进去。可以使用 ES6 的展开语法 (...
)。 - 清理和返回: 调用完临时方法后,必须使用
delete
操作符清理掉在context
上添加的临时属性,并返回临时方法的执行结果。
代码实现
下面是 myApply
的一个完整实现,采用了现代的 ES6 语法。
/**
* 自定义 apply 方法
*
* @param {Object} context - 新函数执行时绑定的 this 对象。
* @param {Array} argsArray - 一个数组或类数组对象,其中的数组元素将作为单独的参数传给 func 函数。
* @returns {*} - 返回调用函数的执行结果。
*/
Function.prototype.myApply = function (context, argsArray) {
// 1. 获取调用 myApply 的函数本身
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;
let result;
// 5. 执行这个临时方法,并传递参数
// 检查 argsArray 是否存在且为数组
if (Array.isArray(argsArray)) {
// 使用展开语法将数组参数展开
result = context[uniqueKey](...argsArray);
} else {
// 如果没有传递参数数组或格式不正确,则不带参数执行
result = context[uniqueKey]();
}
// 6. 从 context 上删除这个临时方法,避免污染
delete context[uniqueKey];
// 7. 返回函数的执行结果
return result;
};
代码解析
- const fn = this;:在
myApply
的执行环境中,this
指向调用它的函数。例如,在someFunc.myApply(...)
调用中,fn
被赋值为someFunc
。 - context = ...:这行代码做了两件事:
- 判断
context
是否为null
或undefined
,若是则将其设置为全局对象window
。 - 使用
Object(context)
将原始值(如1
,'str'
,true
)包装成对应的对象,确保后续可添加属性。
- 判断
- const uniqueKey = Symbol('fn');:
Symbol
能创建全局唯一的原始值,用它作为属性名可确保不会意外覆盖context
对象上已存在的同名属性(如context.fn
)。 - context[uniqueKey] = fn;:实现的核心,将要执行的函数
fn
挂载到context
对象上。 - result = contextuniqueKey;:
- 通过
context[uniqueKey]
调用函数时,根据 JavaScript 的this
绑定规则,被调用函数内部的this
自动指向context
对象。 - 使用
...
展开语法将argsArray
数组中的每个元素作为独立参数传递给函数,同时处理了argsArray
不存在的情况。
- 通过
- delete context[uniqueKey];:获取函数执行结果后,删除临时属性,保持
context
对象纯净,不产生副作用。 - return result;:
apply
方法返回被调用函数的返回值,因此也需将结果返回。