太拘泥于 "this" 的字面意思就会产生一些误解。有两种常见的对于 this 的解释,但是它们都是错误的。
介绍之前先解释下什么是动态作用域
简要地分析一下动态作用域,重申它与词法作用域的区别。但实际上动态作用域是 JavaScript 另一个重要机制 this 的表亲。词法作用域是一套关于引擎如何寻找变量以及会在何处找到变量的规则。词法作用域最重要的特征是它的定义过程发生在代码的书写阶段(假设你没有使用 eval() 或 with)。动态作用域似乎暗示有很好的理由让作用域作为一个在运行时就被动态确定的形式,而不是在写代码时进行静态确定的形式,事实上也是这样的。通过示例代码来说明:
- function foo() {
- console.log( a ); // 2}function bar() {var a = 3;
- foo();
- }
- var a = 2;
- bar();
词法作用域让 foo() 中的 a 通过 RHS(js 中赋值的一种形式) 引用到了全局作用域中的 a,因此会输出 2。而动态作用域并不关心函数和作用域是如何声明以及在何处声明的,只关心它们从何处调用。换句话说,作用域链是基于调用栈的,而不是代码中的作用域嵌套。因此,如果 JavaScript 具有动态作用域,理论上,下面代码中的 foo() 在执行时将会输出 3。
- function foo() {
- console.log( a ); // 3(不是2 !)}function bar() {var a = 3;
- foo();
- }
- var a = 2;
- bar();
1. 指向自身
人们很容易把 this 理解成指向函数自身,这个推断从英语的语法角度来说是说得通的。那么为什么需要从函数内部引用函数自身呢?常见的原因是递归(从函数内部调用这个函数)或者可以写一个在第一次被调用后自己解除绑定的事件处理器。JavaScript 的新手开发者通常会认为,既然函数看作一个对象(JavaScript 中的所有函数都是对象),那就可以在调用函数时存储状态(属性的值)。这是可行的,有些时候也确实有用,但在许多模式中你会发现,除了函数
对象还有许多更合适存储状态的地方。不过现在我们先来分析一下这个模式,让大家看到 this 并不像我们所想的那样指向函数本身。我们想要记录一下函数 foo 被调用的次数,思考一下下面的代码:
- 1
- function foo(num) { 2 console.log( "foo: " + num ); 3 // 记录foo 被调用的次数 4 this.count++; 5 } 6 foo.count = 0; 7 var i; 8 for (i=0; i<10; i++) { 9 if (i > 5) {10 foo( i );11 }12 }13 // foo: 614 // foo: 715 // foo: 816 // foo: 917 // foo 被调用了多少次?18 console.log( foo.count ); // 0 -- WTF?
console.log 语句产生了 4 条输出,证明 foo(..) 确实被调用了 4 次,但是 foo.count 仍然是 0。显然从字面意思来理解 this 是错误的。执行 foo.count = 0 时,的确向函数对象 foo 添加了一个属性 count。但是函数内部代码 this.count 中的 this 并不是指向那个函数对象,所以虽然属性名相同,根对象却并不相同,困惑随之产生。负责的开发者一定会问 "如果我增加的 count 属性和预期的不一样,那我增加的是哪个 count ?" 实际上,如果他深入探索的话,就会发现这段代码在无意中创建了一个全局变量 count(原理参见第 2 章),它的值为 NaN。当然,如果他发现了这个奇怪的结果,那一定会接着问:"为什么它是全局的,为什么它的值是 NaN 而不是其他更合适的值?"(参见第 2 章。)遇到这样的问题时,许多开发者并不会深入思考为什么 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 的含义和工作原理——而是返回舒适区,使用了一种更熟悉的技术:词法作用域。词法作用域是一种非常优秀并且有用的技术。我丝毫没有贬低它的意思(可以参考本书第一部分 "作用域和闭包")。但是如果你仅仅是因为无法猜对 this 的用法,就放弃学习 this 而去使用词法作用域,就不能算是一种很好的解决办法了。如果要从函数对象内部引用它自身,那只使用 this 是不够的。一般来说你需要通过一个指向函数对象的词法标识符(变量)来引用它。
思考一下下面这两个函数:
- function foo() {
- foo.count = 4; // foo 指向它自身}
- setTimeout(
- function() { // 匿名(没有名字的)函数无法指向自身}, 10 );
第一个函数被称为具名函数,在它内部可以使用 foo 来引用自身。但是在第二个例子中,传入 setTimeout(..) 的回调函数没有名称标识符(这种函数被称为匿名函数),因此无法从函数内部引用自身。还有一种传统的但是现在已经被弃用和批判的用法,是使用 arguments.callee 来引用当前正在运行的函数对象。这是唯一一种可以从匿名函数对象内部引用自身的方法。然而,更好的方式是避免使用匿名函数,至少在需要自引用时使用具名函数(表达式)。arguments.callee 已经被弃用,不应该再使用它。所以,对于我们的例子来说,另一种解决方法是使用 foo 标识符替代 this 来引用函数对象:
- function foo(num) {
- console.log( "foo: " + num ); // 记录foo 被调用的次数foo.count++;
- }
- foo.count = 0var i;
- for (i = 0; i < 10; i++) {
- if (i > 5) {
- foo( i );
- }
- }关于this | 79 // 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 确实指向foothis.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,没有回避它。
2 它的作用域
第二种常见的误解是,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 的作用类似于动态作用域, 而动态作用域并不关心函数和作用域是如何声明以及在何处声明的,只关心它们从何处调用。换句话说,作用域链是基于调用栈的,而不是代码中的作用域嵌套。
javascript this 的一些误解
来源: http://www.bubuko.com/infodetail-2064826.html