词法作用域
词法(lexical)一词指的是,词法作用域根据源代码中声明变量的位置来确定该变量在何处可用。嵌套函数可访问声明于它们外部作用域的变量。
| 【示例】这个词法作用域的例子描述了分析器如何在函数嵌套的情况下解析变量名。```javascript function init() { var name = “Mozilla”; // name 是一个被 init 创建的局部变量 function displayName() { // displayName() 是内部函数,一个闭包 alert(name); // 使用了父函数中声明的变量 } displayName(); } init();
init() 创建了一个局部变量 name 和一个名为 displayName() 的函数。<br />displayName() 是定义在 init() 里的内部函数,并且仅在 init() 函数体内可用。<br />displayName() 没有自己的局部变量。然而,因为它可以访问到外部函数的变量,所以 displayName() 可以使用父函数 init() 中声明的变量 name 。 |
| --- |
<a name="rvwKZ"></a>
# 嵌套函数
一、当一个函数是在另一个函数中创建的时,那么该函数就被称为“嵌套”的。
| 【示例】我们可以使用嵌套来组织代码,比如这样:```javascript
function sayHiBye(firstName, lastName) {
// 辅助嵌套函数使用如下
function getFullName() {
return firstName + " " + lastName;
}
alert( "Hello, " + getFullName() );
alert( "Bye, " + getFullName() );
}
1、这里创建的嵌套函数getFullName()是为了更加方便。它可以访问外部变量,因此可以返回全名。嵌套函数在 JavaScript 中很常见。
2、更有意思的是,可以返回一个嵌套函数:作为一个新对象的属性或作为结果返回。之后可以在其他地方使用。不论在哪里调用,它仍然可以访问相同的外部变量。 |
| —- |
| 【示例】下面的makeCounter创建了一个 “counter” 函数,该函数在每次调用时返回下一个数字:```javascript function makeCounter() { let count = 0;
return function() { return count++; }; }
let counter = makeCounter();
alert( counter() ); // 0 alert( counter() ); // 1 alert( counter() ); // 2
1、尽管很简单,但稍加变型就具有很强的实际用途,比如,用作[随机数生成器](https://en.wikipedia.org/wiki/Pseudorandom_number_generator)以生成用于自动化测试的随机数值。<br />2、这是如何运作的呢?如果我们创建多个计数器,它们会是独立的吗?这里的变量是怎么回事? |
| --- |
| 【示例】由于内部函数形成了闭包,因此可以调用外部函数并为外部函数和内部函数指定参数```jsx
function outside(x) {
function inside(y) {
return x + y;
}
return inside;
}
fn_inside = outside(3); // 可以这样想:给一个函数,使它的值加3
result = fn_inside(5); // returns 8
result1 = outside(3)(5); // returns 8
1、保存变量:inside被返回时x是怎么被保留下来的?
(1)一个闭包必须保存它可见作用域中所有参数和变量。因为每一次调用传入的参数都可能不同,每一次对外部函数的调用实际上重新创建了一遍这个闭包。只有当返回的inside没有再被引用时,内存才会被释放。 |
| —- |
词法环境
一、“词法环境”是一个规范对象(specification object):它仅仅是存在于编程语言规范中的“理论上”存在的,用于描述事物如何运作的对象。我们无法在代码中获取该对象并直接对其进行操作。
二、但 JavaScript 引擎同样可以优化它,比如清除未被使用的变量以节省内存和执行其他内部技巧等。
Step 1. 变量
一、在 JavaScript 中,每个运行的函数,代码块{…}以及整个脚本,都有一个被称为词法环境(Lexical Environment)的内部(隐藏)的关联对象。
二、词法环境对象由两部分组成:
- 环境记录(Environment Record)—— 一个存储所有局部变量作为其属性(包括一些其他信息,例如this的值)的对象。
- 对外部词法环境的引用,与外部代码相关联。
三、一个“变量”只是环境记录这个特殊的内部对象的一个属性。“获取或修改变量”意味着“获取或修改词法环境的一个属性”。
【示例】这段没有函数的简单的代码中只有一个词法环境:![]() 1、这就是所谓的与整个脚本相关联的全局词法环境。 2、在上面的图片中,矩形表示环境记录(变量存储),箭头表示外部引用。全局词法环境没有外部引用,所以箭头指向了null。 3、随着代码开始并继续运行,词法环境发生了变化。 |
---|
| 【示例】这是更长的代码:
1、右侧的矩形演示了执行过程中全局词法环境的变化:
(1)当脚本开始运行,词法环境预先填充了所有声明的变量。
- 最初,它们处于“未初始化(Uninitialized)”状态。这是一种特殊的内部状态,这意味着引擎知道变量,但是在用let声明前,不能引用它。几乎就像变量不存在一样。
(2)然后let phrase定义出现了。它尚未被赋值,因此它的值为undefined。从这一刻起,我们就可以使用变量了。
(3)phrase被赋予了一个值。
(4)phrase的值被修改。
2、
- 变量是特殊内部对象的属性,与当前正在执行的(代码)块/函数/脚本有关。
- 操作变量实际上是操作该对象的属性。
|
| —- |
Step 2. 函数声明
一、一个函数其实也是一个值,就像变量一样。
二、不同之处在于函数声明的初始化会被立即完成。
三、当创建了一个词法环境时,函数声明会立即变为即用型函数(不像let那样直到声明处才可用)。
1、这就是为什么我们可以在(函数声明)的定义之前调用函数声明。
| 【示例】这是添加一个函数时全局词法环境的初始状态:
正常来说,这种行为仅适用于函数声明,而不适用于我们将函数分配给变量的函数表达式,例如```javascript
let say = function(name)…
|
| --- |
<a name="V1fTy"></a>
## Step 3. 内部和外部的词法环境
一、在一个函数运行时,在调用刚开始时,会自动创建一个新的词法环境以存储这个调用的局部变量和参数。
| 【示例】对于say("John"),它看起来像这样(当前执行位置在箭头标记的那一行上):<br /><br />1、在这个函数调用期间,我们有两个词法环境:内部一个(用于函数调用)和外部一个(全局):<br />- 内部词法环境与say的当前执行相对应。它具有一个单独的属性:name,函数的参数。我们调用的是say("John"),所以name的值为"John"。<br />- 外部词法环境是全局词法环境。它具有phrase变量和函数本身。<br />
2、内部词法环境引用了outer。<br />3、访问变量时,搜索过程如下:<br />- 对于name变量,当say中的alert试图访问name时,会立即在内部词法环境中找到它。<br />- 当它试图访问phrase时,然而内部没有phrase,所以它顺着对外部词法环境的引用找到了它。<br />
 |
| --- |
二、当代码要访问一个变量时 —— 首先会搜索内部词法环境,然后搜索外部环境,然后搜索更外部的环境,以此类推,直到全局词法环境。<br />1、如果在任何地方都找不到这个变量,那么在严格模式下就会报错(在非严格模式下,为了向下兼容,给未定义的变量赋值会创建一个全局变量)。
<a name="stLdO"></a>
## Step 4. 返回函数
一、所有的函数在创建时都会记住创建它们的词法环境。所有函数都有名为[[Environment]]的隐藏属性,该属性保存了对创建该函数的词法环境的引用。<br />1、[[Environment]]引用在函数创建时被设置并永久保存。
| 【示例】让我们回到makeCounter这个例子。```javascript
function makeCounter() {
let count = 0;
return function() {
return count++;
};
}
let counter = makeCounter();
1、在每次makeCounter()调用的开始,都会创建一个新的词法环境对象,以存储该makeCounter运行时的变量。
2、因此,我们有两层嵌套的词法环境,就像上面的示例一样:
2、不同的是,在执行makeCounter()的过程中创建了一个仅占一行的嵌套函数:return count++。我们尚未运行它,仅创建了它。
3、[[Environment]]隐藏属性保存了对创建该函数的词法环境的引用。
(1)因此,counter.[[Environment]]有对{count: 0}词法环境的引用。这就是函数记住它创建于何处的方式,与函数被在哪儿调用无关。[[Environment]]引用在函数创建时被设置并永久保存。
4、稍后,当调用counter()时,会为该调用创建一个新的词法环境,并且其外部词法环境引用获取于counter.[[Environment]]:
5、现在,当counter()中的代码查找count变量时,它首先搜索自己的词法环境(为空,因为那里没有局部变量),然后是外部makeCounter()的词法环境,并且在哪里找到就在哪里修改。
6、在变量所在的词法环境中更新变量。
7、这是执行后的状态:
(1)如果我们调用counter()多次,count变量将在同一位置增加到2,3等。 |
| —- |
垃圾收集
一、通常,函数调用完成后,会将词法环境和其中的所有变量从内存中删除。因为现在没有任何对它们的引用了。与 JavaScript 中的任何其他对象一样,词法环境仅在可达时才会被保留在内存中。
二、如果有一个嵌套的函数在函数结束后仍可达,则它将具有引用词法环境的[[Environment]]属性。
三、当词法环境对象变得不可达时,它就会死去(就像其他任何对象一样)。换句话说,它仅在至少有一个嵌套函数引用它时才存在。
| 【示例】在下面这个例子中,即使在函数执行完成后,词法环境仍然可达。因此,此嵌套函数仍然有效。```javascript function f() { let value = 123;
return function() { alert(value); } }
let g = f(); // g.[[Environment]] 存储了对相应 f() 调用的词法环境的引用
1、如果多次调用f(),并且返回的函数被保存,那么所有相应的词法环境对象也会保留在内存中。下面代码中有三个这样的函数:```javascript
function f() {
let value = Math.random();
return function() { alert(value); };
}
// 数组中的 3 个函数,每个都与来自对应的 f() 的词法环境相关联
let arr = [f(), f(), f()];
2、嵌套函数被删除后,其封闭的词法环境(以及其中的value)也会被从内存中删除:```javascript function f() { let value = 123;
return function() { alert(value); } }
let g = f(); // 当 g 函数存在时,该值会被保留在内存中
g = null; // ……现在内存被清理了
|
| --- |
<a name="oqqel"></a>
## 实际开发中的优化
一、正如我们所看到的,理论上当函数可达时,它外部的所有变量也都将存在。<br />二、但在实际中,JavaScript 引擎会试图优化它。它们会分析变量的使用情况,如果从代码中可以明显看出有未使用的外部变量,那么就会将其删除。<br />三、在 V8(Chrome,Edge,Opera)中的一个重要的副作用是,此类变量在调试中将不可用。
| 【示例】打开 Chrome 浏览器的开发者工具,并尝试运行下面的代码。<br />1、当代码执行暂停时,在控制台中输入alert(value)。```javascript
function f() {
let value = Math.random();
function g() {
debugger; // 在 Console 中:输入 alert(value); No such variable!
}
return g;
}
let gg = f();
gg();
2、正如你所见的 —— No such variable! 理论上,它应该是可以访问的,但引擎把它优化掉了。
3、这可能会导致有趣的(如果不是那么耗时的)调试问题。其中之一 —— 我们可以看到的是一个同名的外部变量,而不是预期的变量:```javascript
let value = “Surprise!”;
function f() { let value = “the closest value”;
function g() { debugger; // 在 console 中:输入 alert(value); Surprise! }
return g; }
let g = f();
g();
```
4、V8 引擎的这个特性你真的应该知道。如果你要使用 Chrome/Edge/Opera 进行代码调试,迟早会遇到这样的问题。
5、20210429:这不是调试器的 bug,而是 V8 的一个特别的特性。也许以后会被修改。你始终可以通过运行本文中的示例来进行检查。 |
| —- |