第一部分: 作用域与闭包

你不知道的JS

1. 作用域

1.1 编译原理

Javascript是动态或解释执行语言,但事实上是一门编译语言,但是与传统的编译语言不同,他不是提前编译,编译结果也不能在分布式系统中进行移植,尽管JavaScript引擎进行编译的步骤和传统的编译语言非常相似,在某些环节是非常复杂的

编译型语言: 编译型语言是相对于解释语言存在的,编译型语言首先将源代码编译成机器语言,再由机器运行机器码(二进制); 它在执行之前需要一个专门的编译过程,把程序编译成为机器语言文件,运行时不需要重新编译,直接使用编译的结果就行,执行效率依赖编译器

解释型语言: 解释型语言是相对于编译型语言存在的,源代码不是直接翻译成机器语言,而是先翻译成中间代码,再由解释器对中间代码进行解释运行,程序不需要编译,程序在运行时踩翻译成机器语言,每执行一次都要翻译一次

在传统编译语言的流程中,程序中的一段源代码在执行之前会经历三个步骤,统称为编译

1. 分词/词法分析

这个过程会将由字符组成的字符串分解成(对编程语言来说)有意义的代码块,这些代码块被称为词法单元,例如这段程序通常会被分解成下面这些词法单元、、、、、空格会被当成词法单元,取决于空格在这门语言中是否具有含义

2. 解析/语法分析

这个过程是将词法单元流(数组)转成一个由元素逐级嵌套组成的代表了程序语法结构的树.这个树被称为抽象树(AST) 的抽象语法树中可能会有一个叫作VariableDeclaration的顶级节点,接下来是一个叫Identifier(它的值是a)的子节点,以及一个叫做AssignmentExpression的子节点.AssignmentExpression节点有一个叫做NumericLiteral(它的值是2)的子节点

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-1S2TKExT-1582532883299)(C:Usersa4244Desktopimg23.png)]

3. 代码生成

将抽象树(AST)转换为可执行代码的过程被称为代码生成,这个过程与语言,目标平台等息息相关,抛开具体细节,简单来说就是某种方法可以将的AST转换为一组机器指令,用来创建一个叫做a的变量(包括分配内存等),并将一个值存储到a中

Javascript的编译过程不是发生在构建之前,对于JavaScript来说,大部分情况下编译发生在代码执行前的几微秒(甚至更短)的时间内

引擎: 从头到尾负责整个JavaScript程序的编译及执行过程

编译器: 引擎的好朋友之一,负责语法分析及代码生成等脏活累活

作用域: 引擎的另一位好朋友,负责收集并维护由所有声明的标识符(变量)组成的一系列查询,并实施一套非常严格的规则,确定当前执行的代码对这些标识符的访问权限

1. 2. 运行

  1. 遇到,编译器会询问作用域是否已经有一个该名称的变量存在于同一个作用域的集合中,如果有,编译器会忽略该声明,继续进行编译;否则他会要求作用域在当前作用域的集合中声明一个新变量,并命名为a

  2. 接下来编译器会为引擎生成运行时所需要的代码,这些代码被用来处理这个赋值操作.引擎运行时会首先询问作用域,在当前的作用域中是否存在一个叫做变量.如果是,引擎会使用这个变量,否则引擎会继续查找改变量

  3. 如果引擎找到最终的a,就会赋值2给它,否则引擎就会举手示意并抛出异常

总结: 变量赋值会执行两个操作,首先编译器会在当前作用域中声明一个变量(前提之前没有声明过),然后在运行时引擎会在作用域中查找该变量,如果能够找到就会对它赋值

1.3 编译器

编译器在编译过程的第二步中生成了代码,引擎执行它时,会通过查找变量a来判断它是否已经被声明过,查找的过程由作用域进行协助,但是引擎执行怎么样的查找会影响最终的结果

在上述的例子中,引擎会为变量进行LHS查询.另外一个查找的类型叫做RHS

  • RHS查询: 就是查找某个变量
    • foo(…),查询foo函数
    • 函数内Console对象的查询
    • 对函数参数进行RHS查询
  • LHS查询: 试图找到变量的容器本身,从而可以对其赋值,把2赋值给参数a时,进行LHS查询

LHS: 赋值操作的目标是谁

RHS: 谁是赋值操作的源头

最后一行foo(…)函数的调用需要对foo进行RHS引用,一位着”去找到foo”的值,并把它给我并且意味着foo的值需要被执行,因此它最好真的是一个函数类型的值

