this
关键字是JavaScript
中最复杂的机制之一,同时,它也是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(me) // 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);
}
var me = {
name: 'kyle';
}
var you = {
name: 'Reader'
}
identify(you);
speak(me);
this
提供了一种更优雅的方式来隐式传递一个对象的引用,因此可以将API
设计更加简洁且易于复用。
随着使用模式越来越复杂,显示传递上下文对象会让代码变得越来越混乱,使用this
则不会这样。当我们介绍到原型和对象的时候,我们就会明白函数可以自动医用合适的上下文对象多么重要。
我们介绍this
到底如何工作之前,要解除一些关于this
的错误认知。如果太拘泥于this
字面的意思就会产生一些误解。
人们很容易把this
理解未指向函数本身体,这个推断从英语的角度上是说得通的。那么为什么需要从函数内部引用函数自身呢?常见的原因是递归(从函数内部调用这个函数)或者我们写一个在第一次被调用后自己可以接触绑定的事件处理器。Javascript
新手开发者通常会认为,既然把函数看作是一个对象,那就可以在调用函数时存储状态(属性的值)。
function foo(num) {
console.log("foo: " + num);
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
console.log(foo.count); // 0
执行 foo.count = 0
的时候,的确向函数对象foo
添加了一个属性count
,但是函数内部代码this.count
中的this
并不是指向那个函数对象,所以虽然属性名相同,根对象却并不相同。
负责的开发一定会问 :"如果我增加的count
属性和预期的不一样,那我增加的是哪个count
?"
实际上,如果他深入探索的话,他就会发现这段代码在无意间创建了一个全局变量count
,他的值为NaN
。当然,如果他发现了这个奇怪的结果,一定会问 :“为什么时全局,为什么它的值是NaN而不是其他更适合的值”
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
console.log(data.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.bar()
来引用bar()
函数。这是绝对不可能成功的,我们之后会解释原因。调用bar()
最自然的方法是省略前面的this
,直接使用词法引用标识符。此外,编写这段代码的开发者还试图使用this
联通foo()
和bar()
的词法作用域,从而让bar()
可以访问foo()
作用域里的变量a
。这是不可能实现的,你不能使用this
来引用一个词法作用域内部的东西。每当你想要把this
和词法作用域的查找混合使用时,一定要提醒自己,这是无法实现的
this
是在运行时进行绑定的,并不是在编写时绑定,它的上下文取决于函数调用时的各种条件。
this
的绑定和函数声明的位置没有任何关系,只取决于函数的调用方式。
当一个函数被调用时,会创建一个活动记录(有时候也称为执行上下文)。这个记录会包含函数在哪里被调用(调用栈)、函数的调用方法、传入的参数等信息。this
就是记录其中的一个属性,会在函数执行的过程中用到。
对于那些没有投入时间学习 this
机制的JavaScript
开发者来说,this
的绑定一直是一件非常令人困惑的事。this
是非常重要的,但是猜测、尝试并出错和盲目地从Stack Overflow
上复制和粘贴答案并不能让你真正理解this
的机制。学习this
的第一步是明白this
既不指向函数自身也不指向函数的词法作用域,你也许被这样的解释误导过,但其实它们都是错误的。this
实际上是在函数被调用时发生的绑定,它指向什么完全取决于函数在哪里被调用。