JavaScript变量、作用域、垃圾回收机制和闭包理解

作者 Simmin 日期 2016-10-10
JavaScript变量、作用域、垃圾回收机制和闭包理解

一、理解变量

变量

ECMAScript变量可能包含两种不同数据类型的值:基本类型值引用类型值

基本类型值:简单的数据段。按值访问,可以操作保存在变量中的实际的值。

引用类型值:可能有多个值构成的对象。保存在内存中,与其他语言不同,JS不允许直接访问内存中的位置,即不能直接操作对象的内存空间。

操作对象分为两种情况:

  1. 复制保存着对象的某个变量时,操作的是对象的引用

  2. 为对象添加属性时,操作的是实际的对象

基本类型和引用类型的不同之处

【1】只能给引用类型值动态地添加属性

【2】基本类型复制变量值会在变量对象上创建一个新值,然后把该值复制到为新变量分配的位置上,前后两个变量可以参与任何操作而互不影响。
引用类型复制变量值同样也会将存储在变量对象中的值复制一份放到为新变量分配的空间中去。不同的是,这个值的副本实际上是一个指针,这个指针指向在存储在堆中的一个对象,复制操作结束后,两个变量实际上将引用同一个对象。因此改变其中一个变量,就会影响另一个变量。
复制变量值
【3】ECMAScript中所有函数的参数都是按值传递的。(而访问变量有按值和按引用两种方式)
当向参数传递基本类型的值时,被传递的值被复制给一个局部变量。
当向参数传递引用类型的值时,会把这个值在内存中的地址复制给一个局部变量,因此这个局部变量的变化会反映在函数的外部。

function setName(obj){
obj.name = 'Nicholas';
}
var person = new Object();
setName(person);
console.log(person.name); //"Nicholas"

【4】typeof主要用于检测基本数据类型,如果变量的值是一个对象或null,会返回object。typeof对于引用类型用处不大,我们通常并不是想知道某个值是对象,而是它是什么类型的对象,这时使用instanceof操作符。

var a = "abc";
var b = 1;
var c = true;
var d;
var e = null;
var f = new object();
console.log(typeof(a));//string
console.log(typeof(b));//number
console.log(typeof(c));//boolean
console.log(typeof(d));//undefined
console.log(typeof(e));//object
console.log(typeof(f));//object
console.log(person instanceof Object); // 变量person是否为Object
console.log(color instanceof Array); // 变量color是否为Array
console.log(pattern instanceof RegExp); // 变量pattern是否为RegExp

所有引用类型的值都是Object的实例

所以,检测一个引用类型和Object构造函数时,instanceof始终返回true;检测基本类型时,instanceof始终返回false,因为基本类型不是对象。

使用typeof检测函数时,返回function。在 Safari 5 及
之前版本和 Chrome 7 及之前版本中使用 typeof 检测正则表达式时,由于规范的原
因,这个操作符也返回”function”。 ECMA-262 规定任何在内部实现[[Call]]方法
的对象都应该在应用 typeof 操作符时返回”function”。由于上述浏览器中的正则
表达式也实现了这个方法,因此对正则表达式应用 typeof 会返回”function”。在
IE 和 Firefox 中,对正则表达式应用 typeof 会返回”object”。

二、执行环境及作用域

执行环境(execution context,也称“环境”),定义了变量或函数有权访问的其他数据,决定了它们各自的行为。每个执行环境都有一个与之关联的变量对象(variable object),环境中定义的所有变量和函数都保存在这个对象中。(我们编写的代码无法访问这个对象,但是解析器在处理数据时会在后台使用它)。
由于ECMAScript实现所在的宿主环境不同,在web浏览器中,全局执行环境被认为是window对象,因此所有全局变量和函数都是作为window对象的属性和方法创建的。
某个执行环境中的所有代码执行完毕后,该环境被销毁,保存在其中的所有变量和函数定义也随之销毁。
全局执行环境知道应用程序退出(例如关闭网页或浏览器)时才会被销毁。

每个函数都有自己的执行环境。当执行流进入一个函数时,函数的环境就会被推入一个环境栈中。而在函数执行之后,栈将其环境弹出,把控制权返回给之前的执行环境。

当代码在一个环境中执行时,会创建变量对象的一个作用域链(scope chain)。作用域链,是保证对执行环境有权访问的所有变量和函数的有序访问。
作用域链的前端,始终是当前执行的代码所在环境的变量对象。
作用域链的下一个变量对象来自包含环境,再下一个变量对象则来自下一个包含环境。
作用域链的最后一个对象,始终是全局执行环境的变量对象。

标识符解析是沿着作用域链一级一级地搜索标识符的过程。搜索过程始终从作用域链的前端开始,然后逐级地向后回溯,直至找到标识符为止。(如果找不到,通常会导致错误发生)

延长作用域链

执行环境的类型总共只有两种:全局和局部(函数)。

有些语句可以在作用域链的前端临时增加一个变量对象(该变量对象会在代码执行后被移除),从而延长作用域链。在两种情况下可以实现:

  1. try-catch语句的catch块;
  2. with语句

