笔记 - JavaScript 高级程序设计【第 04 章:变量、作用域和内存问题】
第04章:变量、作用域和内存问题
JavaScript 的变量只是在特定时间用于保存特定值的一个名字而已,不限制变量保存的数据类型,变量的值及其数据类型可以在脚本的生命周期内改变;这就是通常所说的有趣、强大但又容易出问题的“灵活性”。
4.1. 基本类型和引用类型的值
ECMAScript 变量可能包含两种不同数据类型的值:基本类型值和引用类型值。基本类型值指的是简单的数据段;引用类型值指的是那些可能由多个值构成的对象。
变量被赋值时,解析器必须确定要赋的值是基本类型值还是引用类型值。扩展:这也是因为需要开辟不同的内存空间,因为基本类型值保存在内存的栈区,而引用类型值保存在内存的堆区(堆比栈大,栈比堆快)。
五种基本数据类型是按值访问的,因为可以操作保存在变量中的实际值。
引用类型值是保存在内存中的对象。与其他语言不同,JavaScript 不允许直接复制保存在内存中的对象,在复制保存着对象的某个变量时,实际上复制的是对象的引用(内存地址)而非对象本身。
4.1.1. 动态的属性
引用类型的值可以为其进行属性和方法的添加、删除和改变,而基本类型的值是不能添加属性的,更谈不到属性的删除和改变了。
4.1.2. 复制变量值
变量之间值的复制,如果是基本类型的值,会在变量对象上创建一个新值,然后把这个值复制到为新变量分配的位置上;原值与新值之间是完全独立的,新值只是原值的一个副本,两个变量可以参与任何操作而不会互相影响。
当变量之间复制的是引用类型的值时,与基本类型值的复制过程是一样的,而不同的是变量中存储的是指向存储在堆中的一个对象的指针;完成复制后,两个变量拥有指向同一对象的指针,所以,改变其中一个变量,就会影响另一个变量。例如:
var obj1 = new Object(); var obj2 = obj1; obj1.name = 'hawk'; alert(obj2.name); //两个变量里值是指针,它们都指向同一个对象,所以结果必然也是hawk;
4.1.3. 传递参数
ECMAScript 中所有函数的参数都是按值传递的。书中所说的“困惑”,个人理解是:由于引用类型值保存的是指向对象的“指针”,而在函数内部修改参数引用函数外对象的属性会反应到这个对象上,会让人误以为参数也会按引用传递。
参数传递与变量复制,两者的机制是相同的;值的副本复制给了参数,而参数可以看做是函数内部的局部变量。
对象的传参比较特殊,例如:
var person = new Object(); person.name = 'Lucy'; function setName(obj) { obj.name = 'Bob'; //在函数内部将传入对象的name属性赋值为Bob; } setName(person); alert(person.name); //Bob,函数外部的对象name属性发生了变化; function setAge(obj) { obj.age = 19; //在函数内部为传入对象添加并设置age属性; obj = new Object(); //这时修改了参数的值,也就是修改了参数存储的指针; obj.age = 99; //再次更改age属性; } setAge(person); alert(person.age); //19,函数内部修改了参数存储的指针后并未影响外部对象;
上面的例子中,函数内部重写了 obj,这个变量的应用已经是一个局部对象了,这个局部对象会在函数执行完毕后立即被销毁。
4.1.4. 检测类型
检测一个变量是否为基本类型,可以使用 typeof 操作符;但是当要检测的变量是 null 或者是一个对象时,typeof 无法做出区分,都会返回 "object"。
由于 typeof 操作符无法准确检测引用类型的值,所以用处不大,如果要检测某个值是什么类型的对象,需要使用 instanceof 操作符。
如果变量是给定引用类型(根据原型链识别)的实例,那么 instanceof 操作符便返回 turn;如果变量是基本数据类型,会直接返回 false,因为基本类型不是对象。
所有引用类型的值都是 Object 类型的实例,所以检测一个引用类型值和 Object 构造函数时,instanceof 操作符会始终返回 turn。
4.2. 执行环境及作用域
执行环境(execution context)是 JavsScript 中最为重要的一个概念。个人理解,“执行环境”就是一般所说的“作用域”。执行环境定义了变量或函数有权访问的其他数据,决定了它们各自的行为。
每个执行环境都有一个与之关联的变量对象(variable object),环境中定义的所有变量和函数都保存在这个对象中;个人理解,函数是一个执行环境,那么函数内部的变量(包括参数)或函数就相当于它的属性和方法。
全局执行环境是最外围的一个执行环境。根据 ECMAScript 实现所在的宿主环境不同,表示执行环境的对象也不一样,在 Web 浏览器中,全局执行环境是 window 对象,所有的全局变量和函数都是 window 对象的属性和方法。
某个执行环境中的所有代码执行完毕后,该环境被销毁,里面保存的所有变量和函数也随之销毁;全局执行环境直到网页关闭才会销毁。
当代码在一个环境中执行时,会创建变量对象的一个作用域链(scope chain),它用来保证对执行环境有权访问的所有变量和函数的有序访问。说白了就像地图一样,按图索骥。
作用域链的前端,始终都是当前执行代码所在环境的变量对象;这里的“前端”,可以理解为“起始点”。如果这个环境是函数,则将其活动对象(activation object)作为变量对象。
全局执行环境的变量对象始终都是作用域链中的最后一个对象。个人理解,例如函数中的变量访问,先从函数内部找,如果内部没有,则向上一层的环境中找,一直追溯到全局执行环境,直至找到,如果最终都没有找到,那么就会出现编译错误。
前面所说的变量追溯就是标识符解析的一种,搜索过程始终从作用域链的前端开始。
变量和函数访问只能通过作用域链在执行环境内和向上级(父执行环境)访问,而不能访问下一级中的变量和函数,如果要访问下一级中的变量和函数,可以使用闭包。
4.2.1. 延长作用域链
执行环境的类型有全局和局部(函数)两种,但有其他办法来延长作用域。作用域链的延长有两种情况,try-catch 语句的 catch 块,以及 with 语句。
这两个语句都会在作用域的前端添加一个变量对象,例如 with 语句,会将括号中制定的对象添加到作用域链中;而 catch 语句会创建一个新的变量对象,其中包含的是被抛出的错误对象的声明。
4.2.2. 没有块级作用域
在 JavaScript 中,没有块级作用域;就是说,不像其他类 C 的语言中,使用花括号封闭的代码块就有自己的作用域(执行环境),从而根据条件来定义变量。
没有块级作用域,使得在 if、for 等语句中的变量,在语句执行完成后,仍然可以在语句外部访问到这些变量。
1. 声明变量
使用 var 声明的变量会被自动添加到最近的环境中,如果没有使用 var 声明,该变量会被自动添加到全局环境中。
实际开发中,初始化变量之前一定要先声明;在严格模式下,初始化未经声明的变量会导致错误。
2. 查询标识符
标识符的查询过程从作用域链的前端开始,向上逐级查找。找到后,查找结束,变量就绪;没有找到,便一直向上查找到全局环境,如果到全局环境还没找到,说明该变量尚未声明。
4.3. 垃圾收集
JavaScript 具有自动垃圾回收机制,执行环境会自动找出不再继续使用的变量,然后释放其占用的内存;为此,垃圾收集器会按照固定时间间隔(或代码预定的间隔)周期性释放内存。
变量或函数的生命周期,指的是变量或函数从声明到其在执行环境中使用完毕后被垃圾回收销毁的过程。
4.3.1. 标记清除
JavaScript 中常用的垃圾收集方式是标记清除(mark-and-sweep)。当变量进入环境时,就将该变量标记为“进入环境”;当变量离开环境时,则将其标记为“离开环境”。垃圾收集器按照标记进行垃圾收集策略。
4.3.2. 引用计数
另一种不太常见的策略叫引用计数(reference counting)。它的含义是追踪记录每个值被引用的次数。
当声明了一个变量并将一个引用类型的值赋给该变量时,则该值引用次数为 1;如果同一值又被赋给另一变量,则该值引用次数加 1;如果包含对该值引用的变量又取得了另一个值,则该值引用次数减 1。当次数为 0 时,则会被回收。
循环引用导致引用次数的策略出现了问题。循环引用指的是对象 A 中包含一个指向对象 B 的指针,而对象 B 中也包含一个指向对象 A 的指针;也就是两者在相互引用。这导致它们的应用次数永远不为 0。
为了避免循环引用造成的内存泄露,最好实在不使用这些对象的时候手工断开,比如给不使用的对象会变量赋值 null。
4.3.3. 性能问题
垃圾收集器的时间间隔策略的不合理,会导致性能问题,例如 IE6 浏览器的垃圾收集器。
在有的浏览器中可以出发垃圾收集过程,例如 IE 中调用 window.CollectGarbage() 方法,在 Opera7 及更高版本中调用 window.opera.collect() 方法。但在实际开发中不建议手动出发垃圾收集过程。
4.3.4. 管理内存
虽然具备垃圾收集机制的 JavaScript 一般不必操心内存管理的问题,但是分配给 Web 浏览器的内存有限。
为了用最少的内存获得更好的性能,在开发中,只为执行中的代码保存必要数据,一旦数据不在有用,最好将其设置为 null 来释放其引用,这就叫做解除引用(dereferencing)。
解除引用不会立即释放值占用的内存,但是会在垃圾收集器下一轮的运行时将其回收。
4.4. 小结
● JavaScript 变量可以用来保存两种类型的值:基本类型和引用类型。基本类型的值源自 5 种基本数据类型。基本类型值和引用类型值具有以下特点:
◎ 基本类型值在内存中占据固定大小的空间,因此被保存在栈内存中;
◎ 从一个变量向另一个变量复制基本类型的值,会创建这个值的一个副本;
◎ 引用类型的值是对象,保存在堆内存中;
◎ 包含引用类型值的变量实际上包含的并不是对象本身,而是一个指向该对象的指针;
◎ 从一个变量向另一个变量复制引用类型的值,复制的是指针,因此两个变量最终指向同一个对象;
◎ 确定一个值是哪种基本类型可以使用 typeof 操作符,而确定一个值是哪种引用类型可以使用 instanceof 操作符。
● 所有变量都存在于一个执行环境中,这个环境决定了变量的生命周期,以及哪部分代码可以访问其中的变量。关于执行环境的总结如下:
◎ 执行环境分为全局执行环境和函数执行环境;
◎ 每次进入一个新执行环境,都会创建一个用于搜索变量和函数的作用域链;
◎ 函数的局部环境不仅有权访问函数作用域中的变量,还有权访问其包含(父)环境,乃至全局环境;
◎ 全局环境只能访问在全局环境中定义的变量和函数,而不能直接访问局部环境中的任何数据(除了没有使用 var 声明的变量);
◎ 变量的执行环境有助于确定应该何时释放内存。
● JavaScript 是一门具有自动垃圾回收机制的语言,开发中不必关心内存的分配和回收问题,关于垃圾回收的总结如下:
◎ 离开作用域的值将被自动标记为可以回收,因此将在垃圾收集期间被删除;
◎ “标记清除”是目前主流的垃圾收集算法,这种算法的思想是给当前不使用的值加上标记,然后再回收其内存;
◎ 另一种垃圾手机算法叫“引用计数”,它的思想是跟踪记录所有值被引用的次数。目前已不使用,但是 IE 中访问非原生 JavaScript 对象(如 DOM 元素)时,这种算法仍然会导致问题;
◎ 当代码中存在循环引用时,“引用计数”算法就会导致问题;
◎ 解除变量的引用不仅有助于消除循环引用,还能有效回收内存,应该及时解除不在使用的全局对象、全局对象属性以及循环引用变量的引用。
(完)

浙公网安备 33010602011771号