Javascript中的this、call、apply、bind的理解与应用
基本概念
在js函数内部有一个特殊的对象就是this,行为和Java中的this大致类似。this引用的是函数据以执行的环境对象-或者也可以说是this的值(当在网页的全局作用域中调用函数时,this对象引用的就是window).1
2
3
4
5
6
7
8window.color = 'red'
var o = {color: 'blue'}
function sayColor() {
alert(this.color)
}
sayColor() // red
o.sayColor = sayColor
o.sayColor() // bule
我们可以清晰的看到sayColor()是在全局作用域中定义的,它引用this对象。由于在调用函数之前,this的值不确定,因此this可能会在代码中执行过程中引用不同的对象。
- 函数名称仅仅只是一个指针变量,即是在不同环境中调用,仍然是同一个函数。
看一个例子增加理解,this永远指向最后调用它的那对象
1 |
|
改变this的指向
根据前文我们知道this 的指向并不是在创建的时候就可以确定的,那么如何来改变或者绑定this的指向呢?
答案有四:
- 在函数内部使用 _this = this
- 使用ES6箭头函数
- new实例一个对象
- 使用apply,call,bind
来个例子:
1 |
|
这个例子这种情况是会报错的,setTimeout作为匿名函数,this是永远指向window,在 window 中并没有 func1 函数。
在函数内部使用 _this = this
1 |
|
首先设置 var _this = this,这里的 this 是调用 func2 的对象 a,为了防止在 func2 中的 setTimeout 被 window 调用而导致的在 setTimeout 中的 this 为 window。我们将 this(指向变量 a) 赋值给一个变量 _this,这样,在 func2 中我们使用 _this 就是指向对象 a 了。
使用ES6箭头函数
1 |
|
键头函数的 this 始终指向函数定义时的 this,而非执行时。,箭头函数需要记着这句话:“箭头函数中没有 this 绑定,必须通过查找作用域链来决定其值,如果箭头函数被非箭头函数包含,则 this 绑定的是最近一层非箭头函数的 this,否则,this 为 undefined”。
new一个对象
如果函数调用前使用了 new 关键字, 则是调用了构造函数。
这看起来就像创建了新的函数,但实际上 JavaScript 函数是重新创建的对象.我们换一个例子1
2
3
4
5
6
7
8
9
10
11function fruits() {}
fruits.prototype = {
color: "red",
say: function() {
console.log("My color is " + this.color);
}
}
var apple = new fruits()
apple.say() //My color is red
这里将一个对象赋值给fruits的原型,实例化的对象都能共享一些属性和方法,具体原型,原型链,和继承在另外一篇文章会详细介绍,我们看到的结果是对于apple调用函数的this同样可以正确识别到color的属性。
apply、call
在js中,call 和 apply 都是在特定的作用域中调用函数,等于设置了函数体内的this对象的值。如果我们有一个对象banana= {color : “yellow”} ,我们不想对它重新定义 say 方法,那么我们可以通过 call 或 apply 用 apple 的 say 方法:1
2
3
4
5banana = {
'color': 'yrllow'
}
apple.say.call(banana) // My color is yellow
apply.say.apply(banana) // My color is yellow
所以,可以看出 call 和 apply 是为了动态改变 this 而出现的,当一个 object 没有某个方法(本例子中banana没有say方法),但是其他的有(本例子中apple有say方法),我们可以借助call或apply用其它对象的方法来操作。
apply、call 区别
对于 apply、call 二者而言,作用完全一样,只是接受参数的方式不太一样。例如,有一个函数定义如下:1
2
3var func = function(arg1, arg2) {
}
call 需要把参数按顺序传递进去,而 apply 则是把参数放在数组里,或者是arguments对象。
就可以通过如下方式来调用:1
2
3func.call(this, arg1, arg2)
func.apply(this, [arg1, arg2])
func.apply(this, arguments)
常见apply call应用
数组之间追加
1 |
|
获取数组中的最大值和最小值
1 |
|
number 本身没有 max 方法,但是 Math 有,我们就可以借助 call 或者 apply 使用其方法。
验证是否是数组(前提是toString()方法没有被重写过)
1 |
|
类(伪)数组使用数组方法
1 |
|
Javascript中存在一种名为伪数组的对象结构。比较特别的是 arguments 对象,还有像调用 getElementsByTagName , document.childNodes 之类的,它们返回NodeList对象都属于伪数组。不能应用 Array下的 push , pop 等方法。
但是我们能通过 Array.prototype.slice.call 转换为真正的数组的带有 length 属性的对象,这样 domNodes 就可以应用 Array 下的所有方法了。
apply,call面试题练习
定义一个 log 方法,让它可以代理 console.log 方法,常见的解决方法是:1
2
3
4
5function log(msg) {
console.log(msg);
}
log(1); //1
log(1,2); //1
上面方法可以解决最基本的需求,但是当传入参数的个数是不确定的时候,上面的方法就失效了,这个时候就可以考虑使用 apply 或者 call,注意这里传入多少个参数是不确定的,所以使用apply是最好的,方法如下:1
2
3
4
5function log(){
console.log.apply(console, arguments);
};
log(1); //1
log(1,2); //1 2
接下来的要求是给每一个 log 消息添加一个”(app)”的前辍,比如:1
log("hello world"); //(app)hello world
该怎么做比较优雅呢?这个时候需要想到arguments参数是个伪数组,通过 Array.prototype.slice.call 转化为标准数组,再使用数组方法unshift,像这样:1
2
3
4
5
6function log(){
var args = Array.prototype.slice.call(arguments);
args.unshift('(app)');
console.log.apply(console, args);
};
bind
bind()最简单的用法是创建一个函数,使这个函数不论怎么调用都有同样的this值。常见的错误就像上面的例子一样,将方法从对象中拿出来,然后调用,并且希望this指向原来的对象。如果不做特殊处理,一般会丢失原来的对象。使用bind()方法能够很漂亮的解决这个问题
一个简单的例子:1
2
3
4
5
6
7
8
9var bar = function(){
console.log(this.x)
}
var foo = {
x:3
}
bar(); // undefined
var func = bar.bind(foo)
func() // 3
这里我们创建了一个新的函数 func,当使用 bind() 创建一个绑定函数之后,它被执行的时候,它的 this 会被设置成 foo , 而不是像我们调用 bar() 时的全局作用域。
上面例题中用bind可以修改未如下:
1 |
|
偏函数
bind()的另一个最简单的用法是使一个函数拥有预设的初始参数。只要将这些参数(如果有的话)作为bind()的参数写在this后面。当绑定函数被调用时,这些参数会被插入到目标函数的参数列表的开始位置,传递给绑定函数的参数会跟在它们后面。1
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
27function list() {
return Array.prototype.slice.call(arguments)
}
var list1 = list(1, 2, 3); // [1, 2, 3]
// 预定义参数37
var leadingThirtysevenList = list.bind(undefined, 37)
var list2 = leadingThirtysevenList(); // [37]
var list3 = leadingThirtysevenList(1, 2, 3); // [37, 1, 2, 3]
和setTimeout一起使用
function Bloomer() {
this.petalCount = Math.ceil(Math.random() * 12) + 1
}
// 1秒后调用declare函数
Bloomer.prototype.bloom = function() {
window.setTimeout(this.declare.bind(this), 100)
};
Bloomer.prototype.declare = function() {
console.log('我有 ' + this.petalCount + ' 朵花瓣!')
};
var bloo = new Bloomer()
bloo.bloom() //我有 5 朵花瓣!
注意:对于事件处理函数和setInterval方法也可以使用上面的方法
绑定函数作为构造函数
绑定函数也适用于使用new操作符来构造目标函数的实例。当使用绑定函数来构造实例,注意:this会被忽略,但是传入的参数仍然可用。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25function Point(x, y) {
this.x = x
this.y = y
}
Point.prototype.toString = function() {
console.log(this.x + ',' + this.y)
}
var p = new Point(1, 2)
p.toString() // '1,2'
var emptyObj = {}
var YAxisPoint = Point.bind(emptyObj, 0/*x*/)
// 实现中的例子不支持,
// 原生bind支持:
var YAxisPoint = Point.bind(null, 0/*x*/)
var axisPoint = new YAxisPoint(5)
axisPoint.toString() // '0,5'
axisPoint instanceof Point // true
axisPoint instanceof YAxisPoint // true
new Point(17, 42) instanceof YAxisPoint // true
捷径
bind()也可以为需要特定this值的函数创造捷径。
例如要将一个类数组对象转换为真正的数组,可能的例子如下:1
2
3
4
5var slice = Array.prototype.slice
// ...
slice.call(arguments)
如果使用bind()的话,情况变得更简单:1
2
3
4
5
6var unboundSlice = Array.prototype.slice;
var slice = Function.prototype.call.bind(unboundSlice)
// ...
slice(arguments)
apply、call、bind比较
那么 apply、call、bind 三者相比较,之间又有什么异同呢?何时使用 apply、call,何时使用 bind 呢。简单的一个例子:1
2
3
4
5
6
7
8
9
10
11
12
13var obj = {
x: 81
}
var foo = {
getX: function() {
return this.x
}
}
console.log(foo.getX.bind(obj)()) //81
console.log(foo.getX.call(obj)) //81
console.log(foo.getX.apply(obj)) //81
三个输出的都是81,但是注意看使用 bind() 方法的,他后面多了对括号。
也就是说,区别是,当你希望改变上下文环境之后并非立即执行,而是回调执行的时候,使用 bind() 方法。而 apply/call 则会立即执行函数。
总结
- this表示函数据以执行的环境对象,this指向的是最后调用它的对象
- apply 、 call 、bind 三者都是用来改变函数的this对象的指向的
第一个参数都是this要指向的对象; - apply 、 call 、bind 三者都可以利用后续参数传参,apply可以传类数组或者arguments,但是call,bind只能单个每个传递(用逗号隔开)
- bind 是返回对应函数,便于稍后调用;apply 、call 则是立即调用 。
本博客所有文章除特别声明外,均采用 CC BY-SA 4.0 协议 ,转载请注明出处!