《你不知道的JavaScript》上卷 的精华整理
本篇讲述:【JavaScript编译】【函数作用域】【块作用域】
第一部分 作用域和闭包
第一章 作用域是什么
1.1 编译原理
- 分词/词法分析
例:var a = 2;
对这段代码进行词法分析,通常会分解成var
a
=
2
;
。 - 解析/语法分析
生成“抽象语法树(Abstract Syntax Tree, AST)” - 代码生成
通过AST编译成一组机器指令
注:JavaScript引擎的编译要比以上复杂的多。例如:在语法分析和代码生成阶段有特定的步骤来对运行性能进行优化,包括对冗余元素进行优化。
1.2 理解作用域
- 引擎
从头到尾负责整个JavaScript程序的编译及执行过程。 - 编译器
引擎的好朋友之一,负责语法分析及代码生成等脏活累活(详见前一节的内容)。 - 作用域
引擎的另一位好朋友,负责收集并维护由所有声明的标识符(变量)组成的一系列查询,并实施一套非常严格的规则,确定当前执行的代码对这些标识符的访问权限。
接下来看一段代码执行过程中他们是如何进行交流
代码:
function foo(a) {
console.log( a ); // 2
}
foo( 2 );
对话:
“引擎:我说作用域,我需要为foo进行RHS引用。你见过它吗? 作用域:别说,我还真见过,编译器那小子刚刚声明了它。它是一个函数,给你。 引擎:哥们太够意思了!好吧,我来执行一下foo。 引擎:作用域,还有个事儿。我需要为a进行LHS引用,这个你见过吗? 作用域:这个也见过,编译器最近把它声名为foo的一个形式参数了,拿去吧。 引擎:大恩不言谢,你总是这么棒。现在我要把2赋值给a。 引擎:哥们,不好意思又来打扰你。我要为console进行RHS引用,你见过它吗? 作用域:咱俩谁跟谁啊,再说我就是干这个的。这个我也有,console是个内置对象。给你。 引擎:么么哒。我得看看这里面是不是有log(..)。太好了,找到了,是一个函数。 引擎:哥们,能帮我再找一下对a的RHS引用吗?虽然[…]” 摘录来自: Kyle Simpson、赵望野、梁杰. “你不知道的JavaScript(上卷)”。 iBooks.
1.3 作用域嵌套
在当前作用域中无法找到某个变量时,引擎就会在外层嵌套作用域中继续查找,知道找到该变量,或抵达最外层的全局作用域为止。
1.4 异常
如果在全局作用域中也无法找到目标变量,全局作用域中就会创建一个具有该名称的变量,并将其返还给引擎,前提是程序运行在非“严格模式”下
如果ES5中引入了“严格模式”,那么严格模式会禁止自动的或隐式的创建全局变量。因此在查询失败的时候会抛出类似ReferenceError异常。
如果你对一个已查询到的变量进行不合理的操作时,比如对一个非函数类型的值进行函数调用,或者引用null
undefined
类型的值中的属性,那么引擎会抛出另外一种类型的异常,叫做TypeError
。
1.5 小结
作用域是一套规则,用于确定在何处以及如何查找变量(标识符)。
var a = 2
首先,var a在其作用域中声明新变量。这会在最开始的阶段,也就是代码执行前进行。接下来,a = 2会查询(LHS查询)变量a并对其进行赋值。 摘录来自: Kyle Simpson、赵望野、梁杰. “你不知道的JavaScript(上卷)”。 iBooks.
第二章 词法作用域
2.2 词法阶段
气泡1包含着整个全局作用域,其中只有一个标识符:foo
。
气泡2包含着foo
所创建的作用域,其中有三个标识符:s
bar
b
。
气泡3包含着bar所创建的作用域,其中只有一个标识符:c
。
作用域查找会在找到第一个匹配的标识符时停止,在多层的嵌套作用域中可以定义同名的标识符,这叫做“遮蔽效应”(内部的标识符“遮蔽”了外部的标识符)
全局变量会自动成为全局对象(比如浏览器中的
window
对象)的属性。
var a = 1;
function fun1() {
var a = 2;
console.log(a)//输出2
}
function fun2() {
var a = 2;
console.log(window.a)//输出1
}
对浏览器中window
全局对象的解释可以进一步到以下网址了解:
- JavaScript全局对象——W3C
- JavaScript声明全局变量三种方式的异同
2.2 欺骗词法
eval
with
等语法会在运行时修改或创建新的作用域,以此来欺骗其他在书写时定义的词法作用域。而且会导致JavaScript引擎在编译阶段无法对它们做任何的优化处理。
简单介绍一下这两个语法:
-
eval
可以对一段包含一个或多个声明的“代码”字符串进行演算,并借此来修改已经存在的词法作用域(在运行时)。 -
with
本质上是通过将一个对象的引用当作作用域来处理,将对象的属性当作作用域中的标识符来处理,从而创建了一个新的词法作用域(同样是在运行时)
这两个机制的副作用会导致代码运行变慢。不要使用它们。
第三章 函数作用域和块作用域
- 每一个函数都会创建一个自己的作用域,如果你在该作用域中声明了变量,且该变量存在于外部作用域,那么它会遮蔽外部作用域中的变量。
- 为了不污染全局作用域,可以将代码写入一个匿名函数中调用
var a = 2;
(function() {
var a = 3;
console.log( a ); // 3
})();
console.log( a ); // 2
- 变量声明应该距离使用的地方越近越好,并最大限度的本地化
- let
{
let a = 10;
var b = 1;
}
a // ReferenceError: a is not defined.
b // 1
- const
var foo = true;
if (foo) {
var a = 2;
const b = 3; // 包含在if中的块作用域常量
a = 3; // 正常!
b = 4; // 错误!
}
console.log( a ); // 3
console.log( b ); // ReferenceError!
博客:
Rainey's Blograiney.space