前端知库

怎样手写 call、apply 和 bind?用 new 调用 bind 返回的函数会怎样?

call 和 apply 把函数临时挂到 this 对象的唯一 Symbol 属性上调用再删除,区别只在参数传法;bind 返回一个预设 this 和部分参数的新函数。用 new 调用 bind 返回的函数时,绑定的 this 被忽略,按原函数构造实例,预设参数仍然生效。

参考答案

简答

  • call(thisArg, ...args) 和 apply(thisArg, argsArray) 立即调用函数,只是参数形式不同。手写时把函数挂到 thisArg 的一个 Symbol 属性上调用,避免覆盖已有属性。
  • 非严格模式下,thisArg 为 null 或 undefined 时 this 是全局对象,原始值会被包装成对象。严格模式函数收到的 this 原样不变,挂属性的手写方法无法模拟这一点。
  • bind 返回新函数,调用时把预设参数放在前面、调用参数接在后面。
  • 用 new 调用 bind 后的函数时,绑定的 this 被忽略,等同于用预设参数 new 原函数,得到的实例 instanceof 原函数和绑定函数都为 true。
  • 手写 bind 用 new.target 判断是否被 new 调用,再用 Reflect.construct 构造。

call 和 apply

JavaScript
Function.prototype.myCall = function (thisArg, ...args) {
  if (typeof this !== 'function') throw new TypeError('not a function');
  // 模拟非严格模式:null/undefined 指向全局对象,原始值被包装
  const context = thisArg == null ? globalThis : Object(thisArg);
  const key = Symbol('fn');
  context[key] = this;
  try {
    return context[key](...args);
  } finally {
    delete context[key];
  }
};

Function.prototype.myApply = function (thisArg, args) {
  return this.myCall(thisArg, ...(args ?? []));
};

function show(a, b) {
  return [this.name, a, b];
}
console.log(show.myCall({ name: 'x' }, 1, 2)); // [ 'x', 1, 2 ]
console.log(show.myApply({ name: 'y' }, [3])); // [ 'y', 3, undefined ]

Symbol 键不会与对象原有属性冲突。finally 保证函数抛错时也会删除临时属性。对冻结对象调用时临时属性无法添加,这是该写法的局限。

bind

JavaScript
Function.prototype.myBind = function (thisArg, ...preset) {
  const target = this;
  if (typeof target !== 'function') throw new TypeError('not a function');

  function bound(...args) {
    // 被 new 调用时忽略绑定的 this,按原函数构造
    if (new.target) {
      return Reflect.construct(target, [...preset, ...args], new.target);
    }
    return target.apply(thisArg, [...preset, ...args]);
  }

  // 让 instanceof bound 与原生行为一致;箭头函数等没有 prototype
  if (target.prototype) bound.prototype = target.prototype;
  return bound;
};

new 调用绑定函数

JavaScript
function Point(x, y) {
  this.x = x;
  this.y = y;
}

const Bound = Point.myBind({ ignored: true }, 10);
const p = new Bound(20);
console.log(p); // Point { x: 10, y: 20 }
console.log(p instanceof Point, p instanceof Bound); // true true

const NativeBound = Point.bind({}, 10);
const q = new NativeBound(20);
console.log(q instanceof Point, q instanceof NativeBound); // true true

原生绑定函数没有 prototype 属性,instanceof 会转而检查目标函数的 prototype。手写版本把 bound.prototype 指向原函数的 prototype,得到同样的判断结果。

常见追问

  • 绑定函数再 bind 一次能改变 this 吗?不能,this 已经固定,再次 bind 只能继续追加预设参数。
  • 箭头函数能用 call 改变 this 吗?不能,箭头函数没有自己的 this,传入的 thisArg 被忽略。
  • 原生 bind 返回函数的 name 和 length 是什么?name 是 bound 原函数名,length 是原函数的形参数减去预设参数个数,最小为 0。上面的手写版用剩余参数,length 为 0、name 为 bound;需要一致时用 Object.defineProperty 改写这两个属性。