手写call全解析:从原理到实现,让this指向不再迷路~
一、JS this 之谜:call 为何是救星?
在 JavaScript 的世界里,this
就像一个调皮的精灵,函数在哪调用,它就指向哪。比如:
const obj = { name: '稀土掘金' };
function sayHi() {
console.log(`Hello, ${this.name}!`); // 直接调用时,this指向window/global
}
sayHi(); // 输出:Hello, undefined!(尴尬~)
这时候,call
闪亮登场!它能强行改变this
的指向,就像给this
戴上 GPS 定位器:
sayHi.call(obj); // 输出:Hello, 稀土掘金!(完美~)
核心作用:让函数在指定的上下文(context
)中执行,精准控制this
的指向。
二、call、apply、bind 大不同:先搞懂兄弟仨的分工
在动手写call
之前,先快速理清这三个高频方法的区别(避免后续混淆):
2.1 相同点:都是 this 的 “搬运工”
- 作用:动态修改函数内部
this
的指向。 - 原则:绝不修改原函数的
this
,只在调用时临时生效(bind
除外)。
2.2 不同点:执行方式、传参、绑定性质大揭秘
特性 | call | apply | bind |
---|---|---|---|
执行时机 | 立即执行(同步) | 立即执行(同步) | 返回新函数(延迟执行) |
传参方式 | 逐个传参(arg1, arg2 ) |
数组传参([arg1, arg2] ) |
可预传参(bind(ctx, a)(b) ) |
this 绑定 | 临时绑定(仅本次调用) | 临时绑定(仅本次调用) | 永久绑定(返回新函数) |
小剧场:
call
和apply
就像急性子,一说 “改 this” 就立刻执行;bind
则像慢性子,先记下来(返回新函数),等你喊 “执行” 才动~
三、手写 call 的核心思路:三步搞定 this 绑定
现在进入正题:如何手写一个myCall
,实现和原生call
一样的效果?
核心原理:把函数 “寄生” 到目标对象上,通过调用对象属性的方式执行函数,这样函数内的this
就会指向该对象。
3.1 第一步:处理 context(防止迷路的保底方案)
-
场景 1:如果用户没传
context
,或者传了null/undefined
,默认指向window
(严格模式下为undefined
,但这里简化处理)。 -
场景 2:如果传的是原始值(如
string/number
),需要用Object()
包装成对象(这是 JS 的隐式转换规则)。
Function.prototype.myCall = function(context) {
// 1. 处理context:默认指向window,原始值转对象
context = context !== null && context !== undefined ? Object(context) : window;
3.2 第二步:寄生函数:用 Symbol 防止属性冲突
-
问题:如果目标对象
context
本身有同名方法(比如context.fn
),直接挂载函数会覆盖原有属性。 -
解决方案:用 ES6 的
Symbol
生成唯一键名(Symbol 就像身份证号,绝对不会重复)。
// 2. 生成唯一键名,避免覆盖context原有属性
const fnKey = Symbol('临时函数');
// 3. 将当前函数“寄生”到context上
context[fnKey] = this; // this指向调用myCall的函数(比如sayHi)
3.3 第三步:执行函数 + 清理现场(做个有素质的 JS 开发者)
-
传参处理:用剩余参数
...args
收集call
的参数(第一个参数是context
,从第二个开始是函数的实参)。 -
清理现场:执行完函数后,删除
context
上的临时属性,避免污染对象。
// 4. 收集参数(第一个参数是context,从第二个开始是函数的参数)
const args = [...arguments].slice(1); // 例如:myCall(obj, a, b) → args = [a, b]
// 5. 执行函数,并接收返回值
const result = context[fnKey](...args);
// 6. 清理现场:删除临时属性
delete context[fnKey];
// 7. 返回函数执行结果
return result;
};
四、完整代码 + 测试:验证 myCall 是否靠谱
4.1 完整实现(带详细注释)
Function.prototype.myCall = function(context) {
// 处理context:null/undefined→window,原始值→对象(如:'稀土'→new String('稀土'))
context = context !== null && context !== undefined ? Object(context) : window;
// 生成唯一键名,避免与context原有属性冲突
const fnKey = Symbol('myCall-temp');
// 将当前函数(this)挂载到context上
context[fnKey] = this;
// 收集参数:排除第一个参数(context),剩余的作为函数参数
const args = [...arguments].slice(1);
// 执行函数并获取结果
const result = context[fnKey](...args);
// 删除临时属性,保持context纯净
delete context[fnKey];
// 返回函数执行结果
return result;
};
4.2 测试案例:验证不同场景
案例 1:普通对象绑定
const obj = { name: '稀土掘金' };
function sayHi(hello) {
return `${hello}, ${this.name}!`;
}
console.log(sayHi.myCall(obj, 'Hi')); // 输出:Hi, 稀土掘金!(正确~)
案例 2:context 为 null/undefined
function sayHello() {
return `Hello, ${this.defaultName}`;
}
const globalObj = { defaultName: 'Global' };
// 当context为null/undefined时,myCall默认指向window(这里用globalObj模拟window)
console.log(sayHello.myCall(null)); // 输出:Hello, Global!(正确~)
案例 3:原始值作为 context
function logType() {
console.log(this instanceof String); // true(包装成String对象)
console.log(`类型:${this}`); // 类型:稀土掘金
}
logType.myCall('稀土掘金'); // 输出:true 和 类型:稀土掘金(正确~)
五、常见坑点与优化:让 myCall 更健壮
5.1 坑点 1:忘记绑定函数类型
-
错误场景:如果调用
myCall
的不是函数(比如null.myCall()
),会报错。 -
解决方案:调用前校验
this
是否为函数类型。
if (typeof this !== 'function') {
throw new TypeError('myCall must be called on a function');
}
5.2 坑点 2:严格模式下的 this 处理
-
规则:在严格模式中,
call
的context
如果是null/undefined
,函数内的this
就是null/undefined
,而不是window
。 -
优化:移除
context = ... || window
的默认处理,严格遵循规范。
// 严格模式下的处理(可选优化)
context = context === null || context === undefined ? context : Object(context);
六、扩展思考:apply 和 bind 该怎么写?(留给聪明的你)
6.1 apply 的实现(与 call 的唯一区别:参数是数组)
Function.prototype.myApply = function(context, args) {
context = context !== null && context !== undefined ? Object(context) : window;
const fnKey = Symbol('myApply-temp');
context[fnKey] = this;
// apply的参数是数组,直接展开即可
const result = context[fnKey](...(args || []));
delete context[fnKey];
return result;
};
6.2 bind 的实现(返回新函数,永久绑定 this)
Function.prototype.myBind = function(context) {
const fn = this;
const args = [...arguments].slice(1);
// 返回新函数,支持new调用(通过原型链继承)
return function NewFn() {
// 如果是new调用,this指向实例对象,否则指向context
const ctx = this instanceof NewFn ? this : Object(context);
return fn.apply(ctx, args.concat([...arguments]));
};
};
七、总结:手写 call 的灵魂三问
-
为什么要用 Symbol?
防止临时属性与目标对象的原有属性冲突,就像给临时变量起了个独一无二的名字。 -
call 和 apply 的本质区别是什么?
只是参数形式不同,call
传散落的参数,apply
传数组,底层原理完全一致。 -
bind 为什么返回新函数?
因为它需要 “记住” 绑定的this
和参数,等后续调用时再执行,就像一个 “延迟执行的函数包裹器”。
通过手写call
,我们不仅深入理解了 JS 的this
机制,还掌握了函数 “寄生”、属性隔离、参数处理等核心技巧。下次遇到this
指向问题,再也不用慌啦~ 😉