怎样手写 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改写这两个属性。