目录
原始值与引用值
变量可以包含两种不同类型的数据,原始值和引用值。原始值就是最简单的数据,引用值则是由多个值构成的对象。
上一章讨论了6种原始值:Undefined、Null、Boolean、Number、String和Symbol。保存原始值的变量是按值访问的,因为我们操作的就是存储在变量中的实际值。
引用值是保存在内存中的对象,与其他语言不同,JavaScript不允许直接访问内存位置,因此也就不能直接操作对象所在的内存空间。在操作对象时,实际上操作的是对该对象的引用,而非实际的对象本身。为此,保存引用值的变量是按引用访问的。
复制值
原始值和引用值在通过变量复制时也有所不同。在通过变量把一个原始值赋值到另一个变量时,原始值会被复制到新变量的位置。这两个变量可以独立使用,互不干扰。
在把引用值从一个变量赋给另一个变量时,存储在变量中的值也会被复制到新变量所在的位置。区别在于,这里复制的值实际上是一个指针,它指向存储在堆内存中的对象。操作完成后,两个变量实际上指向同一个对象,因此一个对象上面的变化会在另一个对象上反映出来。
传递参数
ECMAScript中所有函数的参数都是按值传递的,这意味着函数外的值会被复制到函数内部的参数中,就像从一个变量复制到另一个变量一样。如果是原始值,那么就跟原始值变量的复制一样,如果是引用值,那么就跟引用值变量的赋值一样。
执行上下文与作用域
执行上下文的概念在JavaScript中是颇为重要的。变量或函数的上下文决定了他们可以访问哪些数据,以及他们的行为。每个上下文都有一个关联的变量对象,而这个上下文中定义的所有变量和函数都存在于这个对象上。虽然无法通过代码访问变量对象,但后台处理数据会用到它。
全局上下文是最外层的上下文,根据ECMAScript实现的宿主环境,表示全局上下文的对象可能不一样。在浏览器中,全局上下文就是我们常说的window对象。因此所有通过var定义的全局变量和函数都会成为window对象的属性和方法。使用let和const的顶级声明不会定义在全局上下文中,但在作用域链解析上效果是一样的。上下文在其所有代码都执行完毕后会被销毁,包括定义在它上面的所有变量和函数。
每个函数调用都有自己的上下文,当代码执行流进入函数时,函数的上下文被推到一个上下文栈上,在函数执行完之后,上下文栈会弹出该函数上下文,将控制权返还给之前的执行上下文。
上下文中的代码在执行的时候会创建变量对象的一个作用域链,这个作用域链决定了各级上下文中的代码在访问变量和函数时的顺序。代码正在执行的上下文的变量对象始终位于作用域链的最前端。如果上下文是函数,则其活动对象用作变量对象。活动对象最初只有一个定义变量arguments。作用域链中的下一个变量对象来自包含上下文,再下一个对象来自再下一个包含上下文,以此类推,直至全局上下文。全局上下文的变量对象始终是作用域链的最后一个变量对象。
代码执行时的标识符解析是通过沿作用域链逐级搜索标识符名称完成的。搜索过程始终从作用域链的最前端开始,然后逐级往后,直到找到标识符。
var color = "blue";
function changeColor(){
let anotherColor = "red";
function swapColors(){
let tempColor = anotherColor;
anotherColor = color;
color = anotherColor;
//这里可以访问color、anotherColor和tempColor
}
//这里可以访问color和anotherColor,但访问不到tempColor
swapColors();
}
//这里只能访问color
changeColor();
作用域链增强
虽然执行上下文主要有全局上下文和函数上下文两种,但有其他方式来增强作用域链。某些语句会导致在作用域链前端临时添加一个上下文,这个上下文在代码执行后会被删除。通常在两种情况下会出现这个现象,即代码执行到下面任意一种情况时:
- try/catch语句的catch块
- with语句
这两种情况下,都会在作用域链前端添加一个变量对象。对with语句来说,会向作用域链前端添加指定的对象;对catch语句而言,则会创建一个新的变量对象,这个变量对象会包含要抛出的错误对象的声明。
标识符查找
当在特定上下文中为读取或写入而引用一个标识符时,必须通过搜索确定这个标识符表示什么。搜索开始于作用域链前端,以给定的名称搜索对应的标识符。如果在局部上下文中找到该标识符,则搜索停止,变量确定,如果没有找到变量名,则继续沿作用域链搜索。(注意,作用域链中的对象也有一个原型链,因此搜索可能涉及每个对象的原型链。)这个过程一直持续到搜索至全局上下文的变量对象。如果仍然没有找到标识符,则说明其未声明。
小结
执行上下文可以总结如下:
- 执行上下文分全局上下文、函数上下文和块级上下文。
- 代码执行流每进入一个新上下文,都会创建一个作用域链,用于搜索变量和函数。
- 函数或块的局部上下文不仅可以访问自己作用域内的变量,而且也可以访问任何包含上下文乃至全局上下文中的变量。
- 全局上下文只能访问全局上下文中的变量和函数,不能直接访问局部上下文中的任何数据。
- 变量的执行上下文用于确定什么时候释放内存。
垃圾回收
JavaScript是使用垃圾回收的语言,也就是说执行环境负责在代码执行时管理内存。基本思路很简单:确定哪个变量不会再使用,然后释放它占用的内存。这个过程是周期性的,及垃圾回收程序每隔一定时间就会自动运行。
在浏览器的发展史上,用到过两种主要的标记策略:标记清理和引用计数。
标记清理
JavaScript最常用的垃圾回收策略是标记清理。当变量进入上下文,比如在函数内部声明一个变量时,这个变量会被加上存在于上下文中的标记。当变量离开上下文时,也会被加上离开上下文的标记。
垃圾回收程序运行的时候会标记内存中存储的所有变量,然后它会将所有在上下文中的变量以及被在上下文中的变量引用的变量的标记去掉。在此之后,再被加上标记的变量就是待删除的了,原因是任何在上下文中的变量都访问不到它们了。随后,垃圾回收程序做一次内存清理,销毁带标记的所有值,并回收它们的内存。
引用计数
另一种没那么常用的垃圾回收策略是引用计数,其思路是对每个值都记录它被引用的次数。声明变量并给它赋一个引用值时,这个值的引用数为1。如果同一个值又被赋给另一个变量,那么引用数加1。类似地,如果保存对该值引用的变量被其他值给覆盖了,那么引用数减1。当一个值的引用数为0时,就说明没办法再访问到这个值了,因此可以安全的回收其内存了。垃圾回收程序下次运行的时候就会释放引用数为0的值的内存。
引用计数很快就遇到了严重的问题,循环引用。所谓循环引用,就是对象A有一个指针指向对象B,而对象B也引用了对象A。比如:
function problem(){
let objectA = new Object();
let objectB = new Object();
objectA.someOtherObject = objectB;
objectB.anotherObject = objectA;
}
在这个例子中,objectA和objectB通过各自的属性相互引用,意味着它们的引用数都是2。在标记清理策略下,这不是问题,因为在函数结束后,这两个对象都不在作用域中。而在引用计数策略下,objectA和objectB在函数结束后还会存在,因为它们的引用数永远不会变成0。如果函数被多次调用,则会导致大量内存永远不会被释放。