代码中隐式的操作可能很容易被忽略掉,这个操作发生在2被当做参数传递给foo(…)函数时,2会被分配给参数a,伪类该参数a(隐式的分配值),需要进行一次LHS查询

这里还有对a进行RHS引用,并且将得到值传递给console.log(…) console.log(…)本身也需要一个引用才能执行因此console对象进行RHS查询,并且检查得到的值中是否有一个叫做log的方法

最后在概念上可以理解为LHS和RHS之间通过对值2进行交互来将其传递进log(…)(通过变量a的RHS查询). 假设在log(…)函数的原生实现中他可以接收参数,在将2赋值给其中第一个(也许叫做args1)参数之前,这个参数需要进行LHS引用查询

1.4 对话

  • 引擎: 我说作用域,我需要为foo进行RHS引用.你见过他吗

  • 作用域: 别说,我见过他,编译器那小子刚刚声明过他,它是一个函数,给你

  • 引擎: 根们你太够意思了,好吧我来执行下foo

  • 引擎: 作用域,我还有个事,我需要为a进行LHS引用,你见过吗

  • 作用域: 这个我见过,编译器最近把她声明为foo的一个形式参数了,你拿去吧

  • 引擎: 好的,现在我就把2赋值给a , 还有console进行RHS引用, 你见过他吗/p>

  • 作用域: 这个巧了,我就是干这个的,这个我也有,console是一个内置对象,给你,

  • 引擎: 等下,我看看这里面有没有log(…) , 我看见了,是一个函数

  • 引擎: 能帮我找一个对a的RHS引用吗,虽然我记得它但是我想确认下

  • 作用域: 放心吧,这个变量我没有动过,拿走

  • 引擎: 真棒,我来把a的值,也就是2, 传递进log(…)

1.5 测验

引擎:作用域老哥,问你个事。

作用域:引擎老弟,啥事,尽管说。

引擎:我需要对c进行LHS引用,你见过它吗/p>

作用域:哦,这事啊,见过,刚刚编译器老弟刚声明了它,他是一个变量,拿去吧。

引擎:谢谢老哥,那我给他赋值,还有老哥,我要对foo进行RHS引用。

作用域:这个也有的,我找找,喏,在这,是一个函数,给你。

引擎:好嘞!太感谢了老哥,那我来执行下foo。

作用域:那都不是事!看看还有吗在帮你看看!

引擎:扎心了老铁!还有还有,我要对a进行LHS引用,帮我找找有它吗/p>

作用域:稍等啊,有有,刚刚编译器把他声明为foo的形式参数了,给你。

引擎:好的,老哥我要以身相许!还有我要对b进行LHS引用,你见过吗/p>

作用域:、、、只要别以身相许都好说,b在、、、哦,在这,它是foo函数里的,一个变量,给你 给你!

引擎:么么哒老哥!那这个a呢,我要对它进行RHS引用,虽然有点印象,但是还是确认下。

作用域:是的没错,还是那个a没有动过,放心拿走用吧!

引擎:好的好的!老哥,最后一个问题,帮帮老弟,帮我我就是你的,我要分别对a b 进行RHS引 用,在帮老弟看看确认下,拜托啦,萌萌哒!

作用域:(?Д?)ノ,有有有,都没变过,你放心用吧!

引擎:都不知道怎么谢老哥了,今晚有空不,今晚、、、诶,别走啊老哥,明晚也行、、、

1.6 作用域嵌套

作用域是根据名称查找变量的一套规则

对b进行RHS引用无法在函数foo内部完成,但可以在上一级作用域中完成

1.7 异常

为什么区分 LHS 和 RHS 是一件重要的事情因为在变量还没有声明(在任何作用域中都无法找到该变量)的情况下,这两种查询的行 为是不一样的。

第一次对 b 进行 RHS 查询时是无法找到该变量的。也就是说,这是一个“未声明”的变 量,因为在任何相关的作用域中都无法找到它。

如果 RHS 查询在所有嵌套的作用域中遍寻不到所需的变量,引擎就会抛出 ReferenceError 异常。值得注意的是,ReferenceError 是非常重要的异常类型。

相较之下,当引擎执行 LHS 查询时,如果在顶层(全局作用域)中也无法找到目标变量, 全局作用域中就会创建一个具有该名称的变量,并将其返还给引擎,前提是程序运行在非 “严格模式”下。 “不,这个变量之前并不存在,但是我很热心地帮你创建了一个。”

