一文吃透!手写 call 背后的 JavaScript 关键技术与原理

134 阅读4分钟

在 JavaScript 的世界里,call方法是一个非常重要且常用的工具,它能够手动指定函数内部的this指向,这为我们在编程过程中灵活控制函数执行上下文提供了强大的能力。

一、call 方法概述

call方法是Function原型链上的方法,这意味着所有函数都可以调用它。它的作用是立即执行函数,并可以手动指定函数内部的this指向,同时还能传递参数。

与call方法类似的还有apply和bind方法。

call和apply都是立即执行的,区别在于参数传递的方式,call是一个个传入参数,而apply则是通过数组来传递参数,在实际应用中,根据参数传递的便利性,二者可以互换使用。

bind方法则是延迟执行,它会返回一个新的函数,这个新函数在调用时,this会指向bind方法指定的对象。

手写call的实现思路

Function.prototype.myCall = function(context, ...args) {
    // 1. 处理context为null或undefined的情况
    if (context == null||context == undefined) {
        context = window; // 非严格模式指向全局对象
    }
    
    // 2. 验证调用者是否为函数
    if (typeof this !== 'function') {
        throw new TypeError('this is not a function');
    }
    
    // 3. 使用Symbol创建唯一键值,避免属性冲突
    const fnKey = Symbol('fn');
    
    // 4. 将当前函数绑定到context上
    context[fnKey] = this;
    
    // 5. 执行函数并保存结果
    const result = context[fnKey](...args);
    
    // 6. 删除临时添加的属性,避免污染context对象
    delete context[fnKey];
    
    // 7. 返回函数执行结果
    return result;
};

二、核心知识点

(一)原型(Function)

在 JavaScript 中,函数是一种特殊的对象,所有函数都继承自Function.prototype。call方法正是定义在Function.prototype上的,这使得所有函数实例都能访问到call方法。例如:

function gretting() {
   return `hello`;
}
gretting.call; // 函数实例可以访问call方法

我们手写call方法,就是要在Function.prototype上定义一个新的方法,模拟call的功能。

(二)函数参数的理解

1. 处理context参数

call方法的第一个参数是context,它用于指定函数内部的this指向。 当contextnullundefined时:

  • 在非严格模式下,this指向全局对象(浏览器中为window
  • 在严格模式下,this保持为nullundefined

我们可以通过以下代码来处理:

if (context == null||context == undefined) {
    context = window; 
}

2.rest 运算符

call方法后续的参数是传递给函数的实际参数。在手写call时,我们使用rest运算符...args来收集这些参数。rest运算符可以将剩余的参数收集到一个数组中,方便我们在函数内部进行处理。

function gretting(...args) {
console.log(args);
}
gretting(1, 2, 3); // 输出 [1, 2, 3]

在手写call的实现中,我们通过...args收集参数后,在调用函数时再使用展开运算符...将数组展开传递给函数:

Function.prototype.myCall = function (context, ...args) {
// 处理context...
const result = context[fnKey](...args);
return result;
}

(三)控制函数内部的 this 指向

实现call方法的核心是要让函数在执行时,内部的this指向我们指定的context对象。

我们利用 JavaScript 的动态性,在context对象上挂载一个临时方法,将当前函数赋值给这个临时方法,然后通过调用这个临时方法来执行函数,这样函数内部的this就会指向context。

为了避免覆盖context对象上已有的属性,我们可以使用 ES6 中的Symbol来生成一个唯一的键。每个Symbol()调用都会返回一个唯一的值。例如:

Function.prototype.myCall = function (context, ...args) {
// 处理context...
const fnKey = Symbol('fn');
context[fnKey] = this;
const result = context[fnKey](...args);
delete context[fnKey];
return result;
}

在执行完函数后,我们需要删除挂载在context对象上的临时方法,以避免对context对象造成污染。

(四)处理返回值

call方法执行函数后,会返回函数的执行结果。在手写call时,我们需要捕获函数执行的返回值,并将其返回。

通过将函数执行的结果赋值给一个变量,然后在最后返回这个变量,即可实现这一功能

总结

通过手写实现call方法,我们深入理解了以下核心知识点:

  • JavaScript的原型链机制
  • this绑定的原理与规则
  • ES6的Symbol数据类型及其应用
  • 剩余参数和展开运算符的使用
  • 函数执行上下文的控制