RN-《你不知道的JS》上卷第一部分

第一部分 作用域和闭包

第一章 作用域是什么

编译原理

尽管通常将 JavaScript 归类为 “动态” 或 “解释执行” 语言,但事实上它是一门 编译语言,但与传统的编译语言不同,它不是提前编译的,编译结果也不能在分布式系统中进行移植。

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

分词/词法分析(Tokenizing/Lexing)

这个过程会将由字符组成的字符串分解成(对编程语言来说)有意义的代码块 , 这些代码块被称为词法单元(token)。例如 var a = 2 ,被分解成这些词法单元:var、a、=、2、; 空格是否会被当做词法单元,取决于空格在这门语言中是否具有意义。

解析/语法分析(Parsing)

这个过程将词法单元流(数组)转换成一个由元素逐级嵌套所组成的代表了程序语法结构的树,这个树被称为 “ 抽象语法树 “(Abstract Syntax Tree,AST)。

代码生成

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

与其他语言不同,JavaScript 的编译过程不是发生在构建之前的。大部分的编译发生在代码执行前的几微秒的时间里,比起编译过程只有三步的语言的编译器,JavaScript 引擎要复杂得多

理解作用域

演员表

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

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

  • 作用域:引擎的另一位好朋友,负责收集并维护由所有声明的标识符(变量)组成的一系列查询

变量赋值

对于 var a = 2 ,引擎认为这里有两个完全不同的声明,一个由编译器在编译时处理,另一个则由引擎在运行时处理。

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

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

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

引擎查找

var a = 2 ,这个例子中,引擎会为变量 a 进行 LHS 查询,另一个查找的类型叫做 RHS。

简单理解

如果查找的目标是对变量进行赋值,那么就会使用 LHS 查询;如果目的是获取变量的值,就会使用 RHS 查询; 赋值操作符会导致 LHS 查询,= 操作符调用函数时传入参数的操作都会导致关联作用域的赋值操作。

深入理解

RHS查询 与简单查找某个变量的值别无二致而 LHS查询 则是试图找到变量的容器本身,从而可以对其赋值。在概念上最好将其理解为 “ 赋值操作的目标是谁(LHS)” 以及 “ 谁是赋值操作的源头(RHS)”

1
2
3
console.log(a); // 这里对 a 的引用是一个 RHS引用,因为这里 a 并没有赋予任何值。

var a = 2; // 这里对 a 的引用则是 LHS引用,因为实际上我们并不关心当前的值是什么,只是想为 = 2 这个赋值操作找到一个目标
  1. 实例一理解 LHS RHS
1
2
3
4
5
6
7
8
9
10
function foo(a) {
console.log(a);
}
foo(2);

// LHS: 参数 a 的隐式赋值
// RHS:
// 1. console.log(a) 中对 a 的 RHS查询,将得到的 a 的值传给 console.log(..)
// 2. console.log(..) 本身会对 console 对象进行 RHS查询,检查是否有一个叫做 log 的方法
// 3. foo(..) 的调用需要对 foo 进行 RHS查询,意为找到 foo 的值并把它给我
  1. 实例二理解 LHS RHS
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
function foo(a) {
var b = a;
return a + b;
}
var c = foo(2);

// LHS:
// 1. 参数 a 的隐式赋值
// 2. var b = a; 中对 b 的 LHS查询
// 3. var c = foo(2); 中对 c 的 LHS查询

// RHS:
// 1. var b = a; 中对 a 的 RHS查询
// 2. return a + b 中对 a、b 的 RHS查询
// 3. var c = foo(2) 中对 foo(..) 的 RHS查询

异常

ReferenceError

不成功的 RHS、LHS(严格)查询

1
2
3
4
5
function foo(a) {
console.log(a + b);
b = a;
}
foo(2);

上面这段代码在对 b 进行 RHS 查询时无法找到,引擎会抛出 ReferenceError : Uncaught ReferenceError: b is not defined。

在非严格模式下,对 a 的 LHS 查询并没有抛出错误,而会在全局创建一个 a 的变量,在严格模式下,LHS 查询没有结果也会抛出 ReferenceError

TypeError

成功的 RHS 查询,但是是非法的操作

1
2
var b = null;
b();

如果 RHS 查询到了,但是对其进行不合理的操作,比如对非函数类型的值进行函数调用,那么引擎会抛出 TypeError:Uncaught TypeError: b is not a function

小结

  • ReferenceError 同作用域判别失败相关

  • TypeError 代表作用域判别成功了,但对结果的操作是非法的或不合理的。

  • 不成功的 RHS 引用会导致 ReferenceError 异常

  • 不成功的 LHS 引用

    • 非严格模式下会创建全局变量
    • 严格模式下导致 ReferenceError 异常

第二章 词法作用域

简单的说,词法作用域就是定义在词法阶段的作用域

词法阶段

window a 通过这种技术可以访问那些被同名变量所遮蔽的全局变量,但是非全局变量如果被遮蔽了无论如何都无法被访问到。

词法作用域只会查找一级标识符,如果代码中引用了 foo.bar.baz ,词法作用域只会试图查找 foo 标识符,找到这个变量后,对象属性访问规则会接管对 bar 和 baz 属性的访问。

欺骗词法

不要使用 eval 和 with

第三章 函数作用域和块作用域

函数作用域的含义是指,属于这个函数的全部变量都可以在整个函数的范围内使用及复用

函数作用域

从所写的代码中挑选出一个任意的片段,然后用函数声明对它进行包装,然后再用立即执行函数(IIFE)的方式再进行一层包装。

