一、编译原理
-
传统编译语言编译阶段:分词/词法分析(字符串分解成对编程语言来说有意义的代码块)——> 解析/语法分析(生成抽象语法树AST)——> 代码生成(AST转化成可执行代码)。
-
Javascript区别于它们:Javascript引擎不会有大量的(与其他语言编译器相比)时间用来优化,Javascript的编译过程并非发生在构建之前,而是在代码执行之前的几微秒内。因此,在本部分所要讨论的作用域背后,Javascript用了大量的办法来保证性能最佳(比如JIT,可以延迟编译甚至重编译)。
-
原因:其他语言会在编译时有大量时间进行编译,例如C++,最后会生成一个可执行文件。而javascript只会在代码执行前的几微秒内进行实现。
二、作用域
-
作用域:作用域由书写代码时函数声明的位置来决定。
-
作用域、引擎、编译器如何对话:以变量赋值var a = 2为例,编译器在当前作用域声明一个变量(此前没声明过),当运行时引擎会去作用域中查找是否存在该变量,若找到则进行赋值。
-
引擎在作用域中查找变量的三种方式:LHS查询、RHS查询、LHS和RHS兼具。 简单理解:赋值操作LHS,取值操作RHS
-
当变量出现在左侧时进行LHS查询:可理解为“找到赋值操作的目标”,通过找到容器本身来进行赋值。(“a = 2”,此时并不关心a的当前值是什么,目的只是想要为=2找到一个赋值对象)
-
当变量出现在右侧时进行(更准确为非左侧)RHS查询:可理解为“retrieve his source value(取得它的源值),即‘谁是赋值操作的源头’”,类似于简单地查找某个变量的值。(“console.log(a)”,此时,对a就是RHS引用,a并没有赋予任何值,我们需要去所谓“取得它的源值”来console.log)
-
兼具情况:
-
-
作用域链:作用域与作用域嵌套形成作用域链,变量的查询从当前作用域开始沿作用域链向全局作用域进行查找。
-
Reference同作用域判别失败相关;TypeError为作用域判别成功,但对结果的操作不合理。
-
词法作用域
-
含义:是一套关于引擎如何寻找变量以及会在何处找到变量的规则。
-
全局变量可以间接通过对全局对象属性的引用来避开“遮蔽效应(内部标识符'遮蔽'外部标识符)”从而进行访问。(example: window.a)
-
修改词法作用域方式:
-
eval:在运行期间改变词法作用域,生成自己独立的作用域,通常用来执行动态创建的代码。
-
with(不推荐) 两者均存在性能问题,引擎无法在编译时对作用域进行查找优化(以eval为例,首先会将eval值解析成代码,后再对其进行代码执行,JS执行两次),且严格模式下不支持。
-
-
三、函数作用域和块作用域
-
为了让任意代码段“私有”,通常通过作用域来包装(即封装)。
-
存在两个问题:
- 必须声明一个具名函数,其本身已经污染所在作用域
- 须显示调用才能运行其中代码
-
解决方案:
- 使用立即执行函数IIFE:
(function foo(){ .. })作为函数表达式意味着 foo 只能在 .. 所代表的位置中被访问,外部作用域则不行。foo 变量名被隐藏在自身中意味着不会非必要地污染外部作用域。
- 使用立即执行函数IIFE:
-
-
具名函数-匿名函数比较:
- 匿名函数在栈中追踪不会显示有意义的函数名,调试困难
- 无函数名在需引用自身时,只能使用过期的argument.callee
- 无函数名可读性差
-
let:该关键字可以将变量绑定到所在的任意作用域中(通常是 { .. } 内部)。let为其声明的变量隐式地劫持了所在的块作用域。
- 示例:
- 示例:
四、提升
- 函数声明本身会被提升,而包括函数表达式的赋值在内的赋值操作不会。
- 函数声明优先级>变量声明。
- JS引擎会把赋值操作当作两个独立的声明。左侧是编译阶段的任务,右侧是执行阶段的任务。
五、作用域闭包
-
简单理解:函数嵌套函数,子函数在外部引用,父函数作用域不被销毁。
-
具体理解:函数可以记住并访问所在的词法作用域,即使函数是在当前词法作用域之外执行。
-
模块函数必备特点:
- 必须有外部的封闭函数,该函数必须至少被调用一次(可通过IIFE进行首次自调用)。(每次调用都会创建一个新的模块实例)
- 封闭函数必须返回至少一个内部函数,这样内部函数才能在私有作用域中形成闭包,并且可以访问或者修改私有的状态。
-
模块函数每次被调用都会创建新的模块实例,可对该模式进行简单的改进来实现单例模式。
-
现代模块机制:大多数模块依赖加载器/管理器本质上都将这种模块封装进一个友好的API中。
- 案例:(AMDg规范)
- 案例符合 2 中的两个必备特点:调用包装了函数定义的包装函数,并且将返回值作为该模块的 API。
- 案例:(AMDg规范)
-
es6和conmonjs两种模块的比较:
-
共性:都是js具有模块化功能的规范
-
区别:
-
es6:
- export::可以输出多个,输出方式为 {}
- export default:只能输出一个 ,可以export 同时输出,但是不建议这么做
- 解析阶段确定对外输出的接口,解析阶段生成接口
- 模块不是对象,加载的不是对象,而是声明式代码的集合
- 可以单独加载其中的某个接口(方法)。
- 静态分析(论ES6模块系统的静态解析),动态引用,输出的是值的引用,值改变,引用也改变,即原来模块中的值改变则该加载的值也改变
-
commonJS:
- module.exports = ... :只能输出一个,且后面的会覆盖上面的
- exports. ... :可以输出多个
- 运行阶段确定接口,运行时才会加载模块
- 模块就是对象,加载的是该对象,纯JS系统,就是不依赖其他机制如预处理
- 加载的是整个模块,即将所有的接口全部加载进来
- 输出的是值的拷贝,即原来模块中的值改变不会影响已经加载的该值\
-
-