词法环境(现代JavaScript阅读笔记)

212 阅读6分钟

这是一篇阅读笔记,原文地址:zh.javascript.info/closure

词法环境

Step 1. 变量

JavaScript 中,每个运行的 函数代码块{...} 以及 整个脚本,都有一个被称为 词法环境(Lexical Environment) 的内部(隐藏的)关联对象

关键字:函数 代码块{...} 整个脚本 词法环境 关联对象

词法环境对象由两部分组成:

1.环境记录(Environment Record) —— 一个存储所有 局部变量 作为其 属性(包括一些其他信息,例如 this 的值)的 对象

(通俗的说 环境记录 是一个对象:{局部变量1:value, 局部变量2:value, ...}

2.对 外部词法环境 的引用,与外部代码相关联。

一个 "变量" 只是 环境记录 这个特殊的内部对象的一个属性。"获取或修改变量" 意味着 "获取或修改词法环境的一个属性"

001.png

关键字:环境记录 局部变量 外部词法环境

示例1:

// a.js脚本
const name = "张三";
const age = 23;

002.png

a.js脚本的词法环境分析:

1.环境记录

// 存储a.js脚本中的所有局部变量
{
  name: "张三",
  age: 23
}

nameage 变量只是 环境记录 这个特殊的内部对象的属性。

2.对 外部词法环境 的引用

// 没有外部引用,所以箭头指向了 null
{name: "张三", age: 23} => null

示例2:

// b.js 脚本
const message = "你好";
if (message) {
  alert(message);
}

003.png

b.js脚本的词法环境分析:

b.js脚本有两个词法环境:代码块词法环境脚本词法环境

代码块词法环境

1.环境记录

// 代码块中没有局部变量
{}

2.对 外部词法环境 的引用

// 代码块引用了外部变量
{ } => { message: "你好" }

脚本词法环境

1.环境记录

// 存储b.js脚本中的所有局部变量
{message: "你好"}

2.对 外部词法环境 的引用

// 没有外部引用,所以箭头指向了 null
{message: "你好"} => null

示例3:

// c.js脚本
const message = "你好";

function hello(name: string) {
  alert(message + name)
}

hello("张三")

004.png

c.js脚本的词法环境分析:

c.js脚本有两个词法环境:函数词法环境脚本词法环境

函数词法环境

1.环境记录

// 存储 hello 函数中的变量
{name: "张三"}

2.对 外部词法环境 的引用

// 函数引用了外部变量
{name: "张三"} => { message: "你好" }

脚本词法环境

1.环境记录

// 存储b.js脚本中的所有局部变量
{
  message: "你好",
  hello: function
}

2.对 外部词法环境 的引用

// 没有外部引用,所以箭头指向了 null
{
  message: "你好",
  hello: function
} => null

注意:词法环境是一个规范对象

"词法环境"是一个规范对象(specification object):它仅仅是存在于"编程语言规范"中的"理论上"存在的,用于描述事物如何运作的对象。我们无法在代码中获取该对象并直接对其进行操作。

关键字:规范对象 编程语言规范 理论上 描述事物如何运作

Step 2. 函数声明

一个函数其实也是一个值,就像变量一样。不同之处在于函数声明的初始化会被立即完成

当创建了一个词法环境(Lexical Environment)时,函数声明会立即变为即用型函数(不像 let const 那样直到声明处才可用)。

关键字:函数 变量 函数声明 初始化 立即完成 即用型函数

示例:脚本词法环境初始状态

// c.js脚本
const message = "你好";

function hello(name: string) {
  alert(message + name)
}

hello("张三")

脚本词法环境初始状态:

005.png

1.当脚本开始运行,词法环境预先填充了所有声明的变量。({message: <uninitialized>, hello: function}

2.最初,变量 处于 未初始化(Uninitialized) 状态。这是一种特殊的内部状态,这意味着引擎知道变量,但是在用 let const 声明前,不能引用它。几乎就像变量不存在一样。

3.函数声明 会立即变为 即用型函数。这就是为什么我们可以在(函数声明)的定义之前调用函数声明。这种行为仅适用于函数声明,而不适用于我们将函数分配给变量的函数表达式,例如 const hello = function(name)...

关键字:开始执行 预先填充 变量 未初始化 特殊的内部状态 函数声明 函数表达式

Step 3. 内部和外部的词法环境

在一个函数运行时,在调用刚开始时,会自动创建一个新的词法环境以存储这个调用的局部变量和参数。

关键字:函数运行时 调用开始时 自动创建 新的词法环境 局部变量和参数

示例:

// c.js脚本
const message = "你好";

function hello(name: string) {
  alert(message + name)
}

hello("张三");

脚本词法环境初始状态,只有一个词法环境(脚本词法环境)。

005.png

hello("张三"); 调用开始时,我们有两个词法环境:内部一个(hello 函数词法环境)和外部一个(脚本词法环境)。

004.png

当代码要访问一个变量时 —— 首先会搜索内部词法环境,然后搜索外部环境,然后搜索更外部的环境,以此类推,直到全局词法环境。(有没有想到什么:原型链)

如果在任何地方都找不到这个变量,那么在严格模式下就会报错(在非严格模式下,为了向下兼容,给未定义的变量赋值会创建一个全局变量)。

关键字:访问 变量 搜索 内部词法环境 外部词法环境 更外部词法环境 严格模式 报错

Step 4. 返回函数

// d.js脚本
function makeCounter() {
  let count = 0;

  return function() {
    return count++;
  };
}

let counter = makeCounter();

counter();

006.png

在每次 makeCounter() 调用的开始,都会创建一个新的词法环境对象,以存储该 makeCounter 运行时的变量。

007.png

{makeCounter: function, counter: function} => null

在执行 makeCounter() 的过程中创建了一个嵌套函数:function() {return count++;}。我们尚未运行它,仅创建了它。

所有的函数在"诞生"时都会记住创建它们的词法环境。从技术上讲,这里没有什么魔法:所有函数都有名为 [[Environment]] 的隐藏属性,该属性保存了对创建该函数的词法环境的引用。

当调用 counter() 时,会为该调用创建一个新的词法环境

008.png

{} => {count: 0}   {makeCounter: function, counter: function} => null

counter() 中的代码查找 count 变量时,它首先搜索自己的词法环境(为空,因为那里没有局部变量),然后是外部 makeCounter() 的词法环境,并且在哪里找到就在哪里修改

在变量所在的词法环境中更新变量。

关键字:哪里找到 哪里修改 在变量所在的词法环境中更新变量

闭包

闭包 一个记住其外部变量并可以访问这些变量的函数。

也就是说:JavaScript 中的函数会自动通过隐藏的 [[Environment]] 属性记住创建它们的位置,所以它们都可以访问外部变量。

关键字:闭包 记住 外部变量 访问这些变量 函数

应用

1.函数会选择最新的内容吗?

let name = "John";

function sayHi() {
  alert("Hi, " + name);
}

name = "Pete";

sayHi(); // 会显示什么:"John" 还是 "Pete"?

2.哪些变量可用呢

function makeWorker() {
  let name = "Pete";

  return function() {
    alert(name);
  };
}

let name = "John";

let work = makeWorker();

work(); // 会显示什么?

3.Counter是独立的吗?

function makeCounter() {
  let count = 0;

  return function() {
    return count++;
  };
}

let counter = makeCounter();
let counter2 = makeCounter();

alert( counter() ); // 0
alert( counter() ); // 1

alert( counter2() ); // ?
alert( counter2() ); // ?