您的位置:首页 > Web前端 > JavaScript

JavaScript变量、作用域及内存问题

2014-06-04 22:43 169 查看
一、变量

1、ECMAScript变量包含两种不同数据类型的值:基本类型值和引用类型值。其中基本数据类型包含五种:Undefined、Null、Boolean、Number和String,而且它们都是按值访问的。而引用类型的值是保存在内存中的对象。JavaScript不允许直接访问内存中的位置,也就是说不能直接操作对象的内存空间。所以在操作对象时,实际上是在操作对象的引用而不是实际的对象,所以,引用类型的值是按引用访问的。

2、从一个变量向另一个变量复制基本类型的值时,会在变量对象上创建一个新值,然后把该值复制到为新变量分配的位置上。当从一个变量向另一个变量复制引用类型的值时,同样也会将存储在变量对象中的值复制一份放到为新变量分配的空间中,但是这个值的副本其实是一个指针,该指针指向存储在堆中的一个对象,复制操作结束后,两个变量实际上将引用同一个对象,如下图:



3、ECMAScript中所有函数的参数都是按值传递的。基本类型值的传递如同基本类型变量的复制一样;引用类型值的传递,如同引用类型变量的复制一样。要注意的是,虽然访问变量有按值和按引用两种方式,但是参数只能按值传递。在向参数传递基本类型的值时,被传递的值会被复制给一个局部变量(即命名参数,用ECMAScript的概念来说,就是arguments对象中的一个元素);在向参数传递引用类型的值时,会把这个值在内存中的地址复制给一个局部变量,所以该局部变量的变化会反映到函数的外部。如下代码所示,证明当传递引用类型的值时,不是按引用传递,而也是按值传递的:

function setName(person) {
person.name = "zhangsan";

person = new Object();
person.name = "lisi";
}

var person = new Object();
setName(person);
person.name;  // zhangsan
如果person是按引用传递的,那么person就会自动被修改为指向其name属性为"lisi"的新对象,但是这里访问person.name仍然是"zhangsan",说明在函数内部即使修改了参数的值,但原型的引用还是保持没变。

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

二、执行环境及作用域

1、执行环境定义了变量或函数有权访问的其它数据,决定了它们各自的行为;每个执行环境都有一个与之关联的变量对象,环境中定义的所有变量和函数都保存在该对象中,而且在代码中是无法访问该对象的。全局执行环境是最外围的一个执行环境,根据ECMAScript实现所在的宿主环境不同,表示执行环境的对象也不一样。在Web浏览器中,全局执行环境默认是window对象,因此所有的全局变量和函数都是作为window对象的属性和方法创建的。某个执行环境中的所有代码执行完毕后,该环境被销毁,保存在其中的所有变量和函数定义也都随之被销毁(全局执行环境直到应用程序退出时才会被销毁)。

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

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

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

5、如下代码所示,函数changeColor()的作用域链包含两个对象:它自己的变量对象(其中定义着arguments对象)和全局环境的变量对象,可以在函数内部访问变量color,就是因为可以在这个作用域链中找到它。

var color = "blue";

function changeColor() {
if ("blue" == color) {
color = "red";
} else {
color = "blue";
}
}

changeColor();
6、如下代码所示:

var color = "blue";

function changeColor() {
var anotherColor = "red";

function swapColors() {
var tempColor = anotherColor;
anotherColor = color;
color = tempColor;
// 该函数中可以访问color、anotherColor、tempColor
}

// 该函数中可以访问color、anotherColor,不能访问tempColor
swapColors();
}

// 这里只能访问color
changeColor();
以上代码共涉及了三个执行环境:全局环境、changeColor()函数的局部环境和swapColors()函数的局部环境。其中全局环境中有一个变量color和一个函数changeColor();changeColor()函数的局部环境中有一个名为anotherColor的变量和一个swapColors()函数,但它也可以访问全局环境中的变量color;swapColors()函数的局部环境中有一个变量tempColor,且该变量只能在该环境中访问到,无论是全局环境还是changeColor()函数的局部环境都无权访问tempColor变量,但是在swapColors()函数的局部环境内部却可以访问其它两个环境中的所有变量,因为是它的父执行环境。如下图所示作用域链:其中的矩形表示特定的执行环境,内部环境可以通过作用域链访问所有的外部环境,但是外部环境不能访问内部环境中的任何变量和函数。这些环境之间的联系是线性、有次序的,每个环境都可以向上搜索作用域链来查询变量和函数名;但是任何环境都不能通过向下搜索作用域链而进入另一个执行环境。所以这里的swapColors()函数的作用域链就包含三个对象:swapColors()的变量对象、changeColor()的变量对象和全局变量对象;swapColors()的局部环境开始时会先在自己的变量对象中搜索变量和函数名,如果搜索不到则再搜索上一级作用域链。而对于changeColor()函数而言,其作用域链就只包含两个对象:它自己的变量对象和全局变量对象,所以它不能访问swapColors()的环境。



