这里有新鲜出炉的 Javascript 教程,程序狗速度看过来!
Javascript 是一种由 Netscape 的 LiveScript 发展而来的原型化继承的基于对象的动态类型的区分大小写的客户端脚本语言,主要目的是为了解决服务器端语言,比如 Perl,遗留的速度问题,为客户提供更流畅的浏览效果。
这篇文章主要为大家详细介绍了 Javascript 中的作用域,具有一定的参考价值,感兴趣的朋友可以参考一下
1、编译原理
在传统编译语言的流程中,程序中的一段代码执行前会经历三个步骤。统称为 "编译"。
词法分析
将代码字符串分解成有意义的代码块,这些代码块称为词法单元。例如:在 js 中,var a = 2;。这段程序通常被拆分为以下词法单元。var、a、2、;。至于空格是否会被当成词法单元,取决于空格在这门语言中是否有意思。 语法分析 将词法单元流(数组)转换为 "抽象语法树"(AST,Abstract Syntax Tree。编译原理课程中提到过)。 代码生成 将 AST 转换为可执行代码。与语言,平台有关(java 跨平台)。简单来说:var a = 2; 的 AST 被转换成一组机器指令,用来创建一个 a 的变量(分配内存等),并将 2 存储在 a 中。
而对于 Javascript 而言,尽管通常它被归类为 "动态" 或 "解释执行" 语言,但实际上它是一门编译语言。所不同的是,在它编译时引擎要执行更复杂的操作过程。 首先,Javascript 引擎不会有大量的(向其他编译器那么多的)时间来进行优化,因为与其他语言不同,它的编译过程不是在构建之前的。 对于 Javascript 而言,大部分编译发生在代码执行前的几微秒(甚至更短)。所以引擎会用尽各种方法(比如 JIT)来保证性能最佳。 简单的说,任何 Js 代码在执行前都要编译(几微秒前)。因此,在执行 var a = 2; 这段代码前,引擎会先编译,然后做好执行它的准备(加入到代码队列)。通常是马上执行。
2、理解作用域
引擎 负责整个编译以及执行过程。 编译器 引擎的好朋友之一,负责语法分析和代码生成等脏活累活。 作用域 引擎的另一个好朋友,负责收集和维护所有变量,并实施一套非常严格的规则,以保证当前代码(作用域)对变量的访问权限。
对于 var a = 2;,它不仅仅是一句简单的声明。声明它有两个过程。编译时:编译器进行相关操作。执行时,Js 引擎进行相关操作。
var a,编译器会在当前作用域查找是否有 a 这个变量。如果有,则编译器忽略此声明。否则,在当前作用域创建一个 a 变量(分配内存)。
a = 2,接下来编译器(语法分析,代码生成…)生成运行时所需的代码用来处理这个赋值操作。具体的赋值操作由 Js 引擎负责。Js 引擎会在当前作用域查找 a 这个变量,如果找到,就进行赋值操作。否则,在父级作用域查找(作用域嵌套),直至全局作用域。如果找到,进行赋值操作。找不到抛出异常。
在查找作用域的过程中,会涉及到 LHS 查询和 RHS 查询。它们分别代表赋值操作的目标和赋值操作的源头。不仅仅是赋值操作,更有函数赋值操作等等。比如:
- function foo(a){
- console.log(a);
- }
- foo(2);
最后一行 foo()函数的调用需要对 foo()本身进行 RHS 查询。在全局作用域中找到了 foo 的声明。并且 () 意味着要把 foo 当做一个函数执行,所以 foo 最好是一个函数,否则会报错。
还有一个容易忽视的细节。在把 2 作为实参传入到 foo 的形参时,会有一个隐式的 a=2 操作。a 是赋值操作的源头,2 是赋值操作的目标。所以这里对 a 进行了一次 LHS 查询。由于在编译过程中在当前作用域(函数作用域)将 a 声明为 foo 的一个形参了,所以可以找到。
然后就是 console.log(a);,console 本身也需要一个 LHS 查询,它是在 window 下面的内置对象,所以可以找到。然后对 a 进行 RHS 查询。幸运的是,在将 2 赋值给函数形参 a 的时候,a 已经声明并赋值了。所以这个 RHS 是可以进行的。
3、作用域嵌套
在之前我们说过,作用域负责收集和维护所有变量,并实施一套非常严格的规则,以保证当前代码(作用域)对变量的访问权限。考虑以下代码:
- function foo(a){
- console.log(a+b);
- }
- var b = 2;
- foo(2);
我们只考虑这里对 b 的 RHS 引用。Js 引擎开始试图在 foo 函数作用域查找 b 变量,但是并没有找到。于是,Js 引擎就会突破当前限制,去外层作用域查找。哎呀,找到了!于是就对 b 进行 RHS 引用成功了。当然呢,要是没找到的话,Js 引擎也不会放弃,会继续往外层作用域查找,直到找到全局作用域。然后遵循的规则参照 a=2 赋值那块。
4、异常
在一个变量还没有声明(任何作用域都无法查到)的情况下,LHS 和 RHS 查询失败后的操作是不一样的。可以预料,RHS 查询失败会抛出一个异常,那么 LHS 查询失败呢?
- function foo(a){
- console.log(a+b);
- b = a;
- }
- foo(2);
第一次对b进行 RHS 查询时,在任何作用域无法找到该变量的声明。那么有小伙伴就疑惑了,b=a 呢?不是对 b 的声明吗?答案是:是。这里确实是对 b 的声明。 但在对作用域查找的过程中,只会向上查找声明(涉及到声明提升)。由于这里 b 是在 console.log() 后面定义的。所以是失败的,抛出 ReferenceError 异常。值得注意的是,ReferenceError 是非常重要的异常类型。再考虑下述代码:
- function foo(a){
- b = a;
- console.log(a+b);
- }
- foo(2);
这里呢,第一次对b进行的是 LHS 查询。如果在顶层(全局)作用域也无法查到 foo 的话,那么 Js 引擎就会很热心的帮你在全局作用域创建一个 b 变量,前提是在非 "严格模式" 下,在一个作用域内加上代码 "use strict",表明使用严格模式。在严格模式下,LHS 查询失败时,并不会创建一个全局变量,而是抛出同 RHS 查询失败时类似的 ReferenceError 异常。 接下来,加入你找到了这个变量,但是你试图对这个变量进行不合理的操作。如:对一个非函数类型的变量进行 () 函数调用、对 null 或 undefined 类型的值进行访问,那么引擎会抛出另一种类型的异常,叫做 TypeError。 总之,RefercenError 同作用域判别失败相关,而 TypeError 表示作用域判别成功,但是对结果的操作是不合法的。
5、小结
作用域是一套规则,规定在何处以及如何查找变量(加上之前说的,重要的事情说三遍)。如果查找的目的是赋值,就是进行 LHS 查询。如果目的是获取变量的值,就会进行 RHS 查询。 Js 引擎会在代码执行前对其进行编译。var a = 2;,这样的操作会被分成两个步骤。 1. 编译时, 编译器声明 a 变量,即 var a。 2. 运行时,对 a 变量进行赋值。a=2。 LHS 查询和 RHS 查询失败会进行不同的操作。RHS 查询失败会抛出 ReferenceError 异常。LHS 查询失败会在全局作用域创建变量(非严格模式),在严格模式下抛出 ReferenceError 异常。
来源: http://www.phperz.com/article/17/0513/331416.html