持续创作,加速成长!这是我参与「掘金日新计划 · 6 月更文挑战」的第21天,点击查看活动详情
模块装载
ECMAScript 6 模块是静态装配的,而传统的 Node.js 模块却是动态加载的。
export 在导出的时候,其实是先在某个名字表中登记一个名字 x 就可以了,这个过程也就是 JavaScript 在模块装载之前对 export 所做的全部工作。
JavaScript 就可以依据所有它能在静态文本中发现的 import 语句来形成模块依赖树,最后就可以找到这个模块依赖树最顶端的根模块,并尝试加载。
后续的装配过程,找到并遍历模块依赖树的所有模块(这个树是排序的),然后执行这些模块最顶层的代码,直到所有模块的顶层代码都执行完毕,那么所有的导出名字和它们的值也都必然是绑定完成了的。
所谓模块的装配过程,就是执行一次顶层代码而已。
找到并遍历模块依赖树的所有模块(这个树是排序的),然后执行这些模块最顶层的代码。
导出名字与导出值本质上并没有差异,在静态装配的阶段它们都只是表达为一个名字而已。
export... 语句通常是按它的词法声明来创建标识符的,例如 export var x = ... 就意味着在当前模块环境中创建的是一个变量,并可以修改等操作。但是当它被导入时,在 import 语句所在的模块中却是一个常量,因此总是不可写的。
例如B模块中 export 一个 let 变量,然后在 A 模块中 import 它为 x 。然后你尝试在 A 模块中 x++,你会发现错误提示为常量不可写。
所以 A、B 两个模块中的名字其实并不是同一个变量,它们名字相同(或者不同),但 A 模块中只是通过一个(类似于别名)映射来指向 B 模块中的名字。
如果是引用类型的话,因为引用的是同一个地址,所以是可以修改的。
块级作用域
越少作用域的执行环境调度效率也就越高,执行时的性能也就越好。
所谓的块级作用域有两种形式,一种是静态的词法作用域,另一种动态的块级作用域的实例来说,只有当存在潜在标识符冲突的时候,才有必要新添加一个作用域来管理它们。
在早期的 Javascript 中 由于作用域只有上面所说的两种,所以任何一个 var 声明 的标识符,要么是在函数体内的,要么就是在全局的,没有例外。
按照这个早期设计,如下语句中的变量 x:
for (var x = ...) // 里面的变量 i 被提升到了全局的作用域中
循环语句(对于支持 let/const 的 for 语句来说)通常情况下只支持一个块级作用域。在 JavaScript 引擎实现支持 _let/const_ 的 for 语句时,就在这个地方做了特殊处理(为循环体增加一个作用域)。如果将 for 语句的块级作用域称为 forEnv,并将上述为循环体增加的作用域称为 loopEnv,那么 loopEnv 它的外部环境就指向 forEnv。
在 loopEnv 看来,变量 i 其实是登记在父级作用域 forEnv 中,并且 loopEnv 只能使用它作为名字 i 的一个引用。更准确地说,在 loopEnv 中访问变量 i ,在本质上就是通过环境链回溯来查找标识符(Resolve identifier, or Get Identifier Reference),形成了一个闭包。
for (let i in [1, 2])setTimeout(() => console.log(i), 1000) // 0, 1
上面这个例子创建了一些定时器,当定时器被触发时,函数会通过它的闭包(这些闭包处于 loopEnv 的子级环境中)来回溯,并试图再次找到那个标识符 i 。然而当定时器触发时,整个 for 迭代有可能都已经结束了。
这个 loopEnv 就必须是随每次迭代变化的。也就是说,需要为每次迭代都创建一个新的作用域副本,这称为迭代环境(iterationEnv)。因此每次迭代在实际上都并不是运行在 loopEnv 中,而是运行在该次迭代自有的 iterationEnv 中。也就是说,在语法上这里只需要两个块级作用域,而实际运行时却需要为其中的第二个块级作用域创建无数个副本。
循环与函数递归在语义上等价,所以事实上,上述这种 for 循环并不比使用函数递归节省开销。