本文首发于Liubasara的个人博客,承蒙喜爱,欢迎转载。
第4章 变量、作用域和内存问题
本章涉及到JavaScript运行底层的一些基础问题,适合认真阅读,加深理解。
4.1 基本类型和引用类型的值
ECMAScript变量包含两种不同的数据类型:基本类型值和引用类型值。
基本类型值有以下五种:
- Undefined
- Null
- Boolean
- Number
- String
而引用类型值是保存在内存中的对象,由于JavaScript不允许直接访问内存中的位置,也就是说不能直接操作对象的内存空间,所以在操作对象时,实际上实在操作对象的引用。
4.1.2 复制变量值
复制基本类型值,会在变量对象上创建一个新值,两个变量可以参与任何操作而不会互相影响。
复制引用类型值,同样也会复制一份引用类型值,然而这个引用类型值实际上是一个指针,而这个指针指向存储在堆中的一个对象。复制操作结束后,两个变量实际将引用同一个对象。因此,改变其中一个变量,就会影响另一个变量。
// 示例
var obj1 = new Object()
var obj2 = obj1
obj1.name = "nihao"
alert(obj2.name) // "nihao"
复制代码
4.1.3 传递参数
在ECMAScript中,函数参数的传递就相当于是将函数外部的值从一个变量复制到另一个变量,因此基本类型值会复制一个相同的值,而引用类型值会复制一个相同对象的指针。
4.1.4 检测类型
一般会使用 typeof 操作符来确定一个变量是否为基本数据类型,但是对于引用类型值或是null,则只会返回object。想知道某个引用类型值是什么类型的对象,可以使用 instanceof 操作符。
// result = variable instanceof constructor
alert(person instanceof Object) // 变量person是Object吗
alert(colors instanceof Array) // 变量colors是Array吗
alert(pattern instanceof RegExp) // 变量pattern是RegExp吗
复制代码
根据规定,所有引用类型的值都是Object的实例。因此,在检测一个引用类型值和Object构造的函数时,instanceof操作符会返回true。当然,如果使用instanceof操作符检测基本类型的值,则会始终返回false,因为基本类型不是对象。
4.2 执行环境及作用域
- 执行环境定义了变量或函数有权访问的其它数据,决定了它们各自的行为。每个执行环境都有一个与之关联的变量对象,环境中定义的所有变量和函数都保存在这个对象中。
- 全局执行环境是最外围的一个执行环境。在Web浏览器中,全局执行环境被认为是window对象,因此所有全局变量和函数都是作为window对象的属性和方法创建的。
- 某个执行环境中的所有代码执行完毕后,该环境被销毁,保存在其中的所有变量和函数定义也随之销毁。(全局执行环境知道应用程序退出——例如关闭网页或浏览器——时才会被销毁)
- 作用域链的解释,通俗的来说就是一层执行环境包着一层执行环境,执行时沿着最里层的执行环境一直顺延到最外层。
// 示例
var color = "blue"
function changeColor () {
if (color === "blue") {
color = "red"
} else {
color = "blue"
}
}
changeColor()
alert("Color is now " + color) // Color is now red
复制代码
在上面的例子中,因为我们可以在作用域链中找到color,所以就可以在函数内部调用它,即便这个变量并没有作为参数被传进参数里。每个环境都可以向上搜索作用域链,但不可以向下搜索作用域链而进入另一个执行环境(内部环境可以根据作用域链找到外部环境的变量,但外部环境不能反向查找。)
4.2.1 延长作用域链
有些语句可以在作用域链的前端临时增加一个变量对象,该变量对象会在代码执行后被移除。具体来说,当执行下列任何一个语句时,作用域链就会得到加长:
- try-catch语句的catch块
- with语句
这两个语句都会在作用域链的前端添加一个变量对象。对with语句来说,会将指定的对象添加到作用域链中。对catch语句来说,会创建一个新的变量对象,其中包含的是被抛出的错误对象的声明。
function buildUrl () {
var qs = "?debug=true"
with (location) {
var url = href + qs
}
return url
}
复制代码
在上面的例子中,location对象的所有属性方法被添加到了作用域链的前端。
4.2.2 没有块级作用域
在ES6之前(ES6之后有了伟大的const和let),声明变量的方式只有使用var关键字,因此当一个变量被声明时,如果其不存在于一个局部执行环境之中(通俗点来说就是不在一个函数里面的话),就会被自动添加到全局执行环境中。
4.3 垃圾收集
与类C语言不一样的是,JavaScript不需要开发人员关心内存使用的问题,执行环境会负责管理代码执行中所使用的内存。JavaScript中的垃圾收集器会按照固定的时间间隔(或代码执行中预计的收集时间),周期性的执行这一操作。
用于标识无用变量的策略一般有两种:
4.3.1 标记清除(最常用)
垃圾收集器在运行的时候会给存储在内存中的所有变量都加上标记。然后,它会去掉环境中的变量以及被环境中的变量引用的变量的标记。而在此之后再被加上标记的变量将被视为准备删除的变量,原因是环境中的变量已经无法访问到这些变量了。最后,垃圾收集器完成内存清除工作,销毁那些带标记的值并回收它们所占用的内存空间。
4.3.2 引用计数
引用计数的含义是跟踪记录每个值被引用的次数。当声明了一个变量并将一个引用类型值赋给该变量时,则这个值的引用次数+1。相反,如果包含对这个值引用的变量又取得了另外一个值,则这个值的引用次数减1。当这个值的引用次数变成0时,则说明没有办法再访问这个值了。因而就可以将其占用的内存空间回收回来。当垃圾收集器下次再运行时,它就会释放那些引用次数为零的值所占用的内存。
但这种方式在循环引用时,会导致内存泄漏的问题。
4.3.4 管理内存
解除引用:对于全局变量和全局对象的属性,一旦数据不再有用,可以将变量的值设置为null来解除引用,让其脱离执行环境,以便垃圾收集器下次运行时将其回收。