原文鏈接:ECMA-262-3 in detail. Chapter 3. This.
首先,this也是執行上下文(execution context)的一个属性,就像前一章讲的變量體一样:
activeExecutionContext = {
VO: {...},
this: thisValue
}
所以它的值也跟上下文的类型有关,另外:
- this的值是在進入上下文的步驟里被確定的;
- 在執行代碼階段里,this的值不可被修改。
=============================================================
既然跟类型有关,那么就按照上下文的类型来说一下,首先是全局上下文,在这种情况下,this的值就是全局对象自身,如:
// explicit property definition of
// the global object
this.a = 10; // global.a = 10
console.log(a); // 10
// implicit definition via assigning
// to unqualified identifier
b = 20;
console.log(this.b); // 20
// also implicit via variable declaration
// because variable object of the global context
// is the global object itself
var c = 30;
console.log(this.c); // 30
注:這段代碼在Chrome (Version 56.0.2924.87)和Firefox (51.0.1 (32-bit))中都測試通過。
=============================================================
函数执行上下文里this的值
在一般的情况下:
- this的值由触发当前上下文的调用者提供;
- this的值取决于调用当前函数的方式。
注意:this的值跟函数怎么被声明定义的,定义在哪里没有关系,下面的代码演示了这个问题:
var foo = {x: 10};
var bar = {
x: 20,
test: function () {
console.log(this === bar); // true
console.log(this.x); // 20
//this = foo; // error, can't change this value
//console.log(this.x); // if there wasn't an error, then would be 10, not 20
}
};
// on entering the context this value is
// determined as "bar" object; why so - will
// be discussed below in detail
bar.test(); // true, 20
foo.test = bar.test;
// however here this value will now refer
// to "foo" – even though we're calling the same function
foo.test(); // false, 10
注:這段代碼以及下面两段在Chrome (Version 56.0.2924.87)和Firefox (51.0.1 (32-bit))中都測試通過。
下面的代码则演示了this的值取决于呼叫函数的方式的问题:
function foo() {
console.log(this);
}
foo(); // Window
console.log(foo === foo.prototype.constructor); // true
// but with another form of the call expression
// of the same function, this value is different
foo.prototype.constructor(); // Object {}
再来一例:
var foo = {
bar: function () {
console.log(this);
console.log(this === foo);
}
};
foo.bar(); // Object {}, true
var exampleFunc = foo.bar;
console.log(exampleFunc === foo.bar); // true
// again with another form of the call expression
// of the same function, we have different this value
exampleFunc(); // Window {}, false
=============================================================
下面正文终于开始,到底在函数上下文里,决定this的值的过程和规则是怎样的?
=============================================================
引用(Reference)类型(内部)
它是JavaScript引擎内部使用的一种数据类型,对于JS代码并不可见。它的格式可以表述为:
var valueOfReferenceType = {
base: <base object>,
propertyName: <property name>
};
两种情况下引用类型会出现:
- 一个变量的标识符(identifier)
- 属性访问器(property accessor)
标识符包括:
- 变量名;
- 函数名;
- 函数的形式参数名;
- 还有全局对象上面没有被定义的属性名。
比如下面的声明:
var foo = 10;
function bar() {}
会产生两个引用类型:
var fooReference = {
base: global,
propertyName: 'foo'
};
var barReference = {
base: global,
propertyName: 'bar'
};
属性访问器则是下面的情况:
foo.bar();
foo['bar']();
对应的引用类型变量就是:
var fooBarReference = {
base: foo,
propertyName: 'bar'
};
另外作者还讲到了关于引用类型的一些内部实现的细节,但是我觉得可以暂时省略,之后我会把它们放到附录里。
=============================================================
决定this的值的规则
现在来说一说决定this值的算法吧:
- 调用函数的操作符是一对括号:( ),在这个操作符左边出现的记号如果是一个引用类型的值,那么这个引用类型上的base的值就会被赋值给this;
- 在所有其他的情况下,也就是说( )左边的符号不是一个引用类型,那么this的值就会成为null;
- 在任何情形下,如果this的值是null,就被隐性转换成指向全局对象。
举例一.
function foo() {
return this;
}
foo(); // global
foo是引用类型,它的内部表达是:
var fooReference = {
base: global,
propertyName: 'foo'
};
所以,global,也就是全局对象成了foo函数里this指向的东西。
举例二.
var foo = {
bar: function () {
return this;
}
};
foo.bar(); // foo
注意,函数调用操作符( )左边的东西是foo.bar,属于引用类型,但它的情况是:
var foo-barReference = {
base: foo,
propertyName: 'bar'
};
所以this自然就是指向foo对象了。但是如果这样调用:
var test = foo.bar;
test(); // global
操作符左边是test,也是引用类型,而它的情况则是:
var testReference = {
base: global,
propertyName: 'test'
};
所以this又指向了全局对象。
总结下,就是由于调用函数时左边的引用类型变量会有所不同,它们的base属性的值不同,导致了this会不同,回到前面的一个例子:
function foo() {
console.log(this);
}
foo(); // Window
console.log(foo === foo.prototype.constructor); // true
// but with another form of the call expression
// of the same function, this value is different
foo.prototype.constructor(); // Object {}
调用foo()时,foo的情况是:
var fooReference = {
base: global,
propertyName: 'foo'
};
而调用foo.prototype.constructor()时,foo.prototype.constructor的情况是:
var foo-prototype-constructorReference = {
base: foo.prototype,
propertyName: 'constructor'
};
举例三.
function foo() {
console.log(this.bar);
}
var x = {bar: 10};
var y = {bar: 20};
x.test = foo;
y.test = foo;
x.test(); // 10
y.test(); // 20
注意:在Chrome (Version 56.0.2924.87)和Firefox (51.0.1 (32-bit))中測試的結果都相同
=============================================================
在非引用类型变量上调用函数
规则很简单,如果( )左边的符号不是个引用类型的值,那么this就为null,也就进而指向全局对象。但是从实践上讲都有哪些情况呢?
最常见的第一种就是IIFE:
(function () {
console.log(this); // null => global
})();
下面介绍几种少见的情况:
var foo = {
bar: function () {
console.log(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?
第一个不用说了,先解释第二个调用(foo.bar)():
在这里包住foo.bar的括号被解读为分组操作符(grouping operator),它不会导致在foo.bar上面隐性呼叫引用类型的内部方法[[GetValue]],参见附录关于引用类型的更多细节。所以,foo.bar没有被做改变,在执行到函数调用时它还是引用类型,于是它的结果和第一个调用是一样的。
第三个,第四个,和第五个调用里,foo.bar被分别放在了不同的表达式里,表达式里出现的赋值操作符,逻辑或操作符还有逗号都会导致在foo.bar上呼叫了[[GetValue]],结果它的值--也就是函数对象--被返回了,于是在到了执行调用函数的时候,( )左边的东西已经不再是引用类型了,而变成了函数对象本身,于是,按照第二条规则,this就成了null,紧跟着按照第三条规则就变成了全局对象。
疑问:究竟哪些操作符会导致[[GetValue]]被调用,这个问题在这里没有被细讲,因为这涉及到另一个JS的方面,就是表达式。只有弄清表达式遵循的所有规则,才会搞清楚到底哪些操作里引用类型会被改变,这个细节留待以后研究,或许You Dont Know JS系列的第一本书会有介绍。
=============================================================
引用类型变量上的base属性指向的是函数上下文的触发体(activation object)
当这样的情况发生时,this会被赋值为null,进而指向全局对象。这个情况在实践中发生在在一个函数里直接调用一个在该函数中声明的子函数:
function foo() {
function bar() {
console.log(this); // global
}
bar(); // the same as AO.bar()
}
调用bar()的表达式里的bar是保存在foo执行上下文上面的变量体的一个属性,所以bar作为一个引用类型,它的内部表达是:
var barReference = {
base: AO(foo),
propertyName: 'bar'
};
根据上面的规则,this就是全局对象了。
三个特殊情况:
1. 在with代码块内调用了一个with对象上包含的成员函数,如下:
var x = 10;
with ({
foo: function () {
console.log(this.x);
},
x: 20
}) {
foo(); // 20
}
像示例代码那样,这个情况要满足的条件是:with()内的对象上有foo属性,而且它是函数,并且它在下面的with代码块里被直接调用了。
原因是with会在作用域链里向最前面插入这个被定义的对象,就是( )里面的东西。这样一来,foo就是在它之上,并且是引用类型,而且它的base的值就是这个对象本身。
var fooReference = {
base: __withObject,
propertyName: 'foo'
};
疑问:作用域链要在下一章才说到,另外还有个问题就是,如果foo没有被在对象上定义会怎样?
这个疑问在评论里面被热心的读者提出来了,而且他们设想了一些非常复杂的代码来探索这个问题,不过在深入挖掘与with语句相关的这一切问题之前,我想先提醒一点,《Effective JavaScript》是建议避免使用with的,并且他提供了一个方案来代替with的特殊功能。
先来看第一个:
var x = 20;
function fn(){
function f1(){
console.log(this.x) // 20
}
with({
f2: function(){
console.log(this.x) // 50
},
x:50
}){
function f3(){
console.log(this.x) // 20
}
function f4(){
console.log(x) // ?
}
f1();//20
f2();//50
f3();//20
f4();//50
}
}
fn();
注:這段代碼在Chrome (Version 56.0.2924.87)和Firefox (51.0.1 (32-bit))中都測試通過,与提出者给出的输出结果和作者当时的解释都是符合的(2017-03-23)。
这里面f2的情况和作者文章中提出的例子是一样的,所以不用再多做解释。
f3和f4从声明的方式看最相似,先一起说,再次强调,JS没有代码块作用域,即使with也是一样,而它们又没有被声明在with对象上,所以它们其实被提升到了fn函数的作用域,再具体点说就是,f3和f4最终跟f1一样,在进入fn的执行上下文阶段被创建,并被保存在fn上下文的变量体上面。所以读者的代码等同于下面的写法:
function fn(){
function f1(){
console.log(this.x)
}
function f3(){
console.log(this.x)
}
function f4(){
console.log(x)
}
with (...) {
...
}
}
那么f1跟f3其实是一回事儿了,而它们为什么输出20也一目了然,根据前面的分析,它们的this应该是fn函数的触发体,所以就变成null,进而变成全局对象了。
现在剩下的问题是f4了,它没有通过this寻找x,所以它的问题严格讲跟this不太有关系了,而是如何解析变量标识符的问题了,这个问题在下一章作用域链里才会详细解释。因为我没有完全看懂,所以这里简单概述下作者的大意:fn函数上面维护着一个作用域链,它是以数组的形式表达的:
Scope = [global, AO(fn)]
但是在with的代码块里,这个链上面的尾端会被插入那个新的with的对象,就变成:
Scope = [global, AO(fn), {f2: funciton () {...}, x: 50}]
而寻找x变量的过程是沿着后面往前的,只要找到了就停止,这点有点像原型链。所以,f4输出的就是50了。
2017-03-31插入一个注:作者在表述作用域链时所使用的数组的方向并不一致,在上面这段讨论里,数组最右边是被理解为“前面”,但是在下一章讲作用域链时,又是倒过来,左边才是“前面”。
另外,作者还补充了一点,像f4这种寻找变量的情况,如果它是被用函数表达式定义的,那么即使它在with代码块以外被调用,它在with内时对该变量使用的值也会被永远记住,用文字描述有点费劲,看代码:
var foo;
with ({x: 50}) {
foo = function () {
console.log(x);
};
foo(); // 50
}
var x = 20;
foo(); // 50, but not 20
注:這段代碼在Chrome (Version 56.0.2924.87)和Firefox (51.0.1 (32-bit))中都測試通過。foo在with代码块里是用函数表达式定义的,它里面时,作用域链被插入了对象{x:50},而这个东西它会一直带着,即使with代码块已经结束了。
所以这里又产生个新问题,如果它不是用函数表达式定义的呢?
再来看第二个:
这个跟this其实也没有太大关系,它完全是变量标识符解析相关的问题(我对代码做了修改):
Object.prototype.x = 10;
var x = 20; // window.x
var y = 40; // window.y
var z = 60;
var kk = x + 40;
with ({w:100}) {
var w = 200;
Object.prototype.x = 30;
var x = 50; // because Object.prototype.x is found,
// => x = 50, add x prop to _withObj, neither window.x, nor Object.prototype.x
var y = 70; // because Object.prototype.y is not found, change window.y
var a = 80;
console.log('here');
}
console.log(x); // 20, still window.x
console.log(y); // 70, .
console.log(Object.prototype.x); // 30
注:這段代碼在Chrome (Version 56.0.2924.87)和Firefox (51.0.1 (32-bit))中都測試通過。
这里面有个最大的知识点,就是在with代码块里面遇到var x = 10这样的语句时,如果在对象的原型链上找得到x,那就在当下对象上添加x属性,并赋值;如果找不到,就把它解读为要在全局对象上找x,如果找到了,就更新值。另外,因为这些变量在外面已经用var声明过了,所以在代码块里用不用var关键字已经没有区别。这是导致意外结果的关键原因。
作者为了演示这个问题给的代码(我做了修改):
Object.prototype.x = 10;
var x = 20;
var o = {w: 100};
with (o) {
x = 50;
var w = 200;
}
console.log(
x, // still 20
o, // {w: 100, x: 50}
Object.prototype.x // still 10
);
var x = 20;
var o = {w: 100};
with (o) {
x = 50;
}
console.log(
x, // 50
o // {w: 100}
);
注:這两段代碼在Chrome (Version 56.0.2924.87)和Firefox (51.0.1 (32-bit))中都測試通過。
2. 在catch语句的代码块内调用作为参数传进来的函数,如下:
try {
throw function () {
console.log(this);
};
} catch (e) {
e(); // __catchObject - in ES3, global - fixed in ES5
}
作者指出在ES3里的做法是个错误,也就是将一个新创建的对象插入到作用域链的最前端:
var eReference = {
base: __catchObject,
propertyName: 'e'
};
这个错误在ES5里被修正了,它将会是指向全局对象:
var eReference = {
base: global,
propertyName: 'e'
};
注,在Chrome (Version 56.0.2924.87)和Firefox (51.0.1 (32-bit))中結果都输出全局对象,即Window{}。
3. 迭代调用命名函数表达式,如下:
(function foo(bar) {
console.log(this);
!bar && foo(1); // "should" be special object, but always (correct) global
})(); // global
在这段代码里,foo迭代调用了自己,在第一个调用里,按照上面讲的规则,它的调用者是外面的上下文的变量体,在这个例子里就是全局对象自身,而后面的调用里,由于迭代,作用域链会被插入一个新的特殊对象,按说this应该指向这个特殊对象,可是这种情况this会被隐式地强制指向全局对象。
所以结论是很简单的,就是说不管怎么样,迭代调用函数时,从第一次到最后一次的所有调用里,this都是指向全局对象的,简单记住这个结论就够了。
=============================================================
作为构造函数被调用
结论很简单,就是指向那个新创建的对象:
function A() {
console.log(this); // newly created object, below - "a" object
this.x = 10;
}
var a = new A();
console.log(a.x); // 10
使用new操作符时实际上发生的事情是,函数对象A上面的内部方法[[Construct]]被调用,它会创建一个新对象,然后在调用A上面的内部方法[[Call]]并把新创建的对象作为它的this传给它。
疑问:[[Construct]]和[[Call]]是什么?更多细节等到介绍函数的一章会说明。
=============================================================
在函数调用时手动设定this的值
这个情况也很简单,就是通过apply或者call调用函数,把this的值作为参数传进去。
var b = 10;
function a(c) {
console.log(this.b);
console.log(c);
}
a(20); // this === global, this.b == 10, c == 20
a.call({b: 20}, 30); // this === {b: 20}, this.b == 20, c == 30
a.apply({b: 30}, [40]) // this === {b: 30}, this.b == 30, c == 40
=============================================================
附录
=============================================================
引用类型
为了从引用类型的变量获得真实的值,JS有一个内部的方法[[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]]又是一个内部函数,它的功能很复杂,它能够在对象上获取属性的值,这个操作包括了沿着原型链向上查询。
=============================================================
与<You Don't Know JS - this and Object Prototypes>一书第二章的对比