JavaScript中call、apply、bind的简单实现
先实现个call
call
:可以指定函数运行时的this
。与apply
之间的区别是传参方式不同,call
的参数是若干参数列表,apply
接受的是一个包含多个参数的数组。
首先,我们先实现第一个功能,指定函数运行时的this
:
Function.prototype.fakeCall = function(obj) {
// 在传入的 obj 上创建一个属性,将该属性指向调用的函数
obj.fn = this
// 然后执行 fn,则会将调用的函数的 this 指向 obj
obj.fn()
// 最后将创建的 fn 属性删除
delete obj.fn
}
尝试下效果:
Function.prototype.fakeCall = function(obj) {
obj.fn = this
obj.fn()
delete obj.fn
}
let foo = {
value: 1
}
function bar(name, age) {
console.log(this) // {value: 1, fn: ƒ}
console.log(this.value) // 1
}
bar.fakeCall(foo)
跟预想的一样,已将bar
的this
强行改变成了foo
。
原生的call
方法还可以接受参数,现在实现这个功能。很简单,没错,就是用es6
去实现es3
,当然用eval
也可以。
Function.prototype.fakeCall = function(obj) {
// 取出除 obj 参数外剩下的参数
let args = [].slice.call(arguments, 1)
obj.fn = this
// 传入参数
obj.fn(...args)
delete obj.fn
}
看下效果如何:
Function.prototype.fakeCall = function(obj) {
let args = [].slice.call(arguments, 1)
obj.fn = this
obj.fn(...args)
delete obj.fn
}
let foo = {
value: 1
}
function bar(name, age) {
console.log(name) // xuedinge
console.log(age) // 20
}
bar.fakeCall(foo, "xuedinge", "20")
可以看出,基本上已经实现了原生的call
了。现在考虑一些特殊情况。
1、当调用函数有返回值时,会怎么样。(会是undefined
)
2、当调用函数传入的this参数是null或者是其他基本数据类型时,会发生什么。(会报错)
根据上面个特殊情况,我们对fakeCall
稍作调整。
Function.prototype.fakeCall = function(obj) {
// 处理传入的值是基本数据类型的情况,特别是 null
obj = typeof obj !== "object" ? window : obj || window
let args = [].slice.call(arguments, 1)
obj.fn = this
// 将调用函数的返回值保存下来,然后用 return 返回
let result = obj.fn(...args)
delete obj.fn
return result
}
看下效果如何。
Function.prototype.fakeCall = function(obj) {
obj = typeof obj !== "object" ? window : obj || window
let args = [].slice.call(arguments, 1)
obj.fn = this
let result = obj.fn(...args)
delete obj.fn
return result
}
let foo = {
value: 1
}
function bar(name, age) {
console.log(name) // xuedinge
console.log(age) // 20
return {
color: "yuanliangse"
}
}
console.log(bar.fakeCall(undefined, "xuedinge", "20")) // {color: "yuanliangse"}
apply的实现
aplly
:跟call
的实现基本相同,区别是对除this
外,剩余的参数处理方式不同。直接上代码。
Function.prototype.fakeApply = function(obj) {
obj = typeof obj !== "object" ? window : obj || window
let args = [].slice.call(arguments, 1)
obj.fn = this
// args的第一个值就是传入的数组
let result = obj.fn(...args[0])
delete obj.fn
return result
}
bind的实现
bind:bind方法返回一个新的函数,在调用时,设置this为提供的值。新函数在调用时,将给定的参数列表作为原函数的参数序列的前若干项。
也就是说,bind具有以下功能:
- 返回一个新函数
- 可以为新函数指定
this
的值 bind
方法可以传参,返回的新函数也可以传参
根据这些特性,我们可以实现一个简单的bind
方法先。
Function.prototype.fakeBind = function(context) {
const self = this
// bind 方法传的参数
const bindArgs = [].slice.call(arguments, 1)
return function() {
// bind 方法返回的函数传入的参数
const newArgs = [].slice.call(arguments)
return self.apply(context, bindArgs.concat(newArgs))
}
}
看下效果如何,从下面的结果看,还是可以的。
const foo = {
value: 1
}
function bar(name, age) {
console.log(this.value)
console.log(name)
console.log(age)
}
const bindTar = bar.fakeBind(foo, "daisy")
bindTar("19") // 1、daisy、19
但是,现在有个问题,就是bind
方法返回的新函数当构造函数使用时,bind
方法提供的this
要失效的,this
要指向new
构造出来的实例。关于这一点,简单来说就是新的函数this
指向的问题,那么我们在给新的函数绑定this
时,判断下是不是当构造函数使用就可以了。
Function.prototype.fakeBind = function(context) {
const self = this
// bind 方法传的参数
const bindArgs = [].slice.call(arguments, 1)
const fBound = function() {
// bind 方法返回的函数传入的参数
const newArgs = [].slice.call(arguments)
return self.apply(
// 当作构造函数使用时,this 指向实例,this instanceof fBound 为 true
this instanceof fBound ? this : context,
bindArgs.concat(newArgs)
)
}
// 将实例的原型指向绑定函数的原型
fBound.prototype = this.prototype
return fBound
}
这里需要注意一点,我们修改fBound
的原型时,也会修改绑定函数的原型,所以,我们使用一个空函数中转一下绑定函数的原型。最终版的代码如下:
Function.prototype.fakeBind = function(context) {
const self = this
// bind 方法传的参数
const bindArgs = [].slice.call(arguments, 1)
const fn = function() {}
const fBound = function() {
// bind 方法返回的函数传入的参数
const newArgs = [].slice.call(arguments)
return self.apply(
this instanceof fn ? this : context,
bindArgs.concat(newArgs)
)
}
fn.prototype = this.prototype
fBound.prototype = new fn()
return fBound
}