来了来了 第四章来了 这章节主讲变量、作用域与内存
不同与typeScript的严格要求,JavaScript的变量是松散类型的,这样的变量可以存放数值也可以存放字符串以及对象数组等等,听起来很强大吧,但也容易引出一些问题。
-
原始值与引用值
- 原始值就是最简单的数据,引用值则是由多个值构成的对象。
保存原始值的变量是按值(by value)访问的,因为我们操作的就是存储在变量中的实际值。引用值是保存在内存中的对象. - 原始值不能有属性。
但用new关键词声明的话,就会创建一个object类型的实例。
- 复制值
在把原始值或引用值从一个变量赋给另一个变量时,存储在变量中的值都会被复制到新变量所在的位置。区别在于,引用值复制的值实际上是一个指针,它指向存储在堆内存中的对象。操作完成后,两个变量实际上指向同一个对象。
这里就需要了解到如何复制一个对象,也能像原始值一样,可以独立不影响被复制的对象,那就是我们常说的深拷贝
深拷贝的方法很多,JSON深拷贝只能用于较为简单的对象,当含有函数,层级复杂等等就不适用了。
学完这期,我会出一篇专门如何进行深拷贝,以及各个方法的优缺点的文章(网上也有很多总结完善的文章),我自己梳理一遍,会更加印象深刻一些。
(呐呐呐,2021/10/25补充来了。浅拷贝和深拷贝篇)
好了继续。 - 传递参数
ECMAScript中所有函数的参数都是按值传递的。函数外的值传到函数内部,就像一个变量复制到另一个变量,所以和复制值一样。
- 确定类型
前一章提到的typeof操作符,它是判断一个变量是否为字符串、数值、布尔值或undefined的最好方式。
typeof仅适用于判断原始值,对引用值用处不大。
为了解决这个问题,ECMAScript提供了instanceof操作符,会返回true或false.
看到这是不是又产生一个疑问:为什么b又是数组又是对象的呢?
因为按照定义,所有引用值都是Object的实例,因此通过instanceof操作符检测任何引用值是否为object都会返回true。
- 原始值就是最简单的数据,引用值则是由多个值构成的对象。
-
执行上下文与作用域
变量或函数的执行上下文(简称上下文)决定了它们可以访问哪些数据,以及它们的行为。每个上下文都有一个关联的变量对象(variable object),而这个上下文中定义的所有变量和函数都存在于这个对象上。
上下文中的代码在执行的时候,会创建变量对象的一个作用域链(scope chain)。这个作用域链决定了各级上下文中的代码在访问变量和函数时的顺序。
上下文在其所有代码都执行完毕后会被销毁,包括定义在它上面的所有变量和函数(全局上下文在应用程序退出前才会被销毁,比如关闭网页或退出浏览器)。- 作用域链增强
虽然执行上下文主要有全局上下文和函数上下文两种(eval()调用存在第三种上下文),但有其他方式来增强作用域链。比如try/catch的catch块,之前说到的with语句,都会在作用域链前端添加一个变量对象。 - 变量声明(es6前仅有var唯一一个关键词声明变量,es6后增加了let const 上章有讲到)
- 标识符查找
搜索开始于作用域链前端,找到即停止搜索,找不到就往外沿着作用域链继续搜索持续到搜索至全局上下文的变量对象,若还是未找到,则说明其未声明。
调用aFun()函数会引用变量a,为了确定a的值就会在函数内部找,没找到,往外找找到了全局变量a的值 于是返回“全局变量”
访问局部变量比访问全局变量要快,因为不用切换作用域。 - 垃圾回收
JavaScript是使用垃圾回收的语言,也就是说执行环境负责在代码执行时管理内存。通过自动(周期性的)内存管理实现内存分配和闲置资源回收,基本思路很简单:确定哪个变量不会再使用,然后释放它占用的内存。
垃圾回收过程是一个近似且不完美的方案,因为某块内存是否还有用,属于“不可判定的”问题,靠算法是解决不了的。
那么,如何标记未使用的变量呢,在浏览器的发展史上,用到过两种主要的标记策略:标记清理和引用计数。 - 标记清理
js最常用的垃圾回收策略是标记清理,垃圾回收程序运行的时候,会标记内存中存储的所有变量(标记的方法很多),它会将所有在上下文中的变量,以及被在上下文中的变量引用的变量的标记去掉。在此之后再被加上标记的变量就是待删除的了,原因是任何在上下文中的变量都访问不到它们了,随后垃圾回收程序做一次内存清理,销毁带标记的所有值并收回它们的内存。
到了2008年,IE、Firefox、Opera、Chrome和Safari都在自己的JavaScript实现中采用标记清理(或其变体),只是在运行垃圾回收的频率上有所差异。 - 引用计数
另一种没那么常用的垃圾回收策略是引用计数。对每个值都记录他被引用的次数,被引用数+1,引用该值的标量被其他值覆盖了,引用数-1,当一个值引用数为0时,垃圾回收程序运行时就会将它占的内存释放。
很引用计数并不适用循环引用场景,因为循环引用下,它们的引用数永远不会变成0,多次调用后大量内存不会被释放,因此Netscape在4.0版放弃了引用计数,转而采用标记清理。事实上,引用计数策略的问题还不止于此。 - 性能
垃圾回收程序是周期性运行的,如果内存中分配了很多的变量,可能回造成性能损失,所以垃圾回收的时间调度很重要,因此最好的办法是在写代码时就要做到:无论什么时候开始收集垃圾,都能让它尽快结束工作。
警告 在某些浏览器中是有可能(但不推荐)主动触发垃圾回收的。在IE中,window. CollectGarbage()方法会立即触发垃圾回收。在Opera 7及更高版本中,调用window. opera.collect()也会启动垃圾回收程序。 - 内存管理
JavaScript运行在一个内存管理与垃圾回收都很特殊的环境。分配给浏览器的内存小得多,所以开发者需要考虑将内存尽可能减少,让页面性能更好。优化内存占用的方法有很多种:- 解除引用
数据不再需要时将其置为null,解除引用,下一次垃圾回收程序运行时就会将它的内存收回。 - 通过const和let声明提升性能
块作用域比函数作用域能更早被回收。 - 隐藏类和删除操作
截至2017年,Chrome是最流行的浏览器,使用V8 JavaScript引擎,运行期间,V8会将创建的对象与隐藏类关联起来,以跟踪它们的属性特征。能够共享相同隐藏类的对象性能会更好。 - 内存泄漏
不规范声明全局变量、不规范引用定时器都可能导致内存泄漏。 - 静态分配与对象池
提升JavaScript性能,压榨浏览器,关键问题就是如何减少浏览器执行垃圾回收的次数。
尽量不要动态创建矢量对象。 - 注意 静态分配是优化的一种极端形式。如果你的应用程序被垃圾回收严重地拖了后腿,可以利用它提升性能。但这种情况并不多见。大多数情况下,这都属于过早优化,因此不用考虑。
- 解除引用
- 作用域链增强
-
小结
- JavaScript变量可以保存两种类型的值:原始值和引用值。
- 原始值大小固定,因此保存在栈内存上
- 引用值是对象,存储在堆内存上。
- 执行上下文分全局上下文、函数上下文和块级上下文
- 解除变量的引用不仅可以消除循环引用,而且对垃圾回收也有帮助,全局对象、全局对象的属性和循环引用都应该在不需要时解除引用。