参考资料
[1] 《JavaScript高级程序设计》
PS:下面是我个人的总结,有些所以然都略过了,想理解得更透彻,建议看参考资料。
三者的用法及区别
const obj = { name: 'jack' }
function printMessage(age, sex) {
console.log(`my name is ${this.name}, my age is ${age}, my sex is ${sex}.`);
}
printMessage.apply(obj,[12,'男']) // 数组传参,直接执行
printMessage.bind(obj,12,'男')() // 直接传参,返回函数
printMessage.call(obj,12,'男') // 直接传参,直接执行
下面用通过模拟apply()、bind()、call()的实现来理解它们的原理。
apply()原理
实现apply()的关键点是如何将第二个参数(装着参数的数组)拆开来放到调用函数时的括号里。解决办法就是用eval()函数来执行函数。
模拟实现:
Function.prototype.apply2 = function(ctx, argArr) {
// 如果ctx为空,则指向window
ctx = ctx || window
// 给ctx加上一个临时变量fn,指向当前函数
ctx.fn = this
// 处理一下参数(因为函数不能传入数组作为参数列表,所以要自己拆开来)
const args2 = []
for (var i in argArr) {
args2.push('argArr['+i+']')
}
// 执行函数,获得结果
const result = eval('ctx.fn('+args2+')')
// 清除临时变量
delete ctx.fn
// 返回结果
return result;
}
测试代码:
const obj = { x: 1 }
function test(y, z) {
console.log(this.x, y, z)
}
test.apply2(obj, [2, 3])
输出:
1 2 3
call()原理
原来想按a、b、c的顺序写apply()、bind()、call(),但由于call()和apply()的原理比较类似,且bind()比较复杂,因此call()放在这里了。
模拟实现:
Function.prototype.call2 = function(ctx) {
// 如果ctx为空,则指向window
ctx = ctx || window;
// 给ctx加上一个临时变量fn,指向当前函数
ctx.fn = this;
// 处理一下参数
const args = []
const args2 = []
for(let i=1; i<arguments.length; i++) {
args.push(arguments[i])
args2.push('args['+(i-1)+']')
}
// 执行获得结果
const result = eval('ctx.fn('+args2+')')
// 删除临时变量
delete ctx.fn
// 返回结果
return result
}
测试代码:
const obj = { x: 1 }
function test(y, z) {
console.log(this.x, y, z)
}
test.call2(obj, 2, 3)
输出结果:
1 2 3
bind()原理
先给出bind()的使用方法(有三个要点,因此实现起来比较复杂):
const obj = { x: 1 }
function Test(y, z) {
console.log(this.x, y, z)
}
Test.prototype.w = 4
// 返回的是一个函数。
// 绑定时可以只传入一部分参数,在调用返回的函数时传入剩下的函数(这叫做函数柯里化)。
const Bound = Test.bind(obj, 2)
Bound(3)
// 用bound创建实例时,Test()内部的this指向的是Bound的实例,而不是之前绑定的对象obj。
const bound = new Bound(3)
// 用Bound创建的实例能够通过原型链访问Test.prototype。
console.log(bound.w)
模拟实现:
Function.prototype.bind2 = function(ctx) {
// 如果ctx为空,则指向window
ctx = ctx || window
// 处理一下参数
const args = []
for(let i=1; i<arguments.length; i++) {
args.push(arguments[i])
}
// 创建一个函数
const fn = this // 因为返回的函数中this指向的是调用者,因此这里要用一个新变量暂存当前函数
function fbound() {
// 第一个参数用来处理当返回的函数被当做构造函数的情况
// 第二个参数将在返回的参数中新传入的参数与调用bind2()时传入的参数合并
fn.apply2(
this instanceof fn ? this : ctx,
args.concat(Array.prototype.slice.call2(arguments))
)
}
// 通过fbound创建的实例,我们希望它也是当前函数fn()的实例
// 因此我们用prototype来让fbound继承自fn
// 但又不想修改fbound.prototype直接修改fn.prototype
// 因此创建一个中间对象
function FnProto () {}
FnProto.prototype = fn.prototype
fbound.prototype = new FnProto()
return fbound
}
测试代码:
const obj = { x: 1 }
function Test(y, z) {
console.log(this.x, y, z)
}
Test.prototype.w = 4
// 返回的是一个函数。
// 绑定时可以只传入一部分参数,在调用返回的函数时传入剩下的函数(这叫做函数柯里化)。
const Bound = Test.bind2(obj, 2)
Bound(3)
// 用bound创建实例时,Test()内部的this指向的是Bound的实例,而不是之前绑定的对象obj。
const bound = new Bound(3)
// 用Bound创建的实例能够通过原型链访问Test.prototype。
console.log(bound.w)
输出结果:
1 2 3 // Bound(3)输出
undefined 2 3 // new Bound(3)输出
4 // console.log(bound.w)输出