闭包算是 javascript 中一个比较难理解的概念,想要深入理解闭包的原理,首先需要搞清楚其他几个概念:
一,栈内存和堆内存
学过 C/C++ 的同学可能知道,计算机系统将内存分为栈和堆两部分(大学的基础课,忘掉的赶紧重新捡起来).
栈内存(连续的存储空间,类似数据结构中的栈):主要用来存放数值,字符,内存地址等小数据
堆内存(散列的存储空间,类似数据结构中的链表):存放可以动态变化的大数据
二,基本类型和引用类型
JavaScript 将变量分为两种类型:
基本类型:Number,String,Boolean ,undefined,null(值被保存在栈内存中)
引用类型:Object,Array,function(具体内容被保存在堆内存中,在栈内存中仅保存堆内存的地址)
如上图,当在程序中在执行中有如下情况:
1,声明变量 a 为基本类型时,直接在栈内存中保存它的值为 100;
2,当将 a 赋值给 b 时,b 在栈内存中新建空间,将 a 的值复制过来
(注:之后 a 和 b 就没有关系了,再改变 a 或 b 的值,不影响另外一个,它们是独立的)
3,声明变量 p1 为引用类型时,将 p1 的内容保存在堆内存中,并将堆内存的物理地址保存在栈内存中
4,当将 p1 赋值给 p2 时,p2 在栈内存中新建空间,仅复制堆内存的物理地址
(注:p1 和 p2 中都保存的是指向堆内存的地址,即指的是同一个对象,当修改 p1 对象的属性后,p2 对象的属性同时被修改)
另外,在计算机语言中还有一些很重要的特性:
1,修改基本类型的值,实际上是新建空间存一个新值,然后将变量名指向新的空间(旧值依然存在栈内存中,只是缺少变量名指向它)
2,删除引用类型,其实并不删除堆内存中的内容,仅删除了栈内存中的物理地址(对象的内容依然存在堆内存中,只是缺少了地址的指向)
(注:计算机关于内存的管理,跟我们正常想到的不一样,例如硬盘恢复就是利用这个原理,为删除的内容重新建立一个指向即可访问)
二,变量作用域
javascript 中变量又分为全局变量和局部变量
全局变量:在全局环境中声明的变量
局部变量:在函数中声明的变量
当函数在执行时,会创建一个封闭的执行期上下文环境,函数内部声明的变量仅可在函数内部使用,外部无法访问,而全局变量则在任何地方都可以使用
三,预编译
JavaScript 的运行为三步:语法分析》预编译》解释执行
1,语法分析:通篇扫描 js 文件,检查是否有低级语法错误
2,预编译四部曲:(发生在解释执行的前一刻)
a,创建 AO 对象(执行期上下文对象,全局为 GO)
b,将形参和变量声明作为 AO 对象的属性名,值为 undefined
c,将实参值传递给形参,即赋值给 AO 对象对应属性名
d,将函数声明为 AO 对象的方法名,值为函数体
3,解释执行:解释一行,执行一行.
function test(a){
var b=1;
function c(){}
}
test(2);
/* 函数预编译四部曲(函数执行前一刻,不执行不会预编译),全局预编译同理
* 1---testAO{}
* 2---testAO{a:undefined,b:undefined}
* 3---testAO{a:2,b:undefined}
* 4---testAO{a:2,b:1,c:function(){}}
*/
四,作用域链
每个 JavaScript 函数都是一个对象,对象中有些属性可以访问(比如 name),有些属性不可以访问(比如 [[scope]] 仅供 js 引擎使用)
[[scope]] 用来存储了运行期上下文对象的集合(即作用域链),作用域链中除了自身创建的 AO 对象外,还包括了所有父级运行期上下文对象(AO)
function a() {
function b() {
var b = 234;
}
var a = 123;
b();
}
var glob = 100;
a();
当 b 执行完成后,b 的 AO 要被销毁,即 b 的 [[scope]] 第 0 位将被置空,如果再次执行 b,将新建一个新的 AO 将其地址存到第 0 位,
当 a 也执行完成后,a 的 AO 要被销毁,即 a 的 [[scope]] 第 0 位将被置空,同时 a 的 AO 中存着 b,b 也将被一同销毁
在了解如上这些概念后,我们再来看下面这个经典的闭包,你会有一个全新的认识
function a() {
var b = 123;
function c() {
console.log(b += 1);
}
return c;
}
var d = a();
d();
当这段代码在执行时的顺序如下:
1,预编译全局,生成执行上下文对象 GO{d:undefined,a:function(){}}
2,定义 a 函数,将 a 函数的 [[scope]] 属性设置为{0:GO}
3,预编译 a 函数,生成 a 的执行上下文对象 aAO{b:undefined,c:function(){}},修改 a 函数的 [[scope]] 属性为{0:aAO,1:GO}
4,执行 a 函数,给 aAO 的属性赋值 {b:123,c:function(){}}
5,定义 c 函数,将 c 函数的 [[scope]] 属性设置为{0:aAO,1:GO},并将 c 返回给 d
6,a 函数执行完毕,销毁 [[scope]] 属性第 0 位对 aAO 对象的引用
7,执行 d 函数(等于执行 c 函数)之前,先预编译生成 c 的执行上下文对象 cAO{},修改 c 函数的 [[scope]] 属性为{0:cAO,1:aAO,2:GO}
8,执行 c 函数,b 变量在 cAO 中没有,到 [[scope]] 属性中的下一位 aAO 中获取
来源: https://www.cnblogs.com/gulei/p/8317746.html