数据类型
JS的变量类型分为基础类型数据(Number, string, boolean等)
和 引用类型数据 ( Object , Function , Array 等等)。
栈内存
栈内存是在程序(函数)执行过程中维护的一块内存区,当程序(函数)执行完毕之后,该内存便会自动被垃圾回收。
栈内存中只保存基础类型数据(虽然不客观,但是好理解)。
栈内存中保存引用类型数据的 地址 ,而不是完整的引用类型数据。
function fun(){
let a=0; //函数栈内存保存的是基础类型数据 0
let b={name:"dz"}; //函数栈内存保存的是引用类型数据的地址 0XFD:0001
return;
}
堆内存
如果栈内存是一块暂时性内存存储区,那么堆内存就是一块长期存储区域,它的占用空间很大,用于存储引用类型数据
栈内存只保存地址,而堆内存保存真实数据。
当堆内存的某一个数据失去引用,也就是没有任何一块栈内存内 保存一个指向该引用数据的地址变量。
这么说很抽象,我举个例子。
改写上面的例子:
function fun(){
let a=0; //函数栈内存保存的是基础类型数据 0
let b={name:"dz"}; //函数栈内存保存的是引用类型数据的地址 0XFD:0001
b = 1 ; // b 不再保存该引用对象的地址了,也就是断开了引用。
return;
}
这样一来,堆内存中的 该对象类型数据 {name:“dz”} 就会被垃圾回收。
那么有同学就要问了,那为什么堆内存下面那个函数对象没有被回收???
那是因为,还有一块全局的栈内存,也就是 fun() 的执行环境 , 在关闭整个网页(浏览器环境)/ 程序(Node 环境),该栈内存被释放,那么堆内存中的 fun() 才能被释放(垃圾回收)。
全局栈内存:没想到吧,我是你爹!
这里虚线表示作用域链的继承关系,实线表示 地址到堆内存的映射关系 。
如果你不清楚什么是作用域链,我这里不能详解,请先另找教程学习,我这里暂称
全局栈内存是fun() 栈内存 的 父级作用域 。
这里的作用域一定是指函数作用域, 因为只有在函数执行时,才能生成新的栈内存区。
相应的,函数执行完毕,栈内存区销毁,内部所有基础类型数据全部释放(包括引用类型的地址)。
这里在用白话讲一遍,帮助你加深印象。
函数代码 是 存储于 堆内存的 零件。
函数代码加上小括号被调用时,函数的生命开始了。
函数出生时就 自带一个 栈内存,用于存储在函数中声明的变量。
函数的生命是短暂的,函数死亡后,栈内存销毁。
那么我们引入下一个话题。
函数与对象的区别
大家都知道函数的原型是继承自对象类型的,但是函数和对象还是有天壤之别的。
函数有生命 , 对象没有。
函数能够执行,对象不能执行。
我之前刻意区分 函数代码 和 函数 的意图就在此。 函数代码是死的,函数是活的。
函数代码继承自对象类型,他们都是死的,保存在堆内存。
而运行的函数则拥有一块自己的栈内存,学过汇编的同学可能知道程序在执行过程中CPU的代码段寄存器CS会保存对应的地址,这里不详谈,只要知道两者意义不同。
所以函数内部的数据是 活的(暂时性的) , 对象内部的数据(属性)是 死的(持久性的)。
既然搞清楚了函数内部数据和对象内部数据的区别,那么我们引出下一个话题:闭包。
但在这之前,我还需要为你补充一些基础。
函数的作用
可能大家会很迷惑,函数的作用??好像大家都知道,但大家又说不清。
我们引用上面的结论来讨论:
既然函数的生命转瞬即逝,并且生成的数据也随之立即消失,那么函数的作用是什么?
1.传入参数进行修改
函数可以传入参数,根据上面的理论来解释,我们所传入的参数有两种,基础类型数据和引用类型数据。
在不考虑返回值的情况下,无论在函数内部如何修改基础类型数据,都没有任何作用。
function fun(a){
a=2;
}
let a = 1;
fun(a);
console.log(a); // 1
在不考虑返回值的情况下,修改引用类型参数,则能发挥函数的作用。
function fun(obj){
obj.a=2;
}
let obj = {a:1}
fun(obj);
console.log(obj.a); // 2
原因就在于传入引用类型参数,等于是传入引用类型参数的地址,而此时函数内部的实参和传入的形参,其实是同一个地址,指向同一个堆内存的数据。因此在任何一个栈内存中修改引用类型数据都会直接影响到堆内存 该数据本身。
2.返回数据
如果说传入参数进行修改是函数 生前的影响。
那么返回数据就是函数死后的遗产。
还是按照两种情况进行讨论,返回基础类型数据和返回引用类型数据。
返回基础类型数据之后,函数的寿命终止,函数栈内存销毁。
不管有没有人继承其财产,函数都会正常死去。
function fun(){
let a=2;
return a;
}
fun(); //生命开始,栈内存创建。
//寿命终止,栈内存释放
let a = fun(); //生命开始,栈内存创建。
//寿命终止,栈内存释放
但是返回引用类型数据呢,我们分为两种情况:
返回对象 和 返回函数
function fun(){
let obj = {a:1};
return obj;
}
fun(); //生命开始,栈内存创建。
//寿命终止,栈内存释放
let obj = fun(); //生命开始,栈内存创建。
//寿命终止,栈内存释放, 新的obj指向堆内存地址
然后是返回函数
function fun(){
let fn = function(){
return 1
};
return fn;
}
fun(); //生命开始,栈内存创建。
//寿命终止,栈内存释放
let fn = fun(); //生命开始,栈内存创建。
//寿命终止,栈内存释放, 新的obj指向堆内存地址
这里为什么是一样,可能有些人会感觉意外。我画图给你解释一下。
当返回对象之后,fun() 栈内存销毁,全局 obj 指向该引用数据。
————————————————
同理,函数也是如此.
当返回该函数后fun()就会销毁栈内存。
看到函数返回函数,很多人第一印象就是闭包。
很抱歉,让你失望了,这里函数返回函数,它仍然不是一个闭包。
因为它不具有闭包的几项特征。
闭包
上面的例子虽然是函数返回函数,但是它缺少对函数栈内存变量的引用。
这一点是最关键的。
举一个最简单的闭包例子。
我们把上面的例子改写成闭包
function fun(){
let a = 1;
let fn = function(){
return a;
};
return fn;
}
fun(); //生命开始,栈内存创建。
//寿命终止,栈内存释放
let fn = fun(); //生命开始,栈内存创建。
//寿命未终止,栈内存无法释放,因为全局栈内存中的fn的引用链中保存了对fun()函数栈内存变量a 的引用。
画图解释一下这个过程
这里能看见栈内存有对堆内存的引用,但是堆内存内也有反过来对栈内存的引用。
这样就满足了闭包的基本条件,于是,函数 fun 就是一个闭包。
我们来看一下如果程序栈内存有一个变量接收了该闭包返回值。
这里看来,函数执行完毕,就算fun()的栈内存中的fn 尝试放弃对 堆内存中函数的引用,但还是有一条完整的引用链,全局fn -> 堆内存函数 : a -> fun()栈内存的 a 。
这样一来,有人指向堆内存函数 , 所以 堆内存函数不会被垃圾回收。
有人指向fun()栈内存的 a ,所以栈内存的 a 不会被垃圾回收 , fun() 栈内存也不会销毁。
如果fn中有对a的修改操作,也会通过引用链修改到fun()栈内存中的a。
这样一来,fun() 栈内存的寿命就是无限的了。
但是一旦程序栈内存中失去了对堆内存的引用,那么引用链断开,堆内存中的函数被回收,fun()调用栈失去了引用也相应地被销毁。
————————————————————
——————————————
这也解释了为什么一开始 只执行
fun(); //生命开始,栈内存创建。
//寿命终止,栈内存释放
的时候,栈内存还是迅速释放了。
因为这等同于创建了闭包然后马上断开引用链,闭包销毁。
然后这里最后再做一个总结,闭包其实远没有你们想的那么复杂,闭包只不过是一个函数返回了一个函数,并且返回函数有对父函数变量的引用。
闭包的作用是
1.保存函数(父函数)调用栈(栈内存)。
另外闭包为了解决什么问题而产生呢?
之所以额外维护一个栈内存,是因为栈内存的变量不会与其他栈内存的变量名冲突,我们也称之为作用域。
普通函数寿命极短,因此作用域存活时间短;而闭包则是一个长期存活的函数作用域。
归根到底,
2.闭包是为了解决变量名冲突的问题。
另外插一嘴,为什么别人封装工具函数和模块,都使用闭包?
这和闭包的第一个作用有关。
如果不使用闭包,寿命极短,作用域创建即销毁,那么就是一个一次性工具。
使用闭包,它就能成为一个程序级别的工具,甚至是框架。