据说上卷很不错,先买上卷看,如果不错就在网上啃下卷英文版的~
真的印证了那句话,人的记忆是离散的。两个月前看的前三章,还划线标记各种,但现在基本不记得了,就记得描述作用域时作者用高楼大厦的比喻。哎,看来还是得多记笔记。
- 开篇作者用程序的本质引出了作用域的概念:
- 编程语言中,在一个变量中存储值,并能访问和修改,这种能力使得程序带上了状态的功能。哪这些变量住在哪里、存储在哪里?程序需要的时候如何访问它们呢?
- 这些问题说明需要一套设计良好的规则来存储变量,并且之后可以方便的访问它们。这套规则就程序作用域。
- JavaScript虽归类"动态"或"解释执行"语言,但事实上它是一门编译语言。
- 在传统编译语言的流程中,程序中的一段源代码在执行前会经历三个步骤,统称为"编译"。
- 分词/词法分析(Tokenizing/Lexing)
- 解析/语法分析
- 代码生成
- JavaScript的引擎不会有太多事件来进行优化,因为与其他语言不通,JavaScript的编译过程不是发生在构建之前的。对于JS来说,大部分情况下编译发生在代码执行前的几微秒(甚至更短)的时间内。简单来说,任何JS代码片段执行前都要进行编译(通常在执行前)。
- 引擎
- 从头到尾负责整个JavaScript程序的编译及执行过程;
- 编译器
- 引起的好友之一,负责语法分析及代码生成等脏活累活;(我太喜欢这句话了,哈哈~)
- 作用域
- 引起的另一位好友,负责收集维护由所有声明的标识符(变量)组成的一系列查询,并实施一套非常严格的规则,确定当前执行的代码对这些标识符的访问权限。
总结:变量的赋值操作会执行两个动作,首先编译器会在当前作用域中声明一个变量(如果之前没有生命过),然后在运行时引擎会在作用域中查找该变量,如果能够找到就会给他赋值。
- 1.2.5 小测验
function foo(a) {
var b = a;
return a + b;
}
var c = foo(2);
-
- 找到其中所有LHS查询。(这里有3处!)
- 答案:c; a; b 三处!
- 第一次LHS查询
var c = foo(2);
,引擎会问作用域,挨哥哥找一哈c,看以前给有声明了,声明了就别脱裤子放屁。然后就进行了LHS查询~ - 第二次LHS查询在foo函数执行前,引擎进行预解释时进行的
a = 2
,就是给foo函数形参赋值呢时候查找a时触发了LHS查询! - 第三次LHS查询在foo函数体内
var b = a
时触发的!
-
- 找到其中所有RHS查询。(这里有4处!)
- 答案:foo; a; a; b 四次!
- 第一次RHS查询发生在执行
foo(2)
时,其实了嘛,这里为啥是RHS,归根到底就是foo这个函数名的变量存的一个堆内存地址的值,所以未归到底它就是进行RHS值查询的噻! - 第二次RHS查询发生在
var b = a;
时,因为要查询到a的值到底是啥! - 第三次RHS查询发生在
return a + b;
时的a
! - 第四次RHS查询发生在
return a + b;
时的b
!
-
本小节中作用引出了LHS和RHS两种查询。
- 从字面解释分别是,一个赋值操作的左侧和右侧。
- 换句话说,当变量出现在赋值操作的左侧时进行LHS查询,出现在右侧时进行RHS查询。
- 讲得更精确点就是,RHS查询就是简单地查找某个变量的值;而LHS查询则是试图找到该变量容器的本体!
- RHS可以理解为"retrieve his source value"(取到它的源值)
- 赋值操作其实还有好几种形式,因此在概念上最好将其理解为“赋值操作的目标是谁(LHS)”以及“谁是赋值操作的源头(RHS)”,简单点就是LHS为查询赋值目标,RHS为查询赋值源头。
作用域是根据名称查找变量的一套规则
- 当一个块或函数嵌套在另一个块或函数中时,就发生了作用于嵌套。因此,在当前作用域中无法找到某个变量时,引擎就会在外层嵌套的作用域中继续查找,直到找到该变量,或抵达最外层作用域(也就是全局作用域)为止。
- 如果RHS在所有作用域中寻不到所需变量,引擎就会抛出
ReferneceError
异常; - 如果RHS查找到一个变量,比如试图对一个非函数类型的值进行函数调用,或者引用null或undefined类型的值中的属性,对该变量进行不合理的操作就会触发
TypeError
异常;
作用域就是一套规则,用于确定在何处以及如何查找变量(标识符)。
如果查找的目的是对变量进行赋值,那么就会使用LHS查询;如果目的是获取变量的值,就会使用RHS查询。赋值操作也会导致LHS查询。等号操作符或调用函数时传入参数的操作都会知道关联作用域的赋值操作。(也就是预解释阶段)
-
JavaScript引擎首先会在代码执行前对其编译,在这个过程中,像
var a = 2
这样的声明会被分解成两个独立的步骤:- var 2 在其作用域中声明新变量。这个是在预解释中处理啦~
- a = 2 会查询(LHS查询)变量a并对其进行赋值。
-
LHS和RHS查询都会现在当前作用域中开始查找,如果有需要会逐层向上查找,直至找到全局作用域结束。
-
不成功的RHS引用会导致抛出
ReferenceError
异常。不成功的LHS引用会导致自动隐藏地创建一个全局变量(非严格模式下),或者如果严格模式下则也会抛出ReferenceError
异常。
作用域共分两种主要的工作模型。
第一种是最为普遍的,被大多数编程语言采用的词法作用域。 第二种叫做动态作用域,在一些脚本语言中使用,如Bash、Perl。
- 词法阶段是大部分编译器的第一个工作阶段,也交词法化或单词化。词法化的过程会对源代码中的字符进行检查,如果有状态的解析过程,还会赋予单词语义。
- 简单来说词法作用域就是定义在词法阶段的作用域。
-
气泡(1)包含着整个作用域,其中只有一个标识符:foo(说来也对,我咋知道气泡1就是全局作用域,说不定它还被包着,不能乱下定义)
-
气泡(2)是
foo
函数创建的作用域,形参也算,其中包含三个标识符:a
、bar
和b
-
气泡(3)是
bar
函数创建的作用域,仅包含一个标识符:c
-
作用域气泡由对应的作用域块代码写在哪里决定(也就是说你函数写哪,作用域就被包含在那个里面),它们是逐级包含的
-
这里所说的气泡都是严格包含的,不存在可越界。也即是说,没有任何函数的气泡可以(部分的)同时出现在两个外部作用域的气泡中,好比没有任何函数可以部分地同时出现在两个父级函数中
-
关于查找
- 作用域查找会找到第一个匹配的标识符时停止
- 在多次作用域中定义同名的标识符,这叫“遮蔽效应”,即内部标识符“遮蔽”了外部标识符
- 引擎进行查找始终从运行时所处的最内部作用域开始,逐级向外或向上进行查找,直到遇到第一个匹配的标识符为止
全局变量会自动成为全局对象的属性,因此可以不直接通过全局对象的词法名称,而是间接地通过全局对象属性的引用来对其访问。
window.aa
,通过这种技术可以访问到那些被同名变量说遮蔽的全局变量!但如果非全局变量被遮蔽了,无论如何都无法被访问到。
重点来了!无法函数在哪里被调用,也无论它如何被调用,它的词法作用域都只由函数被声明时所处的位置决定!
- 词法作用域查找只会查找一级标识符,比如a、b和c。如果代码中引用了
foo.bar.baz
,词法作用域查找只会试图查找foo标识符,找到这个变量后,对象属性访问规则会分别接管对bar和baz属性的访问。- 买买,这句话有够绕的,我觉得应该如下理解:
- 词法作用域仅查找一级标识符,如果试图找
foo.bar.baz
时,通过foo
找到对象的地址,然后返回对这个地址的访问权,可以xx.bar.baz来访问三级标识符 - 在简单点就是,词法作用域只找到内存地址,具体是堆还是栈我管不着