想当年,我手持栈、堆两个原理,从 简单赋值 杀到 闭包

·  阅读 3579

写在前面:本段为编者编写全篇后的提示:因为编者也是一个前端小白,因此语言相对偏白话,重在详讲,文字不过精炼,导致文章篇幅较长。因此读者可跳跃性阅读,取需要的部分查阅即可。希望本文对您有所帮助!

引言:老师最近向我们讲解了栈、堆等内存原理,并在上课时讲解了相关原理在闭包等实际操作处的体现。课后复习,觉得收获颇丰,故写一篇文章记录一下,加深印象。同时也作分享,希望能与大家互相探讨。

QQ图片20210512111731.png

注:本文重点在于栈、堆和闭包,前面为铺垫


语言类型(了解即可)

  • 动态类型语言:在运行过程当中需要检查数据类型的语言,比如JS
  • 静态类型语言:它的数据类型是在编译期进行检查的,也就是说变量在使用前要声明变量的数据类型,比如C/C++和Java
  • 强类型语言:一个变量不经过强制转换,它永远是这个数据类型,不允许隐式的类型转换。举个例子:如果你定义了一个double类型变量a,不经过强制类型转换那么程序int b = a无法通过编译。典型代表是Java。
  • 弱类型语言:它与强类型语言定义相反,允许编译器进行隐式的类型转换,典型代表C/C++和JavaScript。

注:这里放一张很经典的图帮助理解

QQ图片20210511151006.jpg


拷贝与数据类型(铺垫)

拷贝

关于拷贝我希望给各位一个初始的概念,之后我们再一点一点丰富:

  • 拷贝,具体分两种:深拷贝、浅拷贝
  • JS中有不同的方法来复制对象,但是在拷贝中存在很多陷阱
  • 理解数据在内存中的存储方式,即理解栈、堆等原理能让我们避开很多拷贝的陷阱

数据类型

JS的数据类型可以根据在计算机中的存储位置分为两种:

  • 基本数据类型:Number、String、Boolean、Null、 Undefined、Symbol(ES6),BigInt(ES10)
  • 引用数据类型:Object

基本数据类型

基本数据类型是指存放在栈中的简单数据段,数据大小确定,内存空间大小可以分配,它们是直接按值存放的,所以可以直接按值访问

引用数据类型

引用数据类型也叫对象数据类型,包括function,object,array,data,RegExp等可以可以使用new创建的数据,又叫对象类型,他们是存放在堆(heap)内存中的数据。

当我们需要访问引用类型的值时,首先得从栈中获得该对象的地址指针,然后再从堆内存中取得所需的数据。


栈与堆

在上面的数据类型中我们知道:

  • 栈内存一般储存基础数据类型
  • 堆内存一般储存引用数据类型

给一个图帮助理解:

461976-20180823211511434-1707579794.png

这里先放一下网上对于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()

复制代码

画一个简单的图解释一下:

QQ图片20210511184628.png

调用栈:相当于引擎的调用部门,会维护程序执行期间的上下文状态

这里主要提一下栈的存储过程:

栈.png

栈的存储遵循 先进后出 的方式。

联系JS引擎的回收机制,打一个比喻帮助理解:栈内数据存储就像往手枪弹夹内压入子弹,而回收数据存储空间时,也就是出栈时,就相当于打出子弹。先压入的子弹会最后被打出。

在这个例子中,先会创建全局作用域,所以全局作用域会被“压入”栈中,放在最底下,再“压入”foo函数的作用域。在foo函数的变量环境中,依次分配空间给变量,因此在栈中变量的排列顺序如图所示。


为什么要有栈、堆两个空间

为什么要有两种空间呢?栈那么好用为什么不全放在栈中呢?

这就像货架和仓库这个比喻,先不考虑全放在栈内是否放得下,就算放的下,拿取货物(数据)也不方便啊

拿正式一点的语言解释:

js引擎需要用栈来维护执行期间的上下文状态。如果栈过大,所有数据都放在栈里面,会导致栈里面的数据取用不方便,会影响上下文的切换效率,进而影响整个程序的执行效率

注:关于影响效率方面做出部分解释:在之前解释栈的时候我们说到,函数作用域会按顺序压入栈中,又因为出栈规则,在底下的函数会等上一个函数释放之后再释放,如果放入的数据多了,整个程序的执行速度自然就下降了。(写到这的时候我突然想到这部分和闭包部分可能会相关,读者到时候注意区分)


栈、堆的实际应用

简单赋值

其实在堆和栈的讲解处的示例就属于这个版块,大家可以再看看。

深拷贝、浅拷贝(对引用类型数据)

注:这一块老师还没具体讲,只是提了一下,编者对这块的了解还比较模糊,因此只给出了概念,具体实现深拷贝和浅拷贝的方法只能下次再写文章总结了,算是挖了个坑吧。

什么是深拷贝、浅拷贝

  • 浅拷贝是创建一个新对象,这个对象有着原始对象属性值的一份精确拷贝。如果属性是基本类型,拷贝的就是基本类型的值,如果属性是引用类型,拷贝的就是内存地址 ,所以如果其中一个对象改变了这个地址,就会影响到另一个对象。
  • 深拷贝是将一个对象从内存中完整的拷贝一份出来,从堆内存中开辟一个新的区域存放新对象,且修改新对象不会影响原对象。

关于深拷贝和浅拷贝与数据类型的具体关联,还有几个图(出自ConardLi大佬)可以帮助理解:

QQ图片20210513132407.png

QQ图片20210513132420.png


闭包

铺垫

在讲闭包前要先有栈堆和JS回收机制的铺垫,这里补充说明部分回收机制:

  • 堆内存释放

