《你不知道的javascript》学习笔记-第二部分第一章:关于this

this关键字是JavaScript中最复杂的机制之一。它是一个很特别的关键字,被自动定义在所有函数
的作用域中。但是即使是非常有经验的JavaScript开发者也很难说清它到底指向什么。

为什么要用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, 我是KYLE
speak.call( you ); // Hello, 我是READER

这段代码可以在不同的上下文对象(me和you)中重复使用函数identify()和speak(),不用针对每
个对象编写不同版本的函数。
如果不使用this,那就需要给identify()和speak()显式传入一个上下文对象。

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, 我是KYLE

误解

指向自身

我们想要记录一下函数foo被调用的次数,思考一下下面的代码:

function foo(num) {
    console.log( "foo: " + num );
    // 记录foo被调用的次数
    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
// foo被调用了多少次?
console.log( foo.count ); // 0 -- WTF?

console.log语句产生了4条输出,证明foo(..)确实被调用了4次,但是foo.count仍然是0。显然从字
面意思来理解this是错误的。
执行foo.count = 0时,的确向函数对象foo添加了一个属性count。但是函数内部代码this.count中
的this并不是指向那个函数对象,所以虽然属性名相同,根对象却并不相同,困惑随之产生。

遇到这样的问题时,许多开发者并不会深入思考为什么this的行为和预期的不一致,也不会试图
回答那些很难解决但却非常重要的问题。他们只会回避这个问题并使用其他方法来达到目的,比
如创建另一个带有count属性的对象

function foo(num) {
console.log( "foo: " + num );
// 记录foo被调用的次数
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
// foo被调用了多少次?
console.log( data.count ); // 4

从某种角度来说这个方法确实“解决”了问题,但可惜它忽略了真正的问题——无法理解this的含
义和工作原理——而是返回舒适区,使用了一种更熟悉的技术:词法作用域。
思考一下下面这两个函数:

function foo() {
foo.count = 4; // foo指向它自身
} 
setTimeout( function(){
// 匿名( 没有名字的) 函数无法指向自身
}, 10 );

第一个函数被称为具名函数,在它内部可以使用foo来引用自身。
但是在第二个例子中,传入setTimeout(..)的回调函数没有名称标识符(这种函数被称为匿名函
数),因此无法从函数内部引用自身

所以,对于我们的例子来说,另一种解决方法是使用foo标识符替代this来引用函数对象:

function foo(num) {
console.log( "foo: " + num );
// 记录foo被调用的次数
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
// foo被调用了多少次?
console.log( foo.count ); // 4

然而,这种方法同样回避了this的问题,并且完全依赖于变量foo的词法作用域。
另一种方法是强制this指向foo函数对象:

function foo(num) {
console.log( "foo: " + num );
// 记录foo被调用的次数
// 注意, 在当前的调用方式下( 参见下方代码) , this确实指向foo
this.count++;
} 
foo.count = 0;
var i;
for (i=0; i<10; i++) {
if (i > 5) {
// 使用call(..)可以确保this指向函数对象foo本身
foo.call( foo, i );
}
}
// foo: 6
// foo: 7
// foo: 8
// foo: 9
// foo被调用了多少次?
console.log( foo.count ); // 4

这次我们接受了this,没有回避它。如果你仍然感到困惑的话,不用担心,之后我们会详细解释具
体的原理。

它的作用域

第二种常见的误解是,this指向函数的作用域。这个问题有点复杂,因为在某种情况下它是正确
的,但是在其他情况下它却是错误的。
需要明确的是,this在任何情况下都不指向函数的词法作用域。在JavaScript内部,作用域确实和
对象类似,可见的标识符都是它的属性。但是作用域“对象”无法通过JavaScript代码访问,它存在于
JavaScript引擎内部。
思考一下下面的代码,它试图(但是没有成功)跨越边界,使用this来隐式引用函数的词法作用域:

function foo() {
var a = 2;
this.bar();
} 
function bar() {
console.log( this.a );
} 
foo(); // ReferenceError: a is not defined

这段代码中的错误不止一个。虽然这段代码看起来好像是我们故意写出来的例子,但是实际上它
出自一个公共社区中互助论坛的精华代码。这段代码非常完美(同时也令人伤感)地展示了this多
么容易误导人。
首先,这段代码试图通过this.bar()来引用bar()函数。这是绝对不可能成功的,我们之后会解释原
因。调用bar()最自然的方法是省略前面的this,直接使用词法引用标识符。
此外,编写这段代码的开发者还试图使用this联通foo()和bar()的词法作用域,从而让bar()可以
访问foo()作用域里的变量a。这是不可能实现的,你不能使用this来引用一个词法作用域内部的东
西。
每当你想要把this和词法作用域的查找混合使用时,一定要提醒自己,这是无法实现的。

this到底是什么

之前我们说过this是在运行时进行绑定的,并不是在编写时绑定,它的上下文取决于函数调用时
的各种条件。this的绑定和函数声明的位置没有任何关系,只取决于函数的调用方式。
当一个函数被调用时,会创建一个活动记录(有时候也称为执行上下文)。这个记录会包含函数在
哪里被调用(调用栈)、函数的调用方法、传入的参数等信息。this就是记录的其中一个属性,会在
函数执行的过程中用到。
在下一章我们会学习如何寻找函数的调用位置,从而判断函数在执行过程中会如何绑定this。

小结

对于那些没有投入时间学习this机制的JavaScript开发者来说,this的绑定一直是一件非常令人
困惑的事。this是非常重要的,但是猜测、尝试并出错和盲目地从Stack Overflow上复制和粘贴答
案并不能让你真正理解this的机制。
学习this的第一步是明白this既不指向函数自身也不指向函数的词法作用域,你也许被这样的解
释误导过,但其实它们都是错误的。
this实际上是在函数被调用时发生的绑定,它指向什么完全取决于函数在哪里被调用。

One More Thing

本文为原创文章,大部分来自于《你不知道的javascript》一书中,转载请注明出处https://xdlmr.github.io


上一篇
《你不知道的javascript》学习笔记-第二部分第二章:this全面解析 《你不知道的javascript》学习笔记-第二部分第二章:this全面解析
在第1章中,我们排除了一些对于this的错误理解并且明白了每个函数的this是在调用时被绑定的,完全取决于函数的调用位置(也就是函数的调用方法)。 调用位置调用位置:调用位置就是函数在代码中被调用的位置(而不是声明的位置)。只有仔细分析调用
2016-08-18
下一篇
《你不知道的javascript》学习笔记—第一部分第五章:作用域闭包 《你不知道的javascript》学习笔记—第一部分第五章:作用域闭包
启示闭包并不是一个需要学习新的语法或模式才能使用的工具,它也不是一件必须接受像Luke2一样的原力训练才能使用和掌握的武器。闭包是基于词法作用域书写代码时所产生的自然结果,你甚至不需要为了利用它们而有意识地创建闭包。闭包的创建和使用在你的代
2016-08-14
目录