手写 call, apply, bind:不使用原生方法如何改变 this 指向

内容分享7天前发布
0 0 0

在 JavaScript 中,
this
的指向通常由调用方式决定。面试中要求手写这三个方法,核心考察的是对 “隐式绑定” 原理的理解。

核心原理
当函数作为某个对象的方法被调用时(例如
obj.func()
),
this
自然指向该对象。
因此,我们要做的就是:把函数临时挂载到目标对象上,执行它,然后再删掉它。


1. 手写
call


call
方法接收两个参数:


context
:this 要指向的对象。
...args
:参数列表(逗号分隔)。

实现步骤

判断
context
是否存在,如果为
null

undefined
,默认指向
window
(或
globalThis
)。为了避免属性名冲突,覆盖了 context 原有的属性,使用
Symbol
创建一个唯一的 key。将当前函数(
this
)赋值给
context[key]
。执行这个函数,并传入参数。删除临时添加的属性(清理现场)。返回函数执行的结果。

源码实现


Function.prototype.myCall = function(context, ...args) {
  // 1. 边界判断与默认值
  if (context === null || context === undefined) {
    context = window; // 浏览器环境
  }
  // 也可以写成: context = context || window;
  
  // 2. 将当前函数 (this) 挂载到 context 上
  // 使用 Symbol 避免覆盖 context 上已有的同名属性
  const fnSymbol = Symbol('fn');
  context[fnSymbol] = this;

  // 3. 执行函数,并展开参数
  const result = context[fnSymbol](...args);

  // 4. 删除临时属性,恢复原样
  delete context[fnSymbol];

  // 5. 返回结果
  return result;
};

测试


const person = { name: 'Alice' };
function say(age, job) {
  console.log(`${this.name} is ${age} years old, working as ${job}`);
  return 'done';
}

say.myCall(person, 25, 'Developer'); 
// 输出: "Alice is 25 years old, working as Developer"

2. 手写
apply


apply

call
的唯一区别在于传参方式:
apply
接收一个数组作为参数。

源码实现


Function.prototype.myApply = function(context, argsArray) {
  if (context === null || context === undefined) {
    context = window;
  }

  const fnSymbol = Symbol('fn');
  context[fnSymbol] = this;

  let result;
  
  // 判断是否有参数数组
  if (argsArray && Array.isArray(argsArray)) {
    result = context[fnSymbol](...argsArray);
  } else {
    result = context[fnSymbol]();
  }

  delete context[fnSymbol];
  return result;
};

3. 手写
bind
(难点 🔥)


bind
不会立即执行函数,而是返回一个新的函数
它有两个复杂的特性需要实现:

柯里化 (Currying):参数可以分两次传(绑定时传一部分,执行时传另一部分)。构造函数效果 (New Binding):如果返回的函数被
new
调用,
this
应该指向新创建的实例,而不是原本绑定的
context

源码实现


Function.prototype.myBind = function(context, ...args1) {
  // 0. 错误判断:调用 myBind 的必须是函数
  if (typeof this !== 'function') {
    throw new TypeError('Error: what is trying to be bound is not callable');
  }

  // 保存原函数
  const self = this;

  // 1. 返回一个新的函数
  // args2 是将来执行时传入的参数
  const fBound = function(...args2) {
    
    // 2. 处理 `new` 调用的情况 (关键点)
    // 如果当前函数被 new 调用,this 会是 fBound 的实例
    // 此时我们要忽略传入的 context,将 this 指向这个实例
    const isNewCall = this instanceof fBound;
    
    return self.apply(
      isNewCall ? this : context, 
      [...args1, ...args2] // 合并参数
    );
  };

  // 3. 维护原型链
  // 确保 new 出来的实例能继承原函数原型链上的属性
  // 使用 Object.create 创建一个空对象作为中介,避免直接修改 prototype 导致副作用
  if (self.prototype) {
    fBound.prototype = Object.create(self.prototype);
  }

  return fBound;
};

核心难点解析:为什么需要
this instanceof fBound

当你使用
new
操作符时,JS 内部会创建一个新对象,并将函数内部的
this
指向这个新对象。
但在
bind
返回的函数中,我们手动指定了
apply(context)

如果不加判断,
new BoundFn()

this
依然会被强行改为
context
,这违背了
new
的语义(
new
的优先级高于
bind
)。

所以需要判断:如果我是被
new
调用的,就不要管那个
context
了,直接用当前的
this

测试验证


// 1. 普通绑定测试
const obj = { val: 100 };
function add(a, b) {
  console.log(this.val + a + b);
}

const boundAdd = add.myBind(obj, 10); // 预传参数 10
boundAdd(20); // 输出 130 (100 + 10 + 20)


// 2. new 调用测试 (高级)
function Person(name) {
  this.name = name;
}
const BoundPerson = Person.myBind({ name: 'BadContext' }); // 试图绑定到一个错误对象

const p = new BoundPerson('GoodName'); 
console.log(p.name); // 输出 'GoodName'。如果实现错误,这里可能会输出 'BadContext'

总结

方法 是否立即执行 传参形式 主要用途
call ✅ 是
arg1, arg2, ...
改变一次执行上下文,借用父类构造函数
apply ✅ 是
[arg1, arg2]
改变上下文,且参数本来就是数组(如求数组最大值
Math.max.apply(null, arr)
bind ❌ 否
arg1, arg2, ...
保存上下文,后续执行(如 React 事件处理、setTimeout 回调)
© 版权声明

相关文章

暂无评论

none
暂无评论...