js中的变量作用域和内存

119 阅读8分钟

这篇文章主要是记录下js中的相关基础知识。我始终认为扎实的基础对一个技术人员来说是必不可少的,希望大家不要忽视基础知识的学习。我认为基础知识的学习完全可以用碎片化的时间去做,比如在地铁上,与其站着没事干,还不如搂几眼基础知识,加深记忆,你说呢?

一:原始值和引用值

ECMAScript变量包含两种不同类型的数据:原始值和引用值。原始值就是最简单的数据,引用值则是由多个值构成的对象
原始值包括六种基本数据类型:String, Number,Boolean,Undefined,Null,Symbol。保存原始值的变量是按值访问的,因为我们操作的就是存储在变量中的实际值。
引用值是保存在内存中的对象,与其他语言不同,在js中我们不能直接访问内存位置,因此也就不能直接操作对象所在的内存空间。在操作对象时,实际上操作的是对该对象的引用,而不是实际的对象本身。因此,保存引用值的变量是按引用访问的。我的理解就是引用就是这一块内存空间的’经纪人‘,有什么事情跟经纪人商量,然后内存会听从引用这个经纪人的安排去做一些事情。内存为了隐私安全是不会主动跟外界商量事情的。

动态属性

原始值和引用值的定义方式很类似。都是创建一个变量,然后给他赋一个值,不过在变量保存了这个值之后,可以对这个值做什么,则大有不同。对于引用值而言,可以随时添加、修改和删除其属性和方法。但是原始值不能有属性,尽管我们尝试给原始值添加属性后并不会报错。但是给原始值添加的属性,我们在访问它的时候是访问不到的。注意的是,原始类型的初始化只能使用原始字面量形式,如果使用的是new关键字,则js会创建一个Object类型的实例,但其行为类似原始值,字面量字符串和使用new关键字创建的字符串变量都可以调用字符串的方法。如下所示:

image.png

image.png

复制值

除了存储方式的不同,原始值和引用值在通过变量复制时也有所不同,在通过变量把一个原始值赋值到另一个变量时,原始值会被复制到新变量的位置。而且复制过后的值跟原来的值是完全独立互补影响的。
引用值复制的时候,实际上复制的是内存对象的引用,等于说是对象又多了一个指针指向自己而已。当通过这两个指针操作对象的时候,这个对象都会反映这两个指针的操作行为。

传递参数

ECMAScript中所有函数的参数都是按值传递的。

确定类型

typeof操作符最适合用来判断一个变量是否为字符串,数值,布尔值,undefined。null则会返回object,函数返回function。所以千万记着,它只适合用来判断原始值类型的变量,对引用值的变量的判断没啥作用。

对引用类型的判断需要使用instanceof,如果变量是给定引用类型的实例,则该操作符返回true。如果用该操作符检测原始值,则始终返回false。

注意的是,使用typeof检测正则表达式的时候,浏览器之间实现的方式不一样,Safari和Chrome会返回function,而IE和Firefox返回object。ECMAScript-262规定,任何实现内部[[Call]]方法的对象都应该在typeof检测时返回function。

执行上下文和作用域

变量和函数的上下文决定了他们可以访问哪些数据,以及他们的行为。每个上下文都有一个关联的变量对象,而这个上下文中定义的所有变量和函数都存在于这个对象上。这个变量对象是不能用代码访问到的,但后台处理数据会用到它。

全局上下文是最外层的上下文,根据ECMAScript实现的宿主环境,表示全局上下文的对象可能不一样,在浏览器中,全局上下文就是window对象,所有通过var定义的全局变量和函数都会成为window对象的属性和方法,但是使用let和const定义的全局变量不会成为window的属性,但是在作用域链上解析的效果是一样的,也就是说使用let和const定义的全局变量我是可以在任何地方去访问的。上下文在其所有代码都执行完毕后会被销毁,包括定义在它上面的所有变量和函数。全局上下文只有在应用程序退出前才会被销毁,比如关闭网页或退出浏览器。

每个函数调用也都有自己的上下文,当开始执行函数时,函数的上下文会被推到一个上下文栈中,在函数执行完毕之后又会被弹出,将控制权返还给之前的上下文。ECMAScript程序的执行流就是通过这个上下文进行控制的。

上下文中的代码在执行的时候,会创建变量对象的一个作用域链,这个作用域链决定了各级上下文中的代码在访问变量和函数时的顺序。代码正在执行的上下文的变量对象始终位于作用域链的最前端。作用域链中的下一个变量对象来自包含上下文,再下一个对象来自再下一个包含上下文,以此类推直至全局上下文,全局上下文的变量对象始终是作用域链的最后一个变量对象。我们可以把作用域链想象成若干个上下文的变量对象按照一个线性顺序组合起来的一个链条,从最下可以依次向上访问,反过来则不行。

垃圾回收

js是使用垃圾回收的语言,也就是说执行环境负责在代码执行时管理内存。在浏览器的发展史上,主要有两种主要的标记策略,分别是标记清理和引用计数。

js最常用的是垃圾回收策略是标记清理,具体做法是当垃圾回收程序运行的时候,会标记内存中存储的所有变量,然后它会将所有在上下文中的变量,以及被在上下文中的变量引用的变量的标记去掉。在此之后还被标记的变量就是待删除的了。

引用计数的问题是存在循环引用的情况下,无法有效的清理内存。因而很好被使用。它的主要做法是,对每个值都记录它被引用的次数。声明变量并给他赋一个引用值的时候,这个值的引用数加1,如果同一个值又被赋给另一个变量,那么引用数加1。如果保存对该值引用的变量被其他值给覆盖了,那么引用数减1。当一个值的引用数为0时,就说明没办法再访问这个值了,就可以安全的回收其内存了。

内存管理

因为操作系统在运行的时候会从整体性能考虑,不会分配给浏览器过多的内存,避免运行大量的js代码而耗尽过多内存导致系统崩溃。所以说将内存占用保持在一个较小的值可以让页面性能更好。优化内存占用的最佳手段就是保证在执行代码时只保存必要的数据。如果数据不再必要,就把它设置为null,从而释放其引用。这个建议适合全局变量和全局对象的属性。

通过const和let提升性能,使用这两个关键字不仅有助于改善代码风格,而且同样有利于改进垃圾回收的过程。因为这两个关键字是以块为作用域,相比于使用var,可能会更早的让垃圾回收程序介入,尽早回收应该回收的内存。

内存泄漏

避免意外地声明全局变量,比如不使用var,直接定义了一个变量,这样该变量会成为window的属性,只要window本身没被清理,该内存就会一直占用。

定时器也可能会悄悄地导致内存泄漏,定时器如果引用了外部变量,只要定时器一直运行,其内部引用的变量就会一直占用内存。

闭包也可能会导致内存泄漏。

总结

js变量保存两种类型的值,原始值和引用值。原始值大小固定保存在栈内存,引用值保存在堆内存。
执行上下文分为全局上下文,函数上下文和块级上下文。
代码执行流每进入一个新上下文,都会创建一个作用域链,用于搜索变量和函数。
为促进内存回收,全局对象,全局对象的属性和循环引用都应该在不需要时解除引用。