type
status
date
slug
summary
tags
category
icon
password

1.2 理解作用域

引擎从头到尾负责整个JavaScript程序的编译及执行过程。· 编译器引擎的好朋友之一,负责语法分析及代码生成等脏活累活(详见前一节的内容)。
· 作用域引擎的另一位好朋友,负责收集并维护由所有声明的标识符(变量)组成的一系列查询,并实施一套非常严格的规则,确定当前执行的代码对这些标识符的访问权限。
变量的赋值操作会执行两个动作,首先编译器会在当前作用域中声明一个变量(如果之前没有声明过),然后在运行时引擎会在作用域中查找该变量,如果能够找到就会对它赋值。
RHS查询与简单地查找某个变量的值别无二致,而LHS查询则是试图找到变量的容器本身
LHS和RHS的含义是“赋值操作的左侧或右侧”并不一定意味着就是“=赋值操作符的左侧或右侧”。赋值操作还有其他几种形式,因此在概念上最好将其理解为“赋值操作的目标是谁(LHS)”以及“谁是赋值操作的源头(RHS)

1.3 作用域嵌套

当一个块或函数嵌套在另一个块或函数中时,就发生了作用域的嵌套。因此,在当前作用域中无法找到某个变量时,引擎就会在外层嵌套的作用域中继续查找,直到找到该变量,或抵达最外层的作用域(也就是全局作用域)为止。

第2章 词法作用域

作用域共有两种主要的工作模型。第一种是最为普遍的,被大多数编程语言所采用的词法作用域,我们会对这种作用域进行深入讨论。另外一种叫作动态作用域,仍有一些编程语言在使用(比如Bash脚本、Perl中的一些模式等)。

2.1 词法阶段

词法作用域就是定义在词法阶段的作用域。换句话说,词法作用域是由你在写代码时将变量和块作用域写在哪里来决定的,因此当词法分析器处理代码时会保持作用域不变(大部分情况下是这样的)。
事实上,让词法作用域根据词法关系保持书写时的自然关系不变,是一个非常好的最佳实践。
没有任何函数的气泡可以(部分地)同时出现在两个外部作用域的气泡中,就如同没有任何函数可以部分地同时出现在两个父级函数中一样。

2.2 欺骗词法

欺骗词法
欺骗词法作用域会导致性能下降。
JavaScript中的eval(..)函数可以接受一个字符串为参数,并将其中的内容视为好像在书写时就存在于程序中这个位置的代码。
eval(..)调用中的"var b = 3; "这段代码会被当作本来就在那里一样来处理。由于那段代码声明了一个新的变量b,因此它对已经存在的foo(..)的词法作用域进行了修改。事实上,和前面提到的原理一样,这段代码实际上在foo(..)内部创建了一个变量b,并遮蔽了外部(全局)作用域中的同名变量。
with可以将一个没有或有多个属性的对象处理为一个完全隔离的词法作用域,因此这个对象的属性也会被处理为定义在这个作用域中的词法标识符。

2.3 小结

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

3.1 函数中的作用域

JavaScript具有基于函数的作用域,意味着每声明一个函数都会为其自身创建一个气泡,而其他结构都不会创建作用域气泡。但事实上这并不完全正确,下面我们来看一下
无论标识符声明出现在作用域中的何处,这个标识符所代表的变量或函数都将附属于所处作用域的气泡
函数作用域的含义是指,属于这个函数的全部变量都可以在整个函数的范围内使用及复用(事实上在嵌套的作用域中也可以使用)

3.2 隐藏内部实现

有很多原因促成了这种基于作用域的隐藏方法。它们大都是从最小特权原则中引申出来的,也叫最小授权或最小暴露原则。这个原则是指在软件设计中,应该最小限度地暴露必要内容,而将其他内容都“隐藏”起来,比如某个模块或对象的API设计。
“隐藏”作用域中的变量和函数所带来的另一个好处,是可以避免同名标识符之间的冲突,两个标识符可能具有相同的名字但用途却不一样,无意间可能造成命名冲突。冲突会导致变量的值被意外覆盖。
bar(..)内部的赋值表达式i = 3意外地覆盖了声明在foo(..)内部for循环中的i。在这个例子中将会导致无限循环,因为i被固定设置为3,永远满足小于10这个条件。
这些库通常会在全局作用域中声明一个名字足够独特的变量,通常是一个对象。这个对象被用作库的命名空间,所有需要暴露给外界的功能都会成为这个对象(命名空间)的属性,而不是将自己的标识符暴露在顶级的词法作用域中。
另外一种避免冲突的办法和现代的模块机制很接近,就是从众多模块管理器中挑选一个来使用。使用这些工具,任何库都无需将标识符加入到全局作用域中,而是通过依赖管理器的机制将库的标识符显式地导入到另外一个特定的作用域中。

