JavaScript ECMA-262-3 深入解析.第三章.this


Posted in Javascript onSeptember 28, 2011

介绍
在这篇文章里,我们将讨论跟执行上下文直接相关的更多细节。讨论的主题就是this关键字。
实践证明,这个主题很难,在不同执行上下文中确定this的值经常会发生问题。
许多程序员习惯的认为,在程序语言中,this关键字与面向对象程序开发紧密相关,其完全指向由构造器新创建的对象。在ECMAScript规范中也是这样实现的,但正如我们将看到那样,在ECMAScript中,this并不限于只用来指向新创建的对象。
下面让我们更详细的了解一下,在ECMAScript中this的值到底是什么?
定义
this是执行上下文中的一个属性:

activeExecutionContext = { 
VO: {...}, 
this: thisValue 
};

这里VO是我们前一章讨论的变量对象。
this与上下文中可执行代码(的类型)直接相关。this的值在进入上下文时确定,并且在上下文运行代码期间不会改变this的值。
下面让我们更详细研究这些场景。
this在全局代码中的值
在这里一切都很简单。在全局代码中,this始终是全局对象本身,这样就有可能间接的引用到它了。
// explicit property definition of 
// the global object 
this.a = 10; // global.a = 10 
alert(a); // 10 
// implicit definition via assigning 
// to unqualified identifier 
b = 20; 
alert(this.b); // 20 
// also implicit via variable declaration 
// because variable object of the global context 
// is the global object itself 
var c = 30; 
alert(this.c); // 30

this在函数代码中的值
在函数代码中使用this时很有趣,这种应用场景很难且会导致很多问题。
在这种类型的代码中,this值的首要(也许是最主要的)特点是它没有静态绑定到一个函数。
正如我们上面曾提到的那样,this的值在进入上下文时确定,在函数代码中,this的值每一次(进入上下文时)可能完全不同。
不管怎样,在代码运行期间,this的值是不变的,也就是说,因为this不是一个变量,所以不可能为其分配一个新值。(相反,在Python编程语言中,它明确的定义为对象本身,在运行期间可以不断改变)。
var foo = {x: 10}; 
var bar = { 
x: 20, 
test: function () { 
alert(this === bar); // true 
alert(this.x); // 20 
this = foo; // error 
alert(this.x); // if there wasn't an error then 20, not 10 
} 
}; 
// 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

那么,在函数代码中,什么影响了this的值发生变化?有几个因素。
首先,在通常的函数调用中,this是由激活上下文代码的调用者来提供的,即调用函数的父上下文(parent context)。this取决于调用函数的方式。(译者注:参考这里)
为了在任何情况下准确无误的确定this值,有必要理解和记住这重要的一点:正是调用函数的方式影响了调用的上下文中this的值,没有别的什么(我们可以在一些文章,甚至是在关于javascript的书籍中看到,它们声称:“this的值取决于函数如何定义,如果它是全局函数,this设置为全局对象,如果函数是一个对象的方法,this将总是指向这个对象。?这绝对不正确”)。继续我们的话题,可以看到,即使是正常的全局函数也会因为不同调用方式而激活,这些不同调用方式产生了this不同的值。
function foo() { 
alert(this); 
} 
foo(); // global 
alert(foo === foo.prototype.constructor); // true 
// but with another form of the call expression 
// of the same function, this value is different 
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 
// again with another form of the call expression 
// of the same function, we have different this value 
exampleFunc(); // global, false

那么,到底调用函数的方式如何影响this的值?为了充分理解this的值是如何确定的,我们需要详细分析一个内部类型(internal type)——引用类型(Reference type)。
引用类型
用伪代码可以把引用类型表示为拥有两个属性的对象——base(即拥有属性的那个对象),和base中的propertyName 。
var valueOfReferenceType = { 
base: <base object>, 
propertyName: <property name> 
};

引用类型的值仅存在于两种情况中:
1. 当我们处理一个标示符时;(when we deal with an identifier;)
2. 或一个属性访问器;(or with a property accessor.)
标示符的处理过程在 Chapter 4. Scope chain中讨论;在这里我们只需要知道,使用这种处理方式的返回值总是一个引用类型的值(这对this来说很重要)。
标识符是变量名,函数名,函数参数名和全局对象中未识别的属性名。例如,下面标识符的值:
var foo = 10;
function bar() {}
在操作的中间结果中,引用类型对应的值如下:
var fooReference = { 
base: global, 
propertyName: 'foo' 
}; 
var barReference = { 
base: global, 
propertyName: 'bar' 
};

