完全理解JavaScript关键字this

这是一篇译文。当时看到原文的时候觉得内容非常易懂,是不可多得的好文。所以一开始就扔进草稿箱准备翻译,不过一晃好久过去了,最新发布的博文ID是1190,而这篇的ID是715,可见其在草稿箱里待了有多久。而这期间网络上也已经有了这篇文章的翻译稿,不过传播并不广泛,我搜索了下并没有多少索引和转发量。总体来说,已有的翻译稿总体来说也挺好,但是但从第二节开始,翻译变的有些随意,并且出现了很多偏差,有大有小,虽不影响阅读却多少妨碍理解。比如将“传递函数作为值”译成“传递函数的值”等等,这很容易引起混淆。另外还遗漏了一小部分内容的翻译,不知道是否是原作者后期更新内容所致。

个人挺喜欢这篇文章,所以网上虽已有一版,但还是希望用自己的翻译语言来描述一遍。尽管可能显得多余,但我对自己的翻译水平还是有些自信,特别是斟词酌句的时候特别爱花时间。如果要说特点就是,尽可能保持原来作者的语句不改动。不到万不得已不分句,所以语言上难免定语比较多,读起来可能有些拗口。不过好处就是很少出现意思的偏差,同时尽可能不修改也是对原作者的尊重。

原文Fully Understanding the this Keyword by Cody Lindley

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的行为看起来就像其他变量一样,除了你不能改变它。
  • 不同于arguments和任何发送给函数的参数,在调用(活动)的对象中,this是一个关键字(而不是属性)。

如何确定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' 12
 
sayFoo(); // logs 'foo'

显然,this的值基于函数被调用的上下文。myObject.sayFoosayFoo都指向相同的函数。然而,根据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();
window.sayFoo();

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

注意事项

  • 除了this以外的所有变量以及参数都遵循词法作用域(Lexical Scoping)

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

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

下面的代码中,在func2func3中的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:'Icanseethelight', 
    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对象的创建(codylisa),以及继承来的包含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());
 
/* 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

译后语

相当花时间,不过,值得~

BGM @ John Adorney - Beckoning


评论加载中...

Disqus提供评论支持,如果评论长时间未加载,请飞跃长城。