this 关键字是JavaScript 中最复杂的机制之一。
而在 JavaScript 中,apply、call和bind函数 都是用来改变this指向的函数,理解这三个函数有助于读者更好的理解this,本文将带读者来从基础用法 ,到理解函数 的原理再到实现自己的apply、call和bind函数。
一、call\apply\bind的用法
1.1 call函数
call
函数是挂载在 Function
对象上的原型上的一个函数,也就是 Function.prototype.call(),传入一个对象和参数。
作用 :立即调用函数,并指定 this
指向,同时传递参数(参数需逐个传入)。
用法 :function.call(thisArg,arg1,arg2,...)
thisArg
:要指向的对象arg1,arg2,...
: 传入的参数(需要依次传入)
用法示例:简单写一个测试用例帮助读者理解
js
function test(a, b) {
console.log(this.name, a + b);
}
const obj = { name: 'test' };
test.call(obj, 1, 2); // 输出 "test 3"
适用场景:当参数数量不确定,或已存在一个数组 / 类数组时(如动态获取的参数列表)。
1.2 apply函数
apply
函数是挂载在 Function
对象上的原型上的一个函数,也就是 Function.prototype.apply(),传入对象和参数数组。
作用 :立即调用函数,并指定 this
指向,同时传递参数(参数以数组或类数组对象传入)。
用法 :function.apply(thisArg, [argsArray])
thisArg
:要指向的对象[argsArray]
: 参数数组(或类数组,如arguments
对象))
用法示例:简单写一个测试用例帮助读者理解
js
function test(a, b) {
console.log(this.name, a + b);
}
const obj = { name: 'test' };
test.apply(obj, [1, 2]); // 输出 "test 3"
// 测试 context 为 null
test.apply(null, [3, 4]); // 非严格模式下输出 "window 7"(浏览器环境)
// 测试 argsArray 为类数组
const args = { 0: 5, 1: 6, length: 2 };
test.apply(obj, args); // 输出 "test 11"
适用场景:当参数数量不确定,或已存在一个数组 / 类数组时(如动态获取的参数列表)。
1.3 bind函数
bind
函数是挂载在 Function
对象上的原型上的一个函数,也就是 Function.prototype.bind(),传入对象和参数,创建一个新函数,返回的函数可以再继续传参。
作用 :创建一个新函数 (绑定函数),指定其 this
指向和预设参数,但不会立即执行。
用法 :function.bind(thisArg, arg1, arg2, ...)
thisArg
:要指向的对象arg1, arg2...
:预设的参数(调用新函数时可补充剩余参数)
用法示例:简单写一个测试用例帮助读者理解
js
//1.基础测试用例
function test(a, b) {
console.log(this.name, a + b);
}
const obj = { name: 'test' };
test.bind(obj, 1, 2)(); // 输出 "test 3"
// 2. context 为 null 测试(非严格模式)
test.bind(null, 3, 4)(); // 输出 "window 7" 浏览器环境
// 3. 类数组参数处理(需手动转为数组)
const args = { 0: 5, 1: 6, length: 2 };
test.bind(obj, ...Array.from(args))(); // 输出 "test 11"(需解构类数组)
// 4. 作为构造函数测试
function Person(name) {
this.name = name;
}
const BoundPerson = Person.bind({}); // 绑定到空对象
const p = new BoundPerson("Bob");
console.log(p.name); // "Bob"(this 指向实例)
console.log(p instanceof Person); // true(继承原型)
适用场景:
- 需要延迟执行函数(如事件回调、定时器)。
- 固定函数的
this
指向(避免回调中this
丢失)。 - 预设部分参数(函数柯里化)。
不知道柯里化是什么?笔者带你理解函数式编程------柯里化 写多参数函数总重复传值?用柯里化3步搞定参数复用与延迟执行(一) - 掘金
二、实现call\apply\bind函数
每个从
Symbol()
返回的 symbol 值都是唯一的 Symbol - JavaScript | MDN
2.1 手搓 myCall()函数
改变this
指向的核心逻辑就是,将函数绑定到需要改变指向的对象上(thisArg),也就是第一个参数thisArg
,再执行函数,此时this
就指向的就是该对象。
这句话没看懂没关系,可以看看笔者实现的一个简约版的 myCall() 函数
js
Function.prototype.myCall = function (context, ...args) {
context.key = this;//这个 this 指向的是 function
//如果这里看不懂:可以看看读者上一篇关于 this 的文章中的 new 绑定规则
const result = context.key(...args);// context.key(...args) === function(...args)
//这里context.key(...args) 中的 this 指向的是 context 对象(注意这里的this不是上方的this)
// 如果这里看不懂:可以看看读者上一篇关于 this 的文章中的 隐式 绑定规则
return result; // 返回结果
};
如果上方的myCall
函数看懂了,恭喜读者理解了本文核心想分享的思想逻辑临时挂载函数到目标上下文
,笔者添加一些细节完全实现 myCall
函数,如下:
js
Function.prototype.myCall = function (context, ...args) {
context = context || window;//为空则默认绑定为 window(非严格 浏览器环境下)
if (typeof context !== 'object' && typeof context !== 'function') {
context = Object(context);//错误传参数或者不传入对象,则手动创建一个对象
}
const key = Symbol('myCall');//确保唯一键
context[key] = this;//将函数挂载到对象 context 中
const result = context[key](...args);// 执行函数
delete context[key];//卸载该函数 确保执行完后 不修改 context 对象
return result;
};
// 测试
function test(a, b) {
console.log(this.name, a + b);
}
const obj = { name: 'test' };
test.myCall(obj, 1, 2); // 输出 "test 3"(正确)
2.2 手搓 myApply()函数
myApply
函数和myCall
函数的实现方法大致一致,只有在参数处理上有一点区别,笔者就直接将函数给出,并附带测试用例,就不赘述 myApply
函数了。
js
Function.prototype.myApply = function (context, argsArray) {
context = context || {};//为空则默认绑定为 {}(这里用 node 环境|严格环境)
const key = Symbol("myApply");
if (typeof context !== 'object' && typeof context !== 'function') {
context = Object(context);
}
context[key] = this
const args = argsArray ? Array.from(argsArray) : []; //类数组的对象转换成数组
const result = args ? context[key](...args) : context[key]();
//是否存在参数 存在则带参调用函数,否则直接调用函数
delete context[key]
return result
};
// 测试基本功能
function test(a, b) {
console.log(this.name, a + b);
}
const obj = { name: 'test' };
test.myApply(obj, [1, 2]); // 输出 "test 3"
// 测试 context 为 null
test.myApply(null, [3, 4]); // 非严格模式下输出 "window 7"(浏览器环境)| 严格模式下输出 "undefined 7"
// 测试 argsArray 为类数组对象(需手动转为数组)
const args = { 0: 5, 1: 6, length: 2 };
test.myApply(obj, args); // 输出 "test 11"
2.3 手搓 myBind函数
myBind
函数核心逻辑和另外两个函数一致,区别于会收集旧参数 和创建一个新函数 ,并在闭包中保存旧参数和原函数 ,最后调用新函数 时调用旧函数再将旧参数和新参数给到旧函数执行。
有前面的基础,myBind
函数读者应该很容易理解了,这里笔者就直接给出该函数的注释、代码和测试代码,提供读者理解myBind
。
js
Function.prototype.mybind = function(context, ...args) {
const self = this; // 保存原函数
if (context === null || context === undefined) {
context = typeof window !== 'undefined' ? window : global;
//这里将 ( context = context || {} )和 (context = context||window)进行改进
//防止写在 myApply 和 myCall 增加代码阅读难度
}
if (typeof context !== 'object' && typeof context !== 'function') {
context = Object(context);
}
function boundFn(...newArgs) {
//闭包:...args self
const ctx = this instanceof boundFn ? this : context;
// 如果是 new 调用,this 指向实例,否则指向 context
return self.apply(ctx, [...args, ...newArgs]);
}
boundFn.prototype = Object.create(self.prototype);
//将原函数的原型挂载到新函数上 确保新函数和原函数的原型一致
boundFn.prototype.constructor = boundFn;
// 修复新函数原型上的的构造器为 新函数boundFn
return boundFn;//返回函数体
};
// 1. 基本绑定测试
function test(a, b) {
console.log(this.name, a + b);
}
const obj = { name: 'test' };
test.mybind(obj, 1, 2)(); // 输出 "test 3"(正确)
// 2. context 为 null 测试(非严格模式)
test.mybind(null, 3, 4)(); // 输出 "window 7"(浏览器环境,正确)
// 3. 类数组参数处理(需手动转为数组)
const args = { 0: 5, 1: 6, length: 2 };
test.mybind(obj, ...Array.from(args))(); // 输出 "test 11"(正确,需解构类数组)
// 4. 作为构造函数测试
function Person(name) {
this.name = name;
}
const BoundPerson = Person.mybind({}); // 绑定到空对象
const p = new BoundPerson("Bob");
console.log(p.name); // "Bob"(this 指向实例,正确)
console.log(p instanceof Person); // true(继承原型,正确)
三、总结
call
、apply
、bind
核心差异在于调用时机与参数传递:
call
和apply
立即执行,前者逐个传参、后者以数组 / 类数组传参。bind
返回新绑定函数不立即执行,可预设参数。
三者适用场景各有侧重:
call
/apply
适配立即执行场景,apply
更适合数组参数。bind
适配延迟执行、固定this
或函数柯里化。