《你不知道的JavaScript(上)》阅读笔记(一)

知乎上无意间看到了这本书,扫了眼内容以后觉得这本书写的很好,因此抽空读一遍。

这个系列的读书笔记主要是我对书中学到的知识进行的摘抄。偶尔会有一些个人想法,

1、作用域是什么

对于 var a = 2 的理解

编译器将 var a 当作一句声明,会查看作用域内是否有a,有的话忽略,没有的话在作用域内新增变量a

引擎将 a = 2 当作一句运行语句,去查找 a

(这里也让我明白了为什么 js会有变量的声明提升,因为他的编译和运行是分开来的两个过程)

LHS 和 RHS

LHS:Left-Hand-Side

RHS:Right-Hand-Side

即赋值的左侧和右侧:当出现在赋值操作的左侧时进行LHS查询,出现在右侧时进行RHS查询。

LHS找引用(地址),RHS获取值即可。前者递归寻找失败会在全局作用域声明该变量(严格模式下会报ReferenceError),后者递归寻找失败也会报ReferenceError,如果对该变量进行不合理操作报TypeError

ReferenceError 同作用域判别失败相关,而TypeError 则代表作用域判别成功了,但是对
结果的操作是非法或不合理的。

2、词法作用域

词法阶段

全局变量会自动成为全局对象(比如浏览器中的window 对象)的属性,因此
可以不直接通过全局对象的词法名称,而是间接地通过对全局对象属性的引
用来对其进行访问。
window.a
通过这种技术可以访问那些被同名变量所遮蔽的全局变量。但非全局的变量
如果被遮蔽了,无论如何都无法被访问到。

无论函数在哪里被调用,也无论它如何被调用,它的词法作用域都只由函数被声明时所处
的位置决定。

欺骗词法

这一节讲了改变词法作用域的一些方式,主要是 eval 和 with

JavaScript 中有两种机制来实现这个目的。社区普遍认为在代码中使用这两种机制并不是
什么好注意。但是关于它们的争论通常会忽略掉最重要的点:欺骗词法作用域会导致性能
下降。

之前早就在知乎上看到人说 eval 和 with 会严重影响性能。现在了解到了 WHY

另外 with 这个语法 我以前是从来没有了解过的。

eval

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

在严格模式的程序中,eval(..) 在运行时有其自己的词法作用域,意味着其
中的声明无法修改所在的作用域。

with

作者举了两个例子,并且对它们进行详解,进而解释了with的用法及缺点

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
var obj = {
a: 1,
b: 2,
c: 3
};
// 单调乏味的重复"obj"
obj.a = 2;
obj.b = 3;
obj.c = 4;
// 简单的快捷方式
with (obj) {
a = 3;
b = 4;
c = 5;
}

上面的例子就是with的大多数用法,这也是大多数入门教程上讲解(没错,看到这里我突然想起来我在菜鸟教程上看到过这个玩意,只不过忘了)

接下来作者举了另外一个例子,这个例子讲揭示 with 的真正作用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
function foo(obj) {
with (obj) {
a = 2;
}
}
var o1 = {
a: 3
};
var o2 = {
b: 3
};
foo( o1 );
console.log( o1.a ); // 2
foo( o2 );
console.log( o2.a ); // undefined
console.log( a ); // 2——不好,a 被泄漏到全局作用域上了!

重点关注o2的部分

with 可以将一个没有或有多个属性的对象处理为一个完全隔离的词法作用域,因此这个对
象的属性也会被处理为定义在这个作用域中的词法标识符。

尽管with 块可以将一个对象处理为词法作用域,但是这个块内部正常的var
声明并不会被限制在这个块的作用域中,而是被添加到with 所处的函数作
用域中。

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

进行正常的 LHS 标识符查找

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

这个例子的讲解非常明确

性能

JavaScript 引擎会在编译阶段进行数项的性能优化。其中有些优化依赖于能够根据代码的
词法进行静态分析,并预先确定所有变量和函数的定义位置,才能在执行过程中快速找到
标识符。

最悲观的情况是如果出现了eval(..) 或with,所有的优化可能都是无意义的,因此最简
单的做法就是完全不做任何优化。

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

函数作用域

es5 中函数会创建一个新的作用域,这个作用域包括函数的参数、函数内部声明的各个变量。并且值得注意的是嵌套在函数内部定义的函数,是可以访问其外层的作用域的。

声明在一个函数内部的变量或函数会在所处的作用域中“隐藏”起来。

块作用域

es6 中let和const声明的变量支持块作用域

es5中 with是块作用域的一个例子

另一个不常被人注意的例子是 try/catch

1
2
3
4
5
6
7
try {
undefined(); // 执行一个非法操作来强制制造一个异常
}
catch (err) {
console.log( err ); // 能够正常执行!
}
console.log( err ); // ReferenceError: err not found

catch语句中是一个块作用域。 (这也是es6语法转化为es5的一个解决方案)

4、提升

变量的声明提升其实按照先前提过的 编译器和引擎 两步走的方式是很容易理解的,这里可以重申一次:

我们的编译器先对代码进行一次执行,它会把所有的变量声明都进行处理。在此之后,引擎再从头开始执行代码,这样我们就会感受到后面声明的变量被提升。

而且变量的声明可以覆盖。

另一个原则是: 函数的声明会被优先提升

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

foo进行了两次函数声明,一次变量声明,这里被提升为了函数,输出了3。而如果第三个函数没有声明的话,输出的结果应当是1,尽管在第一次函数声明之后又再一次对foo进行了变量声明。

一个普通块内部的函数声明通常会被提升到所在作用域的顶部,这个过程不会像下面的代码暗示的那样可以被条件判断所控制:

1
2
3
4
5
6
7
8
foo(); // "b"
var a = true;
if (a) {
function foo() { console.log("a"); }
}
else {
function foo() { console.log("b"); }
}

很明显,因为我们的编译器不会执行代码,它没有办法判断a到底是true还是false

5、作用域闭包

下面这一章的内容十分重要。它让我重新审视闭包,也刷新了我之前对于闭包的粗浅的认识。正如作者说的,闭包它其实并没有特别神秘,相反,它时常出现在我们写下的代码之中。只要我们对于作用域的理解足够深刻,那么闭包的概念就显得十分自然。事实上闭包是作用域下的必然产物。

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

这句话也可以这样理解,函数与其所在的词法作用域共同构成了闭包。(这个词法作用域会被函数记住,并且可以访问)。

写到这里,我决定单独为闭包写一篇博客:闭包到底是什么?,来体现其重要性。

本书的第一部分到这里也就结束了,第二部分讲解了this和对象原型。