嵌套函数
function makeCounter() {
let count = 0;
return function() {
return count++;
};
}
let counter = makeCounter();
alert( counter() ); // 0
alert( counter() ); // 1
alert( counter() ); // 2
词法环境
一.变量的环境
-
每个运行的函数、代码块、整个脚本都有一个被称为词法环境的内部的关联对象。
-
词法环境包含:
①环境记录:一个存储所有 局部变量 作为其 属性 (包括一些其他信息,例如 this 的值)的 对象。
②对 外部词法环境 的引用,与外部代码相关联。 -
全局词法环境
与整个脚本相关联
//①环境记录:phrase: "Hello"
//②外部语法环境:null(全局词法环境没有外部引用)
let phrase = "Hello";
alert(phrase);
- 变量在词法环境中的定义过程
1.phrase:< uninitialized >
当脚本开始运行,词法环境预先填充了所有声明的变量。
最初,它们处于“未初始化(Uninitialized)”状态。这是一种特殊的内部状态,这意味着引擎知道变量,但是在用 let 声明前,不能引用它。几乎就像变量不存在一样。
2.然后 let phrase 定义出现了。它尚未被赋值,因此它的值为 undefined。从这一刻起,我们就可以使用变量了。
3.phrase 被赋予了一个值。
4.phrase 的值被修改。
二、 函数声明
当创建了一个词法环境(Lexical Environment)时,函数声明会立即变为即用型函数(不像 let 那样直到声明处才可用)。
这就是为什么我们可以在(函数声明)的定义之前调用函数声明。
正常来说,这种行为仅适用于函数声明,而不适用于我们将函数分配给变量的函数表达式,例如 let say = function(name)…。
三.内部和外部的词法环境
say函数词法环境:包含属性name:"John"
全局词法环境:包含say: function
phrase:"Hello"
当代码要访问一个变量时 —— 首先会搜索内部词法环境,然后搜索外部环境,然后搜索更外部的环境,以此类推,直到全局词法环境。
四.返回函数
//每次makeCounter()调用的开始,都会创建一个新的词法环境对象
//包含makeCounter词法环境和全局词法环境
function makeCounter() {
let count = 0;
return function() {
return count++;
};
}
//makeCounter返回一个函数,仅仅是返回,并没有运行
//返回该函数的同时创建该函数的词法环境(所有的函数在“诞生”时都会记住创建它们的词法环境)
//包括环境记录和外部词法环境的引用
//因为外部词法环境(makeCounter)有该函数的引用,根据JavaScript垃圾回收机制,外部词法环境(makeCounter)不会消失,也就是该函数的引用指针不会指向null,外部 let count = 0 会被保存下来
//let counter1 = function(){
// return count++;
//};
let counter1 = makeCounter();
//运行的时候counter1的时候,会为该调用创建一个新的词法环境,并且其外部词法环境引用获取于之前保存的外部词法环境。在当前环境中找不到count变量,根据外部词法环境的引用可以找到 count = 0
counter1();
在变量所在的词法环境中更新变量
function makeCounter() {
let count = 0;
return function () {
return count++;
};
}
let counter = makeCounter();
//每次调用的时候,会为该调用创建一个新的词法变量,但是这些新的词法变量的外部环境变量都指向的都是同一个makeCounter,所以呈现递增
alert(counter()); //0
alert(counter()); //1
alert(counter()); //2
alert(counter()); //3
//counter1是通过makeCounter的又一次调用创建的
//和counter不属于同一个
let counter1 = makeCounter();
alert(counter1()); //0
alert(counter1()); //1
alert(counter1()); //2
alert(counter1()); //3
垃圾收集
如果词法环境没有任何引用,那么该环境将会被删除,词法环境仅在可达时才会被保留在内存中
function f() {
let value = 123;
return function() {
alert(value);
}
}
let g = f(); // g.[[Environment]] 存储了对相应 f() 调用的词法环境的引用
function f() {
let value = Math.random();
return function() { alert(value); };
}
// 数组中的 3 个函数,每个都与来自对应的 f() 的词法环境相关联
let arr = [f(), f(), f()];
function f() {
let value = 123;
return function() {
alert(value);
}
}
let g = f(); // 当 g 函数存在时,该值会被保留在内存中
g = null; // ……现在内存被清理了
实际开发中的优化:
但在实际中,JavaScript 引擎会试图优化它。它们会分析变量的使用情况,如果从代码中可以明显看出有未使用的外部变量,那么就会将其删除。
在 V8(Chrome,Edge,Opera)中的一个重要的副作用是,此类变量在调试中将不可用。
function f() {
let value = Math.random();
function g() {
//在函数g中并没有使用外部的变量
//JavaScript引擎试图优化,将外部环境f删除
debugger; // 在 Console 中:输入 alert(value); No such variable!
}
return g;
}
let g = f();
g();
let value = "Surprise!";
function f() {
let value = "the closest value";
function g() {
debugger; // 在 console 中:输入 alert(value); Surprise!
}
return g;
}
let g = f();
//首先在g环境中寻找,没有找到
//接着去f环境中找,f环境被删了
//最后去全局环境中找,找到了
g();
=====================================
练习
一、
let name = "John";
function sayHi() {
alert("Hi, " + name);
}
name = "Pete";
sayHi(); // 会显示Pete
//解答:
//sayHi的外部词法环境是全局环境
//全局环境中保存了name = "John"
//在sayHi()函数调用之前,全局环境中保存的name值被换成了Pete
//所以调用sayHi(),首先在sayHi环境中找,没有找到,之后去全局环境中找,这时name已经被换了,所以是Pete
二、
let phrase = "Hello";
if (true) {
let user = "John";
function sayHi() {
alert(`${phrase}, ${user}`);
}
}
sayHi(); //执行结果是什么
//答案:error。
//函数 sayHi 是在 if 内声明的,所以它只存在于 if 中。外部是没有 sayHi 的。
三、
let x = 1;
function func() {
console.log(x); // ?
let x = 2;
}
func();
//执行结果是error
//程序执行进入代码块(或函数)的那一刻起,变量就开始进入“未初始化”状态。它一直保持未初始化状态,直至程序执行到相应的 let 语句。
//换句话说,一个变量从技术的角度来讲是存在的,但是在 let 之前还不能使用。
//变量暂时无法使用的区域(从代码块的开始到 let)有时被称为“死区”。
四、
function makeArmy() {
let shooters = [];
let i = 0;
while (i < 10) {
let shooter = function() { // 创建一个 shooter 函数,
alert( i ); // 应该显示其编号
};
shooters.push(shooter); // 将此 shooter 函数添加到数组中
i++;
}
// ……返回 shooters 数组
return shooters;
}
let army = makeArmy();
// ……所有的 shooter 显示的都是 10,而不是它们的编号 0, 1, 2, 3...
army[0](); // 编号为 0 的 shooter 显示的是 10
army[1](); // 编号为 1 的 shooter 显示的是 10
army[2](); // 10,其他的也是这样。
让我们检查一下 makeArmy 内部到底发生了什么,那样答案就显而易见了。
1.它创建了一个空数组 shooters:
let shooters = [];
2.在循环中,通过 shooters.push(function) 用函数填充它。
每个元素都是函数,所以数组看起来是这样的:
shooters = [
function () { alert(i); },
function () { alert(i); },
function () { alert(i); },
function () { alert(i); },
function () { alert(i); },
function () { alert(i); },
function () { alert(i); },
function () { alert(i); },
function () { alert(i); },
function () { alert(i); }
];
4.调用这个函数的时候,i的值已经变为10,alert(i)时去本环境中找到i为10,所以输出结果相同
修改:
function makeArmy() {
let shooters = [];
let i = 0;
while (i < 10) {
let j = i;
let shooter = function() { // shooter 函数
alert( j ); // 应该显示它自己的编号
};
shooters.push(shooter);
i++;
}
return shooters;
}
let army = makeArmy();
// 现在代码正确运行了
army[0](); // 0
army[5](); // 5
调用函数的时候,去本环境中去找j,由于是let,每次的j都不一样,所以代码正确运行了
如果我们一开始使用for循环,也可以避免这样的问题
function makeArmy() {
let shooters = [];
for(let i = 0; i < 10; i++) {
let shooter = function() { // shooter 函数
alert( i ); // 应该显示它自己的编号
};
shooters.push(shooter);
}
return shooters;
}
let army = makeArmy();
army[0](); // 0
army[5](); // 5
参考自:
变量作用域,闭包