你不知道的JavaScript上卷(一)
第一章 作用域
存储和访问变量,几乎是所有编程语言的基本功能之一。但如何将变量引入,如何存储,如何查找等这些问题,就需要一套设计良好的规则进行管理。这套规则则被称为作用域
1.1编译原理
尽管通过将JavaScript归为解释性语言或动态语言,但事实上JS也存在编译的步骤,但与传统的编译语言不同,它不是提前编译的,编译结果也不能在分布式系统进行移植。
大部分情况下JS的编译发生在执行前的几微秒(甚至更短)。
1.2理解作用域
几个主要角色如下:
-
引擎
从头代为负责整个JS程序的编译和执行过程
-
编译器
负责语法分析和代码生成
-
作用域
负责收集和维护所有声明的标识符(变量)组成的一系列插叙,并实施一套非常严格的规则,确定当前执行的代码对这些标识符的访问权限
对于var a = a ; 这段程序,很可能认为这是一句声明,但引擎并不这么看。事实上,引擎认为这里有两个完全不同的声明, 一个是有编译器在编译时处理,另一个则由引擎在运行时处理。
首先编译器会将这段代码分解为词法单元,然后将词法单元解析成一个树结构。但当编译器开始进行代码生成时,它对这段代码的操作如下:
-
遇到var a ,编译器会询问作用域是否已经存在一个该名称的变量在同一个作用域的集合中。如果是,编译器则会忽略该声明,继续进行编译,否则它会要求作用域在当前作用域集合中声明一个新的变量,名称为a.
-
接下来编译器会为引擎生成运行时的代码,这些代码被用来处理 a = 2 这个赋值操作。引擎运行时会首先查询作用域,在当前作用域集合中是否存在一个叫做a的变量。如果是,引擎则会使用这个变量,否则,引擎会继续查找该变量。
如果引擎最终找到了a变量,就会将2赋值给它。否则引擎就会抛出一个异常
总结: 变量的赋值操作会执行两个动作,首先编译器会在当前作用域中声明一个变量,然后在运行时引擎会在作用域中查找该变量,如果能够找到就会对它赋值。
1.3 作用域的嵌套
当一个块或者函数嵌套在另外一个块或者函数中时,就发生了作用域嵌套,在当前作用域中无法找到这个变量时,引擎就会在外层嵌套的作用域中继续找,直到找到该变量,或抵挡最外层的作用域位置。
第二章 词法作用域
在上一章中介绍了作用域的概念和如何在嵌套的作用域中进行查找。本章节则会介绍词法作用域。
作用域共有两种工作模式。第一种是最为普遍的词法作用域,另一种是动态作用域。
2.1 词法阶段
词法作用域就是定义在词法阶段的作用域,换句话说,词法作用域是由你在写代码是将变量和块作用域写在哪里决定的。如下两个例子:
function foo(){
var p1 = 1;
bar();
}
function bar(){
console.log(p1);
}
foo();
这段代码中的bar函数内部的作用域是包裹在全局作用内,而p1是在foo的作用域中,因此bar函数是无法访问到p1.
function foo(){
var p1 = 1;
function bar(){
console.log(p1);
}
bar();
}
foo();
而将bar函数声明在foo函数内部,则相当于bar的外部作用域是foo的作用域,因此是可以访问到p1的(这里也是一个闭包)。
2.2 欺骗语法
如果词法作用域完全有写代码期间函数声明的位置来定义的,怎样才能在运行时“修改”(欺骗)词法作用域呢?
JS 中有两个机制可以“欺骗”词法作用域:eval()和with。前者可以对一段包含一个或多个声明的“代码”字符串进行演算,并借此来修改已存在的词法作用域(在运行时)。后者本质上是通过一个对象的引用当做作用域来处理,将对象的属性当作作用域中的标识符来处理,创建了一个新的词法作用域(同样是在运行时)
这两个机制的副作用是引擎无法在编译时对作用域查找进行优化,因此引擎只能谨慎地认为这样的优化是无效的。使用这其中任何一个机制都会导致代码运行变慢。不要使用它们。