几乎所有的编程语言都可以存储, 访问, 修改变量, 那在 JavaScript 中这些变量放在那里? 程序如何找到他们?
JS 被归类于解释执行语言, 但事实上他也是一门编译语言, 因为他也要编译, 但于传统的编译语言不同, 他不是提前编译, 编译结果也不能在分布式系统中进行移植. 但 JS 引擎编译的步骤和传统的编译语言非常相似.
传统的编译会经历 3 个步骤:
分词: 将组成的字符串分解成有意义的代码块(词法单元)for instance:var a = 2; 被分解成 var,a,=,2,;.
语法分析: 将词法单元转换成有元素逐级嵌套所组成的代表了程序语法结构的树, 这个树叫抽象语法树.
代码生成: 将抽象语法树转换成可执行的代码的过程. 简单来说就是将抽象语法树转化为一组机器指令, 用来创建一个叫作 a 的变量(包括分配内存等), 并将值存储到 a 中.
对于传统的编译过程, JS 引擎要复杂的多, JS 编译发生在执行前的几微秒, 然后做好执行他的准备, 并且通常马上就会执行他.
说道这还是没有说这些变量放在那里? 下面介绍 3 位大佬:
引擎: 从头到尾负责整个 JS 程序的变以及执行过程.
编译器: 负责语法分析和代码生成等.
作用域: 负责收集并维护由所有声明的标识符 (变量和函数) 组成的一系列查询, 并实施一套非常严格的规则, 确定当前执行的代码对这些变量的访问权限.
这时候答案就浮出水面. 啊, 是这伙计 -- 作用域.
作用域? 大哥你哪来的啊? 上面提到作用域像一个容器一样收集并维护所有标识符 (变量和函数) 事实上他是一个对象, 收集的东西挂在它的属性上. 作用域的出生是因为函数的产生而产生的. 当函数执行的前一刻的时候, 会创建一个作用域, 这个作用域定义了这个函数执行时的环境, 函数每次执行时的作用域都是独一无二的, 因为他是对象啊, 就像出生的孩子长得都不太一样. 这个作用域小名特别多, 什么执行期上下文, AO(Activation Object)对象, 活动对象. 作用域这帮伙计们有个头叫 -- 全局作用域. 里面的作用域能看到外面的, 哈哈, 你在我面前就是个小透明, 但外的作用域可看不到里面的.
在 JS 高程里解释道作用域.
作用域是因函数产生而产生的, 每个对象都有属性和方法, 函数 (function) 也是一种特殊的对象, 函数可以有 test.name test.prototype ... 这些是可以访问的, 还有一些属性是不可以访问的隐式属性仅供 JavaScript 引擎处理. 比如 [[scope]]: 指的是作用域链, 其中存储了执行期上下文的集合. 这个集合呈现链式连接, 我们把这种连接叫做作用域链. 作用域链本质上是一个指向变量对象的指针列表, 他只是引用, 但不包含实际变量对象..[[scope]] 这里面存的就是作用域. 系统会根据内部的原理去定期调用 scope. 当函数执行的前一刻的时候, 会创建一个称为执行期上下文的内部对象(AO activation object). 一个执行期上下文定义了一个函数执行时的环境. 函数每次执行时对应的上下文都是独一无二的, 即使执行一样的函数但是执行期上下文并不相同, 所以多次调用一个函数会导致创建多个执行上下文, 当函数执行完毕, 他所产生的执行上下文会销毁.
上面还提到了编译, 说道 JS 编译发生在执行前的几微秒. 也讲到变量和函数会放到作用域的问题, 事实上, 是这样的, 在程序执行前不是要进行编译的吗, 在引擎和编译器两位大哥的帮助下 , 在编译时 (函数执行前) 会处理声明的变量和函数, 把他挂到作用域这个对象的属性上. 这个过程叫 -- 见下行.
预编译
当你看见 var a = 2; 这段程序时, 很可能认为这是一句声明, 事实上我们引擎这哥们认为有两个完全不同的声明, 一个由编译器在编译时处理, 另一个在引擎运行时处理.
代码执行前会对其进行编译, 首先编译器会分词, 然后解析成语法树, 最后进行代码生成, 别忘了代码生成就是将语法树转化为一组机器指令.
生成代码前编译器会询问作用域是否有该名称的变量, 如果有, 忽略该声明, 如果没有, 会要求作用域在当前作用域的集合中声明一个新变量, 并命名为 a.
接下来为引擎生成运行时所需要的代码, 这些代码用来处理 a = 2 这个赋值操作, 运行时引擎首先会问作用域, 在当前作用域集合中是否有一个叫作 a 的变量, 如果找到就会对他赋值, 否则就会抛出异常.
也就是说变量和函数在内的所有声明都会在任何代码被执行前首先被处理.-- 先编译, 在执行.
- console.log(a); //undefinde
- var a = 2;
- console.log(a); //2
这也解释了为什么第一行没有报错的原因.
首先代码执行前一刻进行编译, var a; console.log(a); a = 2; console.log(a); 类似这样的执行顺序(就好像变量和函数从他们的代码中出现的位置被移动到了最上面). 编译器看到 var a 会查看当前作用域是否有变量 a, 没有声明一个变量 a, 开始执行代码(JS 是顺序执行).
console.log(a)查看作用域是否有变量 a, 有, 但没有值, a 为 undefined.
var a = 2; 查看作用域是否有变量 a, 有, 对 a 赋值 a = 2.
console.log(a)查看作用域是否有变量 a, 有, 值为 2.
当函数和变量同名时, 函数会覆盖变量.
由此预编译过程可以总结成一下过程
预编译四部曲:
1. 创建 AO 对象 / 活动对象(activation object)(执行期上下文)
2. 找形参和变量声明, 将变量和形参名作为 AO 属性名, 值为 undefined
3. 将实参值和形参统一
4. 在函数体里面找到函数声明, 值赋予函数体
来源: https://www.cnblogs.com/jiaobaba/p/11062280.html