一文搞懂 JS 中 call、apply、bind 及手写实现
在 JavaScript 中,
this的指向一直是核心难点,而call、apply、bind作为改变函数this指向的三大方法,是前端开发中必须掌握的知识点。大家要围绕传参区别 、调用时机 、返回值 三个方面来思考,手写也需要围绕传入什么 ,改变什么 ,返回什么来思考。
一、为什么需要改变 this 指向?
JavaScript 中函数的this指向并非定义时确定,而是执行时确定 ,默认指向调用它的对象。这种特性在某些场景下会导致this指向不符合预期,例如:
javascript
const person = {
name: "小明",
sayHi: function() {
console.log(`你好,我是${this.name}`);
}
};
// 正常调用,this指向person
person.sayHi(); // 输出:你好,我是小明
// 将方法赋值给变量后调用,this指向全局(浏览器为window,Node为global)
const aloneSay = person.sayHi;
aloneSay(); // 输出:你好,我是undefined
此时就需要call、apply、bind来强制改变this的指向,让函数按照我们的预期执行。
二、call、apply、bind 的核心区别
三者的核心功能都是改变函数执行时的 this 指向 ,主要区别体现在传参方式 和执行时机上。
2.1 call:立即执行,参数逐个传递
-
语法 :
函数名.call(thisArg, 参数1, 参数2, ...) -
参数说明:
thisArg:函数执行时要绑定的this对象;- 后续参数:逐个传入函数的参数。
-
特点 :调用后立即执行函数,返回函数执行结果。
示例:
javascript
const person = { name: "小明" };
function sayHi(age, gender) {
console.log(`你好,我是${this.name},${age}岁,${gender}`);
}
// 用call改变this指向person,同时逐个传参
sayHi.call(person, 20, "男"); // 输出:你好,我是小明,20岁,男
2.2 apply:立即执行,参数以数组传递
-
语法 :
函数名.apply(thisArg, [参数1, 参数2, ...]) -
参数说明:
thisArg:与call一致,为绑定的this对象;- 第二个参数:必须是数组 / 类数组,数组元素会被逐个传入函数。
-
特点 :调用后立即执行函数,返回函数执行结果,适合参数数量不确定的场景。
示例:
javascript
// 复用上述sayHi函数和person对象
const args = [20, "男"];
sayHi.apply(person, args); // 输出:你好,我是小明,20岁,男
2.3 bind:不立即执行,返回新函数
-
语法 :
const 新函数 = 函数名.bind(thisArg, 参数1, 参数2, ...) -
参数说明:
thisArg:绑定的this对象;- 后续参数:可选,提前传入函数的参数(预传参)。
-
特点 :不立即执行 函数,返回一个绑定了
this的新函数;后续调用新函数时,可补传剩余参数。
示例:
javascript
// 复用上述sayHi函数和person对象
// bind返回新函数,不立即执行
const bindSayHi = sayHi.bind(person, 20);
// 调用新函数时补传剩余参数
bindSayHi("男"); // 输出:你好,我是小明,20岁,男
2.4 三者核心区别总结表
| 方法 | 执行时机 | 传参方式 | 返回值 |
|---|---|---|---|
| call | 立即执行 | 逐个传递参数 | 函数执行的结果 |
| apply | 立即执行 | 数组 / 类数组传递参数 | 函数执行的结果 |
| bind | 不立即执行 | 可预传参数,后续可补传 | 绑定了 this 的新函数 |
三、手写实现 call、apply、bind
理解了三者的使用方式后,我们通过手写实现来深入其底层逻辑,从易到难依次实现apply、bind、call(call的实现与apply类似)。
3.1 手写实现 apply:newApply
apply的核心逻辑是:将函数挂载到目标对象上,通过对象调用函数实现this绑定,再传递数组参数执行函数,最后删除挂载的函数并返回执行结果。
实现代码:
javascript
function sayHi(name, age) {
console.log(`我是${this.name},昵称${name},年龄${age}`);
return `执行结果:${name}-${age}`; // 函数返回结果,方便测试
}
Function.prototype.newApply = function(content) {
// 边缘检测调用者是否为函数
if (typeof this !== 'function') {
throw new TypeError('The caller must be a function');
}
// 确定绑定的this对象,默认指向window(浏览器环境)
content = content || window;
// 将原函数挂载到目标对象上,此时content.fn就是原函数
content.fn = this;
let result; // 存储函数执行"结果" ->记住apply返回的是函数执行结果
// 处理参数:如果有第二个参数(数组),则解构传递;否则直接执行
// 此时this挂在了content.fn上面
result = arguments[1] ? content.fn(...arguments[1]) : content.fn()
// 删除挂载的函数,避免污染目标对象
delete content.fn;
// 返回结果
return result; // 返回函数执行结果
}
// 测试代码
const person = { name: "张三" };
const finalResult = sayHi.newApply(person, ["小张", 20]);
console.log(finalResult); // 输出:执行结果:小张-20
拓展一个小知识
当遇到单独出现if else的时候,可以换成"三元运算符" ? + :的形式缩短代码,显得高级。
核心逻辑解析:
- 类型校验 :确保
newApply的调用者是函数,否则抛出错误; - 绑定 this 对象 :若未传入目标对象,默认绑定
window; - 挂载原函数 :将原函数(
this指向调用newApply的函数)挂载到目标对象上,通过content.fn()调用时,this自然指向content; - 参数处理:利用三元运算符简洁处理参数传递,解构数组参数执行函数并保存结果;
- 清理与返回 :删除挂载的
fn属性,避免污染对象,最后返回函数执行结果。
3.2 手写实现 bind:newBind
bind的核心逻辑是:返回一个新函数,新函数执行时通过apply调用原函数并绑定this。
代码实现
javascript
Function.prototype.newBind = function () {
// 保存原函数和传入的参数(类数组转数组)
const _this = this, args = Array.prototype.slice.call(arguments);
// 取出第一个参数作为绑定的this对象
const newThis = args.shift();
// 返回新函数,延迟执行
return function () {
// 通过apply调用原函数,绑定this并传递参数
return _this.apply(newThis, args);
}
};
提示
这里的apply也可以用上述手写的newapply代替哦~
3.3 手写实现 call:newCall
call的实现与apply几乎一致,唯一区别是参数传递方式 (call逐个传参,apply数组传参)。
实现代码:
javascript
Function.prototype.newCall = function(content) {
// 类型校验
if (typeof this !== 'function') {
throw new TypeError('The caller must be a function');
}
content = content || window;
content.fn = this;
let result;
// 处理参数:从第二个参数开始,逐个解构传递
const args = Array.prototype.slice.call(arguments, 1);
result = content.fn(...args);
delete content.fn;
return result;
};
// 测试代码
const person = { name: "李四" };
sayHi.newCall(person, "小李", 25); // 输出:我是李四,昵称小李,年龄25
四、常见问题解析
在手写实现的过程中,新手常遇到两个核心问题,这里集中解答:
4.1 为什么要用Array.prototype.slice.call(arguments)?
arguments是函数内部的类数组对象 ,有下标和length属性,但没有数组的slice、concat等方法。Array.prototype.slice.call(arguments)的作用是将类数组的 arguments 转为真正的数组,方便后续操作参数。
ES6 后可简化为:
javascript
const args = Array.from(arguments); // 或 const args = [...arguments];
4.2 为什么要把this存到_this变量中?
在newBind方法中,this原本指向调用newBind的原函数 ,但返回的新函数执行时,其内部的this指向会发生变化(浏览器中默认指向window,严格模式下为undefined)。提前将this保存到_this,是为了保留原函数的引用 ,确保后续能通过_this.apply()调用原函数。
五、总结
call、apply、bind的核心都是改变函数的this指向,区别在于执行时机和传参方式;call和apply立即执行,前者逐个传参,后者数组传参;bind返回新函数,延迟执行且支持预传参;- 手写实现的核心思路是通过对象挂载函数 实现
this绑定,再通过apply/call执行原函数并处理参数。
掌握这三个方法的使用与底层逻辑,能让我们更灵活地处理 JavaScript 中的this指向问题,也是前端面试中的高频考点。希望本文能帮助大家彻底理解并掌握这一知识点!