一篇文章带你搞定javaScript变量作用域和内存问题(变量,作用域,垃圾收集,管理内存)


       相信学过javaScript的同学都了解,javaScript是一门弱类型语言,正是基于这个性质,javaScript中的变量只是在特定时间保存特定值的一个名字而已,由于不存在定义某个变量必须要保存何种数据类型值的规则,变量的值及其数据类型可以在脚本的生命周期内改变。这是一个有趣强大有风险的设定,下面将给大家就 变量作用域垃圾收集管理内存四个方向仔细谈谈javaScript的变量的相关知识。


一、变量类型

1.1 变量类型

       这里我们提到的变量类型是根据数据结构去分类的,可以分为基本类型值引用类型值
       基本类型有:Undefined、String、Null、Number、Boolean
       引用类型有:Object

       简单来说,基本类型就是解析器直接可以按值访问,即可以操作保存在变量中的实际的值,而引用类型的值是保存在内存中的,这里我们要知道一点,javaScript是不允许直接访问内存中的位置的,也就是不能直接操作变量的内存空间的。

       那引用类型js是用怎样的方式去访问的呢?

       是通过引用去访问而不是实际的对象,什么是引用?你可以理解成像一个人的别名,具体的一些逻辑我会在管理内存的部分仔细解释,总之,你需要知道,javaScript中引用类型的值是按引用访问的,而不能直接去访问实际的对象。

       这里有个小小的误区需要提醒一下大家,像new String("")这种使用new定义出来的实例实际上是Object对象,所以是属于引用类型的,而不是基本类型


1.2 复制变量值

       对于基本类型的变量而言,复制变量值的过程只是复制了这个变量的值,复制完以后的变量除了值和基本类型相同,两者就没有任何瓜葛了在这里插入图片描述
       上面的代码b复制了a的值,后面又给赋值b为2,可以看到控制台输出的结果a是1,b是2,两者之间在复制完以后就交易结束了,没有半毛钱关系了。

       但是对于引用类型而言就有很大的不同,引用类型的复制也会把值复制一份放到新变量分配的空间中,不同的是,这个指针指向存储在堆中的一个对象,复制操作结束以后,这两个变量引用的是同一个对象,说白了,引用类型在复制完就穿同一条裤子了,两个变量谁改变了另一个都脱不了干系
在这里插入图片描述
       可以看到上面的例子,在a复制到b以后,不管是a还是b修改对象都会互相影响,这时候它们其实已经指向的都是同一个对象指针了,所以不管谁操作都会影响到另一方,如果想帮某个变量解脱出来,只能把它指向另一个指针地址了,比如用new Object()重新赋值


1.3 检测数据类型

       检测一个对象的类型,javaScript提供了两种方式,typeofinstanceof
在这里插入图片描述
       typeof可以直接输出对象的类型,但是有个缺陷就是不能告诉你更具体的Object类型,比如像上图中的数组对象,它并不支持告诉你这种Object中的哪一种,为了弥补这个缺陷,instanceof可以确认具体的类型,它会沿着你判断的对象的原型链一直向父类查找,直到找到需要判断的类型值,也就是说你的父类里只要包含了这种类型,它就会返回给你true的Boolean值

       这里还有一个点需要大家注意一下:

ECMA-262规定任何在内部实现[[Call]]方法的对象都应该在应用typeof操作符时返回“function”。由于上述浏览器中的正则表达式也实现了这个方法,因此对正则表达式应用typeof会返回“function"
------摘自《javaScript高级程序设计》




1.4 传递参数

       之前我们提到,基础类型靠值访问,引用类型靠引用访问,这也是它们最大的不同,但是在函数参数当中并不是这样的,ECMAScript中所有函数的参数都是按值传递的
       对于基本类型的函数传参,只是将传递的值复制给一个局部变量(即命名参数),而对于引用类型的函数传参,会把引用类型的变量的值所在内存中的地址复制给命令参数,所以函数参数的变化会反映在函数外部的引用变量上
