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

时间:2022-11-01 22:41:01

1、编译原理

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

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


2、理解作用域

当你看到var a=2;这个代码段的时候,你也许只会认为这是一个声明语句,但是事实上,浏览器引擎并不会这么认为!其实浏览器会这么认为:

1、遇到 var a, 编译器会询问作用域是否已经有一个该名称的变量存在于同一个作用域的集合中。 如果是, 编译器会忽略该声明, 继续进行编译; 否则它会要求作用域在当前作用域的集合中声明一个新的变量, 并命名为 a。

2、接下来编译器会为引擎生成运行时所需的代码, 这些代码被用来处理 a = 2 这个赋值操作。 引擎运行时会首先询问作用域, 在当前的作用域集合中是否存在一个叫作 a 的变量。 如果是, 引擎就会使用这个变量; 如果否, 引擎会继续查找该变量。


3、作用域嵌套

当一个块或函数嵌套在另一个块或函数中时, 就发生了作用域的嵌套。 因此, 在当前作用域中无法找到某个变量时, 引擎就会在外层嵌套的作用域中继续查找, 直到找到该变量,或抵达最外层的作用域( 也就是全局作用域) 为止。

function foo(a) {
console.log(a + b);
}
var b = 3;
foo(3);

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

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


4、立即执行函数

var a = 3;
(function IIFE() {
var a = 4;
console.log(a);
})();
console.log(a);

由于函数被包含在一对 ( ) 括号内部, 因此成为了一个表达式, 通过在末尾加上另外一个( ) 可以立即执行这个函数, 比如 (function foo(){ .. })()。 第一个 ( ) 将函数变成表达式, 第二个 ( ) 执行了这个函数。

相较于传统的 IIFE 形式, 很多人都更喜欢另一个改进的形式: (function(){ .. }())。 仔
细观察其中的区别。 第一种形式中函数表达式被包含在 ( ) 中, 然后在后面用另一个 () 括号来调用。 第二种形式中用来调用的 () 括号被移进了用来包装的 ( ) 括号中。这两种形式在功能上是一致的。 选择哪个全凭个人喜好。

提升

考虑以下代码:

a=2;
var a;
console.log(a);

你认为 console.log(..) 声明会输出什么呢?
很多开发者会认为是 undefined, 因为 var a 声明在 a = 2 之后, 他们自然而然地认为变量被重新赋值了, 因此会被赋予默认值 undefined。 但是, 真正的输出结果是 2。

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

考虑另外一段代码:

console.log(a);
var a=2;

鉴于上一个代码片段所表现出来的某种非自上而下的行为特点, 你可能会认为这个代码片段也会有同样的行为而输出 2。 还有人可能会认为, 由于变量 a 在使用前没有先进行声明,因此会抛出 ReferenceError 异常。

不幸的是两种猜测都是不对的。 输出来的会是 undefined。

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

那么到底发生了什么? 看起来我们面对的是一个先有鸡还是先有蛋的问题。 到底是声明( 蛋) 在前, 还是赋值( 鸡) 在前?

正确的思考思路是, 包括变量和函数在内的所有声明都会在任何代码被执行前首先被处理。当你看到 var a = 2; 时, 可能会认为这是一个声明。 但 JavaScript 实际上会将其看成两个声明: var a; 和 a = 2;。 第一个定义声明是在编译阶段进行的。 第二个赋值声明会被留在原地等待执行阶段。

我们的第一个代码片段会以如下形式进行处理:

var a;
a = 2;
console.log( a );

其中第一部分是编译, 而第二部分是执行。

类似地, 我们的第二个代码片段实际是按照以下流程处理的:

var a;
console.log( a );
a = 2;

因此, 打个比方, 这个过程就好像变量和函数声明从它们在代码中出现的位置被“ 移动”到了最上面。 这个过程就叫作提升。换句话说, 先有蛋( 声明) 后有鸡( 赋值)。

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


5、函数优先

函数声明和变量声明都会被提升。 但是一个值得注意的细节( 这个细节可以出现在有多个“ 重复” 声明的代码中) 是函数会首先被提升, 然后才是变量。

考虑以下代码:

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

会输出 1 而不是 2 ! 这个代码片段会被引擎理解为如下形式:

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

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

尽管重复的 var 声明会被忽略掉, 但出现在后面的函数声明还是可以覆盖前面的。

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