前言
在平时面试中,call、apply、bind这三个得用法还是会经常问的,比如三者得区别,作用,或者手写call、apply、bind,因此整理了一下
call、apply、bind作用
call() 方法使用一个指定的 this
值和单独给出的一个或多个参数来调用一个函数。
apply()
方法调用一个具有给定this
值的函数,以及作为一个数组(或类似数组对象)提供的参数
bind()
方法创建一个新的函数,在 bind()
被调用时,这个新函数的 this
被指定为 bind()
的第一个参数,而其余参数将作为新函数的参数,供调用时使用。
这三个解释来自MDN
,我们可以发现这三个函数都和this有关系,也就是改变函数运行时this得指向。
举个栗子
我们先来看一下this指向的代码
function Person() {}
Person.prototype = {
name: 'lee',
showName: function () {
console.log(this.name)
},
}
Person.prototype.showName()
上面得代码中调用showName()
方法会打印出 lee
,因为this得指向在这里指向得是 Person这个对象,这个例子很简单吧,再往下看
function Person() {}
Person.prototype = {
name: 'lee',
showName: function () {
console.log(this.name)
},
}
Person.prototype.showName()
let obj = {
name: 'herry',
}
我们新增了一个对象字面量,也想打印出name这个变量,怎么办呢?难道我们在直接写一个consloe.log()
吗?这多麻烦,那我们可不可以通过Person的showName
方法来调用呢?直接复用showName()
方法不就好了吗?答案是当然可以。
function Person() {}
Person.prototype = {
name: 'lee',
showName: function () {
console.log(this.name)
},
}
Person.prototype.showName() //lee
let obj = {
name: 'herry',
}
Person.prototype.showName.call(obj) //herry
Person.prototype.showName.apply(obj) //herry
Person.prototype.showName.bind(obj)() //herry
上述例子我们就已经看出来call、apply、bind的用法了,他们的作用就是动态改变了上下文,也就是改变了this的指向。
call、apply、bind区别
上面的例子中我们分别通过call、apply、bind来实现想要的效果,结果也都是一模一样的,难道就没有一点区别吗?肯定有区别的,要不然设计ES方案的也都是吃饱了没事干的主,那有什么区别呢?从MDN给出的解释中我们也可以找到去别的。
三者区别
-
call和apply 改变了函数的this上下文之后便立即执行函数,bind则是返回改变了上下文后的一个函数。
也就是call 和apply 立即执行,bind不立即执行
-
call和apply基本类似,但是他们立即传入的参数不一样,call方法接收的时若干个参数列表,apply接收的时一个包含多个的参数的数组
举个栗子
求数组中最大值和最小值
var arr = [34, 5, 3, 6, 54, 6, -67, 5, 7, 6, -8, 687]
//apply接收数组
Math.max.apply(Math, arr)
//call接收若干个参数
Math.max.call(Math, 34, 5, 3, 6, 54, 6, -67, 5, 7, 6, -8, 687)
//bind不会立即执行 要加上()去执行函数
Math.max.bind(Math, 34, 5, 3, 6, 54, 6, -67, 5, 7, 6, -8, 687)()
这个栗子很好的看出来了三者的区别。
手写call、apply、bind
我们已经了解了call、apply、bind的作用,也知道了三者的区别,那我们再进一步去手写三者函数,话不多说,搞起来。
手写call
我们在来看一下call的定义
call() 方法使用一个指定的 this
值和单独给出的一个或多个参数来调用一个函数
也就是说call接收多个参数,最重要的是第一个参数是this,其他的都是函数的参数了。
我们根据这个理解和上面的例子先来写一下
Function.prototype.myCall = function (context) {
//这里得this指向得showName函数
console.log(this)
//context指得就是传进来得obj对象,也就是我们指定得this值
console.log(context)
}
Person.prototype.showName.myCall(obj)
这里我们先想一下,为什么this的指向指得是showName
函数对象呢?
首先我们要明白this的指向,this永远指向最后调用它的那个对象,Person.prototype.showName.myCall(obj)
myCall
最后就是被showName
方法来调用的,明白了吗?下面我们继续
Function.prototype.myCall = function (context) {
//这里得this指向得showName函数
console.log(this)
//context指得就是传进来得obj对象,也就是我们指定得this值
console.log(context)
//传输obj的对象上添加调用的方法
context.fn = this
//执行fn
context.fn()
}
Person.prototype.showName.myCall(obj) //herry
awesome!这么简单的吗? 其实并不是,主要是我们上文这个例子太理想化了,考虑得并不全面,就拿我们上述求数组得最大值和最小值用这个方法都通不过。
- 要考虑call传递参数个数得问题,可能是多个,也可能不传递参数
- 如果是多参数要把参数传给扩展方法
Function.prototype.myCall = function (context) {
//如果没有参数context指向得是window
context = context || window
//传输obj的对象上添加调用的方法,这里this得指向是max
context.fn = this
//处理参数 去除第一个参数this 其它传入fn函数
let arg = [...arguments].slice(1)
//执行fn
let result = context.fn(...arg)
//删除fn
delete context.fn
//返回执行结果
return result
}
console.log(Math.max.myCall(Math, 34, 5, 3, 6, 54, 6, -67, 5, 7, 6, -8, 687)) //687
手写apply
上文中我们已经说了 apply和call作用其实是一样的,就是传递参数不一样,apply传递得是参数,所以稍微修改一下即可
Function.prototype.myApply = function (context) {
//如果没有参数context指向得是window
context = context || window
//传输obj的对象上添加调用的方法,这里this得指向是max
context.fn = this
let result
if (arguments[1]) {
result = context.fn(...arguments[1])
} else {
result = context.fn()
}
delete context.fn
return result
}
console.log(Math.max.myApply(Math, [34, 5, 3, 6, 54, 6, -67, 5, 7, 6, -8, 687])) //687
手写bind
bind返回得是函数,不立即执行
Function.prototype.myBind = function (context) {
//返回一个绑定得this,保存this
let _this = this
let arg = [...arguments].slice(1)
//返回一个函数
return function F() {
// 处理函数使用new的情况
if (this instanceof F) {
return new _this(...arg, ...arguments)
} else {
// 返回函数绑定this,传入两次保存的参数
//考虑返回函数有返回值做了return
return _this.apply(context, arg.concat(...arguments))
}
}
}
console.log(Math.max.myBind(Math, 34, 5, 3, 6, 54, 6, -67, 5, 7, 6, -8, 687)())