手写call、apply、bind

323 阅读3分钟

在 JavaScript 中,callapplybind 方法用于改变函数的 this 上下文。它们都是挂载在 Function.prototype 上的方法。以下是它们的作用以及如何手写实现这些方法的简化版本。

1. 手写 call

call 方法用于调用一个函数,并显式地将 this 绑定到指定的对象上。它可以接收若干个参数,按顺序传递给被调用的函数。

手写 call 实现:
Function.prototype.myCall = function(context, ...args) {
  // 如果 context 是 null 或 undefined,this 指向全局对象 (浏览器中是 window)
  context = context || globalThis;

  // 将函数 (this) 作为 context 对象的临时属性
  const fnSymbol = Symbol();  // 使用 Symbol 防止属性冲突
  context[fnSymbol] = this;

  // 调用函数并传入参数
  const result = context[fnSymbol](...args);

  // 删除临时属性
  delete context[fnSymbol];

  // 返回函数执行结果
  return result;
};

// 使用示例
function greet(age, city) {
  return `Hello, my name is ${this.name}, I'm ${age} years old and live in ${city}.`;
}

const person = { name: "Alice" };
console.log(greet.myCall(person, 25, "New York")); // 输出: "Hello, my name is Alice, I'm 25 years old and live in New York."

2. 手写 apply

apply 方法与 call 类似,不同的是它接受的是参数数组而不是参数列表。

手写 apply 实现:
Function.prototype.myApply = function(context, args) {
  // 如果 context 是 null 或 undefined,this 指向全局对象 (浏览器中是 window)
  context = context || globalThis;

  // 将函数 (this) 作为 context 对象的临时属性
  const fnSymbol = Symbol();
  context[fnSymbol] = this;

  // 调用函数并传入参数数组
  const result = context[fnSymbol](...(args || []));

  // 删除临时属性
  delete context[fnSymbol];

  // 返回函数执行结果
  return result;
};

// 使用示例
function greet(age, city) {
  return `Hello, my name is ${this.name}, I'm ${age} years old and live in ${city}.`;
}

const person = { name: "Alice" };
console.log(greet.myApply(person, [25, "New York"])); // 输出: "Hello, my name is Alice, I'm 25 years old and live in New York."

3. 手写 bind

bind 方法与 callapply 不同,它返回一个新的函数,并且永久绑定 this 到指定的对象。调用新函数时,原函数的 this 值会被永久绑定到 bind 的第一个参数。

手写 bind 实现:
Function.prototype.myBind = function(context, ...args) {
  // 保存 this,即原函数
  const self = this;

  // 返回一个新的函数
  return function(...newArgs) {
    // 如果作为构造函数调用,则使用新对象作为 this,否则使用 context 作为 this
    const isNew = this instanceof self;
    return self.apply(isNew ? this : context, [...args, ...newArgs]);
  };
};

// 使用示例
function greet(age, city) {
  return `Hello, my name is ${this.name}, I'm ${age} years old and live in ${city}.`;
}

const person = { name: "Alice" };
const boundGreet = greet.myBind(person, 25);
console.log(boundGreet("New York")); // 输出: "Hello, my name is Alice, I'm 25 years old and live in New York."

bind绑定之后的新函数作为构造函数调用

在 JavaScript 中,bind 方法不仅可以用于绑定函数的 this 值,还能与构造函数结合使用。使用 bind 时,如果调用的是一个构造函数,bind 返回的函数会保留原构造函数的原型链。

在手写的 myBind 实现中,我们需要确保返回的绑定函数可以正确处理作为构造函数调用的情况。为了演示 myBind 的效果,这里是一个带有构造函数的完整示例:

1. myBind 实现

首先,确保 myBind 方法能够处理构造函数:

Function.prototype.myBind = function(context, ...args) {
  const self = this; // 保存原函数的引用

  // 返回一个新的函数
  function F(...newArgs) {
    // 如果是通过 new 调用,this 是新创建的实例,忽略 context
    if (this instanceof F) {
      return new self(...args, ...newArgs); // 使用原函数作为构造函数
    }
    // 否则是普通调用,将 context 绑定为 this
    return self.apply(context, [...args, ...newArgs]);
  }

  // 设置新函数的原型对象,使其继承原函数的原型
  F.prototype = Object.create(self.prototype);

  return F;
};

2. 使用构造函数的 myBind 示例

接下来,我们演示如何在使用 myBind 时进行构造函数调用:

// 定义一个构造函数
function Person(name, age) {
  this.name = name;
  this.age = age;
}

Person.prototype.sayHello = function() {
  return `Hello, my name is ${this.name}, and I am ${this.age} years old.`;
};

// 创建一个对象用于绑定
const context = {
  name: 'Alice',
  age: 25
};

// 使用 myBind 绑定 context 对象
const BoundPerson = Person.myBind(context, 'Bob');

// 普通调用:this 会绑定到 context
const person1 = BoundPerson(30); // 这里相当于直接调用函数,不是 new 调用
console.log(context.name); // 输出: 'Alice'
console.log(person1); // 输出: undefined,因为普通函数调用不返回值

// 使用构造函数调用:this 应该指向新创建的实例
const person2 = new BoundPerson(40); // 这里是通过 new 调用
console.log(person2.name); // 输出: 'Bob',来自绑定参数
console.log(person2.age);  // 输出: 40,来自 new BoundPerson 时传递的参数

// 原型链中的方法仍然可用
console.log(person2.sayHello()); // 输出: 'Hello, my name is Bob, and I am 40 years old.'

3. 解释

  • 普通调用 BoundPerson(30) :绑定了 context,所以普通函数调用时,this 指向的是 context。但是由于我们没有返回任何值,因此 person1 只是 undefined
  • 构造函数调用 new BoundPerson(40) :在这种情况下,this 指向的是新创建的对象。即使 myBindcontextAlice,构造函数调用忽略了它,this 仍然绑定到新创建的实例。最终,我们得到了一个 nameBobage40 的新对象。

4. 关键点

  1. 构造函数与普通函数的区别:在构造函数调用(new)时,JavaScript 会自动创建一个新的对象,并将 this 指向新对象。bind 需要处理这种情况,并忽略原先绑定的 context,将 this 绑定到新对象上。
  2. 保留原型链:绑定后的函数应当保持原函数的原型链,因此 myBind 返回的函数应该确保 new 调用时新创建的对象能够继承构造函数的原型方法。
  3. 结合参数传递myBind 支持部分参数绑定(即柯里化),并结合新调用时传递的参数一起使用。

这个实现和例子展示了如何手写 myBind 方法,使其能够与构造函数一起正常工作,并展示了如何在构造函数中使用 myBind 进行上下文绑定。

4. 总结

  • myCall:实现了通过指定的 this 值调用函数,接受的参数是普通的参数列表。
  • myApply:与 myCall 类似,不过它接受的参数是数组。
  • myBind:返回一个绑定了 this 的新函数,能够延迟调用,并且支持部分参数绑定。

通过这三种方法,你可以更好地控制函数在不同上下文中的执行。手写这些方法有助于加深对 JavaScript 函数上下文和执行机制的理解。