变量、作用域与内存
原始值与引用值(Primitive and Reference Values)
- 原始值(primitive value):简单的数据 原始值不能有属性,尽管尝试给原始值添加属性不会报错。
- 引用值(reference value): 由多个值构成的对象 只有引用值可以动态添加后面可以使用的属性。
注意: 在很多语言中,字符串是使用对象表示的,因此被认为是引用类型。ECMAScript 打破了这个惯例。
传递参数(Argument Passing)
ECMAScript 中所有函数的参数都是按值传递的。[引用类型传递的为指针的值]
执行上下文与作用域(Execution context and scope)
执行上下文(以下简称“上下文”)的概念在 JavaScript 中是颇为重要的。变量或函数的上下文决定 了它们可以访问哪些数据,以及它们的行为。每个上下文都有一个关联的变量对象(variable object), 而这个上下文中定义的所有变量和函数都存在于这个对象上。虽然无法通过代码访问变量对象,但后台 处理数据会用到它。
上下文在其所有代码都执行完毕后会被销毁,包括定义 在它上面的所有变量和函数(全局上下文在应用程序退出前才会被销毁,比如关闭网页或退出浏览器)。
每个函数调用都有自己的上下文。当代码执行流进入函数时,函数的上下文被推到一个上下文栈上。 在函数执行完之后,上下文栈会弹出该函数上下文,将控制权返还给之前的执行上下文。ECMAScript 程序的执行流就是通过这个上下文栈进行控制的。
上下文中的代码在执行的时候,会创建变量对象的一个 作用域链(scope chain)。这个作用域链决定 了各级上下文中的代码在访问变量和函数时的顺序。代码正在执行的上下文的变量对象始终位于作用域 链的最前端。如果上下文是函数,则其活动对象(activation object)用作变量对象。活动对象最初只有 一个定义变量:arguments。(全局上下文中没有这个变量。)作用域链中的下一个变量对象来自包含上 下文,再下一个对象来自再下一个包含上下文。以此类推直至全局上下文;全局上下文的变量对象始终 是作用域链的最后一个变量对象。
代码执行时的标识符解析是通过沿作用域链逐级搜索标识符名称完成的。搜索过程始终从作用域链 的最前端开始,然后逐级往后,直到找到标识符。(如果没有找到标识符,那么通常会报错。)
执行上下文有
- 全局上下文
- 函数上下文
- 块级上下文
- eval()调用内部上下文 (在 strict 模式下)
作用域链增强(Scope Chain Augmentation)
某些语句会导致在作用域链前端临时添加一个上下文,这个上下文在代码执 行后会被删除。通常在两种情况下会出现这个现象,即代码执行到下面任意一种情况时:
- try/catch 语句的 catch 块
- with 语句 ( 在 strict 模式下 不可使用 with语句)
这两种情况下,都会在作用域链前端添加一个变量对象。对 with 语句来说,会向作用域链前端添 加指定的对象;对 catch 语句而言,则会创建一个新的变量对象,这个变量对象会包含要抛出的错误 对象的声明。
标识符查找(Identifier Lookup)
当在特定上下文中为读取或写入而引用一个标识符时,必须通过搜索确定这个标识符表示什么。搜 索开始于作用域链前端,以给定的名称搜索对应的标识符。如果在局部上下文中找到该标识符,则搜索 停止,变量确定;如果没有找到变量名,则继续沿作用域链搜索。(注意,作用域链中的对象也有一个 原型链,因此搜索可能涉及每个对象的原型链。)这个过程一直持续到搜索至全局上下文的变量对象。 如果仍然没有找到标识符,则说明其未声明。
var color = 'blue';
function getColor() {
return color;
}
console.log(getColor()); // 'blue'
function getColor() {
let color = 'red';
return color;
}
console.log(getColor());
// 'red'
垃圾回收(Garbage Collection)
JavaScript 是使用垃圾回收的语言,也就是说执行环境负责在代码执行时管理内存。
基本思路很简单:确定哪个变量不会再 使用,然后释放它占用的内存。这个过程是周期性的,即垃圾回收程序每隔一定时间(或者说在代码执 行过程中某个预定的收集时间)就会自动运行。
垃圾回收过程是一个近似且不完美的方案,因为某块内存是否还有用,属于“不可判定的”问题,意味着靠算法是解决不了的。
垃圾回收程序必须跟踪记录哪个变量还会使用,以及哪个变量不会再使用,以便回收 内存。如何标记未使用的变量也许有不同的实现方式。不过,在浏览器的发展史上,用到过两种主要的 标记策略:标记清理和引用计数。
标记清理(mark-and-sweep)
JavaScript 最常用的垃圾回收策略是标记清理(mark-and-sweep)。
垃圾回收程序运行的时候,会标记内存中存储的所有变量(记住,标记方法有很多种)。然后,它 会将所有在上下文中的变量,以及被在上下文中的变量引用的变量的标记去掉。在此之后再被加上标记 的变量就是待删除的了,原因是任何在上下文中的变量都访问不到它们了。随后垃圾回收程序做一次内存清理,销毁带标记的所有值并收回它们的内存。
引用计数(reference counting)
其思路是对每个值都记录它被 引用的次数。声明变量并给它赋一个引用值时,这个值的引用数为 1。如果同一个值又被赋给另一个变 量,那么引用数加 1。类似地,如果保存对该值引用的变量被其他值给覆盖了,那么引用数减 1。当一 个值的引用数为 0 时,就说明没办法再访问到这个值了,因此可以安全地收回其内存了。垃圾回收程序 下次运行的时候就会释放引用数为 0 的值的内存。
存在问题:
- 循环引用
性能(Performance)
垃圾回收程序会周期性运行,如果内存中分配了很多变量,则可能造成性能损失,因此垃圾回收的 时间调度很重要。尤其是在内存有限的移动设备上,垃圾回收有可能会明显拖慢渲染的速度和帧速率。 开发者不知道什么时候运行时会收集垃圾,因此最好的办法是在写代码时就要做到:无论什么时候开始 收集垃圾,都能让它尽快结束工作。
现代垃圾回收程序会基于对 JavaScript 运行时环境的探测来决定何时运行。探测机制因引擎而异, 但基本上都是根据已分配对象的大小和数量来判断的。比如,根据 V8 团队 2016 年的一篇博文的说法: “在一次完整的垃圾回收之后,V8 的堆增长策略会根据活跃对象的数量外加一些余量来确定何时再次垃 圾回收。”
内存管理(Managing Memory)
分配给浏览器的内存通常比分配给桌面软件的要少很多,分配给移动 浏览器的就更少了。这更多出于安全考虑而不是别的,就是为了避免运行大量 JavaScript 的网页耗尽系 统内存而导致操作系统崩溃。这个内存限制不仅影响变量分配,也影响调用栈以及能够同时在一个线程 中执行的语句数量。
内存管理的优化方法 - 解除引用: 将内存占用量保持在一个较小的值可以让页面性能更好。优化内存占用的最佳手段就是保证在执行 代码时只保存必要的数据。如果数据不再必要,那么把它设置为 null,从而释放其引用。这也可以叫 作解除引用。
解除对一个值的引用并不会自动导致相关内存被回收。解除引用的关键在于确保相关 的值已经不在上下文里了,因此它在下次垃圾回收时会被回收。
内存管理注意点
-
通过const 和 let 声明提升性能
-
隐藏类和删除操作(4.3.4-2) Chrome 是最流行的浏览器,使用 V8 JavaScript 引擎。V8 在将解释后的 JavaScript 代码编译为实际的机器码时会利用“隐藏类”。 如果你的代码非常注重性能, 那么这一点可能对你很 重要。 解决方案就是避免 JavaScript 的“先创建再补充”(ready-fire-aim)式的动态属性赋值,并在 构造函数中一次性声明所有属性
-
内存泄漏 写得不好的 JavaScript 可能出现难以察觉且有害的内存泄漏问题。在内存有限的设备上,或者在函 数会被调用很多次的情况下,内存泄漏可能是个大问题。JavaScript 中的内存泄漏大部分是由不合理的 引用导致的。
- 意外声明全局变量是最常见但也最容易修复的内存泄漏问题。
- 定时器也可能会悄悄地导致内存泄漏。
- 使用 JavaScript 闭包很容易在不知不觉间造成内存泄漏
-
静态分配与对象池 理论上,如果能够合理使用分配的内存,同时避免多余的垃圾回收,那就可以保住因 释放内存而损失的性能。
浏览器决定何时运行垃圾回收程序的一个标准就是对象更替的速度。如果有很多对象被初始化,然 后一下子又都超出了作用域,那么浏览器就会采用更激进的方式调度垃圾回收程序运行,这样当然会影 响性能。
静态分配是优化的一种极端形式。如果你的应用程序被垃圾回收严重地拖了后腿,可以利用它提升性能。但这种情况并不多见。大多数情况下,这都属于过早优化,因此不用考虑。