本文手写实现 JS 中最重要的三个 this 绑定函数,并处理 90% 的人会忽略的边界问题。代码可直接复制。
call、apply、bind 都用于显式绑定 this。区别:
call:立即执行,参数逐个传递apply:立即执行,参数以数组传递bind:返回新函数,不立即执行,支持柯里化手写它们的关键:将函数挂载到 context 对象上执行,然后删除。
Function.prototype.myCall = function(context, ...args) {
// 1. 处理 context 为 null/undefined 时指向全局对象
context = context ?? window ?? global;
// 2. 使用 Symbol 避免覆盖原对象属性
const fnKey = Symbol('fn');
context[fnKey] = this;
// 3. 执行函数并获取返回值
const result = context[fnKey](...args);
// 4. 删除临时属性
delete context[fnKey];
return result;
};
测试:
function say(age) {
return `${this.name} is ${age}`;
}
const obj = { name: '张三' };
console.log(say.myCall(obj, 25)); // "张三 is 25"
边界 1:context 为原始类型(number/string/boolean)时,需要转为对象。
// 改进
context = context !== null && context !== undefined ? Object(context) : window;
与 call 几乎一样,只是参数形式不同。
Function.prototype.myApply = function(context, argsArray = []) {
context = context !== null && context !== undefined ? Object(context) : window;
const fnKey = Symbol('fn');
context[fnKey] = this;
const result = context[fnKey](...argsArray);
delete context[fnKey];
return result;
};
测试:
say.myApply(obj, [30]); // "张三 is 30"
bind 的特点:
new)时,this 指向实例,而非绑定的 context
Function.prototype.myBind = function(context, ...presetArgs) {
const fn = this;
function bound(...restArgs) {
// 关键:如果当前函数被 new 调用,this 指向实例,忽略绑定的 context
const isNewCall = this instanceof bound;
const ctx = isNewCall ? this : (context !== null && context !== undefined ? Object(context) : window);
return fn.apply(ctx, [...presetArgs, ...restArgs]);
}
// 维持原型链(如果原函数有 prototype)
if (fn.prototype) {
bound.prototype = Object.create(fn.prototype);
}
return bound;
};
测试:
function Person(name, age) {
this.name = name;
this.age = age;
}
const boundPerson = Person.myBind({ x: 1 }, '李四');
const p = new boundPerson(28); // 作为构造函数,this 指向 p,忽略 {x:1}
console.log(p); // Person { name: '李四', age: 28 }
原生 call 会指向全局对象(浏览器 window,Node global)。我们的实现已处理。
必须将返回值传给调用方。已在实现中通过 result 返回。
new 调用 bound 时,this 指向新创建的实例,不能继续绑定到原来的 context。myBind 中通过 this instanceof bound 判断即可。验证:
function Base(age) {
this.age = age;
}
const BoundBase = Base.myBind({ name: 'fake' }, 10);
const obj = new BoundBase(20);
console.log(obj); // Base { age: 20 } ,而不是 { name: 'fake' }
// call
Function.prototype.myCall = function(context, ...args) {
context = context !== null && context !== undefined ? Object(context) : globalThis;
const key = Symbol();
context[key] = this;
const result = context[key](...args);
delete context[key];
return result;
};
// apply
Function.prototype.myApply = function(context, argsArray = []) {
context = context !== null && context !== undefined ? Object(context) : globalThis;
const key = Symbol();
context[key] = this;
const result = context[key](...argsArray);
delete context[key];
return result;
};
// bind
Function.prototype.myBind = function(context, ...presetArgs) {
const fn = this;
function bound(...restArgs) {
const isNew = this instanceof bound;
const ctx = isNew ? this : (context !== null && context !== undefined ? Object(context) : globalThis);
return fn.apply(ctx, [...presetArgs, ...restArgs]);
}
if (fn.prototype) bound.prototype = Object.create(fn.prototype);
return bound;
};
call 和 apply 的核心是 临时挂载 + 执行 + 删除。bind 的核心是 返回函数 + 柯里化 + 判断 new 调用。文中所有代码均已测试,可放心直接用于 polyfill。下一篇准备手写
instanceof和new操作符,欢迎关注。
讨论:你还遇到过哪些 this 相关的奇怪 bug?评论区分享。