ES5 中引入了“严格模式”。同正常模式,或者说宽松 / 懒惰模式相比,严格模式在行为上有很多不同。其中一个不同的行为是严格模式禁止自动或隐式地创建全局变量。因此,在 严格模式中 LHS 查询失败时,并不会创建并返回一个全局变量,引擎会抛出同 RHS 查询 失败时类似的 ReferenceError 异常。 接下来,如果 RHS 查询找到了一个变量,但是你尝试对这个变量的值进行不合理的操作, 比如试图对一个非函数类型的值进行函数调用,或着引用 null 或 undefined 类型的值中的 属性,那么引擎会抛出另外一种类型的异常,叫作 TypeError。 ReferenceError 同作用域判别失败相关,而 TypeError 则代表作用域判别成功了,但是对 结果的操作是非法或不合理的。

2. 作用域

2.1 作用域

作用域共有两种主要的工作模型: 第一种是被大多数编程语言所采用的.另一种是

2.2 词法阶段

词法作用域就是定义在词法阶段的作用域,换句话,词法作用域是由你在写代码时将变量和块作用域写在哪决定的,因此词法分析器处理代码时会保持作用域不变

第一部分: 作用域与闭包
①全局全局作用域,其中只有一个标识符: foo

②foo所创建的作用域,其中有三个标识符: a, bar,b

③bar所创建的作用域,其中只有一个标识符: c

在上一个代码片段中,引擎执行 console.log(…) 声明,并查找 a、b 和 c 三个变量的引 用。它首先从最内部的作用域,也就是 bar(…) 函数的作用域气泡开始查找。引擎无法在 这里找到 a,因此会去上一级到所嵌套的 foo(…) 的作用域中继续查找。在这里找到了 a, 因此引擎使用了这个引用。对 b 来讲也是一样的。而对 c 来说,引擎在 bar(…) 中就找到 了它。

2.3 欺骗词法

这种方法会导致性能下降

2.3.1 eval

Javascript中的eval()函数可以接受一个字符串为参数

使用eval的方法是通过代码欺骗和假装成书写时(也就是词法期)代码就在那,来实现修改词法作用域环境

执行eval之后的代码时,引擎并不”知道”前面的代码是以动态形式插入进来的,并对词法作用域的环境进行修改的.引擎只会如往常的进行词法作用域查找

eval(…) 调用中的 “var b = 3;” 这段代码会被当作本来就在那里一样来处理。由于那段代 码声明了一个新的变量 b,因此它对已经存在的 foo(…) 的词法作用域进行了修改。事实 上,和前面提到的原理一样,这段代码实际上在 foo(…) 内部创建了一个变量 b,并遮蔽 了外部(全局)作用域中的同名变量。

当 console.log(…) 被执行时,会在 foo(…) 的内部同时找到 a 和 b,但是永远也无法找到 外部的 b。因此会输出“1, 3”而不是正常情况下会输出的“1, 2”。

默认情况下,如果 eval(…) 中所执行的代码包含有一个或多个声明(无论是变量还是函 数),就会对 eval(…) 所处的词法作用域进行修改

new Function(…) 函数的行为也很类似,最后一个参数可以接受代码字符串,并将其转 化为动态生成的函数(前面的参数是这个新生成的函数的形参)。这种构建函数的语法比 eval(…) 略微安全一些,但也要尽量避免使用。

2.3.2 with

with 通常被当作重复引用同一个对象中的多个属性的快捷方式,可以不需要重复引用对象 本身。

这个例子中创建了 o1 和 o2 两个对象。其中一个具有 a 属性,另外一个没有。foo(…) 函 数接受一个 obj 参数,该参数是一个对象引用,并对这个对象引用执行了 with(obj) {…}。 在 with 块内部,我们写的代码看起来只是对变量 a 进行简单的词法引用,实际上就是一个 LHS 引用,并将 2 赋值给它。

当我们将 o1 传递进去,a=2 赋值操作找到了 o1.a 并将 2 赋值给它,这在后面的 console. log(o1.a) 中可以体现。而当 o2 传递进去,o2 并没有 a 属性,因此不会创建这个属性,o2.a 保持 undefined。但是可以注意到一个奇怪的副作用,实际上 a = 2 赋值操作创建了一个全局的变量 a。这 是怎么回事/p>

