Light
  • 首页
  • 关于

2015-12-01

Javascript变化多端的this

Javasctipt有着很多诡异莫测的行为,其中函数的this指针就是尝尝会困扰初学者的一个问题,因为它的变化多端,我们经常会搞不明白this指向的到底是什么。其实关于this的问题,《你不知道的Javascript》做出了相当完美的总结,为了不忘,在这里整理记录下来。

一. 函数的调用模式

关于函数,Javascript有着4种不同调用模式,而这几种不同的调用模式下,this的绑定方式也不一样,分别是默认绑定,显式绑定,隐式绑定,new绑定。

首先我们来看看默认绑定:

var a = 12;
function foo() {
    console.log(this.a); //12
}
foo();

上面这段代码在全局环境下运行,在浏览器中,this默认指向的是window,所以打印出的是12,而在NodeJS环境下,this指向的是global对象。

我们一般认为默认绑定指向的都是全局对象,值得注意的是,严格模式下却不是如此,默认绑定会失效。

‘use strict’;
function foo() {
    console.log(this); //undefined
}
foo();

严格模式下,默认绑定的this变成了undefined。

然后是隐式绑定,所谓隐式绑定,是当函数作为对象的方法被调用时,this的绑定方式:

var obj = {
    foo: function (){
        console.log(this);
    }    
}

obj.foo(); //obj

在这里,foo作为obj的方法被调用,这时候this指向的就是obj,值得注意的是,隐式绑定也存在失效的情况,我们接着上面的代码往下写:

var f = obj.foo;
f(); // window

这时候,打印出来的对象就变成了window,也就是说,隐式绑定失效,自动转成了默认绑定,发生这种情况的原因是因为this的指向关键在于函数被调用时的修饰,而不是函数本身。

以上两种是比较常用的绑定方式,接下来我们来看看显式绑定,所谓显式,就是手动地去修改this的指向,使它可以指向我们希望的地方,最常见的方法就是通过Function.prototype中的call和apply方法:

function foo() {
    console.log(this.a);
}

var obj = {
    a:2
};
foo.call(obj); //2

通过call可以修改this的指向,apply的作用也是这样,但他们的区别是call的参数是逐个传入,而apply则是将参数以数组的形式传入。

由于显式绑定是一种非常有用的绑定模式,所以ES5提供内置的方法Function.prototype.bind,call和apply是在函数运行的时候改变this,而bind则是直接返回一个新的函数,这个函数的this指向绑定的对象,用法如下:

funciont foo() {
    console.log(this.a);
}

var obj = {
    a:2
};
var bar = foo.bind(obj);
bar(); //2

Javascript的很多内置函数都提供设置上下文的选项,它的原理和bind是一样的,确保调用的时候能指向我们希望的this:

function(el) {
    console.log(el, this.id);
}

var obj = {
    id : 'someid'
}

[1,2,3].forEach(foo, obj);  // 1 someid 2 someid 3 someid

最后来看看new绑定,其实也就是我们所熟知的构造函数:

function foo(a) {
    this.a = a;
}

var bar = new foo(2);
console.log(bar.a); //2

在使用new构造对象时,其中的this默认指向我们构造出来的对象。构造函数也很匪夷所思的地方,就是它的返回值,一般情况下,如果不写return,返回的就是构造出来的对象啊,当如果有return,情况就不一样了:

function foo(a){
    this.a = a;
    return 3;
}

function goo(a){
    this.a = a;
    return {
        a : 3
    }
}

var obj1 = new foo(2);
var obj2 = new goo(2);

console.log(obj1.a); // 2
console.log(obj2.a); // 3

通过上面的代码我们可以看到,当构造函数return的是基本类型时,默认返回的是this对象,而如果return一个对象,则最后就会返回这个对象。

二. 优先级问题

既然存在不同的绑定,那彼此之间必然也会有优先级的差异,默认绑定的优先级一定是最低的,这点毋庸置疑。那么,首先,我们可以来看看显式绑定和隐式绑定的优先级:

function foo() {
    console.log(this.a);
}
var obj1 = {
    a: 2,
    foo: foo
};

var obj2 = {
    a: 3
};

obj1.foo.call(obj2); // 3

从上面的代码可以看到,显式绑定的优先级是更高的,那么,接下来,可以看看new绑定和隐式绑定的优先级对比:

function foo(a) {
    this.a = a;
}

var obj1 = {
    a: 3,
    foo: foo
};

var obj2 = new obj1.foo(2);

console.log(obj2.a); // 2
console.log(obj1.a); // 3

从上面的代码可以知道,obj1的a尚未被修改,而obj2却多了a属性为2,这说明new绑定的优先级也比隐式绑定要高。那么最后,我们只需要比较new绑定和显式绑定的优先级即可。

然而,如果同时使用new和call,是会报错的,所以,我们只能比较bind和new的优先级,代码如下:

function(a) {
    this.a = a;
}
var obj1 = {};
var bar = foo.bind(obj1);

var obj2 = new bar(2);

console.log(obj1.a); // undefined
console.log(obj2.a); // 2

从上面的代码可以看出,obj1并没有生成a属性,this指向obj2,可见new绑定的优先级比显式绑定还高。

到这里,我们就可以得出结论,优先级的顺序应该是new绑定>显式绑定>隐式绑定>默认绑定。