《JavaScript 高级程序设计》学习笔记——变量、作用域和内存问题
第4章 变量、作用域和内存问题
JavaScript 的变量与其他语言有很大区别,其松散类型的本质决定了它只是在特定时间用于保存特定值的一个名字而已
4.1 基本类型和引用类型的值
- ECMAScipt 变量可能包含两种不同数据类型的值:
- 基本类型值:指的是简单的数据段,在内存中占据固定大小空间,因此被保存在栈内存中
- 引用类型值:指那些可能由多个值构成的对象,保存在堆内存中
- 5种基本数据类型是按值访问的,因为可以操作保存在变量中的实际的值
- 引用类型的值是保存在内存中的对象,JavaScript不允许直接访问内存中的位置,也就是说不能直接操作对象的内存空间
4.1.1 动态的属性
- 只能给引用类型值动态地添加属性,以便将来使用
- 给基本类型值添加属性不会报错,但没有效果
4.1.2复制变量值
- 从一个变量向另一个变量复制基本类型的值,会创建这个值的一个副本,二者的值相同但完全独立,进行任何操作都不会互相影响
- 从一个变量向另一个变量复制引用类型的值,实际上新变量是一个指针,指向一个对象,复制操作结束后二者将引用同一个对象
4.1.3 传递函数
-
ECMAScript 中所有函数的参数都是按值传递的(访问变量有按值和按引用两种方式,而参数只能按值传递)
- 在向参数传递基本类型的值时,被传递的值会被复制给一个局部变量(即命名参数)
- 在向参数传递引用类型的值时,会把这个值在内存中的地址复制给一个局部变量,因此这个局部变量的变化会反映在函数外部
-
证明对象是按值传递的例子:
function setName(obj) { obj.name = "Nicholas"; obj = new Object(); obj.name = "Greg"; } var person = new Object(); setName(person); alert(person); //"Nicholas"
- 如果 person 是按引用传递的,那么 person 就会自动被修改为指向其 name 属性为 "Grey" 的新对象,但接下来访问 person.name 时,显示的值仍然是 "Nicholas"
- 这说明即使在函数内部修改了参数的值,但原始的引用仍然保持不变
实际上,在函数内部重写 obj 时,这个变量引用的就是一个局部对象了 (会在函数执行完毕后被销毁)
-
可以把 ECMAScipt 函数的参数想象成局部变量
4.1.4 检测类型
- typeof 操作符是确定一个变量是字符串、数值、布尔值,还是 undefined 的最佳工具,但在检测引用类型的值时,typeof 的作用不大
- 当我们想知道某个值是什么类型的对象可以使用 instanceof 操作符
-
语法:
result = variable instanceof constructor
-
如果变量是给定引用类型(根据它的原型链来识别,详见第6章)的实例,那么 instanceof 操作符就会返回 true,例如:
alert(person instanceof Object); //变量 person 是 Object 吗? alert(colors instanceof Array); //变量 colors 是 Array 吗?
-
所有引用类型的值都是 Object 的实例,因此在检测一个引用类型的值和 Object 构造函数时,始终返回 true
-
如果使用 instanceof 操作符检测基本类型的值,则始终返回 false
-
4.2 执行环境及作用域
- 执行环境定义了变量或函数有权访问的其他数据,决定了它们各自的行为
- 每个执行环境都有一个与之关联的变量对象
- 环境中定义的变量和函数都保存在这个对象中
- 虽然代码无法访问,但解析器处理数据时会用到它
- 在 Web 浏览器中,全局执行环境被认为是 window 对象,因此所有全局变量和函数都是作为 window 对象的属性和方法去创造的
- 某个执行环境中的所有代码执行完毕后,该环境被销毁,保存在其中的变量和函数也随之销毁(全局执行环境知道应用程序退出————例如关闭网页或浏览器————时才会被销毁)
- 每个函数都有自己的执行环境,当执行流进入一个函数时,函数的环境就会被推入一个环境栈中,而在函数执行之后,栈将其环境弹出,把控制权返回给之前的执行环境
- 当代码在一个环境中执行时,会创建变量对象的一个作用域链
- 作用域链的用途是保证对执行环境有权访问的所有变量和函数的有序访问
- 作用域链的前端始终是当前执行的代码所在环境的变量对象。如果这个环境是函数,则将其活动对象作为变量对象
- 活动对象在最开始时只包含一个变量,即 arguments 对象(找个对象在全局环境中是不存在的)
- 作用域链的下一个变量对象来自包含(外部)环境,而再下一个变量对象则来自下一个包含环境,这样一直延续到全局执行环境(全局执行环境的变量对象始终是作用域链中的最后一个对象)
- 标识符解析是沿着作用域链一级一级地搜索标识符的过程。搜索过程始终从作用域链的前端开始,逐级向后回溯,直到找到标识符为止(如果找不到标识符,通常会导致错误发生)
- 内部环境可以通过作用域链访问所有的外部环境,但外部环境不能访问内部环境中的任何变量和函数
- 函数参数也被当作变量来对待,因此其访问规则与执行环境中的其他变量相同
4.2.1 延长作用域链
- 当执行流进入下列任何一个语句时,作用域链就会得到加长(这两个语句都会在作用域链的前端添加一个变量对象)
- try-catch 语句的 catch 块
- 会创建一个新的变量对象,其中包含的是被抛出的错误对象的声明
- with 语句
- 会将指定的对象添加到作用域链中
- try-catch 语句的 catch 块
4.2.2 没有块级作用域
-
在其他类 C 语言中,由花括号封闭的代码都有自己的块级作用域
for(var i=0; i < 10; i++) { doSomething(i); } alert(i);
-
对于有块级作用域的语言来说,for 循环初始化的变量 i 只会存在于循环的环境中
-
而对于 JavaScript 来说,for 创建的 i 即使在循环结束之后,也会存在于循环的外部环境中
声明变量
- 使用 var 声明的变量会自动添加到最接近的环境中(如果初始化变量时没有使用 var 声明,该变量会自动添加到全局环境)
查询标识符
- 搜索过程从作用域链的前端开始,向上级查询与给定名字匹配的标识符,如果在局部环境找到了该标识符,搜索过程停止
- 如果局部环境存在着同名标识符,就不会使用位于父环境的标识符
4.3 垃圾收集
- 执行环境会负责管理代码执行过程中使用的内存,所需内存的分配以及无用内存的回收完全实现了自动管理
- 机制为周期性执行:找出无用变量,将其所占内存释放
- 用于标识无用变量的策略通常有两个:
4.3.1 标记清除
- 这是 JavaScript 最常用的垃圾收集方式
- 当变量进入环境时,就将其标记为 "进入环境" ,永远不能释放进入环境的变量所占用的内存,当变量离开环境时,则将其标记为 "离开环境",然后清除被标记的无用变量
4.3.2 引用计数
- 不太常见
4.3.3 性能问题
- 确定垃圾收集时间间隔是一个非常重要的问题
- 在所有的浏览器中可以触发垃圾收集过程,但不建议这么做
4.3.4 管理内存
- 优化占用内存的最佳方式就是执行中的代码只保存必要的数据,一旦数据不再有用,将其值设置为 null 来释放其引用,称为解除引用
- 局部变量会在离开执行环境时自动被解除引用,全局变量需要手工为它解除引用
- 解除一个值的引用不必意味着自动回收该值作占用内存,真正作用时让值脱离执行环境,以便垃圾收集器下次运行时将其回收