89%

【转载】完全理解JavaScript关键字this

译者:葵中剑

译文地址:完全理解JavaScript关键字this

英文原文地址:Fully Understanding the this Keyword

this的概念性概述

每当一个函数被创建,一个叫做this的关键字(在幕后)也随之被创建出来,它链接着包含函数进行操作的对象。换句话说,this在其所在函数的作用域里可用,是包含那个函数作为属性和方法的对象的引用。

让我们来看看这个对象:

var cody = {
    living:true,
    age:23,
    gender:'male',
    getGender:function(){return cody.gender;}
};

console.log(cody.getGender()); // logs 'male'

注意里面的getGender函数,我们通过点符号(例如cody.gender)访问cody对象自身的gender属性。这段代码可以被重写,用this来访问cody对象,因为this指向cody对象。

var cody = {
    living:true,
    age:23,
    gender:'male',
    getGender:function(){return this.gender;}
};

console.log(cody.getGender()); // logs 'male'

在this.gender里用到的this简单地指向函数正在操作的cody对象。

关于this的主题会让人感到困惑,但其实未必如此。只要记住,通常,this被用在函数里并指向包含这个函数的对象,而不是这个函数本身(例外情况包括使用new关键字或者call()和apply())。

注意事项

如何确定this的值?

传递给所有函数的this,其值基于函数被调用的运行时的上下文。请注意下这里,因为这是“那些你需要去记下来的怪异(不同寻常)”之一。

下面的代码里,myObject对象被赋予有一个sayFoo属性,其指向sayFoo函数。当sayFoo函数在全局作用域被调用时,this指向window对象。当它作为myObject对象的方法被调用时,this指向myObject。

myObject还有一个foo属性,这里被一同用到。

var foo = 'foo';
var myObject = {foo: 'I am myObject.foo'};

var sayFoo = function() {
    console.log(this['foo']);
};

// give myObject a sayFoo property and have it point to sayFoo function
myObject.sayFoo = sayFoo;
myObject.sayFoo(); // logs 'I am myObject.foo'

sayFoo(); // logs 'foo'

显然,this的值基于函数被调用的上下文。myObject.sayFoo和sayFoo都指向相同的函数。然而,根据sayFoo()从哪里(即:上下文)被调用,this的值不同。

下面是显式使用全局对象(即window)的相同的代码,或许有用吧。

(译注:原文使用 head object 一词,是原作者所著书《JavaScript Enlightenment》里的用语,并不详见于其他地方,其意与全局变量 global object 完全相同。下面的翻译将所有的 head object 译为全局对象)

window.foo = 'foo';
window.myObject = {foo: 'I am myObject.foo'};
window.sayFoo = function() { console.log(this.foo); };
window.myObject.sayFoo = window.sayFoo;
window.myObject.sayFoo(); //logs 'I am myObject.foo'
window.sayFoo(); //logs 'foo'

确保你在传递函数时,或者有多个引用指向一个函数时,你明白this的值会随着你调用函数的上下文的不同而改变。

注意事项

嵌套函数里的this指向全局对象

你也许想知道,当this用在被另一个函数包含的函数里会发生什么。糟糕的是在ECMA 3里,this迷失了方向并指向了全局对象(浏览器的window对象),而不是包含函数定义的对象。

下面的代码中,在func2和func3中的this迷失了方向,不是指向myObject,而是指向了全局对象。

var myObject = {
    func1:function() {
        console.log(this); //logs myObject
        varfunc2=function() {
            console.log(this); //logs window, and will do so from this point on
            varfunc3=function() {
                console.log(this); //logs window, as it’s the head object
            }();
        }();
    }
};

myObject.func1();

好消息是这将会在ECMAScript 5里被修正。现在,你必须意识到这个困境,特别是当你开始将函数作为值传递给其他函数时。

考虑下面的代码,当将一个匿名函数传递到foo.func1时发生了什么。当一个匿名函数在foo.func1的内部调用(函数嵌套在函数中),这个匿名函数中的this的值将会指向全局对象。

var foo = {
    func1:function(bar){
        bar(); //logs window, not foo
        console.log(this);//the this keyword here will be a reference to foo object
    }
};

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

现在你应该不会忘记了:当包含this的函数嵌套在另一个函数中,或者在另一个函数的上下文中被调用时,this的值将总是指向全局对象(再说一次,这将会在ECMAScript 5里被修正)。

解决嵌套函数的问题

为了this的值不丢失,你可以简单地在父函数里使用作用域链来保存this的引用。下面的代码演示了怎么做,用一个叫做that的变量,并利用它的作用域,我们能更好保存上下文的轨迹。

var myObject = {
    myProperty:'I can see the light',
    myMethod:function() {
        var that=this; //store a reference to this (i.e.myObject) in myMethod scope varhelperFunctionfunction(){//childfunction
        var helperFunction = function() { //childfunction
            //logs 'I can see the light' via scope chain because that=this
            console.log(that.myProperty); //logs 'I can see the light'
            console.log(this); // logs window object, if we don't use "that"
        }();
    }
}

myObject.myMethod(); // invoke myMethod