with 可以将一个没有或有多个属性的对象处理为一个完全隔离的词法作用域,因此这个对 象的属性也会被处理为定义在这个作用域中的词法标识符。但是这个块内部正常的 var 声明并不会被限制在这个块的作用域中,而是被添加到 with 所处的函数作 用域中。

eval(…) 函数如果接受了含有一个或多个声明的代码,就会修改其所处的词法作用域,而 with 声明实际上是根据你传递给它的对象凭空创建了一个全新的词法作用域。

当我们传递 o1 给 with 时,with 所声明的作用域是 o1,而这个作用域中含 有一个同 o1.a 属性相符的标识符。但当我们将 o2 作为作用域时,其中并没有 a 标识符, 因此进行了正常的 LHS 标识符查找

o2 的作用域、foo(…) 的作用域和全局作用域中都没有找到标识符 a,因此当 a=2 执行 时,自动创建了一个全局变量(因为是非严格模式)。

2.4 动态作用域

动态作用域是 JavaScript 另一个重要机制 this 的表亲

词法作用域是一套关于引擎如何寻找变量以及会在何处找到变量的规 则。词法作用域最重要的特征是它的定义过程发生在代码的书写阶段(假设你没有使用 eval() 或 with)。

词法作用域让 foo() 中的 a 通过 RHS 引用到了全局作用域中的 a,因此会输出 2。

而动态作用域并不关心函数和作用域是如何声明以及在何处声明的,只关心它们从何处调 用。换句话说,作用域链是基于调用栈的,而不是代码中的作用域嵌套。 因此,如果 JavaScript 具有动态作用域,理论上,下面代码中的 foo() 在执行时将会输出 3。

因为当 foo() 无法找到 a 的变量引用时,会顺着调用栈在调用 foo() 的地 方查找 a,而不是在嵌套的词法作用域链中向上查找。由于 foo() 是在 bar() 中调用的, 引擎会检查 bar() 的作用域,并在其中找到值为 3 的变量 a。

但这其实是因为你可能只写过基于词法作用域的代码(或者至少以词法作用域为基础进行 了深入的思考),因此对动态作用域感到陌生。如果你只用基于动态作用域的语言写过代 码,就会觉得这是很自然的,而词法作用域看上去才怪怪的。 需要明确的是,事实上 JavaScript 并不具有动态作用域。它只有词法作用域,简单明了。 但是 this 机制某种程度上很像动态作用域。 主要区别:词法作用域是在写代码或者说定义时确定的,而动态作用域是在运行时确定 的。(this 也是!)词法作用域关注函数在何处声明,而动态作用域关注函数从何处调用。 最后,this 关注函数如何调用,这就表明了 this 机制和动态作用域之间的关系多么紧密。 如果想了解更多关于 this 的详细内容,需要理解 this 和对象原型。

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

3.1 隐藏内部实现

对函数的传统认知就是先声明一个函数,然后再向里面添加代码。但反过来想也可以带来 一些启示:从所写的代码中挑选出一个任意的片段,然后用函数声明对它进行包装,实际 上就是把这些代码“隐藏”起来了。

实际的结果就是在这个代码片段的周围创建了一个作用域气泡,也就是说这段代码中的任何声明(变量或函数)都将绑定在这个新创建的包装函数的作用域中,而不是先前所在的 作用域中。换句话说,可以把变量和函数包裹在一个函数的作用域中,然后用这个作用域 来“隐藏”它们。

有很多原因促成了这种基于作用域的隐藏方法。它们大都是从最小特权原则中引申出来 的,也叫最小授权或最小暴露原则。这个原则是指在软件设计中,应该最小限度地暴露必 要内容,而将其他内容都“隐藏”起来,比如某个模块或对象的 API 设计。

这个原则可以延伸到如何选择作用域来包含变量和函数。如果所有变量和函数都在全局作 用域中,当然可以在所有的内部嵌套作用域中访问到它们。但这样会破坏前面提到的最小 特权原则,因为可能会暴漏过多的变量或函数,而这些变量或函数本应该是私有的,正确 的代码应该是可以阻止对这些变量或函数进行访问的。

function doSomething(a) {    b

                                                        

声明:本站部分文章及图片源自用户投稿,如本站任何资料有侵权请您尽早请联系jinwei@zod.com.cn进行处理,非常感谢!

上一篇 2020年1月21日
下一篇 2020年1月21日

相关推荐