本文手写实现 JS 中最重要的三个 this 绑定函数,并处理 90% 的人会忽略的边界问题。代码可直接复制。
一、准备知识:this 的优先级
call、apply、bind 都用于显式绑定 this。区别:
call:立即执行,参数逐个传递apply:立即执行,参数以数组传递bind:返回新函数,不立即执行,支持柯里化
手写它们的关键:将函数挂载到 context 对象上执行,然后删除。
二、手写 call
javascript
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;
};
测试:
javascript
function say(age) {
return `${this.name} is ${age}`;
}
const obj = { name: '张三' };
console.log(say.myCall(obj, 25)); // "张三 is 25"
边界 1 :context 为原始类型(number/string/boolean)时,需要转为对象。
javascript
// 改进
context = context !== null && context !== undefined ? Object(context) : window;
三、手写 apply
与 call 几乎一样,只是参数形式不同。
javascript
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;
};
测试:
javascript
say.myApply(obj, [30]); // "张三 is 30"
四、手写 bind(最难)
bind 的特点:
- 返回一个新函数
- 支持柯里化(预设参数)
- 当新函数作为构造函数(
new)时,this指向实例,而非绑定的context
javascript
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;
};
测试:
javascript
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 }
五、3 个最容易忽略的边界情况
边界 1:context 为 null/undefined
原生 call 会指向全局对象(浏览器 window,Node global)。我们的实现已处理。
边界 2:函数有返回值
必须将返回值传给调用方。已在实现中通过 result 返回。
边界 3:bind 后的函数作为构造函数
- 当使用
new调用bound时,this指向新创建的实例,不能继续绑定到原来的context。 - 上述
myBind中通过this instanceof bound判断即可。
验证:
javascript
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' }
六、完整代码(可直接复制)
javascript
// 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 调用。- 边界情况(null/undefined、原始类型、new 优先级)是面试和实际编码的常考点。
文中所有代码均已测试,可放心直接用于 polyfill。下一篇准备手写
instanceof和new操作符,欢迎关注。
讨论:你还遇到过哪些 this 相关的奇怪 bug?评论区分享。