控制this的值

this的值通常取决于函数被调用处的上下文(除了使用new关键字的情况以外,这方面稍后讨论),但是你可以使用apply()和call()来定义调用函数时this指向的对象,以此覆盖/控制this的值。使用这些方法,就好像再说:“嘿,调用 X 函数,但是告诉它,用 Z 对象作为this的值”。这么做的话,JavaScript默认决定this值的方式就被覆盖了。

下面,我们创建了一个对象和一个函数。然后我们通过call()调用函数,以便让函数中this的值使用myObject作为其上下文。于是myFunction函数里的语句就会去填充myObject的属性而不是填充全局对象。我们就改变了(myFunction中的)this所指向的对象

var myObject = {};

var myFunction = function(param1, param2) {
    //setviacall()'this'points to my Object when function is invoked
    this.foo = param1;
    this.bar = param2;
    console.log(this); //logs Object{foo = 'foo', bar = 'bar'}
};

myFunction.call(myObject, 'foo', 'bar'); // invoke function, set this value to myObject

console.log(myObject) // logs Object {foo = 'foo', bar = 'bar'}

在上面的例子里,我们使用了call(),但也可以使用apply()。两者的不同之处在于如何将参数传递给函数。使用call(),参数仅使用逗号分隔。使用apply(),参数放在一个数组里传递。下面是相同的思路,但使用的是apply()。

var myObject = {};

var myFunction = function(param1, param2) {
    //set via apply(), this points to my Object when function is invoked
    this.foo=param1;
    this.bar=param2;
    console.log(this); // logs Object{foo='foo', bar='bar'}
};

myFunction.apply(myObject, ['foo', 'bar']); // invoke function, set this value
console.log(myObject); // logs Object {foo = 'foo', bar = 'bar'}

这里你需要记住的是,你可以覆盖在一个函数作用域里JavaScript默认决定this值的方式。

在自定义构造函数中使用this关键字

当一个函数联合new关键字一起被调用时,this的值——就如同其在构造函数中的表示——指向对象实例本身。换种说法:在构造函数里,我们可以在对象被实际创建之前就通过this指代(控制)对象。这种情况下,this默认值的变化,与使用call()或apply()并没有什么不同。

下面,我们设置了一个Person的构造函数,并用this来指向将要被创建的对象。当Person的实例被创建,this.name将会指向新创建的对象,并用传递给构造函数的参数(name)的值,给新对象里的name属性赋值。

var Person = function(name) {
    this.name = name || 'johndoe'; // this will refer to the instanc ecreated
}

var cody = new Person('Cody Lindley'); // create an instance, based on Person constructor

console.log(cody.name); // logs 'Cody Lindley'

当使用new关键字调用构造函数时this指向“即将被创建的对象”。如果我们没有使用new关键字,那么this的值将会是Person被调用的上下文——这里就是全局对象。让我们检验下这个脚本。

var Person = function(name) {
    this.name=name||'johndoe';
}

var cody = Person('Cody Lindley'); // notice we did not use 'new'
console.log(cody.name); // undefined, the value is actually set at window.name
console.log(window.name); // logs 'Cody Lindley'

prototype方法中的this指向一个构造实例

当函数被添加为构造函数的prototype属性时,这个函数里的this指向调用这个函数的实例。假设我们有一个自定义的Person()的构造函数,其需要人的全名(full name)作为参数。为了访问人的全名,我们将whatIsMyFullName方法添加到Person.prototype,这样,所有的的Person实例都继承了这个方法。当使用this时,这个方法就可以引用调用它的实例(以及这个实例的属性)。

这里我演示了两个Person对象的创建(cody和lisa),以及继承来的包含this关键字的whatIsMyFullName方法如何访问实例。

var Person = function(x){
    if(x){this.fullName = x};
};

Person.prototype.whatIsMyFullName = function() {
    return this.fullName; // 'this' refers to the instance created from Person()
}

var cody = new Person('cody lindley');
var lisa = new Person('lisa lindley');

// call the inherited whatIsMyFullName method, which uses this to refer to the instance
console.log(cody.whatIsMyFullName(), lisa.whatIsMyFullName()); //logs 'cody lindley lisa lindley'

/* The prototype chain is still in effect, so if the instance does not have a
fullName property, it will look for it in the prototype chain.
Below, we add a fullName property to both the Person prototype and the Object
prototype. See notes. */

Object.prototype.fullName = 'John Doe';
var john = new Person(); // no argument is passed so fullName is not added to instance
console.log(john.whatIsMyFullName()); // logs 'John Doe'

这里需要记住的是,当this关键字用在prototype对象的方法里时,其就指向实例。如果实例不包含那个属性,就开始原型查找(搜索查找原型链)。

注意事项

如果this所指向的实例或对象不包含被引用到的属性,那么这里适用任何原型查找都要使用的相同法则,沿着原型链(prototype chain)查找属性。所以在我们的例子里,如果我们的实例中不包含fullName属性,那么就会沿原型链查找fullName,先查找Person.prototype.fullName,然后是Object.prototype.fullName。


Blog

life

Project