前言
本文翻译自this
概述
很多程序员习惯的将this和面向对象紧密紧密连续在一起,this指向了构造函数中新建的对象。虽然在这个说法在ECMAScript也是成立的,但是this不仅仅是指向构造函数的实例
定义
this
是执行上下文的一个属性
activeExecutionContext = {
VO: {...},
this: thisValue
};
this 与上下文的可执行代码类型紧密相关,其值在进入上下文阶段就确定了,并且在执行代码阶段不能被改变
全局代码中的this
局代码中的 this 非常简单,this 始终是全局对象自身,因此,可以间接获取引用
// 显式定义全局对象的属性
this.a = 10; // global.a = 10
alert(a); // 10
// 通过赋值给不受限的标识符来进行隐式定义
b = 20;
alert(this.b); // 20
// 通过变量声明来进行隐式定义
// 因为全局上下文中的变量对象就是全局对象本身
var c = 30;
alert(this.c); // 30
函数代码中的 this
this 在函数代码中的时候,事情就变得有趣多了。这种情况下是最复杂的,并且会引发很多的问题。
函数代码中的 this的第一个(同时也是最主要)的特性就是:它并非静态绑定在函数上。
如上所述,this 的值是进入执行上下文阶段确定的,函数代码中的 this 的值可能每次都不一样。
而且,一旦进入代码执行阶段,其值就维持不变了。也就是说,要给 this 赋一个新值是不可能的,因为 this 根本就不是一个变量:
var foo = {x: 10};
var bar = {
x: 20,
test: function () {
alert(this === bar); // true
alert(this.x); // 20
this = foo; // error, 不能更改this的值
alert(this.x); // 如果没有错误,则其值为10而不是20
}
};
// 在进入上下文的时候,this 的值就确定了是“bar”对象
bar.test(); // true, 20
foo.test = bar.test;
// 但是,这个时候,this的值又会变成“foo”,虽然我们调用的是同一个函数
foo.test(); // false, 10
那么,在函数代码中有哪些因数会影响this值的变化?有如下因数:
首先,在通常的函数调用时,this是由激活上下文代码的调用者(caller)决定的,即调用函数的父级上下文。并且this的值是由调用表达式的形式决定的(换句话说就是,由调用函数的语法决定)。
了解并记住这点非常重要,这样才能在任何上下文中都能准确判断this的值。更确切地讲,调用表达式的形式(或者说,调用函数的方式)影响了 this 的值,而不是其他因素
(一些关于 JavaScript的文章和书籍中指出:“this的值取决于函数定义的方式,如果是全局函数,那么this的值就是全局对象,如果函数是某个对象的方法,那么this的值就是该对象” -- 这绝对不正确)。下面我们将看到,即便是全局函数,this的值也会因为调用函数的方式不同而不同
function foo() {
alert(this);
}
foo(); // global
alert(foo === foo.prototype.constructor); // true
// 然而,同样的函数,以另外一种调用方式的话,this的值就不同了
foo.prototype.constructor(); // foo.prototype
同样,调用对象中的方法,this
值也可能不是该对象
var foo = {
bar: function () {
alert(this);
alert(this === foo);
}
};
foo.bar(); // foo, true
var exampleFunc = foo.bar;
alert(exampleFunc === foo.bar); // true
// 同样地,相同的函数以不同的调用方式,this的值也就不同了
exampleFunc(); // global, false
那么,究竟调用方式是如何影响this
的值?为了完全弄懂其中的奥妙,首选需要了解一种内部类型 - 引用(Reference)类型
引用类型
引用类型可以用伪代码表示为拥有两个属性的对象:base
(即拥有属性的那个对象),和base中的 propertyName
。
var valueOfReferenceType = {
base: ,
propertyName:
};
引用类型的值只有可能是以下两种情况:
- 当处理一个标识的时候
- 进行属性访问的时候
标示符的处理过程在作用域链中讨论;在这里我们只需要知道,使用这种处理方式的返回值总是一个引用类型的值
标识符是变量名,函数名,函数参数名和全局对象中未识别的属性名,如下:
var foo = 10;
function bar() {}
中间过程中,对应的引用类型的值如下所示:
var fooReference = {
base: global,
propertyName: 'foo'
};
var barReference = {
base: global,
propertyName: 'bar'
};
要从引用类型的值中获取一个对象实际的值需要GetValue
方法,该方法用伪代码可以描述成如下形式:
function GetValue(value) {
if (Type(value) != Reference) {
return value;
}
var base = GetBase(value);
if (base === null) {
throw new ReferenceError;
}
return base.[[Get]](GetPropertyName(value));
}
上述代码中的 [[Get]] 方法返回了对象属性实际的值,包括从原型链中继承的属性:
GetValue(fooReference); // 10
GetValue(barReference); // function object "bar"
对于属性访问来说,有两种方式:点符号(这时属性名是正确的标识符并且提前已经知道了)或者中括号符号:
foo.bar();
foo['bar']();
中间过程中,得到如下的引用类型的值:
var fooBarReference = {
base: foo,
propertyName: 'bar'
};
GetValue(fooBarReference); // function object "bar"
那么,引用类型的值与函数上下文中this
的值是如何关联起来的呢?这很重要,也是本文的核心内容。总体来说,确定函数上下文中this
值的一般规则如下:
函数上下文中this
的值由调用者(caller)提供,并由调用表达式的形式确定(函数调用的语法)。
如果在调用括号 () 的左边是引用类型,那么this
的值就是该引用类型值的 base 对象。
在其他情况下(非引用类型),this
的值总是 null。然而,null 对于 this
来说没有任何意义,因此为隐式转换为全局对象
function foo() {
return this;
}
foo(); // global
上面代码中,调用括号左侧是引用类型(因为 foo 是标识符):
var fooReference = {
base: global,
propertyName: 'foo'
};
相应的,this
的值会设置为引用类型值的base对象,这里就是全局对象。
同样,使用属性访问器:
var foo = {
bar: function () {
return this;
}
};
foo.bar();
同样,bar也是引用类型的值,它的base对象是foo对象,当激活bar函数的时,this
的值就设置为 foo 对象:
var fooBarReference = {
base: foo,
propertyName: 'bar'
};
然而,同样的函数以不同的激活方式的话,this 的值就完全不同了:
var test = foo.bar;
test(); // global
因为test也是标识符,这样就产生了其他引用类型的值,该值的 base(全局对象)被设置为this
的值
var testReference = {
base: global,
propertyName: 'test'
};
函数调用和非引用类型
正如此前提到过的,当调用括号左侧为非引用类型的时候,this
的值会设置为 null,并最终成为全局对象
(function () {
alert(this); // null => global
})();
在这个例子中,我们有一个函数对象但不是引用类型的对象(因为它不是标示符,也不是属性访问器),因此 this 的值最终被设为全局对象。
var foo = {
bar: function () {
alert(this);
}
};
foo.bar(); // Reference, OK => foo
(foo.bar)(); // Reference, OK => foo
(foo.bar = foo.bar)(); // global?
(false || foo.bar)(); // global?
(foo.bar, foo.bar)(); // global?
那么,为什么明明是属性访问,而最终this
不是引用类型的base对象(foo),而是全局对象呢?
问题出在后面三个调用,在执行一定的操作运算之后,在调用括号的左边的值不再是引用类型。
第一种情况,很明显是引用类型this
的值为 base 对象,即 foo。
第二种情况,分组操作符没有实际意义,分组操作符返回的仍是一个引用类型,这就是 this
的值为什么再次被设为 base 对象,即 foo。
第三种情况,赋值操作符(assignment operator)与组操作符不同,它会触发调用GetValue方法。最后返回的时候就是一个函数对象了(而不是引用类型的值了),这就意味着 this
的值会设置为 null,最终会变成全局对象。
第四和第五种情况也类似,逗号操作符和 OR 逻辑表达式都会触发调用GetValue
方法,于是相应地就会丢失原先的引用类型值,变成了函数类型,this
的值就变成了全局对象了。
引用类型和 this 为 null
有一种情况,当调用表达式左侧是引用类型的值,但是 this
的值却是null,最终变为全局对象。发生这种情况的条件是当引用类型值的base对象恰好为活动对象
function foo() {
function bar() {
alert(this); // global
}
bar(); // 和AO.bar()是一样的
}
活跃对象总是会返回this
值为null
(用伪代码来表示, AO.bar() 就相当于 null.bar())。然后,如此前描述的,this
的值最终会由null
变为全局对象
作为构造器调用的函数中的 this
这里介绍另外一种情况,当函数作为构造器被调用的时候:
function A() {
alert(this); // newly created object, below - "a" object
this.x = 10;
}
var a = new A();
alert(a.x); // 10
在这种情况下,new 操作符会调用"A" 函数的内部 [[Construct]]。在对象创建之后,会调用内部的 [[Call]] 函数,然后所有 “A” 函数中this
的值会设置为新创建的对象。