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

作用域和闭包

var a = 2

三个角色:

  • 引擎
  • 编译器
  • 作用域

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

作用域嵌套

引擎从当前的执行作用域开始查找变量,如果找不到,就向上一级继续查找。当抵达最外层的全局作用域时,无论找到还是没有找到,查找过程都会停止。

什么是作用域

作用域是一套规则,用于确定在何处以及如何查找变量(标识符)。如果查找的目的是对变量进行赋值,那么就会使用LHS查询;如果目的是获取变量的值,就会使用RHS查询。

不成功的RHS引用会导致抛出ReferenceError异常。不成功的LHS引用会导致自动隐式地创建一个全局变量(非严格模式下),该变量使用LHS引用的目标作为标识符,或者抛出ReferenceError异常(严格模式下)。

词法作用域

词法作用域由函数被声明时所处的位置决定。编译的词法阶段基本能够知道全部的标识符在哪里以及是如何和声明的,从而能够预测在执行过程中如何对他们进行查找。

“欺骗”词法作用域

JavaScript有两个机制可以“欺骗”词法作用域:eval(..)和with。前者可以对一段包含一个或多个声明的“代码”字符串进行验算,并借此来修改已经存在的词法作用域(在运行时)。后者本质上是通过将一个对象的引用当做作用域来处理,将对象的属性当作作用域中的标识符来处理,从而创建了一个新的词法作用域(同样在运行时)。
两个机制的副作用是引擎无法在编译时对作用域查找进行优化,因为引擎只能谨慎地认为这样的优化是无效的。使用这其中任何一个机制都会导致代码运行变慢。

1
2
eval('a+b');
new Function('a','b','return(a+b)'));

效果一样。

函数作用域

函数是JavaScript中最常见的作用域单元。本质上,声明在一个函数内部的变量或函数会在所处的作用域中“隐藏”起来,这是有意为之的良好软件的设计原则。

区分函数声明和函数表达式:如果function是声明中的第一个词,那么就是一个函数声明,否则就是一个函数表达式。函数声明不能省略函数名,否则就是一个匿名函数表达式。

始终给函数表达式命名是一个最佳实践。

块作用域

ES6 中的letconst提供了块作用域。
在之前可以使用try{}catch()()实现块作用域的效果。

1
2
3
4
5
6
try {
throw 2;
} catch (a) {
console.log(a); //2
}
console.log(a); //ReferenceError

提升

引擎将var a = 2;当作是两个单独的声明:var aa = 2;第一个是编译阶段的任务,第二个是执行阶段的任务。这意味着无论作用域中的生命出现在什么地方,都将在代码本身被执行前首先进行处理。这个过程可以看做变量的“提升”。

只有声明本身会被提升,而赋值或其他运行逻辑会留在原地。如果提升改变了代码的执行的顺序,会造成严重的破坏。

函数优先:函数首先会被提升,然后才是变量。

1
2
3
4
5
6
7
8
9
10
foo();// 1

var foo;

function foo() {
console.log(1);
}
foo = function() {
console.log(2);
}

var foo尽管出现在function foo()...的声明之前,但它是重复声明(因此被忽略了),因此函数声明会被提升到普通变量之前。

作用域闭包

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

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

闭包的实现

1
2
3
4
5
6
7
8
9
for(var i = 0;i<5;i++){
(function(i){
// 利用i来保存对变量的引用 相当于var i = i
setTimeout(function timer(){
console.log(i);
},1000);
})(i);
}
// 0,1,2,3,4

模块

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
function CoolModule() {
var something = "cool";
var another = [1, 2, 3];

function doSomething() {
console.log(something);
}

function doAnother() {
console.log(another.join(" ! "));
}

return {
doSomething: doSomething,
doAnother: doAnother
};
}

var foo = CoolModule();

foo.doSomething(); //cool
foo.doAnother(); // 1 ! 2 ! 3

首先,CoolModule()只是一个函数,必须要通过调用它来创建一个模块实例。如果不执行外部函数,内部作用域和闭包都无法被创建。
其次,CoolModule()返回一个用对象字面量语法{ key: value, ... }来表示的对象。这个返回的对象中含有对内部函数而不是内部数据变量的引用。我们保持内部数据变量是隐藏且私有的状态。可以将这个对象类型的返回值看作本质上是模块的公共API

用模块中返回一个实际的对象并不是必须的,也可以直接返回一个内部函数。jQuery就是一个很好的例子。jQuery和$标识符就是jQuery模块的公共API,但他们本身都是函数(由于函数也是对象,他们本身也可以拥有属性)。

doSomething()和doAnother()函数具有涵盖模块实例内部作用域的闭包(通过调用CoolModule()实现)。当通过返回一个含有属性引用的对象的方式来将函数传递到词法作用域外部时,我们已经创建了可以观察和实践闭包的条件。

模块模式具备两个必要条件:
1.必须有外部的封闭函数,该函数必须至少被调用一次(每次调用都会创建一个新的模块实例)。
2.封闭函数必须返回至少一个内部函数,这样内部函数才能在私有作用域中形成闭包,并且可以访问或者修改私有的状态。

一个具有函数属性的对象本身并不是真正的模块。从方便观察的角度看,一个从函数调用返回的,只有数据属性而没有闭包函数的对象并不是真正的模块

上一个示例代码中有一个叫CoolModule()的独立的模块创建器,可以被调用任意多次,创建多个新的模块实例。当只需要一个实例时,可以使用单例模式

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() {
var something = "cool";
var another = [1, 2, 3];

function doSomething() {
console.log(something);
}

function doAnother() {
console.log(another.join(" ! "));
}

return {
doSomething: doSomething,
doAnother: doAnother
};
})();


foo.doSomething(); //cool
foo.doAnother(); // 1 ! 2 ! 3

将模块函数转换成了IIFE(立即执行函数)。

模块有两个主要特征:

  • 为创建内部作用域而调用了一个包装函数
  • 包装函数的返回值必须至少包括一个对内部函数的引用,这样就会创建涵盖整个包装函数内部作用域的闭包。
0%