《你不知道的js》作用域和闭包

前言

储存和访问变量的值,这种能力将状态带给了程序需要讨论的问题:变量储存在哪里?程序需要时如何找到它们?

2020.12.12解答:存储在引擎中,程序需要时通过作用域来找到它

1.作用域是什么

1.1 js的编译原理

程序中的一段源代码在执行之前会经历的三个步骤:

1.分词/词法分析

将字符串分解成有意义的代码块(词法单元)

2.解析/语法分析

将词法单元流(数组)转换成一个由元素逐级嵌套所组成的代表了程序语法

结构的树(抽象语法树AST)

3.代码生成

将 AST 转换为可执行代码

举例:

有某种方法可以将 var a = 2; 的 AST 转化为一组机器指令,用来创建一个叫作 a 的变量(包括分配内存等),并将一个值储存在 a 中。

ps:js的编译发生在代码执行前,JavaScript 引擎用尽了各种办法(比如 JIT,可以延

迟编译甚至实施重编译)来保证性能最佳。

类比:

1.2 理解作用域

作用域是一套规则,用于确定在何处以及如何查找变量(标识符)。

1.规则详情:

+ 1.LHS ,RHS

如果查找的目的是对变量进行赋值,那么就会使用 LHS 查询(查找赋值的目标是否存在);

如果目的是获取变量的值,就会使用 RHS 查询(查找赋值的发起者是否存在)。 

赋值操作符会导致 LHS 查询。=操作符或调用函数时传入参数的操作都会导致关联作用域的赋值操作。

+ 2.编译过程

JavaScript 引擎首先会在代码执行前对其进行编译,在这个过程中,像 var a = 2 这样的声明会被分解成两个独立的步骤:

+ 首先,var a 在其作用域中声明新变量。这会在最开始的阶段,也就是代码执行前进行。

+ 接下来,a = 2 会查询(LHS 查询)变量 a 并对其进行赋值。

LHS 和 RHS 查询都会在当前执行作用域中开始,如果有需要(也就是说它们没有找到所 需的标识符),就会向上级作用域继续查找目标标识符,这样每次上升一级作用域(一层 楼),最后抵达全局作用域(顶层),无论找到或没找到都将停止。 

+ 3.异常

不成功的 RHS 引用会导致抛出 ReferenceError 异常。不成功的 LHS 引用会导致自动隐式 地创建一个全局变量(非严格模式下),该变量使用 LHS 引用的目标作为标识符,或者抛 出 ReferenceError 异常(严格模式下)。

类比:待补充

2. 词法作用域

词法作用域意味着作用域是由书写代码时函数声明的位置来决定的。

编译的词法分析阶段 ,基本能够知道全部标识符在哪里以及是如何声明的,从而能够预测在执行过程中如何对它 们进行查找。

3.函数作用域和块作用域

3.0 学习前的提问

作用域包含了一系列的“气泡”,每一个都可以作为容器,其中包含了标识符(变量、函数)的定义。这些气泡互相嵌套并且整齐地排列成蜂窝型,排列的结构是在写代码时定义的。 但是,究竟是什么生成了一个新的气泡?只有函数会生成新的气泡吗? JavaScript 中的其他结构能生成作用域吗?

回答:块作用域,不是,能,比如说try/catch中的catch,let,const

3.1 函数中的作用域

JavaScript 具有基于函数的作用域,意味着每声明 一个函数都会为其自身创建一个气泡,也有其他结构会创建作用域气泡。

函数作用域的含义是指,属于这个函数的全部变量都可以在整个函数的范围内使用及复用(事实上在嵌套的作用域中也可以使用)。

3.2 隐藏内部实现

可以把变量和函数包裹在一个函数的作用域中,然后用这个作用域 来“隐藏”它们。

3.2.1 为什么“隐藏”变量和函数是一个有用的技术?

1.符合从最小特权原则这个原则是指在软件设计中,应该最小限度地暴露必要内容,而将其他内容都“隐藏”起来,比如某个模块或对象的 API 设计。