为了从引用类型中得到一个对象真正的值,在伪代码中可以用GetValue方法(译者注:11.1.6)来表示,如下:
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的值是如何关联起来的呢?这个关联的过程是这篇文章的核心。(The given moment is the main of this article.) 在一个函数上下文中确定this的值的通用规则如下:
在一个函数上下文中,this的值由调用者提供,且由调用函数的方式决定。如果调用括号()的左边是引用类型的值,this将设为这个引用类型值的base对象,在其他情况下(与引用类型不同的任何其它属性),this的值都为null。不过,实际不存在this的值为null的情况,因为当this的值为null的时候,其值会被隐式转换为全局对象。
下面让我们看个例子:
function foo() { 
return this; 
} 
foo(); // global

我们看到在调用括号的左边是一个引用类型值(因为foo是一个标示符):
var fooReference = { 
base: global, 
propertyName: 'foo' 
};

相应地,this也设置为引用类型的base对象。即全局对象。
同样,使用属性访问器:
var foo = { 
bar: function () { 
return this; 
} 
}; 
foo.bar(); // foo

同样,我们拥有一个引用类型的值,其base是foo对象,在函数bar激活时将base设置给this。
var fooBarReference = { 
base: foo, 
propertyName: 'bar' 
};

但是,如果用另一种方式激活相同的函数,this的值将不同。
var test = foo.bar;
test(); // global
因为test作为标识符,产生了其他引用类型的值,该值的base(全局对象)被设置为this的值。
var testReference = { 
base: global, 
propertyName: 'test' 
};

现在,我们可以很明确的说明,为什么用不同的形式激活同一个函数会产生不同的this,答案在于不同的引用类型(type Reference)的中间值。
function foo() { 
alert(this); 
} 
foo(); // global, because 
var fooReference = { 
base: global, 
propertyName: 'foo' 
}; 
alert(foo === foo.prototype.constructor); // true 
// another form of the call expression 
foo.prototype.constructor(); // foo.prototype, because 
var fooPrototypeConstructorReference = { 
base: foo.prototype, 
propertyName: 'constructor' 
};

另一个通过调用方式动态确定this的值的经典例子:
function foo() { 
alert(this.bar); 
} 
var x = {bar: 10}; 
var y = {bar: 20}; 
x.test = foo; 
y.test = foo; 
x.test(); // 10 
y.test(); // 20