对catch语句来说,会创建一个新的变量对象,其中包含的是被抛出的错误对象的声明。
对with语句来说,会将指定的对象添加到作用域链中。

三、垃圾收集

原理:找出那些不再继续使用的变量,然后释放其占用的内存。
垃圾收集器会按照固定的时间间隔(或代码执行中预定的收集时间),周期性的执行这一操作。

局部变量只在函数执行的过程中存在。

垃圾收集其必须跟踪哪个变量有用哪个变量没用,对于不再有用的变量打上标记,以备将来收回其占用的内存。
用于标识无用变量的策略可能会因实现而异,但具体到浏览器中的实现,则通常有两个策略:标记清除引用计数

标记清除(mark-and-sweep)
当变量进入环境(例如,在函数中声明一个变量)时,就将这个变量标记为“进入环境”。
当变量离开环境时,则将其标记为“离开环境”。

从逻辑上讲,永远不能释放进入环境的变量所占用的内存。

引用计数(reference counting):跟踪记录每个值被引用的次数。JS引擎目前都不再使用这种算法,但是在IE中访问非原生JavaScript对象(如DOM元素)时,这种算法仍然可能会导致问题。
当代码中存在循环引用现象时,“引用计数”算法就会导致问题。
解除变量的引用不仅有助于消除循环引用现象,而且对垃圾收集也有好处。为了确保有效的回收内存,应该及时解除不再使用的全局对象、全局对象属性以及循环引用变量的引用。

事实上,在有的浏览器中可以触发垃圾收集过程,但不建议这样做。
在IE中,调用window.CollectGarbage()方法会立即执行垃圾收集。
在Opera 7及更高版本中,调用window.opera.collect()也会启动垃圾收集例程。

四、闭包

闭包

这是牛客网上一道关于闭包的练习题,最后我们来解决它。

更好地理解闭包的前提:JavaScript的作用域、作用域链以及垃圾回收机制。

简单点,解释的方式简单点

闭包,是指有权访问另一个函数作用域中的变量的函数。

再简单点

闭包,它是个函数!

是的,你没有看错,闭包只是个特殊点的函数。

特殊在哪?

  1. 函数嵌套函数
  2. 函数内部可以引用外部的参数和变量
  3. 参数和变量不会被垃圾回收机制回收

一言不合就举例子:

function a(){
var n = "我是局部变量";
function b(){
return n;
}
return b();   
}
var result = a();
console.log(result); //"我是局部变量"

例子中的b()函数就是闭包。它有权访问父级作用域中变量,但反过来就不行(原因:作用域链),子级作用域中的变量对于父级是不可见的。

在上述例子中,本来全局变量是无法访问到函数a中的局部变量n的,但是由于返回了闭包b函数,以实现了外部环境对内部环境变量的访问。

那关于变量不被回收呢?

一言不合就再来个例子:

function a() {
var n = 1;
return function(){
console.log(n++);
};
}
var result = a();
result();// 1 执行后 a++,,然后a还在~
result();// 2
result();// 3

但也正是由于闭包会使得函数中的变量都被保存在内存中,内存消耗很大,所以不能滥用闭包,否则会造成网页的性能问题,由于IE的js对象和DOM对象使用不同的垃圾收集方法,因此闭包在IE中会导致内存泄露问题,也就是无法销毁驻留在内存中的元素。解决方法是,在退出函数之前,将不使用的局部变量全部删除。

回到开篇图片上的问题,其答案是:

function makeClosures(arr, fn) {
return arr.map(function(n){
return function(){
return fn(n);
}
});
}

五、内存泄漏

内存泄漏

应用程序不再需要占用内存的时候,由于某些原因,内存没有被操作系统或可用内存池回收

造成内存泄漏的主要原因

【1】意外的全局变量

function foo(arg){
bar = "this is a hidden global variable"; //window.bar
}
function foo() {
this.variable = "potential accidental global";
}
// Foo 调用自己,this 指向了全局对象(window)
// 而不是 undefined
foo();

【2】被遗忘的计时器或回调函数

var someResource = getData();
setInterval(function() {
var node = document.getElementById('Node');
if(node) {
// 处理 node 和 someResource
node.innerHTML = JSON.stringify(someResource));
}
}, 1000);

与节点或数据关联的计时器不再需要,node 对象可以删除,整个回调函数也不需要了。可是,计时器回调函数仍然没被回收(计时器停止才会被回收)。同时,someResource 如果存储了大量的数据,也是无法被回收的。

【3】没有清理的DOM元素引用

var elements = {
button: document.getElementById('button'),
image: document.getElementById('image'),
text: document.getElementById('text')
};
function doStuff() {
image.src = 'http://some.url/image';
button.click();
console.log(text.innerHTML);
}
function removeButton() {
document.body.removeChild(document.getElementById('button'));
// 虽然我们用removeChild移除了button,但是还在elements对象里保存着#button的引用
// 换言之, DOM元素还在内存里面.
}

【4】闭包

闭包造成的变量不被垃圾回收机制回收前面已经说过,故不赘述。