让所引用的堆内存空间地址的变量赋值为null即可(没有变量占用这个堆内存了,浏览器会在空闲的时候把它释放)

  • 栈内存释放

一般情况下,当函数执行完成,所形成的私有作用域(栈内存)都会自动释放掉(在栈内存中存储的值也都会释放掉),但是也有特殊不销毁的情况:

  1. 函数执行完成,当前形成的栈内存中,某些内容被栈内存以外的变量占用了,此时栈内存不能释放(一旦释放,外面找不到原有的内容了)。
  2. 全局栈内存只有在页面关闭的时候才会被释放掉

如果当前栈内存没有释放,那么之前在栈内存中存储的基本值也不会释放,能够一直保存下来

什么是闭包

关于这个问题,我们先放一下,因为我认为通过实例来理解会更好。继续往下走

怎么产生闭包

简单来说,就是函数体内的函数被拿到了函数体外面使用,就形成了闭包。

实例讲解

直接上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  张三
复制代码

然后一步一步看。

老规矩,上图(我这里主要对比变量的一些变化):

QQ图片20210513192042.png

这是执行foo()函数的样子(入栈),如果没有后三行,也就是foo啥也没干,那按理说应该要销毁foo的作用域(也就是它的执行上下文)(出栈)。

但是因为有后三行,它里面的函数在外面被调用了! 当foo()要销毁的时候,我模拟一下引擎内的场景:

  • 回收机制:foo老哥,你这执行完了,我可以回收你的作用域了不?
  • foo:这哪行啊,我这的函数还在外面被调用,它待会儿还得找我要变量呢,这哪能销毁啊!我形成了闭包,你不能回收我。

我这样说明,你是否对闭包了解一些了呢?接下来我们结合栈、堆原理来看看到底变量在闭包形成后是怎么被调用的。

重新说:引擎在编译的时候创建一个foo的执行上下文,然后有变量的声明赋值等操作,然后走走走,走到下面的函数setName和getName的时候,然后引擎就要对这两个内部的函数做一次编译(是函数都要进行编译),我们可以称为词法扫描。

然后聚焦到这两个函数,由于这两个内部函数引用了外部变量,所以JS引擎判断这里形成闭包! 那么,引擎就会在堆空间内开辟一个空间,里面有一个closure对象(也叫闭包对象)! 这个closure对象里面就会包含闭包用到的外部变量。然后在栈中相关变量的值,就会变成这个closure对象的地址!注意!也就是说,之后要用这些变量的时候,就会去到堆里面的closure对象里面找!

我总觉得上面一段话有点没咋讲明白,不多说,上图辅助说明!

QQ图片20210513201344.png

注意:注意啊兄弟们,千万注意!聚焦到text2这个变量,我们会发现它没有被内部函数调用,那有些人可能会疑惑,那它在哪?是栈里面?还是堆里面?还是被销毁了?

它还在栈里面! 因为形成闭包的那两个内部函数没有调用到它,它就不会跟着到closure对象中去。只有被闭包引用的对象才会去到堆里面。销毁就更不可能了,想啊,因为闭包的实现,foo的作用域不会被销毁,那foo里的变量咋会被销毁嘞?其实这是闭包的保护机制,能够保护变量不被销毁。

因此text2还会遗留在栈当中。这点大家注意。text2因为没有进入堆当中,所以它就无从被foo外面找到,它只能被foo调用!只能被foo调用,这意味着这个变量变成了一个私有变量!而不是一个全局变量

  • 注:闭包具有“保护”作用:保护私有变量不受外界的干扰(在真实项目,尤其是团队协作开发的时候,应该尽可能地减少全局变量的使用,防止相互之间的冲突(“全局变量污染”)),那么此时我们完全可以把自己这一部分内容封装到一个闭包中,让全局变量转换为私有变量。

但是myName和text1变量现在不在foo的栈里面,而是储存在堆里面,所以这两个变量还能够在全局下还能生效。这就是为什么后面三行代码仍然能够实现作用的原因

注:这里再说明一个问题:如果foo里面所有的变量都去到了堆里面,那foo的执行上下文还在吗?还有意义吗?

是有意义的,foo的执行上下文依然存在。因为所有的变量找值都要先在栈中取地址,再去堆中找值。


总结

在这里再捋一遍文章脉络,希望大家有所收获:

  • 本文讲解了JS中的语言类型、数据类型,然后重点讲解了堆和栈的感念,也分析了一下为什么要分堆和栈。最后通过堆和栈的理论讲解了闭包的部分原理。

终于写完了,但是还是感觉说的有些不清楚,就觉得自己说的有点乱,不知道读到这的好兄弟们会不会有哪里不理解,有疑惑的地方一定要指出来啊兄弟们! 我们可以在评论区一起探讨问题,也可以帮我一起缕缕闭包这个问题。

讲真的,闭包真有点难说清那些头头绪绪,我都在想着之后会不会重新写一篇闭包的文章(怕讲不清楚啊)..........


交个朋友(2022.03.20记)

目前正在疯狂学习前端知识,想要成为更优秀的前端工程师,因此喜欢记录并分享自己的学习笔记。奈何本人知识储备有限,只能做到输出一些些自己的观念。

不过!我真的有做很多很多笔记(确实很多笔记是摘抄而来,因此作为自己的帖子发出不厚道....),我也真的很希望能和志同道合的小伙伴们分享交流更多知识点!

因此我简单搭建了一个自己的博客,希望能结识到更多小伙伴,如果有兴趣,来我博客逛逛吧~ 阿敏的成长日记

分类:
前端
标签:
分类:
前端
标签:
收藏成功!
已添加到「」, 点击更改