立即执行函数表达式

1
2
3
4
5
6
7
8
9
10
var a = 2;

(function IIFE() {

var a = 3;
console.log(a); //3

})()

console.log(a); // 2

立即执行函数传参

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// 传递 window 对象
var a = 2;

(function IIFE(global) {

var a = 3;
console.log(a); //3
console.log(global.a) // 2

})(window);

// 传递 undefined:将一个参数命名为 undefined,对应位置不传入任何值,这样就可以保证是真的 undefined
undefined = true; // 不要这样做
(function IIFE(undefined) {

var a;
if (a === undefined) {
console.log('undefined is safe here!')
}

})();

IIFE 传函数当做参数

1
2
3
4
5
6
7
8
9
10
11
12
13
var a = 2;

(function IIFE(def) {

def(window);

})(function def(global) {

var a = 3;
console.log(a); // 3
console.log(global.a);

});

块作用域

能用 letconst 就不用 var

第四章 提升

无论作用域中的声明出现在什么地方,都将在代码本身被执行前首先进行处理,可以将这个过程形象地想象成所有的声明(变量和函数),都会被 “移动” 到各自作用域的最顶端,这个过程被称为提升。

声明本身会被提升,而包括函数表达式的赋值在内的赋值操作并不会提升。

第五章 作用域闭包

定义

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

1
2
3
4
5
6
7
8
9
function foo() {
var a = 2;
function bar() {
console.log(a);
}
return bar
}
var baz = foo();
baz() // 2 -- 这就是闭包的效果

对于上面的代码,简单来说,bar() 依然持有对该作用域的引用,而这个引用就叫作闭包。

1
2
3
4
5
6
7
8
9
10
11
function foo() {
var a = 2;
function bar() {
console.log(a);
}
baz(bar)
}
function baz(fn) {
fn(); // 2
}
foo()
1
2
3
4
5
6
7
8
9
10
var fn;
function foo() {
var a = 2;
function bar() {
console.log(a);
}
fn = bar
}
foo()
fn() // 2

无论通过何种手段将内部函数传递到所在的词法作用域以外,它都会持有对原始定义作用域的引用,无论在何处执行这个函数都会使用闭包。

本质理解

下面这段代码中 timer 保有对变量 message 的引用,wait(…) 执行 1000 毫秒后,它的内部作用域并不会消失,timer 函数依然保有 wait(..) 作用域的闭包。

在引擎内部,内置的工具函数 setTimeout(..) 持有对一个参数的引用,这个参数也许叫 fn 或者 func,或者其它名字。引擎会调用这个函数,在这个例子中就是内部的 timer 函数。

1
2
3
4
5
6
function wait(message) {
setTimeout(function timer() {
console.log(message) // Hello, closure
}, 1000)
}
wait('Hello, closure')

本质上无论何时何地,如果将函数当做第一级的值类型并到处传递,就可以看到闭包在这些函数中的应用。在定时器、事件监听器、Ajax 请求、跨窗口通信、Web Workers 或者任何其他的异步(或者同步)任务中,只要用了回调函数,实际上就是在使用闭包!

循环和闭包

普通循环

1
2
3
4
5
for(var i = 1; i <= 5; i ++) {
setTimeout(function timer() {
console.log(i);
}, i * 1000)
}

上面这段代码会一秒打印一个6,总共打印5个6

原因就是延时函数的回调会在循环结束时才执行,当定时器运行时即使每个迭代中执行的是 setTimeout(.., 0),即 0ms 后执行 setTimeout,所有的回调函数依然是在循环结束后才会被执行,因此会每次输出一个 6 出来。

IIFE + 闭包

1
2
3
4
5
6
7
for(var i = 1; i <= 5; i ++) {
(function (i) {
setTimeout(function timer() {
console.log(i);
}, i * 1000)
})(i);
}

块作用域 + 闭包

1
2
3
4
5
for(let i = 1; i <= 5; i ++) {
setTimeout(function timer() {
console.log(i);
}, i * 1000)
}

模块

模块模式

模块模式需要两个必要条件:

  1. 必须有外部的封闭函数,该函数必须至少被调用一次(每次调用都会创建一个新的模块实例)。
  2. 封闭函数必须返回至少一个内部函数,这样内部函数才能在私有作用域中形成闭包,并且可以访问或者修改私有的状态。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
function CoolModule() {
var something = "cool";
var another = [1, 2, 3];
function doSomething() {
console.log( something )
}
function doAnother() {
console.log( another.join(" ! ") );
}
return {
doSomething,
doAnother
}
}
var foo = CoolModule();
foo.doSomething(); // cool
foo.doAnother(); // 1 ! 2 ! 3

单例模式

当只需要一个实例时,可以对模块模式进行简单的改进来实现单例模式:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
var foo = (function CoolModule() {
var something = "cool";
var another = [1, 2, 3];
function doSomething() {
console.log( something )
}
function doAnother() {
console.log( another.join(" ! ") );
}
return {
doSomething,
doAnother
}
})()
foo.doSomething(); // cool
foo.doAnother(); // 1 ! 2 ! 3

补充

可以传参,另外可以命名将要作为公共API返回的对象,比如下面的 change() 函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
var foo = (function CoolModule(id) {
function first() {
console.log(id)
}

function second() {
console.log(id.toUpperCase());
}

function change() {
publicAPI.identity = second
}
var publicAPI = {
change,
identity: first
}
return publicAPI;
})(" foo module ")
foo.identity(); // foo module
foo.change();
foo.identity(); // FOO MODULE