函数调用和非引用类型
那么,正如我们已经指出,当调用括号的左边不是引用类型而是其它类型,this的值自动设置为null,实际最终this的值被隐式转换为全局对象。
让我们思考下面这种函数表达式:
(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对象,而是global对象?
问题出现在后面的三个调用,在执行一定的操作运算之后,在调用括号的左边的值不再是引用类型。
第一个例子很明显———明显的引用类型,结果是,this为base对象,即foo。
在第二个例子中,分组操作符(译者注:这里的分组操作符就是指foo.bar外面的括号"()")没有实际意义,想想上面提到的,从引用类型中获得一个对象真正的值的方法,如GetValue (参考11.1.6)。相应的,在分组操作的返回值中———我们得到的仍是一个引用类型。这就是this的值为什么再次被设为base对象,即 foo。
第三个例子中,与分组操作符不同,赋值操作符调用了GetValue方法(参考11.13.1的第三步)。返回的结果已经是函数对象(不是引用类型),这意味着this的值被设为null,实际最终结果是被设置为global对象。
第四个和第五个也是一样——逗号操作符和逻辑操作符(OR)调用了GetValue 方法,相应地,我们失去了引用类型的值而得到了函数类型的值,所以this的值再次被设为global对象。
引用类型和this为null
有一种情况,如果调用方式确定了引用类型的值(when call expression determinates on the left hand side of call brackets the value of Reference type。译者注,原文有点拖沓!),不管怎样,只要this的值被设置为null,其最终就会被隐式转换成global。当引用类型值的base对象是激活对象时,就会导致这种情况。
下面的实例中,内部函数被父函数调用,此时我们就能够看到上面说的那种特殊情况。正如我们在 第二章 学到的一样,局部变量、内部函数、形式参数都储存在给定函数的激活对象中。
function foo() { 
function bar() { 
alert(this); // global 
} 
bar(); // the same as AO.bar() 
}

激活对象总是作为this的值返回——null(即伪代码AO.bar()相当于null.bar())。(译者注:不明白参考这里)这里我们再次回到上面描述的情况,this的值最终还是被设置为全局对象。
有一种情况除外:“在with语句中调用函数,且在with对象(译者注:即下面例子中的__withObject)中包含函数名属性时”。with语句将其对象添加在作用域链最前端,即在激活对象的前面。那么对应的,引用类型有值(通过标识符或属性访问器),其base对象不再是激活对象,而是with语句的对象。顺便提一句,这种情况不仅跟内部函数相关,还跟全局函数相关,因为with对象比作用域链里的最前端的对象(全局对象或一个激活对象)还要靠前。
var x = 10; 
with ({ 
foo: function () { 
alert(this.x); 
}, 
x: 20 
}) { 
foo(); // 20 
} 
// because 
var fooReference = { 
base: __withObject, 
propertyName: 'foo' 
};

在catch语句的实际参数中的函数调用存在类似情况:在这种情况下,catch对象被添加到作用域的最前端,即在激活对象或全局对象的前面。但是,这个特定的行为被确认为是ECMA-262-3的一个bug,这个在新版的ECMA-262-5中修复了。修复后,在特定的激活对象中,this指向全局对象。而不是catch对象。
try { 
throw function () { 
alert(this); 
}; 
} catch (e) { 
e(); // __catchObject - in ES3, global - fixed in ES5 
} 
// on idea 
var eReference = { 
base: __catchObject, 
propertyName: 'e' 
}; 
// but, as this is a bug 
// then this value is forced to global 
// null => global 
var eReference = { 
base: global, 
propertyName: 'e' 
};

同样的情况出现在命名函数(函数的更多细节参考Chapter 5. Functions)的递归调用中。在函数的第一次调用中,base对象是父激活对象(或全局对象),在递归调用中,base对象应该是存储着函数表达式可选名称的特定对象。但是,在这种情况下,this的值也总是被设置为global。
(function foo(bar) { 
alert(this); 
!bar && foo(1); // "should" be special object, but always (correct) global 
})(); // global

this在作为构造器调用的函数中的值
还有一个在函数的上下文中与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的值设置为新创建的对象。
手动设置一个函数调用的this
在Function.prototype中定义了两个方法允许手动设置函数调用时this的值,它们是.apply和.call方法(所有的函数都可以访问它们)。它们用接受的第一个参数作为this的值,this在调用的作用域中使用。这两个方法的区别不大,对于.apply,第二个参数必须是数组(或者是类似数组的对象,如arguments,相反,.call能接受任何参数。两个方法必须的参数都是第一个——this。
例如
var b = 10; 
function a(c) { 
alert(this.b); 
alert(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

结论
在这篇文章中,我们讨论了ECMAScript中this关键字的特征(and they really are features, in contrast, say, with C++ or Java,译者注:这句话没什么大用,还不知道咋翻好,暂不翻了)。我希望这篇文章有助于你准确的理解ECMAScript中this关键字如何工作。同样,我很高兴在评论中回答您的问题。
其他参考
10.1.7 ? This;
11.1.1 ? The this keyword;
11.2.2 ? The new operator;
11.2.3 ? Function calls.
英文地址 : ECMA-262-3 in detail. Chapter 3. This.
中文地址 : [JavaScript]ECMA-262-3 深入解析.第三章.this
翻译声明:
1.因为Denis已经翻译过这篇文章,所以该篇译文在部分章节参考了他的译文,参考引用部分大概占整篇文章的30%左右,另外70%左右完全是重新翻译的。
2.在翻译过程中,跟原作者进行了充分的沟通,大家看译文的时候,可以多参考原文的留言列表。
3.再好的翻译也赶不上原汁原味的原文,所以推荐大家看过译文之后还是要再仔细看看原文。
Javascript 相关文章推荐
避免回车键导致的页面无意义刷新的解决方法
Apr 12 Javascript
一个关于jqGrid使用的小例子(行按钮)
Nov 04 Javascript
什么是DOM(Document Object Model)文档对象模型
Mar 05 Javascript
如何正确使用javascript 来进行我们的程序开发
Jun 23 Javascript
js获取字符串字节数方法小结
Jun 09 Javascript
BootStrap 智能表单实战系列(二)BootStrap支持的类型简介
Jun 13 Javascript
jQuery插件之validation插件
Mar 29 jQuery
深入理解Angular4中的依赖注入
Jun 07 Javascript
vue iview组件表格 render函数的使用方法详解
Mar 15 Javascript
详解jQuery-each()方法
Mar 13 jQuery
JavaScript获取时区实现过程解析
Sep 24 Javascript
vue实现移动端返回顶部
Oct 12 Javascript
JavaScript call apply使用 JavaScript对象的方法绑定到DOM事件后this指向问题
Sep 28 #Javascript
javascript权威指南 学习笔记之变量作用域分享
Sep 28 #Javascript
关于setInterval、setTimeout在jQuery中的使用注意事项
Sep 28 #Javascript
jQuery Ajax 仿AjaxPro.Utility.RegisterTypeForAjax辅助方法
Sep 27 #Javascript
Ext.get() 和 Ext.query()组合使用实现最灵活的取元素方式
Sep 26 #Javascript
一个挺有意思的Javascript小问题说明
Sep 26 #Javascript
Jquery之Ajax运用 学习运用篇
Sep 26 #Javascript
You might like
smarty实例教程
2006/11/19 PHP
php zip文件解压类代码
2009/12/02 PHP
关于php unset对json_encode的影响详解
2018/11/14 PHP
PHP Include文件实例讲解
2019/02/15 PHP
js 格式化时间日期函数小结
2010/03/20 Javascript
Firefox中使用outerHTML的2种解决方法
2014/06/07 Javascript
jQuery条件分页 代替离线查询(附代码)
2017/08/17 jQuery
vue将对象新增的属性添加到检测序列的方法
2018/02/24 Javascript
Node.js使用Angular简单示例
2018/05/11 Javascript
vue-router+nginx 非根路径配置方法
2018/06/30 Javascript
JavaScript中的 new 命令
2019/05/22 Javascript
mpvue 页面预加载新增preLoad生命周期的两种方式
2019/10/17 Javascript
vue+elementUI动态生成面包屑导航教程
2019/11/04 Javascript
JS使用Chrome浏览器实现调试线上代码
2020/07/23 Javascript
浅谈vue.watch的触发条件是什么
2020/11/07 Javascript
[01:20:06]TNC vs VG 2018国际邀请赛小组赛BO2 第二场 8.16
2018/08/17 DOTA
浅析python 中__name__ = '__main__' 的作用
2014/07/05 Python
python threading模块操作多线程介绍
2015/04/08 Python
Python字符串替换实例分析
2015/05/11 Python
在Django的URLconf中使用多个视图前缀的方法
2015/07/18 Python
对python append 与浅拷贝的实例讲解
2018/05/04 Python
python 实现语音聊天机器人的示例代码
2018/12/02 Python
Django 多环境配置详解
2019/05/14 Python
Python算法的时间复杂度和空间复杂度(实例解析)
2019/11/19 Python
wxpython+pymysql实现用户登陆功能
2019/11/19 Python
Python vtk读取并显示dicom文件示例
2020/01/13 Python
Python+OpenCV图像处理—— 色彩空间转换
2020/10/22 Python
美国轻奢时尚购物网站:REVOLVE(支持中文)
2020/07/18 全球购物
Oracle里面常用的数据字典有哪些
2014/02/14 面试题
公司业务员岗位职责
2014/03/18 职场文书
开业典礼主持词
2014/03/21 职场文书
环保建议书600字
2014/05/14 职场文书
群众路线表态发言材料
2014/10/17 职场文书
迎新生欢迎词
2015/01/23 职场文书
小学语文教学随笔
2015/08/14 职场文书
豆瓣2021评分最高动画剧集-豆瓣评分最高的动画剧集2021
2022/03/18 日漫