YDKJS-作用域与闭包

452 阅读5分钟

主要内容:问题回答

这一小册回答了两个问题:

  1. 从JS是编译型语言讲起,形成了什么样的作用域
  2. 观察到闭包,闭包最重要的应用是模块模式

下面的所有内容在回答剖析这些问题。

从JS是编译型语言讲起

作用域是什么?

作用域是一组规则,定义了变量存储在哪,在程序需要的时候如何找到。

JavaScript是解释型语言,还是编译型语言?

答案是编译型语言。虽然它不像许多传统意义上的编译型语言——预先被编译好,编译结果在不同的分布式系统间移植。但是它边编译边执行,也经历“编译”的三个步骤:

  1. 分词/词法分析:一连串字符打断成有意义的片段(token)

  2. 解析:将token的流(数组)转换为AST——抽象语法树

    比如var a = 2的AST:

  1. 代码生成:根据目标平台等因素转换为可执行的代码

var a = 2为例,简述JS引擎、编译器、作用域在这行代码编译执行过程中三个角色的工作。

  1. 编译器编译过程,将var a = 2切分成var aa = 2两部分处理。var a去咨询作用域存在变量a没有,存在则创建,不存在则这一步工作完成。生成a = 2的执行代码。
  2. JS引擎执行过程,拿到a = 2的执行代码。咨询作用域变量a在哪,取变量a,对变量a做赋值操作。如果变量不存在则报错。

LHS(Left-hand Side)、RHS(Right-hand Side)指什么?LHS和RHS还有什么区别?

LHS、RHS均指在代码执行过程,JS引擎向作用域咨询变量。以var a = b为例,咨询a是LHS,咨询b是RHS,LHS是拿变量的地址,接下来对变量赋值,RHS是取变量的值做其他操作。

LHS如果找不到变量,非严格模式下会全局创建这个变量而不报错,严格模式下报错ReferenceError;RHS找不到直接报Reference。(TypeError是在找到变量情况下的错误使用)

形成什么样的作用域

JavaScript的作用域怎么样的?

作用域分两种,一种是词法作用域——作用域在词法分析时确定(变量/函数声明的位置,编写时词法作用域),另一种是动态作用域——函数在哪里调用,运行时确定作用域。

JavaScript是词法作用域,更常见的是嵌套作用域(楼层模型/气泡模型)。产生一个楼层/气泡的方法有两种:函数作为作用域,块儿作为作用域。

补充一个细微的细节是,在作用域内各种位置的声明(变量声明和函数声明)会提升至当前作用域顶部。

欺骗词法作用域是什么?它为什么会导致更低下的性能。

词法作用域在编写时确定,欺骗词法作用域是在执行时改变词法作用域,有evalwith两种机制。eval函数接受字符串做参数值,将字符串内容看作代码在执行阶段执行,如果字符串涉及变量声明,就改变词法作用域。with语句接收一个对象,将对象属性视为这个"作用域"的标识符,创建一个新的词法作用域,如果在with块内有变量声明,归于包含这个作用域的函数作用域。在严格模式下,with不能使用,eval可使用但不允许声明变量。

JS引擎在编译阶段做的性能优化是和词法分析相关的,如果有一个evalwith,存在可能性去改变词法分析时确定的作用域,所以会认为优化会变得没有意义而不去做优化,从而导致性能低下。

观察到闭包

闭包是什么?能写个简单demo吗?实际开发中哪里有用到闭包?

闭包是函数能记住并访问它的词法作用域,无论是在词法作用域之外还是词法作用域内执行。

简单demo(学术派)

回调函数

循环+闭包

闭包最重要的应用是模块模式?

模块是闭包最重要的应用,说它最重要是它不像回调那样浮于表面,是最强大的应用。

demo:模块、揭示模块(基于函数)

现代模块(各种模块依赖加载器/消息机制)实质上是将这种模块定义(参考上面demo)包装进一个友好的API。

demo:现代模块,模块机制包装(基于函数)

基于函数的模块不能被编译器识别,API语义直到运行时才会被考虑,实际上在运行时期间可以修改模块API;另一种方式,ES6为模块增加了头等的语法支持,一个文件一个独立的模块,可以导入其他模块也可以导出自己的API,这样的方式编译器可以识别,运行时不能修改。

在模块文件内部的内容被视为包围在一个作用域闭包内。

主要内容之外

主要内容之后,记录了块儿作用域和函数包围代码两个有意思的点:

  1. 掘金-块儿作用域の使用价值
  2. 作用域-函数包围代码

参考链接:

YDKJS-SCOPE&CLOSURES