JS进阶:作用域链与内存管理

学习自:JS高程

js中引用类型也按值传递的

1
2
3
4
5
6
7
8
function setName(obj) { 
obj.name = "Nicholas";
obj = new Object();
obj.name = "Greg"; }

var person = new Object();
setName(person);
alert(person.name); //"Nicholas"

如果 person 是按引用传递的,那么 person 就会自动被修改为指向其 name 属性值 为”Greg”的新对象。但是,当接下来再访问 person.name 时,显示的值仍然是”Nicholas”。这说明 即使在函数内部修改了参数的值,但原始的引用仍然保持未变。实际上,当在函数内部重写 obj 时,这 个变量引用的就是一个局部对象了。而这个局部对象会在函数执行完毕后立即被销毁。

typeof 操作符确定一个变量是字符串、数值、布尔值,还是 undefined ,如果变 量的值是一个对象或 null,返回”object”

instanceof 操作符 result = variable instanceof constructor

1
2
3
alert(person instanceof Object);  // 变量 person 是 Object 吗?
alert(colors instanceof Array); // 变量 colors 是 Array 吗?
alert(pattern instanceof RegExp); // 变量 pattern 是 RegExp 吗?

作用域链

当代码在一个环境中执行时,会创建变量对象的一个作用域链(scope chain)。作用域链的用途,是 保证对执行环境有权访问的所有变量和函数的有序访问。作用域链的前端,始终都是当前执行的代码所 在环境的变量对象。如果这个环境是函数,则将其活动对象(activation object)作为变量对象。活动对 象在最开始时只包含一个变量,即 arguments 对象(这个对象在全局环境中是不存在的)。作用域链中 的下一个变量对象来自包含(外部)环境,而再下一个变量对象则来自下一个包含环境。这样,一直延 续到全局执行环境;全局执行环境的变量对象始终都是作用域链中的最后一个对象。

标识符解析是沿着作用域链一级一级地搜索标识符的过程。

内部环境可以通过作用域链访问所有的外部环境,但 外部环境不能访问内部环境中的任何变量和函数。

延长作用域链

catch 语句来说,会创建一个新的变量对象,其中包含的是被抛出的错误对象的声明。

with 语句来说,会将指定的对象添加到 作用域链中。

1
2
3
4
5
6
7
function buildUrl() { 
var qs = "?debug=true";

with(location){ var url = href + qs; }

return url;
}

with 语句接收的是 location 对象,因此其变量对象中就包含了 location 对象的所有属 性和方法,而这个变量对象被添加到了作用域链的前端。

垃圾回收

JavaScript 中最常用的垃圾收集方式是标记清除(mark-and-sweep)。

垃圾收集器建立了一个“根节点”列表。根节点通常是那些引用被保留在代码中的全局变量。对于 Javascript 而言,“Window” 对象就是一个能作为根节点的全局变量例子。window 对象是一直都存在的(即:不是垃圾)。所有根节点都是检查过的并且被标记为活动的(即:不是垃圾)。所有的子节点也都被递归地检查过。每块可以从根节点访问的内存都不会被视为垃圾。 所有没有被标记为垃圾的内存现在可以被当做垃圾,而垃圾收集器也可以释放这些内存并将它们返还给操作系统。现代垃圾收集器使用不同的方式来改进这些算法,但是它们都有相同的本质:可以访问的内存块被标记为非垃圾而其余的就被视为垃圾。

可以通过翻转某个特殊的位来记录一个变量何时进入环境, 或者使用一个“进入环境的”变量列表及一个“离开环境的”变量列表来跟踪哪个变量发生了变化。

另一种不太常见的垃圾收集策略叫做引用计数(reference counting)。

一个严重的问题:循 环引用。循环引用指的是对象 A 中包含一个指向对象 B 的指针,而对象 B 中也包含一个指向对象 A 的 引用。

闭包中,循环引用尤其突出。

1
2
3
4
5
6
7
8
9
10
<script type="text/javascript">
window.onload = function closureDemoParentFunction(paramA) {
var a = paramA;
return function closureDemoInnerFunction(paramB) {
alert(a + " " + paramB);
};
};
var x = closureDemoParentFunction("outer x");
x("inner x");
</script>

由于内部函数持有对外部函数的变量的引用,因此这个带属性a的范围对象将不会被垃圾收集。

为了避免类似这样的循环引用问题,最好是在不使用它们的时候手工断开原生 JavaScript 对象与 DOM 元素之间的连接。

  • 主动设置JavaScript对象obj为空,显式打破此循环引用。
  • 通过添加另一个闭包来避免JavaScript对象和DOM对象间的循环引用。
  • 通过添加另一个函数来避免闭包本身,进而阻止内存泄漏。

确保占用最少的内存可以让页面获得更好的性能。而优化内存占用的最佳方式,就是为执行 中的代码只保存必要的数据。一旦数据不再有用,最好通过将其值设置为 null 来释放其引用——这个 做法叫做解除引用(dereferencing)。这一做法适用于大多数全局变量和全局对象的属性。

内存泄露的几种情况

  1. 意外的全局变量:一个未声明变量的引用会在全局对象中创建一个新的变量。

如果你必须使用全局变量来存储很多的数据,请确保在使用过后将它设置为 null 或者将它重新赋值。常见的和全局变量相关的引发内存消耗增长的原因就是缓存。

  1. 被遗漏的定时器和回调函数

将整个代码块放在周期处理函数中并不是必要的, 由于周期函数一直在运行,处理函数并不会被回收(只有周期函数停止运行之后才开始回收内存)

当它们本身的实例被销毁之前销毁所有指向回调的引用

  1. DOM 之外的引用,如将 DOM 结点存储到数据结构
  2. 闭包, 创建了一个闭包链表(根节点是 theThing 形式的变量),而且每个闭包作用域都持有一个对大数组的间接引用,这导致了一个巨大的内存泄露。

GC优化策略

和其他语言一样,javascript的GC策略也无法避免一个问题:GC时,停止响应其他操作,这是为了安全考虑。

(1)分代回收(Generation GC)

这个和Java回收策略思想是一致的。目的是通过区分“临时”与“持久”对象;多回收“临时对象”区(young generation),少回收“持久对象”区(tenured generation),减少每次需遍历的对象,从而减少每次GC的耗时

(2)增量GC

这个方案的思想很简单,就是“每次处理一点,下次再处理一点,如此类推”。

这种方案,虽然耗时短,但中断较多,带来了上下文切换频繁的问题。

低 (对象/s) 比率时,中断执行GC的频率,simple GC更低些;如果大量对象都是长期“存活”,则分代处理优势也不大。