开启掘金成长之旅!这是我参与「掘金日新计划 · 12 月更文挑战」的第15天,点击查看活动详情
4.1 原始值与引用值
ECMAScript 变量可以包含两种不同类型的数据:原始值(栈)和引用值(堆)。
原始数据类型和引用数据类型:
- 栈:原始数据类型(
Undefined
、Null
、Boolean
、Number
、String
、Symbol
、BigInt
) - 堆:引用数据类型(
对象
、数组
和函数
)
只有引用值可以动态添加后面可以使用的属性。
注意:ECMAScript 中函数的参数就是局部变量。
typeof
操作符最适合用来判断一个变量是否为原始类型。它是判断一个变量是否为字符串、数值、布尔值或 undefined
的最好方式。如果值是对象或 null
,那么 typeof
返回 "object"
。
typeof
虽然对原始值很有用,但它对引用值的用处不大。
ECMAScript 提供了 instanceof
操作符,通过 instanceof
操作符检测任何引用值和Object
构造函数都会返回 true
。
如果用 instanceof
检测原始值,则始终会返回 false
,因为原始值不是对象。
4.2 执行上下文与作用域
执行上下文
1. 全局执行上下文
在浏览器中,全局上下文就是我们常说的 window
对象,所有通过 var
定义的全局变量和函数都会成为 window
对象的属性和方法。使用 let
和 const
的顶级声明不会定义在全局上下文中。
上下文在其所有代码都执行完毕后会被销毁,包括定义在它上面的所有变量和函数(全局上下文在应用程序退出前才会被销毁,比如关闭网页或退出浏览器)。
一个程序中只有一个全局执行上下文。
2. 函数执行上下文
每个函数调用都有自己的上下文。当一个函数被调用时,就会为该函数创建一个新的执行上下文,函数的上下文可以有任意多个。
3. eval 执行函数上下文
eval()
调用内部存在第三种上下文,eval()
函数不常用,不做介绍。
变量声明
1. 使用 var 的函数作用域声明
在使用 var
声明变量时,变量会被自动添加到最接近的上下文。在函数中,最接近的上下文就是函数的局部上下文。在 with
语句中,最接近的上下文也是函数上下文。如果变量未经声明就被初始化了,那么它就会自动被添加到全局上下文。
2. 使用 let 的块级作用域声明
块级作用域由最近的一对包含花括号{}界定。换句话说,if 块、while 块、function 块,甚至连单独的块也是 let 声明变量的作用域。
3. 使用 const 的常量声明
除了 let
,ES6
同时还增加了 const
关键字。使用 const
声明的变量必须同时初始化为某个值。一经声明,在其生命周期的任何时候都不能再重新赋予新值。
注意:赋值为对象的
const
变量不能再被重新赋值为其他引用值,但对象的键则不受限制。
如果想让整个对象都不能修改,可以使用 Object.freeze()
,这样再给属性赋值时虽然不会报错,但会静默失败。
4. 标识符查找
如果在局部上下文中找到该标识符,则搜索停止,变量确定;如果没有找到变量名,则继续沿作用域链搜索。
这个过程一直持续到搜索至全局上下文的变量对象。如果仍然没有找到标识符,则说明其未明。
在局部变量声明之后的任何代码都无法访问全局变量,除非使用完全限定的写法 window.变量。
4.3 垃圾回收
通过自动内存管理实现内存分配和闲置资源回收。这个过程是周期性的,即垃圾回收程序每隔一定时间(或者说在代码执行过程中某个预定的收集时间)就会自动运行。
垃圾回收程序必须跟踪记录哪个变量还会使用,以及哪个变量不会再使用,以便回收内存。
垃圾回收的两种主要的标记策略:标记清理
和引用计数
。
标记清理
最常用的垃圾回收策略是标记清理。标记过程的实现并不重要,关键是策略。
垃圾回收程序运行的时候,会标记内存中存储的所有变量(记住,标记方法有很多种)。然后,它会将所有在上下文中的变量,以及被在上下文中的变量引用的变量的标记去掉。在此之后再被加上标记的变量就是待删除的了,原因是任何在上下文中的变量都访问不到它们了。随后垃圾回收程序做一次内存清理,销毁带标记的所有值并收回它们的内存。
引用计数
引用计数没那么常用。其思路是对每个值都记录它被引用的次数。
声明变量并给它赋一个引用值时,这个值的引用数为 1
。如果同一个值又被赋给另一个变量,那么引用数加 1
。类似地,如果保存对该值引用的变量被其他值给覆盖了,那么引用数减 1
。当一个值的引用数为 0
时,就说明没办法再访问到这个值了,因此可以安全地收回其内存了。垃圾回收程序下次运行的时候就会释放引用数为 0
的值的内存。
存在循环引用的问题,就是对象 A 有一个指针指向对象 B,而对象 B 也引用了对象 A。**通过各自的属性相互引用,它们的引用数永远不会变成 0。**如果函数被多次调用,则会导致大量内存永远不会被释放。
为了补救这一点,
IE9
把BOM
和DOM
对象都改成了JavaScript
对象,这同时也避免了由于存在两套垃圾回收算法而导致的问题,还消除了常见的内存泄漏现象。
性能
垃圾回收程序会周期性运行,垃圾回收的时间调度很重要。
垃圾回收有可能会明显拖慢渲染的速度和帧速率。
只要满足其中某个条件,垃圾回收程序就会运行。
IE7 发布后,JavaScript 引擎的垃圾回收程序被调优为动态改变分配变量、字面量或数组槽位等会触发垃圾回收的阈值。
在某些浏览器中是有可能(但不推荐)主动触发垃圾回收的。在
IE
中,window.CollectGarbage()
方法会立即触发垃圾回收。在Opera 7
及更高版本中,调用window.opera.collect()
也会启动垃圾回收程序。
内存管理
优化内存占用的最佳手段就是保证在执行代码时只保存必要的数据。如果数据不再必要,那么把它设置为 null
,从而释放其引用。这也可以叫作解除引用。这个建议最适合全局变量和全局对象的属性。
-
通过
const
和let
声明提升性能:有助于改善代码风格,有助于改进垃圾回收的过程。 -
隐藏类和删除操作
-
内存泄漏
意外声明全局变量是最常见但也最容易修复的内存泄漏问题。没有使用任何关键字声明变量;
定时器也可能会悄悄地导致内存泄漏;
闭包很容易在不知不觉间造成内存泄漏;
-
静态分配与对象池