在这里插入图片描述
       上面的例子中modifyObject()方法给obj增加了名为prop1的属性,而addOne其中对num的++操作并没有影响到a,a还是之前的值1
       看到这里,可能大家就有点懵逼了,卧槽,你在写个啥子,这obj不是通过函数参数修改了?这不是按引用传递么? 之前也说了,我可以很明确地告诉大家,的确是按值传递,只不过传递的是这个引用对象地址的值,但是并不像之前复制一样,之间把函数参数指向那个对象,他们操作的并不是同一个对象,如果您还是不太理解,请看下面的例子
在这里插入图片描述
       大家可以看到,obj的name属性并不是“name2”而是"name1",如果是按引用传递的,那么obj也会被重新指向到新的对象,接下来访问obj.name的时候就是name2了,这说明即使在函数内部修改了参数的值,但原始的引用仍然保持未变。
所以,不管是基本类型和引用类型,ECMAScript中所有函数的参数都是按值传递的


二、作用域

2.1 作用域链

当代码在一个环境中执行时,会创建变量对象的一个作用域链,作用域链的用途,是保证对执行环境有权访问的所有变量和函数的有序访问,作用域链的前端,始终都是当前执行的代码所在环境的变量对象,如果这个环境是函数,则将其活动对象作为变量对象,全局执行环境的变量对象始终都是作用域中的最后一个对象
----《javaScript高级程序设计》

       这里引用《javaScript高级程序设计》中对作用域的解释,简单的来说,js的函数执行的时候会将由局部到全局的顺序,把变量和函数放到在作用域链上,然后从局部作用域向全局作用域搜索,搜索到函数执行的同名标识符为止。
       也就是说,内部环境可以通过作用域访问所有的外部环境,但是外部环境不能访问内部环境中的任何变量或函数
在这里插入图片描述
       上面这个例子就可以很好地说明,全局环境不能向下搜索访问局部环境下定义的b,但是局部环境下可以向上搜索全局环境下定义的a。每个环境都可以向上搜索作用域,以查询变量和函数名,但是任何环境都不能通过向下搜索作用域链而进入另一个执行环境


2.2 变量提升

       javaScript中,函数及变量都将被提升到函数最顶端,也就是最接近的环境,在函数内部,最接近的环境就是函数的局部环境,而不会提升到全局环境,更不能在全局访问



三、垃圾收集

       javaScript具有自动垃圾收集机制,也就是说执行环境会负责管理代码执行过程中使用的内存,不用像C语言或者C++一样需要编程人员手工跟踪内存的使用情况,这里我将给大家介绍一下js垃圾收集器涉及到的两种垃圾收集原理:标记清除引用计数


3.1 标记清除

        ”标记清除“是目前主流的垃圾收集算法,这种算法的思想是给当前不使用的值加上标记,然后再回收其内存。
       垃圾收集器在运行的时候会给存储在内存中的所有变量都加上标记(当然,可以使用任何标记方式)。然后,它会去掉环境中的变量以及被环境中的变量引用的变量的标记。而在此之后再被加上标记的变量将被视为准备删除的变量,原因是环境中的变量已经无法访问到这些变量了。最后,垃圾收集器完成内存清除工作,销毁那些带标记的值并回收它们所占用的内存空间。


3.2 引用计数

       另一种不太常见的垃圾收集策略叫做引用计数(reference counting)。引用计数的含义是跟踪记录每个值被引用的次数。
       当声明了一个变量并将一个引用类型值赋值该变量时,则这个值的引用次数就是1.如果同一个值又被赋给另外一个变量,则该值得引用次数加1。相反,如果包含对这个值引用的变量又取 得了另外一个值,则这个值的引用次数减 1。当这个值的引用次数变成 0时,则说明没有办法再访问这个值了,因而就可以将其占用的内存空间回收回来。这样,当垃圾收集器下次再运行时,它就会释放那 些引用次数为零的值所占用的内存。
       IE 中有一部分对象并不是原生 JavaScript 对象。例如,其 BOM 和 DOM 中的对象就是使用 C++以 COM(Component Object Model,组件对象模型)对象的形式实现的,而 COM对象的垃圾 收集机制采用的就是引用计数策略。
       因此,即使 IE的 JavaScript引擎是使用标记清除策略来实现的,但 JavaScript访问的 COM对象依然是基于引用计数策略的。换句话说,只要在IE中涉及 COM对象,就会存在循环引用的问题。