2.规避冲突可以避免同名标识符之间的冲突,冲突会导致变量的值被意外覆盖。

典型例子:

1.全局命名空间当程序中加载了多个第三方库时,如果它 们没有妥善地将内部私有的函数或变量隐藏起来,就会很容易引发冲突。这些库通常会在全局作用域中声明一个名字足够独特的变量,通常是一个对象

这个对象 被用作库的命名空间,所有需要暴露给外界的功能都会成为这个对象(命名空间)的属 性,而不是将自己的标识符暴漏在顶级的词法作用域中。(比如jq)

2.模块管理另外一种避免冲突的办法和现代的模块机制很接近,就是从众多模块管理器中挑选一个来 使用。使用这些工具,任何库都无需将标识符加入到全局作用域中,而是通过依赖管理器 的机制将库的标识符显式地导入到另外一个特定的作用域中。

显而易见,这些工具并没有能够违反词法作用域规则的“神奇”功能。它们只是利用作用 域的规则强制所有标识符都不能注入到共享作用域中,而是保持在私有、无冲突的作用域 中,这样可以有效规避掉所有的意外冲突。

3.3 函数作用域

在任意代码片段外部添加包装函数,可以将内部的变量和函数定义“隐 藏”起来,外部作用域无法访问包装函数内部的任何内容

3.3.1 添加包装函数的问题和解决方案

1.问题

1.声明具名函数,对当前作用域产生污染

2.需要调用这个具名函数

2.解决方案

(functionfoo(){

vara=3;

console.log(a);

})();

函数会被当作函数表达式而不是一个标准的函数声明来处理函数声明和函数表达式之间最重要的区别是它们的名称标识符将会绑定在何处

3.匿名和具名

1.匿名函数的缺点

1.匿名函数在栈追踪中不会显示出有意义的函数名,使得调试很困难

2.如果没有函数名,当函数需要引用自身时只能使用已经过期的 arguments.callee 引用, 比如在递归中。另一个函数需要引用自身的例子,是在事件触发后事件监听器需要解绑 自身。

3.匿名函数省略了对于代码可读性 / 可理解性很重要的函数名。一个描述性的名称可以让 代码不言自明

2.解决办法

给函数表达式命名

4.立即执行函数表达式IIFE

IIFE 的另一个非常普遍的进阶用法是把它们当作函数调用并传递参数进去ps:一个应用场景是解决 undefined 标识符的默认值被错误覆盖导致的异常一个应用场景是倒置代码的运行顺序,

3.4 块作用域

变量的声明应该距离使用的地方越近越好,并最大限度地本地化。

3.4.1 with

用 with 从对象中创建出的作用域仅在 with 声明中而非外 部作用域中有效

3.4.2 try/catch

try/catch 的 catch 分句会创建一个块作 用域,其中声明的变量仅在 catch 内部有效

3.4.3 let

在声明中的任意位置都可以使用 { .. } 括号来为 let 创建一个用于绑 定的块但是使用 let 进行的声明不会在块作用域中进行提升。声明的代码被运行之前,声明并不 “存在

1.应用场景

1.垃圾收集不需要使用的变量可以被回收

2.let循环

3.4.4 const

可以用来创建块作用域变量,但其值是固定的 (常量)

3.5 总结

函数是 JavaScript 中最常见的作用域单元。本质上,声明在一个函数内部的变量或函数会 在所处的作用域中“隐藏”起来,这是有意为之的良好软件的设计原则。

但函数不是唯一的作用域单元。块作用域指的是变量和函数不仅可以属于所处的作用域, 也可以属于某个代码块(通常指 { .. } 内部)。 从 ES3 开始,try/catch 结构在 catch 分句中具有块作用域。 在 ES6 中引入了 let 关键字(var 关键字的表亲),用来在任意代码块中声明变量。if (..) { let a = 2; } 会声明一个劫持了 if 的 { .. } 块的变量,并且将变量添加到这个块中。

你可能感兴趣的:(《你不知道的js》作用域和闭包)