《JavaScript高级程序设计》第四章,变量、作用域和内存问题。
基本类型和引用类型的值
ECMAScript变量可能包含两种不同数据类型的值:基本类型值(简单的数据段)、引用类型值(那些可能由多个值构成的对象)。基本数据类型按值访问,因为可以操作保存在变量中的实际的值;引用类型值按引用访问,因为Javascript不允许直接访问内存中的位置,即不能直接操作对象的内存空间,因此在操作对象时,实际上是在操作对象的引用而非实际的对象。基本类型值由于固定大小而被保存于栈内存中;引用类型保存于堆内存中。
动态的属性
引用类型的值可以动态地添加、改变、删除属性和方法,基本类型的值则不行。
1 | // 可 |
复制变量值
一个变量向另一个变量复制基本类型的值时,复制的内容是值(二者互不干扰);复制引用类型的值时,复制的是指针(二者指向同一对象)。
传递参数
ECMAScript中所有参数都是按值传递的,因此都和上述变量值的复制一样。在传递对象时同样是传递值(内容为地址),也就是指向同一对象。但有一些表现会开发人员容易错误地认为:在局部作用域中修改地对象会在全局作用域反映出来,就说明参数是按引用传递的。但这是不对的。
1 | // 局部修改,全局反映 |
如果person是按引用传递的,那么person就会自动被修改为指向name属性值为”grey”的新对象,person.name就会是”grey”。
检测类型
对于基本数据类型的检测,可以用之前讲过的typeof
操作符;对于引用类型的检测,typeof
则用处不大,ECMAScript也因此提供了instanceof
操作符。
1 | // 用法 |
一般有以下规定:
- 基本类型 instanceof Object => false
- 引用类型 instanceof Object => true
* 补充typeof
以前typeof用于函数和正则表达式时会返回”function”,后规定任何在内部实现[[call]]
方法的对象都应在使用typeof操作符时返回”function”。
执行环境及作用域
执行环境
执行环境(execution context)定义了变量或函数有权访问的其他数据,决定了它们各自的行为。每个执行环境都与一个变量对象(variable object)关联,环境里所有的变量和函数都存在这个对象中。我们无法在代码里访问这个对象,但解析器处理数据的时候会在后台用它。
全局
最外围的执行环境是全局执行环境。不同宿主环境下的执行环境的对象不太一样,在Web浏览器中全局执行环境是window对象,因此所有全局变量和函数都是作为window对象的属性和方法创建的。
局部/函数
每个函数都有自己的执行环境,其活动对象(activation object)被作为变量对象。活动对象在最开始时只包含一个变量——arguments对象(全局环境无arguments对象)。执行流进出函数会导致函数的环境被压入和弹出环境栈。
作用域链
代码在一个环境中执行时,会创建变量对象的一个作用域链(scope chain),也就是一个变量对象串。它是从内往外一层层的,最内层的也即当前环境下的变量对象为作用域的前端,最外层的也即全局环境下的变量对象为作用域链的最后一个对象。标识符的解析就是沿着作用域链来的。内部环境可通过作用域链访问所有外部环境,反之不行。
1 | var a = "blue"; |
上例有3个执行环境,每个环境有各自的变量和函数,能访问不同的内容:
- 全局环境
- 有变量a、函数func1()
- func1()的局部环境
- 有变量b、函数func2(),还可访问上层的变量a
- func2()的局部环境
- 有变量c,还可访问其他两个环境的所有变量
延长作用域链
两种情况下可以在作用域链的前端增加一个变量对象:
- try-catch语句的catch块
- with语句
catch语句创建新的变量对象,其中包含被抛出的错误对象的声明;with语句将指定的对象添加到作用域链中。新添加的变量对象会在代码执行后被移除。
1 | // 作用域链延长 |
with语句接收的是location对象,location对象的所有属性和方法都被添加到了作用域链的前端。with语句中引用的变量href(即location.href)可在当前执行环境的变量对象中找到,with语句中引用的变量qs可在buildUrl()函数的执行环境的变量对象中找到,二者得到的url变量也成了函数执行环境的一部分,所以可以作为函数的值被返回。
我的疑问点在于:为什么with的外面可以访问到with内的url?
目前搜索得到的解释:
- with虽然有一个变量对象,但它并没有自己的执行环境,只是提供变量寻址而没有产生独立的作用域。参考:JavaScript延长作用域的问题。
- 使用var声明的变量会自动被添加到最近的环境中。在函数内部,最接近的环境就是函数的局部环境;在with语句中,最接近的环境是函数环境。参考:with语句延长作用域链时,在with中定义变量为何可以在外边访问到?。
1 | // 新添加的变量对象在执行后被移除 |
过程:
- x=10,y=10
- 对象{x: 20}被添加到作用域链的前端
- with语句内的var行并没有创造任何东西,因为所有的变量在进入执行环境时已经被解析和添加
- “x”的值被修改了,正在处理的这个”x”正是第二步添加到作用域链前端的对象,这个”x”的值由20变为30
- 上述变量对象中的”y”的值也被修改了,”y”的值由10变为30
- 当with语句结束时,之前添加到作用域链前端的对象被移除(更改后的值”x”——30也被移除),作用域链又回到了with语句之前的状态
- 至此,目前变量对象中”x”的值保持不变,”y”的值在with语句内的修改下变为30
参考:ECMA-262-3 in detail. Chapter 4. Scope chain.
没有块级作用域
JavaScript没有块级作用域,容易导致理解上的困惑。
1 | if(true) { |
声明变量
使用var声明的变量会自动被添加到最近的环境中。在函数内部,最接近的环境就是函数的局部环境;在with语句中,最接近的环境是函数环境。如果初始化变量时没有用var声明,该变量会自动被添加到全局环境。
1 | function add(num1, num2) { |
注意这个和上面的区别,不是所有花括号都是一个性质的。
1 | function add(num1, num2) { |
查询标识符
标识符的查询是从作用域前端开始逐级向上直到全局环境的,当然如果在局部环境中找到了该标识符,搜索便可以停止,变量就绪;如果到全局环境也未找到,说明该变量尚未声明。
垃圾收集
JavaScript具有自动垃圾收集机制,其原理也很简单:找出那些不再继续使用的变量,然后释放其占用的内存。为此,垃圾收集器会按照固定的时间间隔周期性地执行这一操作。
对于函数中的局部变量,它只在函数执行的过程中存在,执行结束就可以释放内存以供将来使用。而有些情况不太容易判断变量是否还需要,垃圾收集器就必须进行跟踪,将无用的变量打上标记来进行标识。针对标识的策略通常有下面两种。
标记清除
JavaScript中最常用的垃圾收集方式是标记清除(mark-and-sweep)。变量进入环境时被标记为”进入环境”,变量离开环境时被标记为”离开环境”。
可以使用任何方式标记变量。如使用一个特殊的位进行标记,或使用”进入环境的”列表和”离开环境的”列表来进行变量跟踪等。
垃圾收集器运行过程如下:
- 给存储在内存的所有变量打上标记
- 给环境中的变量以及被环境中的变量引用的变量去掉标记
- 被加上标记的变量被视为准备删除的变量,因为环境中已经无法访问它们了
- 销毁带标记的值并回收其内存空间,完成内存清除工作
引用计数
引用计数(reference counting)跟踪记录每个值被引用的次数。规则如下:
- 声明一个变量并赋予一个引用类型值,该值引用次数为1
- 同一值又被赋给另一变量,该值引用次数加1
- 包含该值引用的变量取了另外一个值,该值引用次数减1
- 如果引用次数为0,意味着没有办法再访问到该值,它会在垃圾收集器运行时被释放
Netscape Navigator 3.0浏览器在最早使用引用计数时发现,引用计数策略存在一个严重的问题——循环引用:对象A中包含一个指向对象B的指针,而对象B中也包含一个指向对象A的引用。
1 | function problem() { |
上述例子如果使用标记清除,会在函数执行结束后被清除,但如果使用引用计数,由于两个对象的引用计数都为2并且永远不会为0,因此即使以后不会再使用,这两个对象也无法被清除。而且如果这个函数被调用多次,就会导致大量的内存得不到回收。而后Netscape Navigator 4.0就放弃了引用计数方式,但这并不能完全回避开问题。
IE中有部分对象并非JavaScript原生对象,如使用C++以COM(Component Object Model)对象的形式实现的BOM和DOM,而COM对象的垃圾回收机制采用了引用计数策略。因此即使IE的JavaScript引擎使用标记清除,JavaScript访问到COM对象也依然是基于引用计数的,也依然存在循环引用问题。
1 | var element = document.getElementById("id"); |
上述例子在DOM元素(element)和JavaScript原生对象(object)中创建了循环引用。这种情况下即使将例子中的DOM从页面中移除,它也永远不会被回收。
解决策略:
- 在不使用它们时手动断开二者连接
1
2object.someElement = null;
element.someObject = null; - IE9将BOM和DOM对象转换为了真正的JavaScript对象
性能问题
垃圾收集器周期性运行,回收工作量有时候也很大,因此确定垃圾收集的时间间隔很关键。举一个不太聪明的例子,IE曾经按照内存分配量来运行垃圾收集器,具体来说为256*变量、4096*对象(或数组)字面量和数组元素、64KB字符串,达到上述任一临界值,垃圾收集就会运行。之所以说不太聪明,是因为如果一个脚本真的有那么多变量,它很可能整个生命周期都一直要有那么多变量,这样垃圾收集器就会频繁运行,导致性能问题。针对这个问题,IE7将其优化为临界值动态调整:临界值初始与IE6一样,当回收率低于15%就将临界值加倍;当回收率到达85%,将临界值设为默认值。
管理内存
why?
通常来说,当使用具有垃圾收集机制的语言,开发者无需担心内存管理问题。但JavaScript为何需要呢?因为分配给Web浏览器的可用内存通常比分配给桌面应用程序的少,这样做是为了安全方面的考量,目的是防止运行JavaScript的网页耗尽全部系统内存而导致系统崩溃。
how?
因此需要确保占用最少的内存可以让页面获得更好的性能。而优化内存占用的最佳方式,是只保存必要的数据,一旦数据不再有用,最好将其设置为null来释放其引用——又称解除引用(dereferencing)。该做法适用于大多数全局变量和全局对象的属性。局部变量在离开执行环境时会自动被解除引用。