《JavaScript高级程序设计》第三版 笔记
第4章 变量、作用域和内存问题
本章内容:
·理解基本类型和引用类型的值
·理解执行环境
·理解垃圾收集
4.1 基本类型和引用类型的值
ECMAScript 变量可能包含两种不同数据类型的值:基本类型值和引用类型值 。
在将一个值赋给变量时,解析器必须确定这个值是基本类型值还是引用类型值。
基本类型值 :
指的是简单的数据段。
基本数据类型(Undefined、Null、Boolean、Number、String)是按值访问的,因为可以操作保存在变量中的实际的值。
引用类型值 :
指那些可能由多个值构成的对象。
引用类型的值是保存在内存中的对象。
与其他语言不同,JavaScript 不允许直接访问内存中的位置,也就是说不能直接操作对象的内存空间;
在操作对象时,实际上是在操作对象的引用而不是实际的对象。
为此,引用类型的值是按引用访问的 。
*在很多语言中,字符串以对象的形式来表示,因此被认为是引用类型的。ECMAScript 放弃了这一传统。
4.1.1 动态的属性
定义基本类型值和引用类型值的方式是类似的:创建一个变量并为该变量赋值;
但是当这个值保存到变量中以后,对不同类型值可以执行的操作则大相径庭。
对于引用类型的值 :
可以为其添加属性和方法,也可以改变和删除其属性和方法。
对象不被销毁或属性不被删除,则属性将一直存在。
example:
var person = new Object();
person.name = "aim";
console.log(person.name); //aim
对于基本类型 :
不能给基本类型的值添加属性(尽管这样做不会导致任何错误)。
example:
var name = "aim";
name.age = 27;
console.log(name.age); //undefined
4.1.2 复制变量值
除了保存的方式不同之外,在从一个变量向另一个变量复制基本类型值和引用类型值时,也存在不同。
复制基本类型值:
复制和被复制的值是完全独立的,可以参与任何操作而不会互相影响。
example:
var num1 = 5;
var num2 = num1;
num2 = 10;
console.log(num1); //5
console.log(num2); //10
复制引用类型值 :
被复制的实际是一个指针,指向存储在堆中的一个对象;
复制与被复制的两个变量将引用同一个对象,因此改变其中一个变量,就会影响另一个变量。
example:
var obj1 = new Object();
var obj2 = obj1;
obj1.name = "alex";
console.log(obj2.name); //alex
4.1.3 传递参数
ECMAScript 中所有函数的参数都是按值传递的 。
即,把函数外部的值复制给函数内部的参数(可以把函数的参数想象成局部变量)。
基本类型值的传递如同基本类型变量的复制一样;
引用类型值的传递,如同引用类型变量的复制一样;
访问变量有按值和按引用两种方式,而参数只能按值传递 。
向参数传递基本类型的值:
被传递的值会被复制给一个局部变量(即命名参数,或者说是 arguments 对象中的一个元素)。
example:
function addTen(num) {
num += 10;
return num;
}
var count = 20;
console.log(addTen(count)); //30,数值20被复制给参数 num ,函数内部变化不会影响函数外部的 count 变量。
var result = addTen(count);
console.log(result); //30
console.log(count); //20,全局变量 count 值不变。
向参数传递引用类型的值:
会把这个值在内存中的地址复制给一个局部变量;
因此这个局部变量的变化会反映在函数的外部(在原始引用地址未改变的情况下)。
example:
function setName(obj) {
obj.name = "aim";
/*对象 person 被当做参数传入后,会把 person 在内存中的地址复制给局部变量 obj ,在函数内部, obj 和 person 引用的是同一个对象,因此在函数内部为 obj 添加属性,函数外部的 person 也将有所反应。因为 person 指向的对象在堆内存中只有一个,而且是全局对象。*/
obj = new Object();
/*为 obj 重新定义对象,当对象 person 被当做参数传入后,它的引用地址将被改变,但函数外部的对象 person 引用地址未变,因此新定义的属性不会反映在外部。实际上,当在函数内部重写 obj 时,这个变量引用的就是一个局部对象了,这个局部对象会在函数执行完毕后立即被销毁。*/
obj.name = "lisa";
console.log(obj.name); //lisa
}
var person = new Object();
setName(person);
console.log(person.name); //aim
/* person.name值为 "aim",这说明对象是按值传递的,如果 person 是按引用传递的,那么 person 就会自动被修改为指向其 name 属性值为"lisa" 的新对象。 */
+将外部变量作为参数传递给函数后,函数内部的变化不会影响外部变量的值。对于引用类型值来说,函数内部的变化不会影响函数外部引用类型值的引用地址。
4.1.4 检测类型
typeof 操作符是检测基本数据类型 的最佳工具。
instanceof 操作符用来检测为对象的某个值是什么类型的对象;
如果变量是给定引用类型(根据它的原型链来识别)的实例,那么 instanceof 操作符就会返回 true 。
*所有引用类型的值都是 Object 的实例,因此检测一个引用类型值和 Object 构造函数时始终会返回 true 。
用于检测基本类型的值会返回 false,因为基本类型不是对象。
result = variable instanceof constructor;
example:
var arr = [0,1];
var obj = {name: "aim", age: 12};
var regE = /a/;
console.log(arr instanceof Array); //true
console.log(arr instanceof Object); //true
console.log(obj instanceof Object); //true
console.log(regE instanceof RegExp); //true
console.log(regE instanceof Object); //true
console.log(num instanceof Object); //false
4.2 执行环境及作用域
执行环境(execution context):
执行环境定义了变量或函数有权访问的其他数据,决定了它们各自的行为。
全局执行环境是最外围的一个执行环境。
根据 ECMAScript 实现所在的宿主环境不同,表示执行环境的对象也不一样。
在 Web 浏览器中,全局执行环境是 window 对象;
因此所有全局变量和函数都是作为 window 对象的属性和方法创建的。
某个执行环境中的所有代码执行完毕后,该环境被销毁,保存在其中的所有变量和函数定义也随之销毁;
全局执行环境直到应用程序退出(例如关闭网页或浏览器)时才会被销毁。
每个函数都有自己的执行环境;
当执行流进入一个函数时,函数的环境就会被推入一个环境栈中;
在函数执行之后,栈将其环境弹出,把控制权返回给之前的执行环境。
ECMAScript 程序中的执行流正是由这个方便的机制控制着。
变量对象(variable object):
每个执行环境都有一个与之关联的变量对象;
环境中定义的所有变量和函数都保存在这个对象中。
(我们编写的代码无法访问这个对象,解析器处理数据时会在后台使用它。)
作用域链(scope chain):
当代码在一个环境中执行时,会创建变量对象的一个作用域链。
作用域链的前端,始终都是当前执行的代码所在环境的变量对象;
如果这个环境是函数,则将其活动对象(activation object)作为变量对象。
活动对象在最开始时只包含一个变量,即 arguments 对象(这个对象在全局环境中是不存在的);
作用域链中的下一个变量对象来自包含(外部)环境,再下一个变量对象来自下一个包含环境;
这样一直延续到全局执行环境;
全局执行环境的变量对象始终都是作用域链中的最后一个对象。
*标识符解析是沿着作用域链一级一级地搜索标识符的过程;
搜索过程始终从作用域链的前端开始,然后逐级地向后回溯,直至找到标识符为止(如果找不到标识符,通常会导致错误发生)。
用途:
保证对执行环境有权访问的所有变量和函数的有序访问。
*内部环境可以通过作用域链访问所有的外部元素,但外部环境不能访问内部环境中的任何变量和函数 。
这些环境之间的联系是线性、有次序的。
每个环境都可以向上搜索作用域链,以查询变量和函数名;
但任何环境都不能通过向下搜索作用域链而进入另一个执行环境。
*函数参数也被当做变量来对待,因此其访问规则与执行环境中的其他变量相同。
example:
var outerNum = 1;
/* middlelayer() 的作用域链中只包含两个对象,它自己的变量对象和全局变量对象,即,它不能访问 innerMost() 的环境。 */
function middleLayer(){
var outerNum = 2;
// console.log(innerNum); *ReferenceError: innerNum is not defined
/* 全局环境和 middleLayer() 的局部环境是 innerMost() 的父执行环境。对innerMost() 而言,其作用域链中包含 3 个对象:innerMost() 的变量对象、middleLayer() 的变量对象和全局变量对象。innerMost() 的局部环境开始时会先在自己的变量对象中搜索变量和函数名,如果搜索不到再搜索上一级作用域链。 */
function innerMost(){
var innerNum = 3;
console.log(innerNum); //3
console.log(outerNum); //2
}
innerMost();
}
middleLayer();
console.log(outerNum); //1
+函数定义之后未调用之前无法访问其中变量。
4.2.1 延长作用域链
虽然执行环境的类型总共只有两种–全局和局部(函数),但还是有其他办法来延长作用域链。
有些语句可以在作用域链的前端临时增加一个变量对象,该变量对象会在代码执行后被移除。
在两种情况下会发生这种现象:
try-catch 语句的 catch 块;
with 语句。
具体来说,就是当执行流进入下列任何一个语句时,作用域链就会得到加长。
这两个语句都会在作用域链的前端添加一个变量对象。
对 with 语句来说,会将指定的对象添加到作用域链中。(with 语句的变量对象中包含了 with 语句接收的对象。)
对 catch 语句来说,会创建一个新的变量对象,其中包含的是被抛出的错误对象的声明。
4.2.2 没有块级作用域
if 语句、for 语句中定义的变量会被添加到当前的执行环境中,即使是在 if 语句执行结束后或 for 循环执行结束后,该变量也依旧会存在于语句外部的执行环境中。
1. 声明变量
使用 var 声明的变量会自动被添加到最接近的环境中。
在函数内部,最接近的环境就是函数的局部环境;
在 with 语句中,最接近的环境是函数环境。
如果初始化变量时没有使用 var 声明,该变量会自动被添加到全局环境 。
2. 查询标识符
当在某个环境中为了读取或写入而引用一个标识符时,必须通过搜索来确定该标识符实际代表什么。
搜索过程从作用域链的前端开始,向上逐级查询与给定名字匹配的标识符。
如果在局部环境中找到了该标识符,搜索过程停止,变量就绪。(如果局部变量中存在着同名标识符,就不会使用位于父环境中的标识符。)
如果在局部环境中没有找到该变量名,则继续沿作用域链向上搜索。
搜索过程将一直追溯到全局环境的变量对象。
如果在全局环境中也没有找到这个标识符,则意味着该变量尚未声明。
example:
var color = "red";
function changeColor() {
var color = "blue";
console.log(color); //blue
console.log(window.color); //red,使用 window.color 访问全局变量 color。
}
changeColor();
*如果有一个操作数是对象,而另一个不是,就会在对象上调用 valueOf() 方法以取得基本类型的值,以便根据前面的规则进行比较。
*访问局部变量要比访问全局变量更快,因为不用向上搜索作用域链。
4.3 垃圾收集
JavaScript 具有自动垃圾收集机制,即执行环境会负责管理代码执行过程中使用的内存。
原理:
找出那些不再继续使用的变量,然后释放其占用的内存。
为此,垃圾收集器会按照固定的时间间隔(或代码执行中预定的收集时间),周期性的执行这一操作。
函数中局部变量的正常生命周期:
局部变量只在函数执行的过程中存在;
在这个过程中,会为局部变量在栈(或堆)内存上分配相应的空间,以便存储它们的值;
然后在函数中使用这些变量,直至函数执行结束;
此时,局部变量就没有存在的必要了,因此可以释放它们的内存以供将来使用;
在这种情况下,很容易判断变量是否还有存在的必要;
但并非所有情况下都这么容易就能得出结论;
垃圾收集器必须跟踪哪个变量有用哪个变量没用,对于不再有用的变量打上标记,以备将来收回其占用的内存。
用于标识无用变量的策略可能会因实现而异,但具体到浏览器中的实现 ,则通常有两个策略。
4.3.1 标记清除
JavaScript 中最常用的垃圾收集方式是标记清除(mark-and-sweep)。
当变量进入环境(例如,在函数中声明一个变量)时,就将这个变量标记为“进入环境” 。
从逻辑上讲,永远不能释放进入环境的变量所占用的内存,因为只要执行流进入相应环境,就可能会用到它们。而当变量离开环境时,则将其标记为“离开环境” 。
*可以使用任何方式来标记变量。比如,可以通过翻转某个特殊的位来记录一个变量何时进入环境,或者使用一个“进入环境的”变量列表及一个“离开环境的”变量列表来跟踪哪个变量发生了变化。说到底,如何标记变量其实并不重要,关键在于采取什么策略。
垃圾收集器在运行的时候会给存储在内存中的所有变量都加上标记(当然,可以使用任何标记方式)。然后,它会去掉环境中的变量以及被环境中的变量引用的变量的标记。而在此之后再被加上标记的变量将被视为准备删除的变量,原因是环境中的变量已经无法访问到这些变量了。最后,垃圾收集器完成内存清除工作,销毁那些带标记的值并回收它们所占用的内存空间。
*到2008年为止,IE、 Firefox、 Opera、 Chrome、 Safari 的JavaScript 实现使用的都是标记清除式的垃圾收集策略(或类似的策略),只不过垃圾收集的时间间隔互有不同。
4.3.2 引用计数
另一种不太常见的垃圾收集策略叫做引用计数(reference counting)。
引用计数的含义是跟踪记录每个值被引用的次数;
当声明了一个变量并将一个引用类型值赋给该变量时,这个值的引用次数就是 1 ;
如果同一个值又被赋给另一个变量,则该值的引用次数加 1 ;
相反,如果包含对这个值引用的变量又取得了另一个值,则这个值的引用次数减 1 ;
当这个值的引用次数变成 0 时,则说明没有办法再访问这个值了,因而就可以将其占用的内存空间回收回来;
这样,当垃圾收集器下次再运行时,它就会释放那些引用次数为 0 的值所占用的内存空间。
问题:
1.循环引用:
example:
function problem() {
var objectA = new Object();
var objectB = new Object();
objectA.someOtherObject = objectB;
objectB.anotherObject = objectA;
}
这两个对象通过各自的属性相互引用,引用次数都是 2 ;
在采用标记清除策略的实现中,由于函数执行之后,这两个对象都离开了作用域,因此这种相互引用不是个问题;
但在采用引用计数策略的实现中,当函数执行完之后,objectA 和 objectB 还将继续存在,因为它们的引用次数永远不会是 0 ;
假如这个函数被重复多次调用,就会导致大量内存得不到回收。
2.IE9 之前(不包括 IE9),在IE 浏览器中,其 BOM 和 DOM 中的对象是使用 C++ 以 COM(Component Object Model,组件对象模型)对象的形式实现的,COM 对象采用引用计数策略,因此,只要在 IE 中涉及 COM 对象,就会存在循环引用的问题。
解决方法:在不使用这些变量时,将它们的值设置为 null。
*IE9 把 BOM 和 DOM 对象转换成了真正的 JavaScript 对象,这样就避免了两种垃圾收集算法并存导致的问题,也消除了常见的内存泄漏现象。
4.3.3 性能问题
在 IE 中,调用 window.CollectGarbage() 方法会立即执行垃圾收集。
在 Opera 7 及更高版本中,调用 window.opera.collect() 也会启动垃圾收集例程。
4.3.4 管理内存
为防止运行 JavaScript 的网页耗尽全部系统内存而导致系统崩溃,分配给 Web 浏览器的可用内存数量通常要比分配给桌面应用程序的少。
内存限制问题不仅会影响给变量分配内存,同时还会影响调用栈以及在一个线程中能够同时执行的语句数量。
因此,确保占用最少的内存可以让页面获得更好的性能。而优化内存占用的最佳方式,就是为执行中的代码只保存必要的数据。一旦数据不再有用,最好通过将其值设置为 null 来释放其引用–这个做法叫做解除引用(dereferencing)。这一做法适用于大多数全局变量和全局对象的属性。局部变量会在它们离开执行环境时自动被解除引用。解除一个值的引用并不意味着自动回收该值所占用的内存。解除引用的真正作用是让值脱离执行环境,以便垃圾收集器下次运行时将其回收。
example:
function createPerson(name) {
var localPerson = new Object();
localPerson = name;
return localPerson;
}
var globalPerson = createPerson("aim");
//手工解除 globalPerson 的引用
globalPerson = null;
4.4 小结
基本类型值在内存中占据固定大小的空间,因此被保存在栈内存中;
引用类型的值是对象,保存在堆内存中。
确定一个值是哪种基本类型可以使用 typeof 操作符;
确定一个值是哪种引用类型可以使用 instanceof 操作符。
所有变量(包括基本类型和引用类型)都存在于一个执行环境(也称为作用域)当中,这个执行环境决定了变量的生命周期,以及哪一部分代码可以访问其中的变量。
每次进入一个新执行环境,都会创建一个用于搜索变量和函数的作用域链。