太拘泥于“this”的字面意思就会产生一些误解。有两种常见的对于this 的解释,但是它们都是错误的。

介绍之前先解释下什么是动态作用域

简要地分析一下动态作用域,重申它与词法作用域的区别。但实际上动态作用域是JavaScript 另一个重要机制this 的表亲。词法作用域是一套关于引擎如何寻找变量以及会在何处找到变量的规则。词法作用域最重要的特征是它的定义过程发生在代码的书写阶段(假设你没有使用eval() 或with)。动态作用域似乎暗示有很好的理由让作用域作为一个在运行时就被动态确定的形式,而不是在写代码时进行静态确定的形式,事实上也是这样的。通过示例代码来说明:

function foo() {
console.log( a ); //
}
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();

为什么会这样?因为当foo() 无法找到a 的变量引用时,会顺着调用栈在调用foo() 的地方查找a,而不是在嵌套的词法作用域链中向上查找。由于foo() 是在bar() 中调用的,
引擎会检查bar() 的作用域,并在其中找到值为3 的变量a。很奇怪吧?现在你可能会这么想。但这其实是因为你可能只写过基于词法作用域的代码(或者至少以词法作用域为基础进行
了深入的思考),因此对动态作用域感到陌生。如果你只用基于动态作用域的语言写过代码,就会觉得这是很自然的,而词法作用域看上去才怪怪的。需要明确的是,事实上JavaScript 并不具有动态作用域。它只有词法作用域,简单明了。但是this 机制某种程度上很像动态作用域。
主要区别:词法作用域是在写代码或者说定义时确定的,而动态作用域是在运行时确定的。(this 也是!)词法作用域关注函数在何处声明,而动态作用域关注函数从何处调用。
最后,this 关注函数如何调用,这就表明了this 机制和动态作用域之间的关系多么紧密。

可以在chrome中的Call Stack中查看调用栈,需要在调试模式下(当然,这是废话)

1.指向自身

人们很容易把this 理解成指向函数自身,这个推断从英语的语法角度来说是说得通的。那么为什么需要从函数内部引用函数自身呢?常见的原因是递归(从函数内部调用这个函数)或者可以写一个在第一次被调用后自己解除绑定的事件处理器。
JavaScript 的新手开发者通常会认为,既然函数看作一个对象(JavaScript 中的所有函数都是对象),那就可以在调用函数时存储状态(属性的值)。这是可行的,有些时候也确实有用,但在许多模式中你会发现,除了函数

对象还有许多更合适存储状态的地方。不过现在我们先来分析一下这个模式,让大家看到this 并不像我们所想的那样指向函数本身。
我们想要记录一下函数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 并不是指向那个函数对象,所以虽然属性名相同,根对象却并不相同,困惑随之产生。负责的开发者一定会问“如果我增加的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 ); //

从某种角度来说这个方法确实“解决”了问题,但可惜它忽略了真正的问题——无法理解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=0
var 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 ); //

然而,这种方法同样回避了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 ); //

这次我们接受了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的一些误解的更多相关文章

  1. javascript——从「最被误解的语言」到「最流行的语言」

    JavaScript曾是"世界上最被误解的语言".由于它担负太多的特性.包含糟糕的交互和失败的设计,但随着Ajax的到来.JavaScript"从最受误解的编程语言演变为 ...

  2. 理解JavaScript 的原型属性

    1.原型继承 面向对象编程可以通过很多途径实现.其他的语言,比如 Java,使用基于类的模型实现: 类及对象实例区别对待.但在 JavaScript 中没有类的概念,取而代之的是一切皆对象.JavaS ...

  3. #JavaScript对象与继承

    JavaScript对象与继承 JavaScript是我在C语言之后接触的第二门编程语言,大一暑假的时候在图书馆找了一本中国人写的JavaScript程序设计来看.那个时候在编程方面几乎还是小白,再加 ...

  4. javascript线程解释(setTimeout,setInterval你不知道的事)

    john resig写的一篇文章: 原文地址:http://ejohn.org/blog/how-javascript-timers-work/ 作为入门者来说,了解JavaScript中timer的 ...

  5. 《JavaScript语言精粹》【PDF】下载

    <JavaScript语言精粹>[PDF]下载链接: https://u253469.pipipan.com/fs/253469-230382204 内容简介 javascript曾是&q ...

  6. javascript大神修炼记(1)——入门介绍

    读者朋友们好,从今天开始,我将带领新朋友们,从了解javascript开始,一步一步地进阶到大神境界,别的不废话,现在开始,我们就一点一点地从入门阶段开始. 我们还是介绍一下javascript的身世 ...

  7. ES6中的Class

    对于javascript来说,类是一种可选(而不是必须)的设计模式,而且在JavaScript这样的[[Prototype]] 语言中实现类是很蹩脚的. 这种蹩脚的感觉不只是来源于语法,虽然语法是很重 ...

  8. setTimeout()和setInterval() 何时被调用执行

    定义 setTimeout()和setInterval()经常被用来处理延时和定时任务.setTimeout() 方法用于在指定的毫秒数后调用函数或计算表达式,而setInterval()则可以在每隔 ...

  9. 你不知道的JavaScript--Item23 定时器的合理使用

    1.定时器概述 window对象提供了两个方法来实现定时器的效果,分别是window.setTimeout()和window.setInterval.其中前者可以使一段代码在指定时间后运行:而后者则可 ...

随机推荐

  1. [Colony]RHCS集群理论

    什么是集群?     集群是一组(>2)相互独立的,通过高速网络互联的计算机组成的集合.群集一般可以分为科学集群,负载均衡集群,高可用性集群三大类.     科学集群是并行计算的基础.它对外就好 ...

  2. Laravel 数据插入

    Laravel 的数据库操作基于 Eloquent ORM,在插入数据时有以下几种方式,返回结果也不会不同: 1.insert 插入后会返回 true or false: 2.create 插入成功后 ...

  3. 《Android学习指南》目录

    源:<Android学习指南>目录 Android学习指南的内容分类: 分类 描述 0.学习Android必备的Java基础知识 没有Java基础的朋友,请不要先看Android的课程,这 ...

  4. linux命令学习-2-dmesg

    DMESG NAME dmesg - print or control the kernel ring buffer(打印或者控制内核环缓冲) Usage: dmesg [options] Optio ...

  5. C++中string类的使用方法

    如果所比较的两个string 相等,则返回0: 操作string 大于参数string,返回 正数:操作string 小于参数string,返回负数. (1) 比较操作string 与 _Str 或C ...

  6. flask-sqlalchemy relationship

    http://www.ergo.io/blog/sqlalchemy-relationships-from-beginner-to-advanced class Cabinet(db.Model): ...

  7. JQuery的 jQuery.fn.extend() 和jQuery.extend();

    原文链接:http://caibaojian.com/jquery-extend-and-jquery-fn-extend.html jQuery.fn.extend(); jQuery.extend ...

  8. PowerShell学习小结

    1. 获取所有别名信息Get-Alias 2. 获取指定别名信息Get-Alias xx 3. 通过command name获得指定别名信息Get-Alias -Definition xx-xxx 4 ...

  9. 数据结构-String、char

    String 常用方法: 获取长度:int length = strl.length() 获取第i个字符:char iChar =  str.charAt(i) String转成字符数组:char[] ...

  10. assert 实现分析

    一直以来,对于assert的实现总是不太理解,现在深入assert背后的代码,总算对assert的实现有了一个清醒的认识. assert基于宏定义与宏展开实现.首先介绍一下assert的功能:它能够断 ...