3.3 函数作用域

在任意代码片段外部添加包装函数,可以将内部的变量和函数定义“隐藏”起来,外部作用域无法访问包装函数内部的任何内容。
首先,必须声明一个具名函数foo(),意味着foo这个名称本身“污染”了所在作用域(在这个例子中是全局作用域)。其次,必须显式地通过函数名(foo())调用这个函数才能运行其中的代码。如果函数不需要函数名(或者至少函数名可以不污染所在作用域),并且能够自动运行,这将会更加理想。
函数会被当作函数表达式而不是一个标准的函数声明来处理。
区分函数声明和表达式最简单的方法是看function关键字出现在声明中的位置(不仅仅是一行代码,而是整个声明中的位置)。如果function是声明中的第一个词,那么就是一个函数声明,否则就是一个函数表达式。
立即执行函数表达式
函数名对IIFE当然不是必须的,IIFE最常见的用法是使用一个匿名函数表达式。虽然使用具名函数的IIFE并不常见,但它具有上述匿名函数表达式的所有优势,因此也是一个值得推广的实践。
IIFE还有一种变化的用途是倒置代码的运行顺序,将需要运行的函数放在第二位,在IIFE执行之后当作参数传递进去

3.4 块作用域

我们在for循环的头部直接定义了变量i,通常是因为只想在for循环内部的上下文中使用i,而忽略了i会被绑定在外部作用域(函数或全局)中的事实。

4.2 编译器再度来袭

foo函数的声明(这个例子还包括实际函数的隐含值)被提升了

4.3 函数优先

函数声明和变量声明都会被提升。但是一个值得注意的细节(这个细节可以出现在有多个“重复”声明的代码中)是函数会首先被提升,然后才是变量。
一个普通块内部的函数声明通常会被提升到所在作用域的顶部,这个过程不会像下面的代码暗示的那样可以被条件判断所控制:

5.1 启示

JavaScript中闭包无处不在,你只需要能够识别并拥抱它。闭包并不是一个需要学习新的语法或模式才能使用的工具,它也不是一件必须接受像Luke[插图]一样的原力训练才能使用和掌握的武器。闭包是基于词法作用域书写代码时所产生的自然结果,你甚至不需要为了利用它们而有意识地创建闭包。

5.2 实质问题

在foo()执行后,通常会期待foo()的整个内部作用域都被销毁,因为我们知道引擎有垃圾回收器用来释放不再使用的内存空间。由于看上去foo()的内容不会再被使用,所以很自然地会考虑对其进行回收。
bar()依然持有对该作用域的引用,而这个引用就叫作闭包。

5.3 现在我懂了

将一个内部函数(名为timer)传递给setTimeout(..)。timer具有涵盖wait(..)作用域的闭包,因此还保有对变量message的引用。
在定时器、事件监听器、Ajax请求、跨窗口通信、Web Workers或者任何其他的异步(或者同步)任务中,只要使用了回调函数,实际上就是在使用闭包!
因为函数(示例代码中的IIFE)并不是在它本身的词法作用域以外执行的。它在定义时所在的作用域中执行(而外部作用域,也就是全局作用域也持有a)。a是通过普通的词法作用域查找而非闭包被发现的。

5.4 循环和闭包

在迭代内使用IIFE会为每个迭代都生成一个新的作用域,使得延迟函数的回调可以将新的作用域封闭在每个迭代内部,每个迭代中都会含有一个具有正确值的变量供我们访问。

5.6 小结

当函数可以记住并访问所在的词法作用域,即使函数是在当前词法作用域之外执行,这时就产生了闭包。

附录A 动态作用域

主要区别:词法作用域是在写代码或者说定义时确定的,而动态作用域是在运行时确定的。(this也是!)词法作用域关注函数在何处声明,而动态作用域关注函数从何处调用。

附录B 块作用域的替代方案

Google维护着一个名为Traceur的项目,该项目正是用来将ES6代码转换成兼容ES6之前的环境(大部分是ES5,但不是全部)。

附录C this词法

问题在于cool()函数丢失了同this之间的绑定。解决这个问题有好几种办法,但最常用的就是var self = this;。
 
 
《哲学纲要》你不知道的JavaScript(中卷)