更改记录:
19年 11月27日,修改!
上一节说了执行上下文,这节咱们就乘胜追击来搞搞闭包!头疼的东西让你不再头疼!
名词解释:
变量对象
变量对象是根据(Variable Object) 来翻译过来的,也可以翻译成可变对象, 就是保存变量的对象,活动对象,闭包对象都保存着变量,因此也可以称为变量对象。
注:这里解释下,是因为各个书中对这几个名词的使用,搞的好远。
执行上下文
是根据(Execution Context)翻译过来的,也可译为执行环境。
在函数执行时,就会首先创建执行上下文来运行代码。
活动对象
是根据(Activation Object)翻译过来的,也可译为激活的对象。
在函数执行时,创建的变量对象,不仅含有变量,还有特殊的this,arguments。
闭包
是根据(closure) 翻译过来的,也可译为 闭合,使结束等,我认为可以理解为 封闭的环境。
把当前作用域外的环境封闭起来,以备 其他作用域环境使用。
总结:好多名词都是英译过来的,看原著或根据上下文来理解才是这些单词真正的意思,只可意会不可言传。。。
一、函数也是引用类型的。
function f(){ console.log("not change") };
var ff = f;
function f(){ console.log("changed") };
ff();
//"changed"
//ff 保存着函数 f 的引用,改变f 的值, ff也变了
//来个对比,估计你就明白了。
var f = "not change";
var ff = f;
f = "changed";
console.log(ff);
//"not change"
//ff 保存着跟 f 一样的值,改变f 的值, ff 不会变
其实,就是引用类型 和 基本类型的 区别。
<br/>
二、函数创建一个参数,就相当于在其内部声明了该变量
function f(arg){
console.log(arg)
}
f();
//undefined
function f(arg){
arg = 5;
console.log(arg);
}
f();
//5
<br/>
三、参数传递,就相当于变量复制(值的传递)
基本类型时,变量保存的是数据,引用类型时,变量保存的是内存地址。参数传递,就是把变量保存的值 复制给 参数。
var o = { a: 5 };
function f(arg){
arg.a = 6;
}
f(o);
console.log(o.a);
//6
<br/>
四、垃圾收集机制
JavaScript 具有自动垃圾收集机制,执行环境会负责管理代码执行过程中使用的内存。函数中,正常的局部变量和函数声明只在函数执行的过程中存在,当函数执行结束后,就会释放它们所占的内存(销毁变量和函数)。
而js 中 主要有两种收集方式:
- 标记清除(常见) //给变量标记为“进入环境” 和 “离开环境”,回收标记为“离开环境”的变量。
- 引用计数 // 一个引用类型值,被赋值给一个变量,引用次数加1,通过变量取得引用类型值,则减1,回收为次数为0 的引用类型值。
知道个大概情况就可以了,《JavaScript高级程序设计 第三版》 4.3节 有详解,有兴趣,可以看下。.
<br/>
五、作用域
在 JavaScript 中, 作用域(scope,或译有效范围)顾名思义就是变量和函数的作用范围(可访问范围)。
- 作用域可以实体化为一个 可变对象(Variable Object 变量对象)
- JavaScript中的作用域有:全局作用域和局部作用域(函数作用域)。ES6 新增了块级作用域
全局作用域(Global Scope)
(1)不在任何函数内定义的变量就具有全局作用域。(非严格模式下)
(2)实际上,JavaScript默认有一个全局对象window,全局作用域的变量实际上被绑定到window的一个属性。
局部作用域(Local Scope)
(1)JavaScript的作用域是通过函数来定义的,在一个函数中定义的变量只对这个函数内部可见,称为函数(局部)作用域。
块级作用域
块级作用域指在If语句,switch语句,循环语句等语句块中定义变量,这意味着变量不能在语句块之外被访问。
六、函数跟作用域链间的关系
每个函数都有一个[[scope]] 的内部属性(可以通过console.dir(fn),来查看),它保存着作用域链(一个对象数组),而作用域链中是一个个可变对象(Variable Object 变量对象)(一个保存当前作用域中用到的变量,函数等的对象)。当函数创建时,一个代表全局环境的可变对象会被插入到作用域的第一个位置。该全局可变对象保存着window,navigator,document 等。
例如如下 声明一个全局函数:
function add(num1, num2) {
return num1 + num2
}
当函数执行时,会创建执行上下文(执行环境),随后创建一个执行上下文对象,它有自己的作用链。刚开始,它会用函数自身的 [[scope]] 中的作用域链初始化自己(也就是复制)。
随后一个活动对象被创建(也可以说是变量对象,可变对象),它保存着当前函数作用域里的变量,arguments,this 等。最后,该活动对象会被推到执行上下文的作用域链的最前端。
注:执行上下文(执行环境)在函数执行完毕后就会被销毁,里面的作用域链,变量,函数,活动对象,this 等也会一同销毁。
七、作用域链查找
在函数执行过程中,每遇到一个变量,都会经历一次标识符解析过程以决定从那里获取存储数据。该过程搜索执行环境的作用域链,查找同名的标识符。搜索过程从作用域链头部开始也就是当前运行的作用域。如果找到,就使用这个标识符对应的变量;如果没找到,继续搜索作用域链中的下一个对象。搜索过程会持续进行,直到找到标识符,若无法搜索到匹配的对象,那么标识符将被视为未定义的。
八、闭包函数 与 闭包对象
当函数嵌套时,例如有一个A函数,内部有个v1 的变量,有一个B函数,B 中使用了v1 变量。这时,为了让 B 执行时,能访问 v1(其实就是为了形成作用域链),会有以下两个变化:
- 形成一个闭包函数,生成一个闭包对象 A,包含了 B 中用到 v1 变量
- 在B 闭包函数的[[scope]] 属性中 推入 闭包对象A。
function A(){
var v1 = 666
function B() {
return v1
}
console.dir(B)
B()
}
A()
执行结果,看函数的[[scope]] 属性:
注:可以通过 debugger 来在谷歌浏览器控制台里看。具体怎么用,可以自行百度。
现在来分析一下过程:
1、首先,A 函数执行,一开始 它的[[scope]] 内的作用域链中只有全局的可变对象,然后 创建一个执行上下文对象,有一个作用域链,根据 [[scope]] 复制来 来初始化自己。
2、创建 A 函数的活动对象,并推到 执行上下文对象的作用域链中。
3、当发现 B 中用到 v1 时,B 就会变成一个封闭的函数(闭包函数),然后,生成一个关于A 函数的封闭对象(闭包对象),保存着 v1(因为它存在于A,在B中使用)。随后,把这个封闭的对象推到 B 函数 的[[scope]] 作用域链中。
注:这时,B 函数还没有执行。至于什么机制导致js 能够发现未执行的函数内使用了 A 函数内的变量,目前的知识还得不到答案。
4、当B 函数执行时,创建执行上下文,创建执行上下文对象,初始化执行上下文对象的作用域链(复制B 函数的[[scope]] 属性)。
5、随后创建一个活动对象,并推到 执行上下文对象的作用域链中。
这样,B 在执行时,就可以访问 v1 了,因为在一个作用域链中。
下面来总结下作用链的变化过程:
- 全局下的 A函数执行时,内部的[[scope]] 保存的作用域链只有一个全局的变量对象。创建A 函数的执行上下文对象,根据 [[scope]] 复制初始化 A函数的 执行上下文对象的作用域链。
- 创建 A 函数的活动对象,推到 A函数执行上下文对象的作用域链前端。
- 当发现 A 函数内部(不管层级多深)有 一个函数使用了 A函数内的 变量或函数。
- 则 A 内(不管层级多深)所有函数 都会形成 闭包函数。
- 然后创建一个关于 A 的闭包对象,对象内含有被使用的变量或函数(通过复制)。
- 最后把该闭包对象 推到 所有闭包函数的 [[scope]] 内。
可以得出以下结论:
- 有两个作用域链,一个存与函数的[[scope]] 中,用来保存作用域,以备执行上下文对象初始化自身作用域链。
- 执行上下文对象中的作用域链,会添加活动函数,作用域链的查找,查的就是这条作用域链。(一般我们说的作用域链就是指这条)
- 活动函数只会存在于执行上下文对象的作用域链中。
- 有闭包函数和闭包对象,闭包函数的[[scope]] 保存闭包对象,而闭包对象,封闭的是 父或祖级函数作用域中的变量或对象。
- 闭包函数的存在是因为 执行上下文环境 会在执行完后销毁,而其中的作用域链,活动对象,变量等等就丢失了,通过闭包函数 就可以保存着作用域链,而链中的变量对象又保存着变量,函数等。
来一个难一点的例子,大家可以先自己分析分析。
function A(){
var va = 'aaa'
function B() {
var vb = 'bbb'
function C() {
var vc1 = 'ccc'
return va
}
function D() {
var vd = 'ddd'
return vb
}
console.dir(C)
console.dir(D)
C()
}
console.dir(B)
function E () {
var vd = 'eee'
}
console.dir(E)
B()
}
console.dir(A)
A()
根据上面分析,可以得出各个函数的[[scope]]:
- A 只有一个全局变量对象
- B 和 E 有两个变量对象,关于 A 的闭包对象,全局变量对象。
- C 和 D 有三个变量对象,关于 B 的闭包对象,关于 A 的闭包对象,全局变量对象。
控制台:
通过 debugger 来单步调试,无非就是能看到每个执行环境内的作用域链中 含有 活动对象。
有兴趣的可以试试。
至于闭包的内存泄漏,这里面牵扯到 js 的垃圾回收机制。不过可以看到,[[scope]] 中保存着 变量,如果 该变量 占的内存不被释放,一旦这样的情况过多,内存占用过大,就会造成内存泄漏 和 性能问题。
九、闭包的概念
一般说的闭包指的都是闭包函数。
引用高程(《JavaScript高级程序设计》)中关于闭包说法:
闭包是指有权访问另一个函数作用域中变量的函数
通过上面说的那么多,你品,你细品。。。
<br/>
十、闭包的本质
我认为就是为了形成作用域链。你品,你细品。。。
再来个有趣经典的例子:
function timer () {
for (var i=1; i<=5; i++) {
setTimeout(function(){
console.log(i);
},i*1000);
}
}
timer()
//每隔一秒输出一个6,共5个。
是不是跟你想的不一样?其实,这个例子重点就在setTimeout函数上,这个函数的第一个参数接受一个函数作为回调函数,这个回调函数并不会立即执行,它会在当前代码执行完,并在给定的时间后执行。这样就导致了上面情况的发生。
注:这里用一个函数包裹起来了,这样,你可以通过 debugger,会发现,这里也形成闭包了。闭包函数是每一个匿名函数,闭包对象是是关于timer 的,保存着变量 i
。
可以下面对这个例子进行变形,可以有助于你的理解把:
function timer () {
var i = 1;
while(i <= 5){
setTimeout(function(){
console.log(i);
},i*1000)
i = i+1;
}
}
timer()
正因为,setTimeout
里的第一个函数不会立即执行,当这段代码执行完之后,i
已经 被赋值为6
了(等于5
时,进入循环,最后又加了1
),所以 这时再执行setTimeout
的回调函数,读取 i
的值,回调函数作用域内没有i,向上读取,上级作用域内i
的值就是6
了。但是 i * 1000
,是立即执行的,所以,每次读的 i
值 都是对的。
这时候,就需要再用个闭包函数来保存每个循环时 i
不同的值。
function makeClosures(i){ // 这个函数使用了 上级作用域中的 `i`,形成闭包函数。
var i = i; //这步是不需要的,为了让看客们看的轻松点
return function(){
console.log(i); //匿名没有执行,它可以访问i 的值,保存着这个i 的值。
}
}
function timer() {
for (var i=1; i<=5; i++) {
setTimeout(makeClosures(i),i*1000);
//这里简单说下,这里makeClosures(i), 是函数执行,并不是传参,不是一个概念
//每次循环时,都执行了makeClosures函数,形成一个闭包函数,保存含有 `i` 的闭包对象(这个例子就是 5个 闭包函数保存各自的闭包对象)。
//然后每次都返回了一个没有被执行的匿名函数,(这里就是返回了5个匿名函数)。
//每个匿名函数都是一个局部作用域,它的上级作用域就是 makeClosures 闭包函数。
//因此,每个匿名函数执行时,读取`i`值,都是上级作用域内保存的值,是不一样的。所以,就得到了想要的结果
}
}
timer()
//1
//2
//3
//4
//5
你可能在别处,或者自己想到了下面这种解法:
for (var i=1; i<=5; i++) {
(function(i){
setTimeout(function(){
console.log(i);
},i*1000);
})(i);
}
这个例子不仅利用了闭包,而且还利用了立即执行函数 来模拟 函数作用域 来解决的。
做下变形,你再看看:
for (var i=1; i<=5; i++) {
function f(i){
setTimeout(function(){
console.log(i);
},i*1000);
};
f(i);
}
<br/>
<br/>
附录:
其实这道题,知道ES6
的 let
关键词,估计也想到了另一个解法:
for (let i=1; i<=5; i++) { //这里的关键就是使用的let 关键词,来形成块级作用域
setTimeout(function(){
console.log(i);
},i*1000);
}
我不知道,大家有没有疑惑啊,为啥使用了块级作用域就可以了呢。反正我当初就纠结了半天。
18年 11月 2日修正:
这个答案的关键就在于 块级作用域的规则了。它让let
声明的变量只在{}
内有效,外部是访问不了的。
做下变形,这个是为了方便理解的,事实并非如此:
for (var i=1; i<=5; i++) {
let j = i;
setTimeout(function(){
console.log(j);
},j*1000);
}
当for 的()
内使用 let
时,for 循环就存在两个作用域,()
括号里的父作用域,和 {}
中括号里的 子作用域。
每次循环都会创建一个 子作用域。保存着父作用域传来的值,这样,每个子作用域内的值都是不同的。当setTimeout 的匿名函数执行时,自己的作用域没有i
的值,向上读取到了该 子作用域 的 i
值。因此每次的值才会不一样。
你要是喜欢折腾,你会发现,块级作用域的表现跟函数作用域一样,子作用域中使用它的变量,它也会形成一个块级对象,被写入到 函数的 [[scope]] 中。
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。