[翻译]Chapter1 this or that
第一次翻译,翻译的不好,已经再尽全力s去翻译了,如果哪里看不明点,请出门左转下边原文地址
javascript中最令人困惑的东西就是this关键字,它在每个函数作用域中都会自动定义的一个特殊的标识符,但是,它折磨着每一个javascript开发者,哪怕是有经验的。
任何科技的充分进步,都和魔法没什么区别
javascript的this机制事实上并不是先进的,但是开发者经常按照他们自己的观点把this解释的复杂和混乱。毫无疑问这是因为缺少深刻的理解。
why this
既然this机制让开发者甚至是经验丰富的程序员感到困惑。那为什么会是有用的,thia弊大于利吗?在此之前,我们先跳过how的问题,去审查一下why的问题。
让我们来说明使用this的动机和实用性。
function identify() {
return this.name.toUpperCase();
}
function speak() {
var greeting = "Hello, I'm " + identify.call( this );
console.log( greeting );
}
var me = {
name: "Kyle"
};
var you = {
name: "Reader"
};
identify.call( me ); // KYLE
identify.call( you ); // READER
speak.call( me ); // Hello, I'm KYLE
speak.call( you ); // Hello, I'm READER
如果看不明白这个代码块,不要着急!我们很快会解释,把这些问题放到一边,我们先来看关于why的问题。
这个代码块允许identify()和speak()两个函数分别被两个对象me和you重用,而不是为每个对象单独写一个版本的函数。
通过依赖this。你可以用一种更明确的方式在环境对象中使用两个方法。
function identify(context) {
return context.name.toUpperCase();
}
function speak(context) {
var greeting = "Hello, I'm " + identify( context );
console.log( greeting );
}
identify( you ); // READER
speak( me ); // Hello, I'm KYLE
this机制提供一种更加优雅的方式去传递一个对象引用,从而实现更加清楚的API设计和更简单的重用。
你用的模式越复杂,你就越能清晰的看到,传递上下文的时候一个显示的参数要比传递一个this上下文更加麻烦。当我们查看对象和原型的时候,你将能看到一个能够自动引用正确的函数集合上下文对象的实用性。
困惑点
我们很快会开始解释this实际上是如何工作的,但是首先要纠正一下错误的观念。
itself
第一个经常被解释成困惑的是this指代函数本身,至少这倒是一个合理的解释。
为什么你会需要在函数内部引用函数自身,最可能的原因是递归(在函数内部调用函数自身)或者事件处理的时候当第一次调用时解绑事件。
开发者最新的js机制是把函数当成一个对象一样来引用(js里所有的函数都是对象),这样会导致需要在函数互相调用之间存储状态(属性值)。当然这种机制可行的,也有一些有限的用处。这本书剩下的部分将会阐述许多其他的模式,以便更好的存储函数对象之外的状态。
但是等一会,我们将会探索一种模式,来说明this是如何不让函数获得自身的引用,就像我们之前假设的一样。
考虑下边的代码,我们尝试去跟踪一下foo函数被调用了多少次。
function foo(num) {
console.log( "foo: " + num );
// keep track of how many times `foo` is called
this.count++;
}
foo.count = 0;
var i;
for (i=0; i<10; i++) {
if (i > 5) {
foo( i );
}
}
// foo: 6
// foo: 7
// foo: 8
// foo: 9
// how many times was `foo` called?
console.log( foo.count ); // 0 -- WTF?
foo.count依然是0,即使经过了四次console之后我们能清晰的看到foo事实上调用了四次,这个让人失望的源头在于对this是什么的解释太字面。
当代码执行到foo.count = 0的时候,确实,它给foo函数对象添加了一个属性count,但是this.count只是在函数内部引用,this并不是指向所有其他的函数对象,即使属性名称相同,因为this所在的对象不一样,所以困惑就产生了。
一个有责任的开发者应该问到这一点:如果我增加一个count属性但是这个属性并不是我想要的,那我是否增加了。事实上,如果开发者更深的挖掘,她会发现她创建了一个变量count,这个变量当前的值是NAN,一旦她注意到这个奇怪的结果,她将有一系列的疑问,全局对象是什么,为什么这个值是NAN而不是数值。(在foo中打印count,会显示出NAN)。
有责任的开发者不应该在这一点停止而是应当深入挖掘为什么这个引用的表现没有想预想的那样,去回答这个棘手但是很重要的问题。许多开发者简单的避免这个问题,并且采取一些其他的解决办法,比如创建另一个对象去存储count这个属性:
function foo(num) {
console.log( "foo: " + num );
// keep track of how many times `foo` is called
data.count++;
}
var data = {
count: 0
};
var i;
for (i=0; i<10; i++) {
if (i > 5) {
foo( i );
}
}
// foo: 6
// foo: 7
// foo: 8
// foo: 9
// how many times was `foo` called?
console.log( data.count ); // 4
虽然这个确实解决了问题,但是不幸的是它忽略了真正的问题,不能理解this到底是什么以及她是如何工作的,而是回到了一个更加熟悉的舒适环境,词法作用域(静态作用域)。
词法作用域是一个完美而有用的机制,我不会以任何方式轻视它,但是,不断的猜测如何使用this,但是通常会带来错误,退回到词法作用域去解决问题并不是一个好原因。
为了在一个函数对象引用自身,this有着显而易见的不足,你通常需要通过指向词法标识符(变量)去引用函数对象。
function foo() {
foo.count = 4; // `foo` refers to itself
}
setTimeout( function(){
// anonymous function (no name), cannot
// refer to itself
}, 10 );
在第一个方法中,称为命名函数,foo是一个引用,可以被用来在函数内部引用函数本身。
但是第二个例子,这个没有名称标识的函数是通过setTimeout执行回调(所以叫匿名函数),所以,这个时候没有合适的方法通过函数名引用函数对象自身。
上边是旧的用法,现在已经弃用,一个函数中的arguments.callee引用指向当前执行的函数的函数对象。this引用是从匿名函数内部引用自身的唯一方法,不过,最好的方法是避免使用匿名函数,至少在那些需要引用自身的时候,使用命名函数或者表达式。而arguments.callee已经被弃用,不建议使用。
所以,另外一个解决方法解决我们运行的例子是我们在每个地方用foo这个标识符作为函数对象的引用,而不是this。
function foo(num) {
console.log( "foo: " + num );
// keep track of how many times `foo` is called
foo.count++;
}
foo.count = 0;
var i;
for (i=0; i<10; i++) {
if (i > 5) {
foo( i );
}
}
// foo: 6
// foo: 7
// foo: 8
// foo: 9
// how many times was `foo` called?
console.log( foo.count ); // 4
然而这个方法依然侧重于this的实际理解,并且依赖于foo的词法作用域中的变量。
还有另外一种方法更关注this在foo函数对象中实际指向的问题。
function foo(num) {
console.log( "foo: " + num );
// keep track of how many times `foo` is called
// Note: `this` IS actually `foo` now, based on
// how `foo` is called (see below)
this.count++;
}
foo.count = 0;
var i;
for (i=0; i<10; i++) {
if (i > 5) {
// using `call(..)`, we ensure the `this`
// points at the function object (`foo`) itself
foo.call( foo, i );
}
}
// foo: 6
// foo: 7
// foo: 8
// foo: 9
// how many times was `foo` called?
console.log( foo.count ); // 4
避免用this,我们拥抱这种方法,我们将会稍微解释一下这个技术是如何更完整的工作的,所以,如果你仍然有一点困惑,别着急。
its scope
第二个常见的错误观点是认为this的意思是以某种形式指代函数作用域,这是一个让人困惑的问题,因为在一定意义上这是有道理的,但是另一方面,这是很让人误导的。
要明确的是,在任何情况下,this并不是指代函数的词法作用域,在内部,作用域是一个类似可以访问每个标识符属性的对象,但是这个作用域对象在js代码中是不能访问的,他是引擎内部执行的一部分。
下边代码尝试去跨越代码的边界去使用this隐式的指向函数作用域。
function foo() {
var a = 2;
this.bar();
}
function bar() {
console.log( this.a );
}
foo(); //undefined
这个代码块里有不止一个错误,似乎它可能得出想要的结果,这个你看到的代码。。。(原文这里嘲讽了这块代码)。
首先,你想通过this.bar()来引用bar方法,当运行起来无疑会出事故,我们要尽快解释为何报错,调用bar()最自然的方式是省略this,只对标识符进行词法引用。
然而,开发者尝试使用this的目的是在foo和bar两个函数之间创造一个桥梁,是的bar可以访问到foo内部作用域中的变量,但是并没有这样的桥梁,你不能用this引用去查找词法作用域下的一些东西,这是不可能的。
每当你感觉你想尝试把慈父作用域的查找和this混合的时候,记得,这样的桥是不存在的。
what this
抛开那些不正确的解释,现在我们把注意力,转移到this机制是如何工作的。
我们之前说过,this是运行的时候绑定而不是声明的时候绑定,它是基于函数调用情况下的上下文,this绑定和函数生命的位置没有关系,而是与函数的调用方式有关系。
当一个函数被调用,将会创建一个激活记录,也就是所谓的执行上下文。该记录包含了函数调用的位置信息(堆栈),以及函数是如何被调用的,还有参数是如何传递的等等,这个记录的其中一个属性是this引用,将会在函数执行的期间被使用。
在下一章,我们要学习去找一个函数的调用点去确定在函数执行的时候是如何绑定this的。
回顾
this绑定对于没有花时间去搞懂这个机制具体如何工作的js开发者来说是一个恒定不断的困难。来自stackoverfolw的回答者的猜测,试错和盲目的复制粘贴并不是利用这种机制的有效方法。
学习this,首先要去了解this不是什么,尽管任何的假设或者误解都可能导致你。。。this既不是函数本身的引用,也不是函数词法作用域的引用。
this实际上是函数调用时进行的绑定,它的引用绑定完全由函数的调用位置所决定。