7、函数参数也被当作变量来对待,所以其访问规则与执行环境中的其他变量一样。

8、延长作用域链

虽然执行环境的类型只有两种:全局和局部(函数),但是可以延长其作用域链,因为有些语句可以在作用域链的前端临时增加一个变量对象,该变量对象会在代码执行后被移除。以下两种语句可以发生这种情况,也就是说当执行流进入以下任何一个语句时,作用域链就会得到加长:

try-catch语句的catch块
with语句

它们都会在作用域链的前端添加一个变量对象,对with语句来说,会将指定的对象添加到作用域链中;对catch语句来说,会创建一个新的变量对象,其中包含的是被抛出的错误对象的声明。如下:
function buildUrl() {
var qs = "?debug=true";

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

return url;
}

这里的with语句接收的是location对象,所以其变量对象中就包含了location对象的所有属性和方法,而这个变量对象被添加到了作用域链的前端。当在with语句中引用变量href时(实际引用的是location.href),所以可以在当前执行环境的变量对象中找到,而变量qs位于函数环境的变量对象中。至于with语句内部定义的url变量,由于JavaScript中没有块级作用域,所以url变量就成为了函数执行环境的一部分,所以可以作为函数的返回值返回。
在IE8以及之前版本的实现中,存在一个与标准不一致的地方,那就是在catch语句中捕获的错误对象会被添加到执行环境的变量对象中,而不是catch语句的变量对象中,也就是说即使在catch块的外部也可以访问到错误对象。
9、没有块级作用域
JavaScript中是没有块级作用域的,如下代码所示:在if语句中定义了变量color,在JavaScript中这里的if语句中的变量声明会将变量添加到当前的执行环境,在这里即是全局执行环境。同样的情况,也适用于for语句等语句块中定义的变量。
if (true) {
var color = "red";
}

alert(color);  // red
10、变量的声明
使用var声明的变量会自动被添加到最接近的环境中,在函数内部,最接近的环境就是函数的局部环境;在with语句中,最接近的环境是函数环境;如果初始化变量时没有使用var声明,则该变量会自动被添加到全局环境,但是在严格模式下,初始化未经声明的变量会导致错误。
11、查询标识符
当使用到某个标识符时,需要通过搜索来确定该标识符实际代表的内容,搜索过程从作用域链的前端开始,向上逐级查询与给定名字匹配的标识符,如果在局部环境中找到了该标识符,则搜索停止,变量就绪;否则,继续沿作用域链向上搜索直到全局环境的变量对象,如果在全局环境也没有找到,则表示该变量未声明。如下:搜索过程从getColor()函数的局部环境的变量对象到全局环境的变量对象,如果在getColor()函数中声明了一个color变量,则返回的是这个局部的color变量,如果不使用window.color的方式就不会访问到全局的那个color变量;此外,搜索过程中如果有一个操作数是对象,而另一个不是,则会在对象上调用valueOf()方法来取得基本类型的值再进行比较。



从搜索过程来看,访问局部变量比访问全局变量更快,因为不用向上搜索作用域链。
三、垃圾收集
1、JavaScript具备自动垃圾收集机制,执行环境会负责管理代码执行过程中使用的内存。
2、各个浏览器实现垃圾收集时通常采用的是标记清除式的垃圾收集策略。在IE中,有一部分对象并不是原生JavaScript对象,比如BOM和DOM中的对象就是使用C++以COM对象的形式实现的,而COM对象的垃圾收集机制采用的是引用计数策略,引用计数策略存在的一个严重问题就是循环引用的问题,所以即使IE的JavaScript引擎是使用标记清除策略来实现的,但JavaScript访问的COM对象依然是采用引用计数策略的,所以依然存在循环引用的问题,如下:这里的DOM元素element所引用的对象和JavaScript原生对象obj所引用的对象就存在循环引用的问题,所以即使将这个DOM元素从页面中移除,也永远不会被回收。所以应该在不再使用时手动断开它们之间的引用关系。在IE9中,把DOM和BOM对象都转换成了真正的JavaScript对象。
var element = document.getElementById("id");
var obj = new Object();

obj.element = element;
element.obj = obj;


参考书籍:《JavaScript高级程序设计》(第三版)
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: