但是如果给 a2 添加了一个属性,而 a1 没有,则他俩的 hidden class 就不同了,这样性能也会不好
a2.author = 'Jake';
如果两个对象的属性一样,但是用 delete 删除了一个属性,也会导致 hidden class 不同,也会对性能造成不好的影响
function Article() {
this.title = 'Inauguration Ceremony Features Kazoo Band';
this.author = 'Jake';
}
let a1 = new Article();
let a2 = new Article();
delete a1.author;
因此,为了性能优化,不要的变量可以赋值 null,不要用 delete 删掉。
内存泄漏
无用的变量仍在内存中,就叫内存泄漏。以下是常见的内存泄漏的情况及解决方法
全局变量
function setName() {
name = 'Jake';
}
name 会被绑定在 window 上,而无法释放。因此要用 var,let 或 const 来声明
Interval 函数引用的外部变量
let name = 'Jake';
setInterval(() => {
console.log(name);
}, 100);
name 被回调函数引用,无法释放。
闭包
let outer = function() {
let name = 'Jake';
return function() {
return name;
};
};
04 变量、作用域、内存管理
基本类型和引用类型
基本类型大小固定,存储在内存的栈区
引用类型存储在内存的堆上
动态属性
函数传参
类型判断
typeof
instanceof
如果左值是基本类型,结果永远是false
,因为基本类型不是对象执行上下文(context)与作用域链
在 JS 内部,每个上下文都绑定了一个 变量对象(variable object)。代码不能直接访问该对象,但是内部通过这个对象来操作数据
全局上下文位于最外层,浏览器中是
window
对象,在全局用var
声明的变量会绑定在window
对象的属性上let
和const
不会绑定在全局对象,而是在作用域链(scope chain,作用域链看下文)上单独处理一个 context 的代码执行完毕后,会被销毁,内部定义的变量和函数也会随之销毁
全局 context 只有当整个程序结才会销毁(比如网页关闭)
每个函数有自己的上下文,调用函数时,函数上下文会入栈,调用结束后会出栈,并把控制权交给前一个上下文(调用栈)
上下文的代码执行时,变量对象的作用域链会被创建,用来提供上下文可以访问的变量的可访问权限
作用域链的开头是需要执行代码的变量对象
如果上下文是一个函数,则 activation object (不知道官方咋翻译的,意思大概就是活跃的变量的对象?)被用作开头的变量对象。activation object 以一个名为
arguments
的变量开头,(全局上下文没有该对象)作用域链的下一个对象是外部包裹的 context,再下一个是包含该 context 的 context,以此类推,直到到达全局 context。全局 context 永远是作用域链的最后一个 context
标识符会沿着作用域链向后搜索,如果找不到,就会报错
一般来说,内部上下文可以访问外部的变量,但是外部上下文不可以访问内部(局部作用域)的变量
作用域链延长
有两种执行上下文,函数和全局。(第三种是
eval()
)以下情况会临时延长作用域链(在作用域链前部临时添加,执行之后会删除)
try-catch
中catch
代码块捕获到的
error
变量会在catch
的局部作用域内有效,外部无效with
语句(不常用)变量声明
var
不要再用了,就不多说了let
和const
定义了块级对象,块级对象为{}
内的代码,如if
while
function
try
或单独的
{}
代码块如果不会改变变量,尽量优先使用
const
变量查找
JS 沿着作用域链查找,如果找不到,就在下一个变量对象查找
访问局部变量的性能比访问全局对象高,因为查找路径短(但是 JS 有优化,影响不大)
垃圾回收 (GC)
浏览器为了确定不再使用的变量,传统上有 2 种策略
Mark-and-Sweep:标记和清除
Reference counting:引用计数
Mark-and-Sweep
当变量进入上下文时,会被标记“进入上下文”
被标记的变量,不应该被释放,因为上下文中后续可能会用到
当变量出上下文时,会被标记“出上下文”
标记的方法有多种(不重要)
可以使用一个二进制位的反转来标记
也可以维护一个“上下文中”的变量列表和一个“上下文外”的变量列表
GC 运行时,会标记所有在内存中的变量。然后清除在上下文中的变量或被上下文中变量引用的标记
此时,仍有标记的变量则被认为可以清除,因为它们再也访问不到了
然后,GC 会进行 memory sweep(垃圾清扫),删除所有被标记的变量,并回收这些变量所使用的内存
现在主流浏览器都是用的这种 GC 策略
引用计数
现在主流浏览器已经不使用这种策略了
定义
变量声明、赋值会使引用计数递增
如果变量的值被另一个值覆盖,则原来的变量的引用计数递减
当引用计数到 0 时,则该变量将不会被使用
每次 GC 运行时,会清除引用计数为 0 的变量
循环引用的问题
当变量存在循环引用时,引用计数永远不会到 0,则变量永远不会被回收。如果函数多次调用,会造成大量的内存无法被释放,造成内存泄漏
在这个例子中,
objectA
和objectB
互相引用,使得他们的引用计数都为 2。如果需要释放这两个变量,需要手动清除老的浏览器,内置对象(如 DOM 和 BOM)对象,并非 JS 对象,而是 C++ 的 COM 对象,因此使用引用计数的策略,会存在内存泄漏的问题。后续浏览器把内置对象改为了 JS 对象,统一使用一套 GC 策略。
还有其他情况会造成内存泄漏,后面的内容会说。
性能优化
GC 周期性的运行,如果内存中变量多了,会有性能损耗
现代 GC 通过 JS 运行环时境的启发式(heuristics)方法,判断 GC 运行的时机
不同引擎的启发式方法不同,但是基本上都是通过当前分配的对象的大小和数量来判断
IE 曾经因为其 GC 运行频率而导致的性能问题而臭名昭著。。。
[汗]
IE 设置了固定的阀值,每次达到阀值都会运行 GC。IE 7 之后改成了动态阀值,如果每次回收的内存小于 15%,则阀值加倍;如果回收的内存大于 85%,则调整至默认值。(比较有意思,所以我记下来了)。这样就大大提高性能了。有些浏览器可以通过调用方法,来手动触发 GC(不太推荐使用)。
IE:
window.CollectGarbage()
Opera:
window.opera.collect()
内存管理
虽然 JS 是 GC 语言,但是我们还是要注意不要占用太多的系统内存,注意以下几点:
变量内存分配
调用栈
单个线程内执行语句的数量
我们要尽量使内存最小化,比如可以给不再使用的对象赋值 null 以释放内存(dereference,解引用)
手动赋 null 一般用于全局变量,因为函数内的局部变量会自动被解引
解引本身不会自动回收内存,解引是为了保证下次 GC 可以回收被解引的内存
使用 const 和 let 可以提升性能
因为使用
const
和let
定义的变量的作用域是块级的,在代码块结束之后会被释放。而var
只有在函数作用域结束后才释放。Hidden Class 和 delete 操作
了解一下 V8 引擎的编译时使用的 Hidden Class 工具
运行时,V8 会给每个创建的对象绑定一个 hidden class(隐藏的 class?),用以追踪有哪些属性
用用相同 hidden class 的对象性能更好(V8 对其有优化,但是有例外情况)
上述代码的
a1
和a2
有相同的属性,所以在 V8 下,他们使用了同一个 hidden class。这样 V8 对其有优化,性能更好。但是如果给
a2
添加了一个属性,而a1
没有,则他俩的 hidden class 就不同了,这样性能也会不好如果两个对象的属性一样,但是用
delete
删除了一个属性,也会导致 hidden class 不同,也会对性能造成不好的影响因此,为了性能优化,不要的变量可以赋值
null
,不要用delete
删掉。内存泄漏
无用的变量仍在内存中,就叫内存泄漏。以下是常见的内存泄漏的情况及解决方法
全局变量
name
会被绑定在window
上,而无法释放。因此要用var
,let
或const
来声明Interval 函数引用的外部变量
name
被回调函数引用,无法释放。闭包
即使调用完了
outer
,name
仍被其返回的函数引用,导致无法被释放。静态分配、Object Poll(对象池)
如果对性能有极致要求才这么做,一般情况用不到
GC 频繁调用会影响性能
如果对性能有极致要求,减少 GC 操作的次数。因此可以围绕启发式的 GC 调度来进行优化
浏览器决定是否调用 GC,一个重要的维度:如果大量的对象被创建,然后出作用域,则 GC 会更主动地被调度
举例
如果某个函数内部会创建局部的对象,而这个函数会被频繁调用,GC 就会更频繁地被触发
为了防止 GC 被频繁调度,可以维护一个对象池,提前把需要用的对象创建好,需要用到时,从对象池里面取,并且可以复用。这样就避免大量频繁的创建新的对象了。
贪婪算法,如果对象不存在,就创建一个新的,如果存在,就直接复用。
这样会导致内存一直增长,因此可以使用数组来管理对象池。但是使用数组时,也要注意,不要造成额外的垃圾回收
因为 JS 的数组是动态的。数组的长度原本是 100,但是
push
操作,会导致 JS 把原有的数组删掉,然后在内存中分配一个新的长度为 200 的数组。之前的数组的删除需要调用额外的 GC 操作。因此最开始就要创建长度足够长的数组。