- 使用闭包简化代码
- 使用执行上下文跟踪 JavaScript 程序的执行
- 使用词法环境(Lexical Environment)跟踪变量的作用域
- 理解变量的类型
- 探讨闭包的工作原理
var outerValue = 'samurai';
var later; // 声明一个空变量,稍后在后面的代码中使用
function outerFunction() {
var innerValue = 'ninja'; // 在函数内部声明一个值,该值在作用域局限于函数内部,在函数外部不允许访问
function innerFunction() {
// 在 outerFunction 函数中声明一个内部函数,声明该内部函数时,innerValue 是在内部函数的作用域内的
console.log(outerValue === 'samurai', 'I can see the samurai.');
console.log(innerValue === 'ninja', 'I can see the ninja.');
}
later = innerFunction; // 将内部函数 innerFunction 的引用存储在变量 later 上,因为 later 在全局作用域内,所以我们可以对它进行调用
}
outerFunction(); // 调用 outerFunction 函数,创建内部函数 innerFunction,并 将内部函数赋值给变量later
later(); // 通过 later 调用内部函数。我们不能直接调用内部函数,因为它的作用域(和 innerValue 一起) 被限制在外部函数 outerFunction 之内
每一个通过闭包访问变量的函数都具有一个作用域链,作用域链包含闭包的全部信息
使用闭包模拟私有变量
function Ninja() {
var feints = 0;
this.getFeints = function() {
return feints;
};
this.feint = function() {
feints++;
};
}
var ninja1 = new Ninja();
ninja1.feint();
console.log(ninja1.feints === undefined); // true
var ninja2 = new Ninja();
console.log(ninja2.getFeints() === 0); // true
function skulk(ninja) {
report(ninja + 'skulking');
}
function report(message) {
console.log(message);
}
skulk('Kuma');
- 每个 JavaScript 程序只创建一个全局执行上下文,并从全局执行上下文开始执行 (在单页应用中每个页面只有一个全局执行上下文)。当执行全局代码时,全局执行上下文处于活跃状态。
- 首先在全局代码中定义两个函数:
skulk
和report
,然后调用skulk("Kuma")
。由于在同一个特定时刻只能执行特定代码,所以 JavaScript 引擎停止执行全局代码,开始执行带有Kuma
参数的skulk
函数。创建新的函数执行上下文,并置入执行上下文栈的顶部。 skulk
函数进而调用report
函数。又一次因为在同一个特定时刻只能执行特定代码,所以,暂停skulk
执行上下文,创建新的Kuma
作为参数的report
函数的执行上下文,并置入执行上下文栈的顶部。report
通过内置函数console.log
打印出消息后,report
函数执行完成,代码又回到了skulk
函数。report
执行上下文从执行上下文栈顶部弹出,skulk
函数执行上下文重新激活,skulk
函数继续执行。skulk
函数执行完成后也发生类似的事情:skulk
函数执行上下文从栈顶端弹出,重新激活一直在等待的全局执行上下文并恢复执行。JavaScript 的全局代码恢复执行。
词法环境(lexical environment)是 JavaScript 引擎内部用来跟踪标识符与特定变量之间的映射关系。
var ninja = 'Hattori';
console.log(ninja);
当 console.log
语句访问 ninja
变量时,会进行词法环境的查询。
词法环境是 JavaScript 作用域的内部实现机制,人们通常称为作用域(scopes)。在 JavaScript 的 ES6 初版中,词法环境只能与函数关联。变量只存在于函数作用域中。这也带来了一些混淆。因为 JavaScript 是一门类 C 的语言,从其他类 C 语言(如 C++、 C#、Java 等)转向 JavaScript 的开发者通常会预期一些初级概念,例如块级作用域。在 ES6 中最终修复了块级作用域问题。
无论何时创建函数,都会创建一个与之相关联的词法环境,并存储在名为 [[Environment]]
的内部属性上(也就是说无法直接访问或操作)。两个中括号用于标志内部属性,这些环境是在函数创建时决定的
在 JavaScript 中,我们可以通过 3 个关键字定义变量:
按变量可变性:var
、let
为一类,const
为一类。
与词法环境的关系:可以将 var
分为一组,let
与 const
分为一组。
- var
- 使用关键字
var
时,该变量是在距离最近的函数内部或是在全局词法环境中定义的。(注意:忽略块级作用域)
- 使用关键字
- let
let
和const
更加 直接。let
和const
直接在最近的词法环境中定义变量(可以是在块级作用域内、循环内、函数内或全局环境内)。我们可以使用let
和const
定义块级别、函数级别、全局级别的变量。
- const
- 不需要重新赋值的特殊变量。
- 指向一个固定的值,例如球队人数的最大值,可通过 const 变量 MAX_RONIN_COUNT 来表示,而不是仅仅通过数字 234 来表示。这使得代码更加易于理解和维护。虽然在代码里没有直接使用数字 234,但是通过语义化 的变量名 MAX_RONIN_COUNT 来表示,MAX_RONIN_COUNT 的值只能指定一次。
JavaScript 代码的执行事实上是分两个阶段进行的。
- 一旦创建了新的词法环境,就会执行第一阶段。在第一阶段,没有执行代码,但是 JavaScript 引擎会访问并注册在当前词法环境中所声明的变量和函数。
- JavaScript 在第一阶段完成之后开始执行第二阶段,具体如何执行取决于变量的类型(let、var、const 和函数声明)以及环境类型(全局环境、函数环境或块级作用域)。
- 如果是创建一个函数环境,那么创建形参及函数参数的默认值。如果是非函数环境,将跳过此步骤。
- 如果是创建全局或函数环境,就扫描当前代码进行函数声明(不会扫描其他函数的函数体),但是不会执行函数表达式或箭头函数。对于所找到的函数声明,将创建函数,并绑定到当前环境与函数名相同的标识符上。若该标识符已经存在,那么该标识符的值将被重写。如果是块级作用域,将跳过此步骤。
- 扫描当前代码进行变量声明。在函数或全局环境中,查找所有当前函数以及其他函数之外通过
var
声明的变量,并查找所有通过let
或const
定义的变量。在块级环境中,仅查找当前块中通过let
或const
定义的变量。对于所查找到的变量,若该标识符不存在,进行注册并将其初始化为undefined
。若该标识符已经存在,将保留其值。
在函数声明之前调用函数
console.log(typeof fun === 'function'); // true
console.log(typeof myFunExp === 'undefined'); // true
console.log(typeof myArrow === 'undefined'); // true
function fun() {}
var myFunExpr = function() {};
var myArrow = x => x;
函数重载
console.log(typeof fun === 'function'); // true
var fun = 3;
console.log(typeof fun === 'number'); // true
function fun() {}
console.log(typeof fun === 'number'); // true
<div id="box1">First Box</div>
<div id="box2">Second Box</div>
<script>
function animateIt(elementId) {
var elem = document.getElementById(elementId);
var tick = 0;
var timer = setInterval(function(){
if (tick < 100) {
elem.style.left = elem.style.top = tick + "px";
tick++;
} else {
clearInterval(timer);
console.log(tick === 100);
console.log(elem);
console.log(timer);
}
}, 10);
}
animateIt("box1");
animateIt("box2");
</script>
每次调用 animateIt 函数时,均会创建新的词法环境,该词法环境保存了动画所需的重要变量(elementId
、elem
、动画元素
、tick
、计数次数
、timer
、动画计数器的 ID
)。只要至少有一个通过闭包访问这些变量的函数存在,这个环境就会一直保持。
<div id="box1">First Box</div>
<div id="box2">Second Box</div>
<script>
function animateIt(elementId) {
var elem = document.getElementById(elementId);
var tick = 0;
var timer = setInterval(function(){
if (tick < 100) {
var position = tick + 'px';
elem.style.left = position;
elem.style.top = position;
tick++;
} else {
clearInterval(timer);
console.log(tick === 100);
console.log(elem);
console.log(timer);
}
}, 10);
}
animateIt("box1");
animateIt("box2");
</script>
在本例中,浏览器会一直保持 setInterval
的回调函数,直到调用 clearInterval
方法。随后,当一个计 时器到期,浏览器会调用对应的回调函数,通过回调函数的闭包访问创建闭包时的变量。这样避免了手动匹配回调函数的麻烦,并激活变量,极大地简化代码。
- 通过闭包可以访问创建闭包时所处环境中的全部变量。闭包为函数创建时所处 的作用域中的函数和变量,创建“安全气泡”。通过这种的方式,即使创建函数 时所处的作用域已经消失,但是函数仍然能够获得执行时所需的全部内容。
- 我们可以使用闭包的这些高级功能:
- 通过构造函数内的变量以及构造方法来模拟对象的私有属性。
- 处理回调函数,简化代码。
- JavaScript 引擎通过执行上下文栈(调用栈)跟踪函数的执行。每次调用函数时,都会创建新的函数执行上下文,并推入调用栈顶端。当函数执行完成后,对应的执行上下文将从调用栈中推出。
- JavaScript 引擎通过词法环境跟踪标识符(俗称作用域)。
- 在 JavaScript 中,我们可以定义全局级别、函数级别甚至块级别的变量。可以使用关键字 var、let 与 const 定义变量:
- 关键字 var 定义距离最近的函数级变量或全局变量。
- 关键字 let 与 const 定义距离最近级别的变量,包括块级变量。块级变量在 ES6 之前版本的 JavaScript 中是无法实现的。此外,通过关键字 const 允许定义只能赋值一次的变量。
- 闭包是JavaScript作用域规则的副作用。当函数创建时所在的作用域消失后,仍然能够调用函数。