这是一篇阅读笔记,原文地址:zh.javascript.info/closure
词法环境
Step 1. 变量
在 JavaScript 中,每个运行的 函数,代码块{...} 以及 整个脚本,都有一个被称为 词法环境(Lexical Environment) 的内部(隐藏的)关联对象。
关键字:函数 代码块{...} 整个脚本 词法环境 关联对象
词法环境对象由两部分组成:
1.环境记录(Environment Record) —— 一个存储所有 局部变量 作为其 属性(包括一些其他信息,例如 this 的值)的 对象。
(通俗的说 环境记录 是一个对象:{局部变量1:value, 局部变量2:value, ...})
2.对 外部词法环境 的引用,与外部代码相关联。
一个 "变量" 只是 环境记录 这个特殊的内部对象的一个属性。"获取或修改变量" 意味着 "获取或修改词法环境的一个属性"。
关键字:环境记录 局部变量 外部词法环境
示例1:
// a.js脚本
const name = "张三";
const age = 23;
a.js脚本的词法环境分析:
1.环境记录
// 存储a.js脚本中的所有局部变量
{
name: "张三",
age: 23
}
name 和 age 变量只是 环境记录 这个特殊的内部对象的属性。
2.对 外部词法环境 的引用
// 没有外部引用,所以箭头指向了 null
{name: "张三", age: 23} => null
示例2:
// b.js 脚本
const message = "你好";
if (message) {
alert(message);
}
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("张三")
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("张三")
脚本词法环境初始状态:
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("张三");
脚本词法环境初始状态,只有一个词法环境(脚本词法环境)。
hello("张三"); 调用开始时,我们有两个词法环境:内部一个(hello 函数词法环境)和外部一个(脚本词法环境)。
当代码要访问一个变量时 —— 首先会搜索内部词法环境,然后搜索外部环境,然后搜索更外部的环境,以此类推,直到全局词法环境。(有没有想到什么:原型链)
如果在任何地方都找不到这个变量,那么在严格模式下就会报错(在非严格模式下,为了向下兼容,给未定义的变量赋值会创建一个全局变量)。
关键字:访问 变量 搜索 内部词法环境 外部词法环境 更外部词法环境 严格模式 报错
Step 4. 返回函数
// d.js脚本
function makeCounter() {
let count = 0;
return function() {
return count++;
};
}
let counter = makeCounter();
counter();
在每次 makeCounter() 调用的开始,都会创建一个新的词法环境对象,以存储该 makeCounter 运行时的变量。
{makeCounter: function, counter: function} => null
在执行 makeCounter() 的过程中创建了一个嵌套函数:function() {return count++;}。我们尚未运行它,仅创建了它。
所有的函数在"诞生"时都会记住创建它们的词法环境。从技术上讲,这里没有什么魔法:所有函数都有名为 [[Environment]] 的隐藏属性,该属性保存了对创建该函数的词法环境的引用。
当调用 counter() 时,会为该调用创建一个新的词法环境
{} => {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() ); // ?