普通视图

发现新文章,点击刷新页面。
昨天 — 2025年7月1日首页

手写call全解析:从原理到实现,让this指向不再迷路~

作者 十盒半价
2025年7月1日 17:28

一、JS this 之谜:call 为何是救星?

在 JavaScript 的世界里,this就像一个调皮的精灵,函数在哪调用,它就指向哪。比如:

const obj = { name: '稀土掘金' };
function sayHi() {
  console.log(`Hello, ${this.name}!`); // 直接调用时,this指向window/global
}
sayHi(); // 输出:Hello, undefined!(尴尬~)

这时候,call闪亮登场!它能强行改变this的指向,就像给this戴上 GPS 定位器:

sayHi.call(obj); // 输出:Hello, 稀土掘金!(完美~)

核心作用:让函数在指定的上下文(context)中执行,精准控制this的指向。

二、call、apply、bind 大不同:先搞懂兄弟仨的分工

在动手写call之前,先快速理清这三个高频方法的区别(避免后续混淆):

2.1 相同点:都是 this 的 “搬运工”

  • 作用:动态修改函数内部this的指向。
  • 原则:绝不修改原函数的this,只在调用时临时生效(bind除外)。

2.2 不同点:执行方式、传参、绑定性质大揭秘

特性 call apply bind
执行时机 立即执行(同步) 立即执行(同步) 返回新函数(延迟执行)
传参方式 逐个传参(arg1, arg2 数组传参([arg1, arg2] 可预传参(bind(ctx, a)(b)
this 绑定 临时绑定(仅本次调用) 临时绑定(仅本次调用) 永久绑定(返回新函数)

小剧场

  • callapply就像急性子,一说 “改 this” 就立刻执行;
  • bind则像慢性子,先记下来(返回新函数),等你喊 “执行” 才动~

三、手写 call 的核心思路:三步搞定 this 绑定

现在进入正题:如何手写一个myCall,实现和原生call一样的效果?
核心原理:把函数 “寄生” 到目标对象上,通过调用对象属性的方式执行函数,这样函数内的this就会指向该对象。

3.1 第一步:处理 context(防止迷路的保底方案)

  • 场景 1:如果用户没传context,或者传了null/undefined,默认指向window(严格模式下为undefined,但这里简化处理)。

  • 场景 2:如果传的是原始值(如string/number),需要用Object()包装成对象(这是 JS 的隐式转换规则)。

Function.prototype.myCall = function(context) {
  // 1. 处理context:默认指向window,原始值转对象
  context = context !== null && context !== undefined ? Object(context) : window;

3.2 第二步:寄生函数:用 Symbol 防止属性冲突

  • 问题:如果目标对象context本身有同名方法(比如context.fn),直接挂载函数会覆盖原有属性。

  • 解决方案:用 ES6 的Symbol生成唯一键名(Symbol 就像身份证号,绝对不会重复)。

  // 2. 生成唯一键名,避免覆盖context原有属性
  const fnKey = Symbol('临时函数');
  // 3. 将当前函数“寄生”到context上
  context[fnKey] = this; // this指向调用myCall的函数(比如sayHi)

3.3 第三步:执行函数 + 清理现场(做个有素质的 JS 开发者)

  • 传参处理:用剩余参数...args收集call的参数(第一个参数是context,从第二个开始是函数的实参)。

  • 清理现场:执行完函数后,删除context上的临时属性,避免污染对象。

  // 4. 收集参数(第一个参数是context,从第二个开始是函数的参数)
  const args = [...arguments].slice(1); // 例如:myCall(obj, a, b) → args = [a, b]
  // 5. 执行函数,并接收返回值
  const result = context[fnKey](...args);
  // 6. 清理现场:删除临时属性
  delete context[fnKey];
  // 7. 返回函数执行结果
  return result;
};

四、完整代码 + 测试:验证 myCall 是否靠谱

4.1 完整实现(带详细注释)

Function.prototype.myCall = function(context) {
  // 处理context:null/undefined→window,原始值→对象(如:'稀土'→new String('稀土'))
  context = context !== null && context !== undefined ? Object(context) : window;
  // 生成唯一键名,避免与context原有属性冲突
  const fnKey = Symbol('myCall-temp');
  // 将当前函数(this)挂载到context上
  context[fnKey] = this;
  // 收集参数:排除第一个参数(context),剩余的作为函数参数
  const args = [...arguments].slice(1);
  // 执行函数并获取结果
  const result = context[fnKey](...args);
  // 删除临时属性,保持context纯净
  delete context[fnKey];
  // 返回函数执行结果
  return result;
};

4.2 测试案例:验证不同场景

案例 1:普通对象绑定

const obj = { name: '稀土掘金' };
function sayHi(hello) {
  return `${hello}, ${this.name}!`;
}
console.log(sayHi.myCall(obj, 'Hi')); // 输出:Hi, 稀土掘金!(正确~)

案例 2:context 为 null/undefined

function sayHello() {
  return `Hello, ${this.defaultName}`;
}
const globalObj = { defaultName: 'Global' };
// 当context为null/undefined时,myCall默认指向window(这里用globalObj模拟window)
console.log(sayHello.myCall(null)); // 输出:Hello, Global!(正确~)

案例 3:原始值作为 context

function logType() {
  console.log(this instanceof String); // true(包装成String对象)
  console.log(`类型:${this}`); // 类型:稀土掘金
}
logType.myCall('稀土掘金'); // 输出:true 和 类型:稀土掘金(正确~)

五、常见坑点与优化:让 myCall 更健壮

5.1 坑点 1:忘记绑定函数类型

  • 错误场景:如果调用myCall的不是函数(比如null.myCall()),会报错。

  • 解决方案:调用前校验this是否为函数类型。

if (typeof this !== 'function') {
  throw new TypeError('myCall must be called on a function');
}

5.2 坑点 2:严格模式下的 this 处理

  • 规则:在严格模式中,callcontext如果是null/undefined,函数内的this就是null/undefined,而不是window

  • 优化:移除context = ... || window的默认处理,严格遵循规范。

// 严格模式下的处理(可选优化)
context = context === null || context === undefined ? context : Object(context);

六、扩展思考:apply 和 bind 该怎么写?(留给聪明的你)

6.1 apply 的实现(与 call 的唯一区别:参数是数组)

Function.prototype.myApply = function(context, args) {
  context = context !== null && context !== undefined ? Object(context) : window;
  const fnKey = Symbol('myApply-temp');
  context[fnKey] = this;
  // apply的参数是数组,直接展开即可
  const result = context[fnKey](...(args || []));
  delete context[fnKey];
  return result;
};

6.2 bind 的实现(返回新函数,永久绑定 this)

Function.prototype.myBind = function(context) {
  const fn = this;
  const args = [...arguments].slice(1);
  // 返回新函数,支持new调用(通过原型链继承)
  return function NewFn() {
    // 如果是new调用,this指向实例对象,否则指向context
    const ctx = this instanceof NewFn ? this : Object(context);
    return fn.apply(ctx, args.concat([...arguments]));
  };
};

七、总结:手写 call 的灵魂三问

  1. 为什么要用 Symbol?
    防止临时属性与目标对象的原有属性冲突,就像给临时变量起了个独一无二的名字。

  2. call 和 apply 的本质区别是什么?
    只是参数形式不同,call传散落的参数,apply传数组,底层原理完全一致。

  3. bind 为什么返回新函数?
    因为它需要 “记住” 绑定的this和参数,等后续调用时再执行,就像一个 “延迟执行的函数包裹器”。

通过手写call,我们不仅深入理解了 JS 的this机制,还掌握了函数 “寄生”、属性隔离、参数处理等核心技巧。下次遇到this指向问题,再也不用慌啦~ 😉

❌
❌