javascript 拥有一套设计良好的规则来存储变量,并且之后可以方便地找到这些变量,这套规则被称为作用域。作用域貌似简单,实则复杂,由于作用域与 this 机制非常容易混淆,使得理解作用域的原理更为重要。
作用域理解:定义的变量、函数生效的范围,可访问变量,对象,函数的集合。javascript 有全局作用域和函数作用域两种。
- 全局:函数声明、变量声明 。范围:一段内或者一个函数内;
- 函数:函数声明、变量声明、this、arguments。范围:一个函数内部;
编译
以 var a = 2;为例,说明 javascript 的内部编译过程,主要包括以下三步:
-
分词(tokenizing)
把由字符组成的字符串分解成有意义的代码块,这些代码块被称为词法单元(token)
var a = 2;被分解成为下面这些词法单元:var、a、=、2、;。这些词法单元组成了一个词法单元流数组;
// 词法分析后的结果 [ "var": "keyword", "a" : "identifier", "=" : "assignment", "2" : "integer", ";" : "eos" (end of statement) ] 复制代码
-
解析(parsing)
把词法单元流数组转换成一个由元素逐级嵌套所组成的代表程序语法结构的树,这个树被称为“抽象语法树” (Abstract Syntax Tree, AST)(看不懂参考计算操作系统堆处理)
{ operation: "=", left: { keyword: "var", right: "a" } right: "2" } 复制代码
-
代码生成
将 AST 转换为可执行代码的过程被称为代码生成(给计算机执行的代码)
var a=2;的抽象语法树转为一组机器指令,用来创建一个叫作 a 的变量(包括分配内存等),并将值 2 储存在 a 中
实际上,javascript 引擎的编译过程要复杂得多,包括大量优化操作,上面的三个步骤是编译过程的基本概述; 任何代码片段在执行前都要进行编译,大部分情况下编译发生在代码执行前的几微秒。javascript 编译器首先会对 var a=2;这段程序进行编译,然后做好执行它的准备,并且通常马上就会执行它
执行
简而言之,编译过程就是编译器把程序分解成词法单元(token),然后把词法单元解析成语法树(AST),再把语法树变成机器指令等待执行的过程(整体的过程是执行)
实际上,代码进行编译,还要执行。下面仍然以 var a = 2
为例,深入说明编译和执行过程
-
编译
-
编译器查找作用域是否已经有一个名称为 a 的变量存在于同一个作用域的集合中。如果是,编译器会忽略该声明,继续进行编译;否则它会要求作用域在当前作用域的集合中声明一个新的变量,并命名为 a
-
编译器将 var a = 2;这个代码片段编译成用于执行的机器指令
javascript 中的重复声明是合法的(不建议)
//test 在作用域中首次出现,所以声明新变量,并将 20 赋值给 test var test = 20; //test 在作用域中已经存在,直接使用,将 20 的赋值替换成 30 var test = 30; 复制代码
-
-
执行
-
引擎运行时会首先查询作用域,在当前的作用域集合中是否存在一个叫作 a 的变量。如果是,引擎就会使用这个变量;如果否,引擎会继续查找该变量
-
如果引擎最终找到了变量 a,就会将 2 赋值给它。否则引擎会抛出一个异常
-
顺带一下 异常
-
ReferenceError – 引用错误异常
//对b进行RHS查询时,无法找到该变量。也就是说,这是一个 未声明的变量 function foo(a) { a = b; } foo(); //ReferenceError: b is not defined 复制代码
-
TypeError – 类型错误异常
function foo() { var b = 0; b(); b.map() ... } foo(); //TypeError: b is not a function 复制代码
-
SyntaxError – 语法上不合法的代码的错误
console.log('' // SyntaxError: missing ) after argument list 复制代码