写在前面:本段为编者编写全篇后的提示:因为编者也是一个前端小白,因此语言相对偏白话,重在详讲,文字不过精炼,导致文章篇幅较长。因此读者可跳跃性阅读,取需要的部分查阅即可。希望本文对您有所帮助!
引言:老师最近向我们讲解了栈、堆等内存原理,并在上课时讲解了相关原理在闭包等实际操作处的体现。课后复习,觉得收获颇丰,故写一篇文章记录一下,加深印象。同时也作分享,希望能与大家互相探讨。
注:本文重点在于栈、堆和闭包,前面为铺垫
语言类型(了解即可)
- 动态类型语言:在运行过程当中需要检查数据类型的语言,比如JS
- 静态类型语言:它的数据类型是在编译期进行检查的,也就是说变量在使用前要声明变量的数据类型,比如C/C++和Java
- 强类型语言:一个变量不经过强制转换,它永远是这个数据类型,不允许隐式的类型转换。举个例子:如果你定义了一个double类型变量a,不经过强制类型转换那么程序int b = a无法通过编译。典型代表是Java。
- 弱类型语言:它与强类型语言定义相反,允许编译器进行隐式的类型转换,典型代表C/C++和JavaScript。
注:这里放一张很经典的图帮助理解
拷贝与数据类型(铺垫)
拷贝
关于拷贝我希望给各位一个初始的概念,之后我们再一点一点丰富:
- 拷贝,具体分两种:深拷贝、浅拷贝
- JS中有不同的方法来复制对象,但是在拷贝中存在很多陷阱
- 理解数据在内存中的存储方式,即理解栈、堆等原理能让我们避开很多拷贝的陷阱
数据类型
JS的数据类型可以根据在计算机中的存储位置分为两种:
- 基本数据类型:Number、String、Boolean、Null、 Undefined、Symbol(ES6),BigInt(ES10)
- 引用数据类型:Object
基本数据类型
基本数据类型是指存放在栈中的简单数据段,数据大小确定,内存空间大小可以分配,它们是直接按值存放的,所以可以直接按值访问
引用数据类型
引用数据类型也叫对象数据类型,包括function,object,array,data,RegExp等可以可以使用new创建的数据,又叫对象类型,他们是存放在堆(heap)内存中的数据。
当我们需要访问引用类型的值时,首先得从栈中获得该对象的地址指针,然后再从堆内存中取得所需的数据。
栈与堆
在上面的数据类型中我们知道:
- 栈内存一般储存基础数据类型
- 堆内存一般储存引用数据类型
给一个图帮助理解:
这里先放一下网上对于JS中栈、堆的定义:
- 栈(stack):由操作系统自动分配释放 ,存放函数的参数值,局部变量的值等。其操作方式类似于数据结构中的栈;
- 堆(heap):一般由程序员分配释放, 若程序员不释放,程序结束时可能由OS回收,分配方式倒是类似于链表。
简单理解定义部分:
- 栈:提供代码运行的环境,存储各种基本类型的变量
- 堆:主要负责像对象Object这种引用变量类型的存储
然后写一下对比部分:
- 栈(类比货架):栈为自动分配的内存空间。栈内存中存储的数据一般都是已知大小的,算是一种简单储存。栈中数据读取速度相对较快
- 堆(类比仓库):堆是动态分配的内存。堆内存中存储的数据在大小方面,似乎没有规定,而且一般大小都是未知的。堆中数据读取速度相对较慢(要拿着地址值去取数据)
接下来结合例子进行讲解
function foo(){
var a = '张三' //变量a储存在栈内存中,值为String类型的'张三'
var b = a //变量b储存在栈内存中,通过深拷贝,值也为'张三'
var c = {name:'李四'} //变量c储存在栈内存中,{name:''李四}作为对象储存在堆内存中,c的值为{name:'李四'}在堆中的地址值
var d = c //变量d存储在栈内存中,通过浅拷贝,值也为{name:'李四'}在堆中的地址值
console.log(a); //张三
console.log(b); //张三
console.log(c); //李四
console.log(d); //李四
d.name = '王五'
console.log(c); //注意:这里打印王五
console.log(d); //注意:这里打印王五
}
foo()
画一个简单的图解释一下:
调用栈:相当于引擎的调用部门,会维护程序执行期间的上下文状态 这里主要提一下栈的存储过程:
栈的存储遵循 先进后出 的方式。
联系JS引擎的回收机制,打一个比喻帮助理解:栈内数据存储就像往手枪弹夹内压入子弹,而回收数据存储空间时,也就是出栈时,就相当于打出子弹。先压入的子弹会最后被打出。
在这个例子中,先会创建全局作用域,所以全局作用域会被“压入”栈中,放在最底下,再“压入”foo函数的作用域。在foo函数的变量环境中,依次分配空间给变量,因此在栈中变量的排列顺序如图所示。
为什么要有栈、堆两个空间
为什么要有两种空间呢?栈那么好用为什么不全放在栈中呢?
这就像货架和仓库这个比喻,先不考虑全放在栈内是否放得下,就算放的下,拿取货物(数据)也不方便啊
拿正式一点的语言解释:
js引擎需要用栈来维护执行期间的上下文状态。如果栈过大,所有数据都放在栈里面,会导致栈里面的数据取用不方便,会影响上下文的切换效率,进而影响整个程序的执行效率
注:关于影响效率方面做出部分解释:在之前解释栈的时候我们说到,函数作用域会按顺序压入栈中,又因为出栈规则,在底下的函数会等上一个函数释放之后再释放,如果放入的数据多了,整个程序的执行速度自然就下降了。(写到这的时候我突然想到这部分和闭包部分可能会相关,读者到时候注意区分)
栈、堆的实际应用
简单赋值
其实在堆和栈的讲解处的示例就属于这个版块,大家可以再看看。
深拷贝、浅拷贝(对引用类型数据)
注:这一块老师还没具体讲,只是提了一下,编者对这块的了解还比较模糊,因此只给出了概念,具体实现深拷贝和浅拷贝的方法只能下次再写文章总结了,算是挖了个坑吧。
什么是深拷贝、浅拷贝
- 浅拷贝是创建一个新对象,这个对象有着原始对象属性值的一份精确拷贝。如果属性是基本类型,拷贝的就是基本类型的值,如果属性是引用类型,拷贝的就是内存地址 ,所以如果其中一个对象改变了这个地址,就会影响到另一个对象。
- 深拷贝是将一个对象从内存中完整的拷贝一份出来,从堆内存中开辟一个新的区域存放新对象,且修改新对象不会影响原对象。
关于深拷贝和浅拷贝与数据类型的具体关联,还有几个图(出自ConardLi大佬)可以帮助理解:
闭包
铺垫
在讲闭包前要先有栈堆和JS回收机制的铺垫,这里补充说明部分回收机制:
-
堆内存释放 让所引用的堆内存空间地址的变量赋值为null即可(没有变量占用这个堆内存了,浏览器会在空闲的时候把它释放)
-
栈内存释放 一般情况下,当函数执行完成,所形成的私有作用域(栈内存)都会自动释放掉(在栈内存中存储的值也都会释放掉),但是也有特殊不销毁的情况:
- 函数执行完成,当前形成的栈内存中,某些内容被栈内存以外的变量占用了,此时栈内存不能释放(一旦释放,外面找不到原有的内容了)。
- 全局栈内存只有在页面关闭的时候才会被释放掉
如果当前栈内存没有释放,那么之前在栈内存中存储的基本值也不会释放,能够一直保存下来
什么是闭包
关于这个问题,我们先放一下,因为我认为通过实例来理解会更好。继续往下走
怎么产生闭包
简单来说,就是函数体内的函数被拿到了函数体外面使用,就形成了闭包。
实例讲解
直接上JS
function foo(){
var myName = '欧文'
let text1 = 1
const text2 = 2
var innerBar = {
setName: function(newName){
myName = newName
},
getName:function(){
console.log(text1);
return myName
}
}
return innerBar
}
var bar = foo()
console.log(bar.getName()); //1 欧文
bar.setName('张三')
console.log(bar.getName()); //1 张三
然后一步一步看。
老规矩,上图(我这里主要对比变量的一些变化):
这是执行foo()函数的样子(入栈),如果没有后三行,也就是foo啥也没干,那按理说应该要销毁foo的作用域(也就是它的执行上下文)(出栈)。
但是因为有后三行,它里面的函数在外面被调用了! 当foo()要销毁的时候,我模拟一下引擎内的场景:
- 回收机制:foo老哥,你这执行完了,我可以回收你的作用域了不?
- foo:这哪行啊,我这的函数还在外面被调用,它待会儿还得找我要变量呢,这哪能销毁啊!我形成了闭包,你不能回收我。
我这样说明,你是否对闭包了解一些了呢?接下来我们结合栈、堆原理来看看到底变量在闭包形成后是怎么被调用的。
重新说:引擎在编译的时候创建一个foo的执行上下文,然后有变量的声明赋值等操作,然后走走走,走到下面的函数setName和getName的时候,然后引擎就要对这两个内部的函数做一次编译(是函数都要进行编译),我们可以称为词法扫描。
然后聚焦到这两个函数,由于这两个内部函数引用了外部变量,所以JS引擎判断这里形成闭包! 那么,引擎就会在堆空间内开辟一个空间,里面有一个closure对象(也叫闭包对象)! 这个closure对象里面就会包含闭包用到的外部变量。然后在栈中相关变量的值,就会变成这个closure对象的地址!注意!也就是说,之后要用这些变量的时候,就会去到堆里面的closure对象里面找!
我总觉得上面一段话有点没咋讲明白,不多说,上图辅助说明!
注意:注意啊兄弟们,千万注意!聚焦到text2这个变量,我们会发现它没有被内部函数调用,那有些人可能会疑惑,那它在哪?是栈里面?还是堆里面?还是被销毁了?
它还在栈里面! 因为形成闭包的那两个内部函数没有调用到它,它就不会跟着到closure对象中去。只有被闭包引用的对象才会去到堆里面。销毁就更不可能了,想啊,因为闭包的实现,foo的作用域不会被销毁,那foo里的变量咋会被销毁嘞?其实这是闭包的保护机制,能够保护变量不被销毁。
因此text2还会遗留在栈当中。这点大家注意。text2因为没有进入堆当中,所以它就无从被foo外面找到,它只能被foo调用!只能被foo调用,这意味着这个变量变成了一个私有变量!而不是一个全局变量!
- 注:闭包具有“保护”作用:保护私有变量不受外界的干扰(在真实项目,尤其是团队协作开发的时候,应该尽可能地减少全局变量的使用,防止相互之间的冲突(“全局变量污染”)),那么此时我们完全可以把自己这一部分内容封装到一个闭包中,让全局变量转换为私有变量。
但是myName和text1变量现在不在foo的栈里面,而是储存在堆里面,所以这两个变量还能够在全局下还能生效。这就是为什么后面三行代码仍然能够实现作用的原因
注:这里再说明一个问题:如果foo里面所有的变量都去到了堆里面,那foo的执行上下文还在吗?还有意义吗?
是有意义的,foo的执行上下文依然存在。因为所有的变量找值都要先在栈中取地址,再去堆中找值。
总结
在这里再捋一遍文章脉络,希望大家有所收获:
- 本文讲解了JS中的语言类型、数据类型,然后重点讲解了堆和栈的感念,也分析了一下为什么要分堆和栈。最后通过堆和栈的理论讲解了闭包的部分原理。
终于写完了,但是还是感觉说的有些不清楚,就觉得自己说的有点乱,不知道读到这的好兄弟们会不会有哪里不理解,有疑惑的地方一定要指出来啊兄弟们! 我们可以在评论区一起探讨问题,也可以帮我一起缕缕闭包这个问题。
讲真的,闭包真有点难说清那些头头绪绪,我都在想着之后会不会重新写一篇闭包的文章(怕讲不清楚啊)..........
交个朋友(2022.03.20记)
目前正在疯狂学习前端知识,想要成为更优秀的前端工程师,因此喜欢记录并分享自己的学习笔记。奈何本人知识储备有限,只能做到输出一些些自己的观念。
不过!我真的有做很多很多笔记(确实很多笔记是摘抄而来,因此作为自己的帖子发出不厚道....),我也真的很希望能和志同道合的小伙伴们分享交流更多知识点!
因此我简单搭建了一个自己的博客,希望能结识到更多小伙伴,如果有兴趣,来我博客逛逛吧~ 阿敏的成长日记