call、apply、bind
# call 和 apply
# call和apply的区别
Function.prototype.call 和 Function.prototype.apply 都是非常常用的方法。它们的作用一模一样,区别仅在于传入参数形式的不同。
apply 接受两个参数,第一个参数指定了函数体内 this 对象的指向,第二个参数为一个带下标的集合,这个集合可以为数组,也可以为类数组,apply 方法把这个集合中的元素作为参数传递给被调用的函数:
var func = function( a, b, c ){
alert ( [ a, b, c ] ); // 输出 [ 1, 2, 3 ]
};
func.apply( null, [ 1, 2, 3 ] );
2
3
4
call 传入的参数数量不固定,跟 apply 相同的是,第一个参数也是代表函数体内的 this 指向,从第二个参数开始往后,每个参数被依次传入函数:
var func = function( a, b, c ){
alert ( [ a, b, c ] ); // 输出 [ 1, 2, 3 ]
};
func.call( null, 1, 2, 3 );
2
3
4
当调用一个函数时,JavaScript 的解释器并不会计较形参和实参在数量、类型以及顺序上的区别,JavaScript 的参数在内部就是用一个数组来表示的。从这个意义上说,apply 比 call 的使用率更高,我们不必关心具体有多少参数被传入函数,只要用 apply 一股脑地推过去就可以了。
call 是包装在 apply 上面的一颗语法糖,如果我们明确地知道函数接受多少个参数,而且想一目了然地表达形参和实参的对应关系,那么也可以用 call 来传送参数。
有时候我们使用 call 或者 apply 的目的不在于指定 this 指向,而是另有用途,比如借用其他对象的方法。那么我们可以传入 null 来代替某个具体的对象:
Math.max.apply( null, [ 1, 2, 5, 3, 4 ] ) // 输出:5
# 用途
# 改变this指向
var obj1 = {
name: 'sven'
};
var obj2 = {
name: 'anne'
};
window.name = 'window';
var getName = function(){
alert ( this.name );
};
getName(); // 输出: window
getName.call( obj1 ); // 输出: sven
getName.call( obj2 ); // 输出: anne
2
3
4
5
6
7
8
9
10
11
12
13
在实际开发中,经常会遇到 this 指向被不经意改变的场景,比如有一个 div 节点,div 节点的 onclick 事件中的 this 本来是指向这个 div 的:
document.getElementById( 'div1' ).onclick = function(){
alert( this.id ); // 输出:div1
};
2
3
假如该事件函数中有一个内部函数 func,在事件内部调用 func 函数时,func 函数体内的 this 就指向了 window,而不是我们预期的 div,见如下代码:
document.getElementById( 'div1' ).onclick = function(){
alert( this.id ); // 输出:div1
var func = function(){
alert ( this.id ); // 输出:undefined
}
func();
};
2
3
4
5
6
7
这时候我们用 call 来修正 func 函数内的 this,使其依然指向 div:
document.getElementById( 'div1' ).onclick = function(){
var func = function(){
alert ( this.id ); // 输出:div1
}
func.call( this );
};
2
3
4
5
6
# Function.prototype.bind
Function.prototype.bind,用来指定函数内部的 this指向
实现bind
Function.prototype.bind = function( context ){
var self = this; // 保存原函数
return function(){ // 返回一个新的函数
return self.apply( context, arguments ); // 执行新的函数的时候,会把之前传入的 context
// 当作新函数体内的 this
}
};
var obj = {
name: 'sven'
};
var func = function(){
alert ( this.name ); // 输出:sven
}.bind( obj);
func();
2
3
4
5
6
7
8
9
10
11
12
13
14
我们通过 Function.prototype.bind 来“包装”func 函数,并且传入一个对象 context 当作参数,这个 context 对象就是我们想修正的 this 对象。
在 Function.prototype.bind 的内部实现中,我们先把 func 函数的引用保存起来,然后返回一个新的函数。当我们在将来执行 func 函数时,实际上先执行的是这个刚刚返回的新函数。在新函数内部,self.apply( context, arguments )这句代码才是执行原来的 func 函数,并且指定 context 对象为 func 函数体内的 this。
这是一个简化版的 Function.prototype.bind 实现,通常我们还会把它实现得稍微复杂一点,使得可以往 func 函数中预先填入一些参数:
Function.prototype.bind = function(){
var self = this, // 保存原函数
context = [].shift.call( arguments ), // 需要绑定的 this 上下文
args = [].slice.call( arguments ); // 剩余的参数转成数组
return function(){ // 返回一个新的函数
return self.apply( context, [].concat.call( args, [].slice.call( arguments ) ) );
// 执行新的函数的时候,会把之前传入的 context 当作新函数体内的 this
// 并且组合两次分别传入的参数,作为新函数的参数
}
};
var obj = {
name: 'sven'
};
var func = function( a, b, c, d ){
alert ( this.name ); // 输出:sven
alert ( [ a, b, c, d ] ) // 输出:[ 1, 2, 3, 4 ]
}.bind( obj, 1, 2 );
func( 3, 4 );
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
# 借用其他对象的方法
场景一
借用构造函数实现继承
var A = function (name) {
this.name = name
}
var B = function () {
A.apply(this, arguments)
}
B.prototype.getName = function () {
return this.name
}
var b = new B('even')
b.getName() // even
2
3
4
5
6
7
8
9
10
11
场景二
参数arguments借用Array的能力
(function(){
Array.prototype.push.call( arguments, 3 );
console.log ( arguments ); // 输出[1,2,3]
})( 1, 2 );
2
3
4
# 实现call
# 实现apply
# bind
先看使用方法
const obj1 = {
name: 'lili'
}
const obj2 = {
name: 'xiaoming'
}
window.name = 'window'
function who (text) {
console.log(`我是${this.name}, ${text}`)
}
who('我是工具') // 我是window,我是工具
const fn1 = who.bind(obj1)
fn1('我是女孩') // 我是lili,我是女孩
const fn2 = who.bind(obj2, '我是男孩')
fn2() // 我是xiaoming,我是男孩
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
由上看出几点:
- bind()函数执行,不会执行原函数,而是返回一个新函数,新函数执行时才会执行原函数
- bind()可以改变this的指向,第一次执行时,who为普通函数,this指向window,后两次输出分别指向obj1和obj2
- bind()接受多个参数,第一个参数为原函数执行过程中this的指向,从第二个参数开始,参数是可选的,可以在bind时传入,也可以在返回的新函数执行时传入
原函数参数及顺序,是bind时第二个参数开始的参数+新函数执行时传入的参数顺序,多传入的参数不生效。利用这个特性,可以使一个函数拥有预设的初始参数,看代码:
function list () {
return Array.prototype.slice.call(arguments)
}
const list1 = list(1, 2, 3) // [1, 2, 3]
const paramToList = list.bind(null, 5)
const lsit2 = paramToList(1, 2) // [5, 1, 2]
const list3 = paramToList() // [5]
2
3
4
5
6
7
8
9
10
这样,在bind时传入的参数5,就会作为paramToList函数的预设参数,在调用时总是出现在第一个参数位置。
# 实现bind
既然已经知道了 bind() 函数的作用,我们就可以通过它的作用,慢慢还原它的实现经过
首先,biind() 调用是 fun.bind(obj),直接由一个函数发起调用,且函数上没有这个方法,证明 bind() 存在于函数的原型链上,如下
Function.prototype.bind = function (obj) {
//
}
2
3
其次,bind() 函数执行不会执行原函数,会返回一个新函数,新函数执行时才会执行原函数,即,bind中返回的函数执行时,才会打印“执行”,如下
Function.prototype.bind = function (obj) {
// 保存原函数
// bind作为对象的方法调用,this指向调用方法的对象,即执行 func.bind(obj) 时,this 指向 func
const self = this
// 返回新函数
return function () {
// 需要在这里执行原函数
self()
}
}
2
3
4
5
6
7
8
9
10
实际使用一下看看,如下
Function.prototype.bind = function (obj) {
// 保存原函数
// bind作为对象的方法调用,this指向调用方法的对象,即执行 func.bind(obj) 时,this 指向 func
const self = this
// 返回新函数
return function () {
// 需要在这里执行原函数
self()
}
}
const person = {
legs: 2
}
function showLegs () {
console.log('执行')
console.log('legs', this.legs)
}
const func = showLegs.bind(person)
func() // 执行 // legs undefined
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
然后,bind() 返回了一个函数,函数再执行,执行的是原函数,但是原函数中的this并没有被绑定到person,要想将 this 绑定到 person,要在新函数,即 func 执行时做改变 this 指向的操作,this 由指向 func 改为指向 person,如下
Function.prototype.bind = function (obj) {
// 保存原函数
// bind作为对象的方法调用,this指向调用方法的对象,即执行 func.bind(obj) 时,this 指向 func
const self = this
// 返回新函数
return function () {
// 需要在这里执行原函数,并且改变this指向
self.apply(obj)
}
}
const person = {
legs: 2
}
function showLegs () {
console.log('legs', this.legs)
}
const func = showLegs.bind(person)
func()
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
到这里,bind() 就成功实现了,但是我们还 bind() 可以接受多个参数,从第二个开始作为参数传入,而且返回的新函数中也可以传入参数,所以我们还需要将实现优化,支持传入参数,如下
// 最终版
Function.prototype.bind = function () {
// 保存原函数
// bind作为对象的方法调用,this指向调用方法的对象,即执行 func.bind(obj) 时,this 指向 func
const self = this
// 拆解参数
const obj = [].shift.call(arguments)
const args = [].slice.call(arguments)
// 返回新函数
return function () {
// 拆解参数
const allArgs = [].concat.call(args, [].slice.call(arguments))
// 需要在这里执行原函数,并且改变this指向
self.apply(obj, allArgs)
}
}
const person = {
legs: 2
}
function showLegs (name, endText) {
console.log('legs', this.legs)
console.log(`${name} has ${this.legs} legs, ${endText}`)
}
const func = showLegs.bind(person, 'lili')
func(' right') // lili has 2 legs, right
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
# 实现bind总结
上述过程中,我们由bind的作用推导出bind的实现,根据以上代码查看下面的一些要点,帮助我们更快的实现bind
- bind作用:返回一个新函数func,新函数func执行时执行被改变了this指向的原函数showLegs
- bind实现:返回新函数,在新函数中改变原函数指向