var element = document.getElementById("some_element"); 
var myObject = new Object();
myObject.element = element; 
element.someObject = myObject; 

       这个例子在一个 DOM元素(element)与一个原生 JavaScript对象(myObject)之间创建了循环引用。其中,变量 myObject 有一个名为 element 的属性指向 element 对象;而变量 element 也有 一个属性名叫 someObject 回指 myObject。由于存在这个循环引用,即使将例子中的 DOM从页面中移除,它也永远不会被回收。
       为了避免类似这样的循环引用问题,最好是在不使用它们的时候手工断开原生 JavaScript 对象与 DOM元素之间的连接。比如将变量置null的方式。


四、管理内存

在js引擎中,对变量的存储主要有两种方式,堆内存栈内存。栈内存主要存储各种基本类型的变量,即基本类型以及对象变量的指针,而堆内存主要负责像对象Object的引用类型。这里将就栈内存和堆内存,为大家讲解一下其中的区别,以及为什么这么进行类型存储的原因。


4.1 栈区

       栈区有两个特点:
       1、栈区内存空间由操作系统自动分配与释放
       2、栈区内的空间是有限的
       因为这两个特点,所以延申出了栈内存,由程序自动向操作系统申请分配以及回收,速度快,使用方便但程序员无法控制。若分配失败,则提示栈溢出,const局部变量也储存在栈区,栈区地址向减少的方向增长


4.2 堆区

       堆区内存空间手动申请和释放,而且空间很大自由,几乎没有空间限制,所以创建引用变量会遍历一个记录空闲内存地址的链表,寻找第一个空间大于所申请空间的堆结点,然后将该结点从空闲结点链表中删除,并将该结点的空间分配给程序,也容易导致分配的速度较慢,地址不连续,容易碎片化等问题
       所以new关键词初始化的变量是不存储在栈内存之中的,因为新实例生成的是对象而不是基本类型


4.3 这样进行类型存储的原因

       一般来说,栈内存线性有序存储,容量小,系统分配效率高,所以适合用于在定义时就可以快速确定大小的基本类型变量。而存储到栈内存中,效率虽然相对要低一些,但是内存足够大,灵活性强,所以适合不确定大小的引用类型
       而在垃圾回收方面,栈内存变量基本上用完就回收了,而堆内存中的变量因为存在很多不确定的引用,只有当所有调用的变量全部销毁之后才能回收


五、总结

       1、javaScript变量可以用来保存两种类型的值:基本类型和引用类型值。
       2、基本类型值源自以下5种基本数据类型:undefined, null, boolean, number和String,引用类型为Object对象
       3、基本类型按值访问,引用类型按内存访问,但是当作为函数参数的时候,都是按值访问
       4、内部环境可以通过作用域访问所有的外部环境,但是外部环境不能访问内部环境中的任何变量或函数
       5、javaScript具有自动垃圾收集机制,执行环境会负责管理代码执行过程中使用的内存,目前主流的垃圾收集算法是”标记清除“
       6、在js引擎中,对变量的存储主要有两种方式,堆内存和栈内存。栈内存主要存储各种基本类型的变量,即基本类型以及对象变量的指针,而堆内存主要负责像对象Object的引用类型。


小伙伴们今天的学习就到这里了,如果觉得本文对你有帮助的话,欢迎转发,评论,收藏,点赞!!!
每天学习进步一点点,就是领先的开始。如果想继续提高,欢迎关注我,或者关注公众号”祯民讲前端“。大量前端技术文章,面试资料,技巧等助你更进一步!
在这里插入图片描述

  • 2
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 2
    评论
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值