JavaScript中call、apply、bind的区别及手动实现
1. 三种方法的共同点
三者之间的共同点是它们都可以用来改变函数或者方法执行时this的指向。
2. 三种方法的区别
下面逐一讲解这三种方法及其手动实现,以帮助理解这三者的区别。
2. 1 call方法
call方法挂载于Function这一构造函数的原型对象上。该方法接受两个参数,一个是想要改变的this指向,另一个是被改变this指向的原函数的参数列表。
call方法具有以下特征
1. 挂载于Function的原型对象上
2. 以参数列表的形式传入原函数的参数
3. 临时改变this指向
4. 调用一次原函数
5. 如果新指向为null或者undefined,则指向全局对象
示例
// 挂载于全局对象上
var a = 1,
b = 2
let obj = {
a: 10,
b: 20
}
function f(c, d) {
console.log(this)
return this.a + this.b + c + d
}
// 直接调用该函数,执行者为全局对象
console.log(f(1, 2)) // 打印出 全局对象 以及 6
// 使用call改变this指向
console.log(f.call(obj, 1, 2)) // 打印obj 以及33
// call只会临时改变this指向,而非永久改变
console.log(f(1, 2)) // 打印出 全局对象 以及 6
// 新指向为null
console.log(f.call(null, 1, 2)) // globalThis 6
// 新指向为undefined
console.log(f.call(undefined, 1, 2)) // globalThis 6
通过这个示例,我们看到
call方法改变了f函数执行时的this指向。- 在改变指向后,
call方法直接调用了原函数。 call方法只会临时改变this的指向,而非永久改变。
下面让我们来手动实现call方法
// call方法具有以下特征
// 1. 挂载于Function的原型对象上
// 2. 以参数列表的形式传入原函数的参数
// 3. 临时改变this指向
// 4. 调用一次原函数
// 5. 如果新指向为null或者undefined,则指向全局对象
Function.prototype.myCall = function (thisArg, ...args) {
// 1. 判断是否指向null或者undefined
thisArg = thisArg || globalThis
// 2. 这里的this就是原函数,因为调用时是这样的 f.myCall()
// 普通函数中的this是谁调用就指向谁,所以这里的this就是原函数
const fn = this
// 3. 我们将原函数挂载到this的新指向中
// 这里为了防止和新指向重名,所以使用Symbol
const key = Symbol('fn')
thisArg[key] = fn
// 4. 调用一次原函数,只不过是让新指向来调用
const res = thisArg[key](...args)
// 5. 复原thisArg,因为call不会永久改变this指向
delete thisArg[key]
return res
}
// 挂载于全局对象上
var a = 1,
b = 2
let obj = {
a: 10,
b: 20
}
function f(c, d) {
console.log(this)
return this.a + this.b + c + d
}
// 直接调用该函数,执行者为全局对象
console.log(f(1, 2)) // 打印出 window 以及 6
// 使用myCall改变this指向
console.log(f.myCall(obj, 1, 2)) // 打印obj 以及33
// myCall只会临时改变this指向,而非永久改变
console.log(f(1, 2)) // 打印出 全局对象 以及 6
// 新指向为null
console.log(f.myCall(null, 1, 2)) // globalThis 6
// 新指向为undefined
console.log(f.myCall(undefined, 1, 2)) // globalThis 6
2.2 apply方法
apply方法也能够改变this的指向,它接受两个参数,一个是新指向,第二个是包含原函数参数的数组。该方法和call方法的使用很类似,它们具备很多相同的特征。
apply方法具有以下特征
1. 挂载于Function的原型对象上
2. 以数组的形式传入原函数的参数
3. 临时改变this指向
4. 调用一次原函数
5. 如果新指向为null或者undefined,则指向全局对象
可以看到和call的唯一不同就是传入参数的形式不同,apply要求将原函数的参数包含在一个数组中进行传递,而call要求以参数列表的形式来传递。
示例
// 挂载于全局对象上
var a = 1,
b = 2
let obj = {
a: 10,
b: 20
}
function f(c, d) {
console.log(this)
return this.a + this.b + c + d
}
// 直接调用该函数,执行者为全局对象
console.log(f(1, 2)) // 打印出 window 以及 6
// 使用apply改变this指向
// 注意传递参数时使用数组
console.log(f.apply(obj, [1, 2])) // 打印obj 以及33
// apply只会临时改变this指向,而非永久改变
console.log(f(1, 2)) // 打印出 全局对象 以及 6
// 新指向为null
console.log(f.apply(null, [1, 2])) // globalThis 6
// 新指向为undefined
console.log(f.apply(undefined, [1, 2])) // globalThis 6
通过这个示例,我们看到apply和call的区别只在于传递参数的形式上。
现在让我们手动实现apply
// apply方法具有以下特征
// 1. 挂载于Function的原型对象上
// 2. 以数组的形式传入原函数的参数
// 3. 临时改变this指向
// 4. 调用一次原函数
// 5. 如果新指向为null或者undefined,则指向全局对象
Function.prototype.myApply = function (thisArg, arr) {
// 1. 判断是否指向null或者undefined
thisArg = thisArg || globalThis
// 2. 这里的this就是原函数,因为调用时是这样的 f.myApply()
// 普通函数中的this是谁调用就指向谁,所以这里的this就是原函数
const fn = this
// 2. 我们将原函数挂载到this的新指向中
// 这里为了防止和新指向重名,所以使用Symbol
const key = Symbol('fn')
thisArg[key] = fn
// 4. 调用一次原函数,只不过是让新指向来调用
const res = thisArg[key](...arr)
// 5. 复原thisArg,因为call不会永久改变this指向
delete thisArg[key]
return res
}
// 挂载于全局对象上
var a = 1,
b = 2
let obj = {
a: 10,
b: 20
}
function f(c, d) {
console.log(this)
return this.a + this.b + c + d
}
// 直接调用该函数,执行者为全局对象
console.log(f(1, 2)) // 打印出 window 以及 6
// 使用apply改变this指向
// 注意传递参数时使用数组
console.log(f.myApply(obj, [1, 2])) // 打印obj 以及33
// apply只会临时改变this指向,而非永久改变
console.log(f(1, 2)) // 打印出 全局对象 以及 6
// 新指向为null
console.log(f.myApply(null, [1, 2])) // globalThis 6
// 新指向为undefined
console.log(f.myApply(undefined, [1, 2])) // globalThis 6
2.3 bind方法
bind方法也可以改变this的指向。但是它和apply以及call具有很大的区别。
bind具有以下特征。
- 不会执行需要被改变
this指向的方法 - 返回一个绑定了指定
this的新方法,注意是永久绑定。 - 将调用时传递的参数插入到新方法的参数列表中。
示例
// 挂载于全局对象上
var a = 1,
b = 2
let obj = {
a: 10,
b: 20
}
function f(c, d) {
console.log(this)
return this.a + this.b + c + d
}
console.log(f.bind(obj, 1, 2)) // 不会输出this和加值,而是会输出f函数
let f1 = f.bind(obj, 1, 2) // bind返回一个新的函数
console.log(f1()) // obj 33,可以看到绑定时传递的1和2被添加到新函数的参数列表前
console.log(f1(5, 6)) // obj 33 函数的参数列表被绑定时传递的参数占满了,因此这里读不到新传的5和6
console.log(f1.call(null)) // 尝试使用call方法改变新方法的this指向
// 结果输出 obj 和 33 这说明call方法改变新方法的this指向失败
console.log(f1.apply(null)) // 尝试使用apply方法改变新方法的this指向
// 结果输出 obj 和 33 这说明apply方法改变新方法的this指向失败
// 以上两个例子说明了bind绑定后的新方法的this指向被永久改变了
// 不能通过call和apply进行改变
// 即使我们通过重新绑定的方式也不能改变f1中this的指向
let obj1 = {
a: 10,
b: 10
}
f1 = f1.bind(obj1, 1, 2)
console.log(f1()) // obj 33
手动实现bind方法
需要利用闭包这一特性才能实现。
Function.prototype.myBind = function (thisArg) {
// 保存thisArg
const self = thisArg
// 保存原函数
const fn = this
// 接受传递来的参数
// 忽略第一个参数
const contextArgs = Array.prototype.slice.call(arguments, 1)
// 准备新函数以返回
const bound = function () {
const innerArgs = Array.prototype.slice.call(arguments)
const finalArgs = contextArgs.concat(innerArgs)
return fn.apply(self, [...finalArgs])
}
return bound
}
function f(c, d) {
console.log(this)
return this.a + this.b + c + d
}
f.myBind(1, 2, 3)
// 挂载于全局对象上
var a = 1,
b = 2
let obj = {
a: 10,
b: 20
}
console.log(f.myBind(obj, 1, 2)) // 不会输出this和加值,而是会输出f函数
let f1 = f.myBind(obj, 1, 2) // bind返回一个新的函数
console.log(f1()) // obj 33,可以看到绑定时传递的1和2被添加到新函数的参数列表前
console.log(f1(5, 6)) // obj 33 函数的参数列表被绑定时传递的参数占满了,因此这里读不到新传的5和6
console.log(f1.call(null)) // 尝试使用call方法改变新方法的this指向
// // 结果输出 obj 和 33 这说明call方法改变新方法的this指向失败
console.log(f1.apply(null)) // 尝试使用apply方法改变新方法的this指向
// // 结果输出 obj 和 33 这说明apply方法改变新方法的this指向失败
// // 以上两个例子说明了bind绑定后的新方法的this指向被永久改变了
// // 不能通过call和apply进行改变
// // 即使我们通过重新绑定的方式也不能改变f1中this的指向
let obj1 = {
a: 10,
b: 10
}
f1 = f1.myBind(obj1, 1, 2)
console.log(f1()) // obj 33
修复小BUG
这里的myBind方法似乎是正确的,然而如果返回的新函数被当作构造函数来使用,即使用new关键字来构造新实例时就会有问题。因为新函数的this一直指向我们绑定的指向,而作为构造函数时,this应当指向当前正在生成的实例。我们对myBind进行修改。
Function.prototype.myBind = function (thisArg) {
// 保存原函数
const fn = this
// 接受传递来的参数
// 忽略第一个参数
// === 构造原型链
const F = function () {}
F.prototype = this.prototype
const contextArgs = Array.prototype.slice.call(arguments, 1)
// 准备新函数以返回
const bound = function () {
const innerArgs = Array.prototype.slice.call(arguments)
const finalArgs = contextArgs.concat(innerArgs)
// 这里的this是新函数执行时的执行者,我们判断是不是作为构造函数来使用
// 如果是就将函数的调用者交给this,否则就交给self
return fn.apply(this instanceof F ? this : thisArg, finalArgs)
}
// 链接原型链
bound.prototype = new F()
return bound
}