第一遍学习廖雪峰的Javascript教程时,对闭包的理解就一直很模糊。当然,也可能是因为还没有主动去留意过。这周让已经工作的朋友推荐了一些闭包相关的资料,深入地来复习一下闭包的概念。
# 从一段代码说起
现在,有一个需求:
创建10个<a>
标签,点击对应标签,弹出来对应的序号。
于是,你开始构思这样一段代码:
<div id="app"></div>
<script>
(function () {
//获取div节点
var div = document.getElementById("app");
//创建10个<a>标签,并添加到div中
for (var i = 0; i < 10; i++) {
//创建一个<a>标签
const a = document.createElement("a");
//给a标签href赋#值,防止跳转页面
a.href = "#";
//给a标签内部赋值,让它显示得个性一些
a.innerHTML = "第" + i + "个链接\n\n";
//向div中添加这个a子节点
div.appendChild(a);
}
//找到html中的所有<a>标签
var nodes = document.getElementsByTagName("a");
//给所有<a>标签添加点击事件,并弹出对应序号j
for (var j = 0; j < nodes.length; j++) {
nodes[j].addEventListener("click", function () {
alert(j);
});
}
})();
</script>
Nice!
你对自己的Javascript
基础表示很满意,并竖起了大拇指。
好了,我们开始运行吧!
你用浏览器打开了这个html
页面,不错,所有的<a>
标签当然都正确地生成了。
但是,当你开始点击标签的时候,却发现,不管点击哪一个链接,弹框里竟然显示的都是10
!
# 哪里出了问题?
让我们冷静一下。
好了,仔细想想,可以想象:代码应该是在给所有<a>
标签添加点击事件时这里出了问题:
for (var j = 0; j < nodes.length; j++) {
nodes[j].addEventListener("click", function () {
alert(j);//不知道为什么,运行之后这里全部都变成了10,而不是预期中的j
});
}
我们不难得出结论,结果是10
,是因为总共有10个<a>
标签(可以通过改变第一段代码中生成的<a>
数量来判断)。而j
在循环结束后,最终值正好是10
!
那么,是怎样的运行机制,导致结果都变成了最终值呢?
这里我们不妨先做一下假设:
所有的
<a>
的click
事件,是在for循环结束后,被绑定了alert(10)
所有的
<a>
的click
事件直接被绑定了alert(j)
,而在调用的时候,j
变成10
了,所以最终都变成了alert(10)
让我们带着疑问,继续往下看吧。
# 另一段代码
在下面这段代码中,我们定义了一个count()
函数,其内部引用了局部变量arr
。我们希望实现的是,创建3个函数,并且把函数都保存在arr
数组中去。
function count() {
var arr = [];
for (var i=1; i<=3; i++) {
arr.push(function () {
return i * i;
});
}
return arr;
}
var results = count();
var f1 = results[0];//=>arr[0]
var f2 = results[1];//=>arr[1]
var f3 = results[2];//=>arr[2]
看了这段代码,我们可以很容易得出“f1()
,f2()
,f3()
运行后的结果分别为1
,4
,9
”,这样一个结论。
但是,实际运行时,却发现:
f1();//16
f2();//16
f3();//16
# 熟悉而陌生
上面两段代码,除了都涉及到循环外,我们还可以看到很强的相似性:
- 调用的方法是在函数内部定义的,且引用了函数内部的局部变量
- 调用的方法是在函数外部执行
实际上,这两个例子就是Javascript
中的闭包
在“作祟”。
# 作用域
# 编译过程
通常,编程语言在一段代码执行之前,会进行三个主要步骤:
- 词法分析: 把字符串分解成
词法单元(token)
- 语法分析: 将词法单元流(数组)转换成由元素逐级嵌套组成的
抽象语法树(Abstract Syntax Tree,AST)。
; - 代码生成:将
AST
转换为可执行代码
当然,JavaScript的处理过程并不仅仅是如此。例如,JavaScript引擎还会经过特定的步骤优化运行性能,包括对冗余元素的优化。
# 作用域
# 词法作用域
定义 :是定义在词法(分析)阶段的作用域,是由写代码时变量和块作用域的位置决定的。
作用:词法分析阶段,JavaScript引擎就能知道标识符的位置以及如何声明的。这使JavaScript能够预测在执行过程中查找对应的标识符
# 函数作用域
定义:是定义在函数声明时的作用域。
作用:
1. 属于这个函数的全部变量都可以在整个函数范围内,包括嵌套在其内部的作用域内使用和复用。
2. 隐藏内部实现
3. 规避命名冲突
# 块作用域
定义:具有块作用域的变量或函数,仅可以在当前的块(通常在{ ... }内)中被调用,而无法在该块作用域外进行调用。
作用:避免变量被混乱地复用,提升代码可维护性
# let
对于下面的代码,使用var
声明变量i
:
for(var i = 0; i < 10; i++){
//一段代码
}
console.log(i);//10
虽然i
仅仅在for
循环内被使用,但其却被绑定到上层的作用域中去了。
而使用let
标识符定义变量i
, 则在for循环外部将无法访问到i
的值。
for(let i = 0; i < 10; i++){
//一段代码
}
console.log(i);//Uncaught ReferenceError: i is not defined
# try { ... } catch{ ... }
try {
undefined(); // 执行一个非法操作来强制制造一个异常
}
catch (err) {
console.log( err ); // 能够正常执行!
}
console.log( err ); // ReferenceError: err not found
# 作用域链
当JavaScript进行变量解析(即查找变量)时,
在示例1中,我们使用var
关键字定义了变量j
,它是该匿名函数内的局部变量,作用于整个函数。
# 作用域链
# 概念
概念1:当函数可以记住并访问所在的词法作用域,就产生了闭包,即使函数是在当前词法作用域之外执行[1]
概念2:当一个嵌套的内部(子)函数引用了嵌套的外部(父)函数的变量或函数时,就产生了闭包
产生的条件
# 引用
[1] 你不知道的JavaScript(上卷),第五章
[2] JavaScript权威指南(第6版)