《你不知道的JavaScript》笔记-作用域

作用域是什么

作用域是一套规则,用于确定在何处以及如何查找变量(标识符)。

编译原理

当我们看到 var a=2; 的时候引擎和编译器会做什么呢?

  1. 遇到 var a,编译器会询问作用域是否已经有一个该名称的变量存在于同一个作用域的合集中.如果是.编译器会忽略该声明,继续进行编译.否则它会要求作用域在当前的作用域合集中声明一个新的变量,并且命名为 a.
  2. 接下来编译器会为这个引擎生成运行时所需的代码,这些代码被用来处理 a = 2 这个赋值操作.引擎运行时会首先询问作用域,在当前的作用域合集中是否存在一个叫 a 的变量,如果否,引擎就会使用这个变量;如果不是,引擎就会继续查找该变量.

理解作用域

RHS 引用是找到这个变量所在的地址,但是不赋值 赋值是等号做的事情 LHS 引用是赋值时把 RHS 找到的地址赋值给 LHS
如果查找的目的是对变量进行赋值,那么就会使用 LHS 查询;如果目的是获取变量的值,就会使用 RHS 查询。

作用域嵌套

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

异常

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

词法作用域

词法阶段

词法作用域也叫静态作用域.其作用域只在引擎初始化的时候就已经定好了.不会跟随代码的执行而动态改变作用域

1
2
3
4
5
6
7
8
9
10
11
function foo(a) {
var b = a * 2;

function bar(c) {
console.log(a, b, c);
}

bar(b * 3);
}

foo(2); // 2, 4, 12

这里面有三个嵌套的作用域 这里来分析一下:

  • window(全局作用域)
  • window=>foo
  • window=>foo=>bar

词法作用域意味着作用域是由书写代码时函数声明的位置来决定的。编译的词法分析阶段基本能够知道全部标识符在哪里以及是如何声明的,从而能够预测在执行过程中如何对它们进行查找。

欺骗词法

JavaScript 中有两个机制可以 “欺骗” 词法作用域:eval(..)和 with。前者可以对一段包含一个或多个声明的“代码”字符串进行演算,并借此来修改已经存在的词法作用域(在运行时)。后者本质上是通过将一个对象的引用当作作用域来处理,将对象的属性当作作用域中的标识符来处理,从而创建了一个新的词法作用域(同样是在运行时)。

这两个机制的副作用是引擎无法在编译时对作用域查找进行优化,欺骗词法作用域会导致性能下降。不要使用它们。

在严格模式下 with 被完全禁止,eval 所生成的变量只能用于 eval 内部。

函数作用域和块作用域

函数中的作用域

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

但函数不是唯一的作用域单元。块作用域指的是变量和函数不仅可以属于所处的作用域,也可以属于某个代码块(通常指{ .. }内部)。

块作用域

从 ES3 开始,try/catch 结构在 catch 分句中具有块作用域。

在 ES6 中引入了 let/const 关键字,用来在任意代码块中声明变量。if (..) { let a = 2; } 会声明一个劫持了 if 的{ .. }块的变量,并且将变量添加到这个块中。

有些人认为块作用域不应该完全作为函数作用域的替代方案。两种功能应该同时存在,开发者可以并且也应该根据需要选择使用何种作用域,创造可读、可维护的优良代码。

提升

所有的声明(变量和函数)都会被“移动”到各自作用域的最顶端,这个过程被称为提升。

1
2
3
4
5
6
foo();

function foo() {
console.log(a); // undefined
var a = 2;
}

当你看到 var a = 2;时,可能会认为这是一个声明。但 JavaScript 实际上会将其看成两个声明:var a;和 a = 2;。第一个定义声明是在编译阶段进行的。第二个赋值声明会被留在原地等待执行阶段。
实际是按照以下流程处理的:

1
2
3
4
5
var a;

console.log(a);

a = 2;

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

1
2
3
4
5
6
7
8
9
10
11
foo(); // 会输出1而不是2!

var foo;

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

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

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

1
2
3
4
5
6
foo(); // TypeError
bar(); // ReferenceError

var foo = function bar() {
// ...
};

要注意避免重复声明,特别是当普通的 var 声明和函数声明混合在一起的时候,否则会引起很多危险的问题!