作用域、闭包、内存泄露
作用域
作用域指的是变量的有效访问范围。作用域对Javascript有重要意义,了解作用域的工作原理是在性能角度和功能角度理解Javascript的关键。
每一个JavaScript函数都被表示为对象,是一个函数实例。以下两种定义函数的方式是等价的。
var sayName = function(){ alert('hello world!'); } var sayName = new Function('alert("hello world!")');
函数对象正如其他对象那样,拥有可以被Javascript代码访问的属性,和一系列不能被Javascript代码访问,仅供JavaScript引擎使用的内部属性。其中一个内部属性是[[Scope]]。
内部[[Scope]]属性包含一个函数被创建的作用域中对象的集合。此集合被称为函数的作用域链,它决定哪些数据可由函数访问。此函数作用域链中的每个对象被称为一个可变对象,每个可变对象都以“键值对”的形式存在。当一个函数创建后,它的作用域链被填充以对象,这些对象代表创建此函数的环境中可访问的数据。例如下面这个全局函数:
function add(num1, num2) { var sum = num1 + num2; return sum; }
当add()函数创建后,它的内部属性指向一个作用域链,该作用域链中被填入一个单独的可变对象,这个可变对象是一个全局对象。此全局对象包含诸如窗口、浏览器和文档之类的访问接口。
而当add函数运行时会建立一个内部对象,称作“运行期上下文”。一个运行期上下文定义了一个函数运行时的环境。对函数的每次运行而言,每个运行期上下文都是独一的,所以多次调用同一个函数就会导致多次创建运行期上下文(execution context)。当函数执行完毕,运行期上下文就被销毁。
一个运行期上下文有它自己的作用域链,用于标识符解析。当运行期上下文被创建时,它的作用域链被初始化。包括函数的[[Scope]]属性中所包含的对象会按照它们出现在作用域链中的顺序,被复制到运行期上下文的作用域链中。这项工作一旦完成,一个被称作“激活对象”的新对象就为运行期上下文创建好了。此激活对象作为函数执行期的一个可变对象,包含访问所有局部变量,命名参数,参数集合,和this的接口。然后,此对象被推入作用域链的前端。当作用域链被销毁时,激活对象也一同销毁。下图显示了add函数运行时所对应的运行期上下文和它的作用域链。
var total = add(5, 10);
需要记住的是两点:
- [[scope]]属性是函数创建到运行时一直存在的,知道函数被销毁后占用的内存才会被释放
- 运行期上下文只存在于函数运行期间,函数运行结束后该对象被销毁
闭包、内存泄露
闭包是JavaScript最强大的一个方面,它允许函数访问局部范围之外的数据。
function assignEvents() { var id = "xdi9592"; document.getElementById('save').onclick = function(event){ saveDocument(id); } }
当assignEvents()被执行时,一个激活对象被创建,并包含了该函数作用域内所有可访问的变量和函数,其中包括id变量。它将成为运行期上下文作用域链上的第一个对象,全局对象是第二个。当闭包创建时,[[Scope]]属性包含了作用域内所有对象的集合(等于assignEvents运行期上下文的作用域链,即assignEvents的激活对象,全局对象)。
由于闭包的[[Scope]]属性包含与运行期上下文作用域链相同的对象引用,会产生副作用。通常,一个函数的激活对象与运行期上下文一同销毁。当涉及闭包时,运行期上下文对象,以及他的作用域链被销毁,但激活对象就无法销毁,因为引用仍然存在于闭包的[[Scope]]属性中。除非手动接触所有对匿名函数的引用,等到垃圾收集器下次运行时,assignEvents的激活对象才会随着匿名函数一同被销毁。
document.getElementById('save').onclick = null;
所以在Javascript代码中闭包与非闭包函数相比,需要更多内存开销。在大型网页应用中,这可能会导致内存泄露与难以排查的性能问题。
随着浏览器的升级,大部分浏览器对于闭包引起的循环引用问题都能够顺利解决。但IE9之前使用非本地JavaScript对象实现DOM对象,对于Javascript对象跟DOM对象使用不同的垃圾收集器。所以闭包在IE的这些版本中发生循环引用时便会导致内存泄露。