在之前的对象原型的文章中,我们讲到了在函数前面加new然后进行调用之后发生的4件事情,当时只把跟原型有关的东西介绍了一下。现在我们来学习一下其他的内容。
首先先来回顾一下会有哪四件事情发生吧:
- 一个全新的对象被创建
- 这个新的对象会被接入到原型链
- 这个新的对象被设置为函数调用的this绑定
- 除非函数有返回对象,否则这个被new调用的函数将自动返回这个新的对象
这里有个新的东西this绑定
,也就是接下来我们要介绍的东西啦。
第一个问题就是this是什么?(以下回答摘自You-Dont-Know-JS)
this是在函数被调用的时候建立的一个绑定,指向的内容完全由函数被调用的调用点来决定的。
简单点说,this就是一个绑定,指向一个内容。
那么this指向的内容又是什么呢?前面说到这个内容由函数被调用的调用点来决定。所谓的调用点,就是函数在代码中被调用的地方。也就是说,我们需要找到函数在哪里被调用,从而确定this指向的内容。考虑这个问题还需要了解一个概念:调用栈(到达当前执行位置而被调用的所有方法的堆栈)。
看段代码来深入理解一下调用栈和调用点这两个概念:
function foo() {
// 调用栈是: `foo`
// 调用点是global scope(全局作用域)
console.log( "foo" );
bar(); // <-- `bar`的调用点
}
function bar() {
// 调用栈是: `foo` -> `bar`
// 调用点位于`foo`
console.log( "bar" );
baz(); // <-- `baz`的调用点
}
function baz() {
// 调用栈是: `foo` -> `bar` -> `baz`
// 调用点位于`bar`
console.log( "baz" );
}
foo(); // <-- `foo`的调用点
上面这个代码跟注释应该已经很清楚了解释了调用栈和调用点这两个概念了。
搞清楚这些概念之后,我们还是不知道this会指向什么。既然说this指向的内容完全由调用点决定,那么调用点又是怎么决定的呢?
还记得文章最开始提到的东西么,关于new的4件事情,第三点讲的是新对象被设置为函数调用的this绑定。
看下代码:
function foo(){
this.a = a;
}
var bar = new foo(2); //调用foo函数来创建一个新对象bar
console.log(bar.a);
使用new来调用函数foo的时候,我们创建了一个新对象bar并且把bar绑定到了foo()里面的this.这就是所谓的new绑定。
那么在JavaScript中,关于this绑定,除了new绑定,还有3种其它的规则:
- 默认绑定
- 隐式绑定
- 显示绑定
下面我们依次来一一介绍。
-
默认绑定
看名字我们就能看出来,这是最普通最基础的绑定。一般来说,独立函数调用的时候this就是默认绑定。
来看个例子:function foo(){ console.log(this.a); } var a = 2; foo(); //2
代码很简单,我们主要关心的是
this
。我们先看结果:this
绑定到了全局变量。
具体分析一下也很简单,这里的函数调用就是我们平常在使用的最简单的独立函数的调用,跟前面介绍的规则也很符合。
这里有一个要注意的小细节就是如果是在严格模式下,默认绑定的值会变成undefined。如果是非严格模式的话,就是绑定到全局变量了。 -
隐式绑定
这个规则一般是看函数调用的位置是否有上下文对象,或者说是否被某个对象拥有或者包含。
通过代码来深入理解一下:function foo(){ console.log(this.a); } var obj = { a:2, foo:foo }; obj.foo(); //2
代码同样很好理解,函数foo()作为引用属性添加在对象obj里面,但这并不能说明函数foo()属于obj对象。但是从调用的位置上看,会使用obj这个对象来引用函数foo,那么函数在被调用的时候,是被obj这个对象拥有或者包含的。
简单点说,函数在被调用的时候,是通过对象来引用的,那么函数里的this
就会绑定到这个对象上面。
再来看一个稍微复杂一点的例子:function foo(){ console.log(this.a); } var obj = { a:1, foo:foo }; var obj1 = { a:2, obj:obj } obj1.obj.foo(); //1
这里的话,我们会发现多了一个obj1这个对象,而且这个对象里有属性a和对象obj。然后我们调用的时候会发现结果输出的是obj里面的属性a的值。
简单的结论就是,在多层的对象引用属性中,只有最顶层或者说最后一层才会影响调用位置。 -
显式绑定
通过上面隐式绑定的规则介绍可以知道,它是通过对象间接绑定this的,那么很明显显式绑定就是直接的,或者说就是强行指定我们想要让this绑定的对象。那么怎么来进行强行绑定呢?
一般来说,是使用函数的call()和apply()方法(绝大部分函数都会有这两个方法)。
这两个方法的作用都是一样的,就是替换this指向。唯一不同的就是接收参数的方法不一样。apply()方法接收两个参数,第一个参数是一个对象(也就是我们想要让this指向的新的对象,不填的话就是全局对象),第二个参数一个参数数组。call()方法的话第一个参数跟apply是一样的,但是后面要把传递的参数全部都列举出来。
简单来看个例子:function foo(){ console.log(this.a); } var obj = { a:2 }; foo.call(obj); //2
最后一行代码,函数foo调用了call方法,强行把this绑定到了obj对象上。
至此,关于this绑定的基础的4种规则就介绍得差不多了,实际上有些规则在应用的时候可能不那么尽如人意,我们依旧从代码入手:
function foo(){
console.log(this.a);
}
var obj = {
a:2,
foo:foo
};
var bar = obj.foo;
var a = 1;
bar(); //1
一开始我们可能都会觉得输出的结果应该是2。因为bar这个对象在创建的时候调用了obj里面的foo函数。但实际上只是另一个foo自己的引用。而且bar函数在调用的时候是作为独立函数调用的,跟我们前面讲的默认绑定的规则很符合,所以这里的this就绑定到了全局对象。
这种情况在回调函数里更容易发生,比如下面的代码:
function foo(){
console.log(this.a);
}
function doFoo(f){
f();
}
var obj = {
a:2,
foo:foo
};
var a = 1;
doFoo(obj.foo); //1
最后一行代码实际上就是f = obj.foo
,自然结果就跟上面是一样的。
那么有什么方法可以解决这个问题呢?
在显示绑定中,有一个它的变种,我们称之为硬绑定,可以解决上面的问题。
继续看代码:
function foo(){
console.log(this.a);
}
var obj = {
a:2
};
var bar = function(){
foo.call(obj);
}
bar(); //2
setTimeout(bar,1000);
bar.call(window); //2
这段代码解释了硬绑定的工作原理:它创建了一个函数bar,然后在函数里面通过foo.call(..)强行把this绑定到了obj对象上面。之后只要调用函数bar,就会调用函数foo,绑定的值始终不变。
然后我们稍微改变一下,让它变成一个可复用的帮助函数:
function foo(){
console.log(this.a);
}
function bind(f,obj){
return function(){
return f.apply(obj,arguments);
};
}
var obj = {
a:2
};
var bar = bind(foo,obj);
var b = bar(3);
console.log(b); //2
由于硬绑定经常被使用,所以它在ES5的时候就作为内建工具了:Function.prototype.bind。上面的代码就是bind方法的原理。
bind方法的作用和call和apply一样,都是替换this指向,它的参数也和call一样。不一样的就是bind方法返回的是一个函数。
然后我们要介绍一个比较特殊的函数,因为它不能根据前面介绍的4条规则来判断this的指向。就是ES6中新增的函数:箭头函数(=>)。它是根据外层作用域或者全局作用域来决定this指向的。
看段代码:
function foo(){
return (a) => {
console.log(this.a);
};
}
var obj1 = {
a:1
};
var obj2 = {
a:2
};
var bar = foo.call(obj1);
bar.call(obj2);//1
foo()内部创建的箭头函数会捕获调用时foo()的this。因为foo使用了call方法,所以foo()的this绑定到了obj1。然后bar对象被创建的时候引用了箭头函数,所以bar的this也被绑定到了obj1上面。而且箭头函数的绑定是无法被修改的。所以最后输出的结果是1而不是2。
最后,虽然我们已经了解了this绑定的基本规则,但是如果说我们找到了函数在哪里调用,然后又发现4种规则里有多种规则可以适用,那我们应该选择哪一种呢?
这就涉及到了这些规则的优先级:
- 首先看是不是有new调用,如果是的话就绑定到新创建的对象;
- 然后看是不是有call或者apply或者bind调用,如果是那就绑定到指定对象;
- 再之后看是不是由上下文调用,如果是就绑定到那个上下文对象;
- 最后的话就只剩下默认绑定了(注意严格模式下是undefined,非严格模式下绑定到全局对象)。
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。