# JavaScript 基础

JavaScript 是一种高级的、解释执行的编程语言;是一种动态类型、弱类型、基于原型的语言;支持面向对象编程、命令式编程以及函数式编程。

# 基本概念

JavaScript 诞生于 1995 年。当时,它的主要目的是处理以前由服务器端语言(如 Perl)负责的一些输入验证操作。

如今,它的用途早已不再局限于简单的数据验证,而是具备了与浏览器窗口及其内容等几乎所有方面交互的能力。一个完整的 JavaScript 实现由下列三个不同的部分组成:

  • 核心(ECMAScript)
  • 文档对象模型(DOM)
  • 浏览器对象模型(BOM)

ECMAScript:是一种由 Ecma 国际(前身为欧洲计算机制造商协会)通过 ECMA-262 标准化的脚本程序设计语言。 这种语言在万维网上应用广泛,它往往被称为 JavaScript 或 JScript,但实际上后两者是 ECMA-262 标准的实现和扩展。

文档对象模型(DOM,Document Object Model)是针对 XML 但经过扩展用于 HTML 的应用程序编程接口(API,Application Programming Interface)。DOM 把整个页面映射为一个多层节点结构。

浏览器对象模型支持访问和操作浏览器窗口,功能包括但不仅是:

  • 弹出新浏览器窗口的功能。
  • 移动、缩放和关闭浏览器窗口的功能。
  • 提供浏览器详细信息的 navigator 对象。
  • 提供浏览器所加载页面的详细信息的 location 对象。
  • 提供用户显示器分辨率详细信息的 screen 对象。
  • cookies 的支持。
  • 像 XMLHttpRequest 和 IE 的 ActiveXObject 这样的自定义对象。

# 在 HTML 中使用 JavaScript

针对不支持或关闭了脚本支持的情况提供了 <noscript> 元素,其中的内容会在前面的情况发生时才会显示。而向 HTML 页面中插入 JavaScript 的主要方法,就是使用 <script> 元素,包含以下可用属性:

  • src:可选。表示包含执行代码的外部文件。
  • type: 可选。默认值为 text/javascript
  • async:可选。表示应该立即下载脚本,但不应妨碍页面中的其他操作,比如下载其他资源或等待加载其他脚本。只对外部脚本文件有效。
  • defer:可选。表示脚本可以延迟到文档完全被解析和显示之后再执行,IE7 及更早版本对嵌入脚本也支持这个属性。只对外部脚本文件有效。
  • charset:可选。表示通过 src 属性指定的代码的字符集。由于大多数浏览器会忽略它的值, 因此这个属性很少有人用。

WARNING

在使用 <script> 嵌入 JavaScript 代码时,记住不要在代码中的任何地方出现 </script> 字符串,因为按照解析嵌入式代码的规则,当浏览器遇到字符串 </script> 时,就会认为那是结束的 </script> 标签。而通过转义字符 “\” 可以解决这个问题。

<script type="text/javascript">
  function sayScript() {
    console.log('<\/script>');
  }
</script>

# 延迟脚本

  • <script> 元素中设置 defer 属性,相当于告诉浏览器立即下载但延迟执行。
  • 延迟脚本在 HTML5 规范中规定浏览器在遇到 </html> 标签之后再按出现的顺序执行,且会先于 DOMContentLoaded 事件。
  • 在现实当中,延迟脚本并不一定会按照顺序执行,也不一定会在 DOMContentLoaded 事件触发前执行,因此最好只包含一个延迟脚本。

# 异步脚本

  • <script> 元素中设置 async 属性,相当于告诉浏览器立即下载但不保证脚本文件的执行顺序,因此应确保异步加载的脚本文件互补依赖。

  • 指定 async 属性的目的是不让页面等待两个脚本下载和执行,从而异步加载页面其他内容。

  • 异步脚本一定会在页面的 load 事件前执行,但可能会在 DOMContentLoaded 事件触发之前或之后执行。

  • 建议异步脚本不要在加载期间修改 DOM。

  • 带有 src 属性的 <script> 元素只会下载和执行外部脚本文件,其中嵌入的代码会被忽略。

  • 通过 <script> 元素的 src 属性还可以包含来自外部域的 JavaScript 文件。

  • 无论如何包含代码,只要不存在 deferasync 属性,浏览器都会按照 <script> 元素在页面中出现的先后顺序对它们依次进行解析。

  • 与解析嵌入式 JavaScript 代码一样, 在解析外部 JavaScript 文件(包括下载该文件)时,页面的处理也会暂时停止。

传统的做法是把所有外部文件(包括 CSS 文件和 JavaScript 文件)的引用都放在相同的地方(<head> 元素中),这意味着必须等到全部 JavaScript 代码都被下载、解析和执行完成以后,才能开始呈现页面的内容(浏览器在遇到 <body> 标签时才开始呈现内容)。

因此这有可能导致浏览器在呈现页面时出现明显的延迟,而延迟期间的浏览器窗口中将是一片空白,为了避免这个问题,现代 Web 应用程序一般都把全部 JavaScript 引用放在 <body> 元素中页面内容的后面。

# 文档模式

IE5.5 引入了文档模式的概念,而这个概念是通过使用文档类型(doctype)切换实现的。

模式主要包括混杂模式、准标准模式和标准模式,一般来说准标准模式和标准模式相差甚微,所以人们所说的标准模式可能是准标准模式或标准模式。

HTML 5 文档模式声明:

<!DOCTYPE html>

如果在文档开始处没有发现文档类型声明,则所有浏览器都会默认开启混杂模式。

模式的差异主要影响 CSS 内容的呈现,但某些情况也会影响 JavaScript 的解释执行。

# 语法中的基本概念

  • ECMAScript 中的一切(变量、函数名和操作符)都区分大小写。
  • ECMAScript 使用 C 风格的注释,包括单行注释和块级注释。
  • 标识符:就是指变量、函数、属性的名字,或者函数的参数。
  • 严格模式:要在整个脚本中启用严格模式,可以在顶部添加如右边所示代码: 'use strict'
  • 语句:ECMAScript 中的语句以一个分号结尾;如果省略分号,则由解析器确定语句的结尾。
  • 变量:变量定义时要使用 var 操作符,可以用来保存任何值(未经过初始化的 变量,会保存为 undefined)。
  • 数据类型:JavaScript 中的数据类型包括基本数据类型(Undefined、Null、Boolean、String 和 Number)和复杂数据类型(Object,本质上是由一组无序的名值对组成)。
  • 操作符typeof 是一个操作符而不是函数,能对基本数据类型进行检测。

ECMAScript 标识符采用驼峰大小写格式。注意不能把关键字、保留字、truefalsenull 用作标识符。

另外,虽然条件控制语句(如 if 语句)只在执行多条语句的情况下才要求使用代码块,但最佳实践是始终在控制语句中使用代码块——即使代码块中只有一条语句。

# 数据类型

ECMAScript 中有基础数据类型和复杂类型两种。

基本类型值在内存中占据固定大小的空间,因此被保存在栈内存中,引用类型的值是对象,保存在堆内存中。

不建议修改变量所保存值的类型,即使这种操作在 ECMAScript 中完全有效。

比如下面我们申明变量(test_variable_type)保存值的类型为字符串,然后将其保存值改为数字类型

var test_variable_type = 'string'
text_variable_type = 2

# Undefined 类型

  • Undefined 类型只有一个值,即特殊的 undefined
  • 在使用 var 声明变量但未对其加以初始化时,这个变量的值就是 undefined
  • 字面值 undefined 的主要目的是用于比较,以正式区分空对象指针与未经初始化的变量。
  • 对于尚未声明过的变量,只能执行一项操作,即使用 typeof 操作符检测其数据类型。
  • 对未初始化和未声明的变量执行 typeof 操作符都返回 undefined 值。

# Null 类型

  • Null 类型只有一个值 null,表示一个空对象指针。
  • 意在保存对象的变量还没有真正保存对象,应该明确地让该变量保存 null 值。
  • null 等于但不严格等于 undefined

# Boolean 类型

  • Boolean 类型只有两个值即 truefalse
  • 可以对任何数据类型的值调用 Boolean() 函数,而且总会返回一个 Boolean 值。
  • 转换为 false 的值:0、-0、null''falseundefinedNaN

# Number 类型

JavaScript 使用 IEEE754 格式来表示整数和浮点数值(浮点数值在某些语言中也被称为双精度数值)。

在进行算术计算时,所有以八进制和十六进制表示的数值最终都将被转换成十进制数值。

  • 浮点数:
    • 所谓浮点数值,就是该数值中必须包含一个小数点,并且小数点后面必须至少有一位数字。虽然小数点前面可以没有整数,但我们不推荐这种写法。
    • 保存浮点数值需要的内存空间是保存整数值的两倍, 因此 ECMAScript 会不失时机地将浮点数值转换为整数值,比如小数点后面没有任何数值或者浮点数本身表示的就是整数(1.0)。
    • 浮点数值的最高精度是 17 位小数,但在进行算术计算时其精确度远远不如整数:0.1 加 0.2 的结果不是 0.3,而是 0.30000000000000004。
  • NaN:
    • 表示一个本来要返回数值的操作数但未返回数值的情况。
    • NaN 与任何值都不相等,包括其本身。
    • 函数 isNaN() 在接收到一个值之后,会尝试将这个值转换为数值,对于任何不能被转换为数值的值都会返回 true
    • 在基于对象调用 isNaN() 函数时,会首先调用对象的 valueOf() 方法,然后确定该方法返回的值是否可以转换为数值。如果不能,则基于这个返回值再调用 toString() 方法,再测试返回值。
  • 数值的转换:
    • Number() 可用于任何数据类型,而 parseInt()parseFloat() 专门用于将字符串转换为数字。
    • 需要注意的是 Number() 方法对 null 返回 0,而对 undefined 返回 NaN
    • Number() 将空字符串和无参数解析会 0,而 parseInt()parseFloat() 会解析为 NaN
    • parseInt() 会忽略字符串前面的空格,直至找到第一个非空格字符。如果第一个字符不是数字字符或者负号,parseInt() 就会返回 NaN
    • 如果第一个字符是数字字符,parseInt() 会继续解析第二个字符,直到解析完所有后续字符或者遇到了一个非数字字符,比如小数点。
    • parseInt() 不会解析 8 进制数,除非提供第二个参数:转换时使用的基数(即多少进制)。
    • parseFloat()parseInt() 的区别在于字符串中的第一个小数点是有效的,而且它始终都会忽略前导的零。

# String 类型

String 类型用于表示由零或多个 16 位 Unicode 字符组成的字符序列,即字符串。

ECMAScript 中的字符串是不可变的,要改变某个变量保存的字符串,首先要销毁原来的字符串,然后再用另一个包含新值的字符串填充该变量。

任何字符串的长度都可以通过访问其 length 属性取得,如果字符串中包含双字节字符,那么 length 属性可能不会精确地返回字符串中的字符数目。

字符串的转换:

  • 通常使用 toString()String()
  • 数值、布尔值、对象和字符串值(每个字符串也都有一个 toString() 方法,该方法返回字符串的一个副本)都有 toString() 方法,但 nullundefined 值没有这个方法。
  • 在不知道转换的值是不是 nullundefined 的情况下,还可以使用转型函数 String(),这个函数能够将任何类型的值转换为字符串。
  • 对于 String() 方法,如果值有 toString() 方法,则调用该方法(没有参数)并返回相应的结果,如果是 null 则返回 'null',如果是 undefined 则返回 'undefined'

TIP

默认情况下,数值的 toString() 方法以十进制格式返回数值的字符串表示。而通过传递基数,toString() 可以输出以二进制、八进制、十六进制,乃至其他任意有效进制格式表示的字符串值。

# Object 类型

ECMAScript 中的对象其实就是一组数据和功能的集合,通过 new 操作符创建对象在不传递参数的情况下,可以省略圆括号(但这不是推荐的做法)。

Object 类型是所有它的实例的基础,,它的每个实例都具有下列属性和方法:

  • constructor:保存着用于创建当前对象的函数。
  • hasOwnProperty(propertyName):用于检查给定的属性在当前对象实例中(而不是在实例的原型中)是否存在。
  • isPrototypeOf(object):检查传入的对象是否是传入对象的原型。
  • propertyIsEnumerable(propertyName):检查给定的属性是否能够使用 for-in 语句来枚举。与 hasOwnProperty() 方法一样,作为参数的属性名必须以字符串形式指定。
  • toLocaleString():返回对象的字符串表示,该字符串与执行环境的地区对应。
  • toString():返回对象的字符串表示。
  • valueOf():返回对象的字符串、数值或布尔值表示,通常与 toString() 方法的返回值相同。

# 操作符

一元操作符:只能操作一个值的操作符叫做一元操作符。

  • 递增和递减操作符都是一元操作符,且它们都有前置型和后置型,执行前置递增和递减操作时,变量的值都是在语句被求值以前改变的。
    • 在应用于不同的值时,递增和递减操作符会像 Number() 将其转换为数值。
  • 加减操作符也是一元操作符。
    • 在对非数值应用一元加操作符时,该操作符会像 Number() 转型函数一样对这个值执行转换。

位操作符:位操作符位于最基本的层次上,即按内存中表示数值的位来操作数值,默认情况下 ECMAScript 中的所有整数都是有符号整数。

  • 按位非:
    • 按位非操作符由一个波浪线(~)表示,执行按位非的结果就是返回数值的反码。
    • 按位非操作的本质:(二进制码取反)操作数的负值减 1。
  • 按位与:
    • 按位与操作符由一个和号字符(&)表示,它有两个操作符数。
    • 按位与操作只在两个数值的对应位都是 1 时才返回 1,任何一位是 0,结果都是 0。
  • 按位或:
    • 按位或操作符由一个竖线符号(|)表示,同样也有两个操作数。
    • 按位或操作在有一个位是 1 的情况下就返回 1, 而只有在两个位都是 0 的情况下才返回 0。
  • 按位与或:
    • 按位异或操作符由一个插入符号(^)表示,也有两个操作数。
    • 两个数值对应位上只有一个 1 时才返回 1。
  • 左移:
    • 左移操作符由两个小于号(<<)表示,这个操作符会将数值的所有位向左移动指定的位数。
    • 注意,左移不会影响操作数的符号位。
    • 左移的结果在大多数时候就是操作数乘以 2 的移动位数次方的积。
  • 右移:
    • 无符号右移:操作符由 3 个大于号(>>>)表示,这个操作符会将数值的所有 32 位都向右移动。对正数来说,无符号右移的结果与有符号右移相同。

在对特殊的 NaNInfinity 值应用位操作时,这两个值都会被当成 0 来处理。

对非数值应用位操作符,会先使用 Number() 函数将该值转换为一个数值(自动完成),然后再应用位操作,得到的结果将是一个数值。

布尔操作符:用于将表达式转为布尔值。

  • 逻辑非:
    • 逻辑非操作符由一个叹号(!)表示,可以应用于 ECMAScript 中的任何值,无论这个值是什么数据类型,这个操作符都会返回一个布尔值。
    • 同时使用两个逻辑非操作符(!!),实际上就会模拟 Boolean() 转型函数的行为。
  • 逻辑与:
    • 逻辑与操作符由两个和号(&&)表示,有两个操作数。
    • 逻辑与操作可以应用于任何类型的操作数,而不仅仅是布尔值,在有一个操作数不是布尔值的情况下,逻辑与操作就不一定返回布尔值。
    • 其它情况下如果第一个求值为 true 时,则返回第二个操作数,否则返回第一个操作数。
    • 在使用逻辑与操作符时要始终铭记它是一个短路操作符。
  • 逻辑或:
    • 逻辑或操作符由两个竖线符号(||)表示,有两个操作数。
    • 与逻辑与操作符相似,逻辑或操作符也是短路操作符,在有一个操作数不是布尔值的情况下,逻辑与操作就不一定返回布尔值。
    • 其它情况下如果第一个求值为 true 时,则返回第一个操作数,否则返回第二个操作数。

乘法操作符:由一个星号(*)表示,用于计算两个数值的乘积。

  • 如果有一个操作数不是数值,则在后台调用 Number() 将其转换为数值。
  • 如果是 Infinity 与 0 相乘,则结果是 NaN

除法操作符:除法操作符由一个斜线符号(/)表示,执行第二个操作数除第一个操作数的计算。

  • Infinity 除以自己为 NaN,除以非自己的数结果为 Infinity-Infinity,取决于有符号操作数的符号。
  • 如果是零被零除,则结果是 NaN
  • 如果是非零的有限数被零除,则结果是 Infinity-Infinity,取决于有符号操作数的符号。
  • 如果有一个操作数不是数值,则在后台调用 Number() 将其转换为数值。

求模:求模(余数)操作符由一个百分号(%)表示。

  • 如果被除数是零,则结果是零。
  • 如果被除数是无穷大值而除数是有限大的数值,则结果是 NaN
  • 如果被除数是有限大的数值而除数是无穷大的数值,则结果是被除数。
  • 如果有一个操作数不是数值,则在后台调用 Number() 将其转换为数值。

加法操作符(+)

  • 如果是 Infinity-Infinity,则结果是 NaN
  • 如果是 +0 加 -0,则结果是 +0。
  • 如果有一个操作数是对象、数值、布尔值、undefinednull则将其转换为字符串,然后进行拼接。

减法操作符(-)

  • 如果有一个操作数是字符串、布尔值、nullundefined,则先在后台调用 Number() 函数将其转换为数值,然后再根据前面的规则执行减法计算。
  • 如果操作数是一个对象则将其转换为字符串再转换为数字再进行运算。
  • 如果是 +0 减 +0,则结果是 +0。
  • 如果是 +0 减 -0,则结果是 -0。
  • 如果是 -0 减 -0,则结果是 +0。
  • 如果是 InfinityInfinity,则结果是 NaN
  • 如果是 -Infinity-Infinity,则结果是 NaN
  • 如果是 Infinity-Infinity,则结果是 Infinity
  • 如果是 -InfinityInfinity,则结果是 -Infinity

关系操作符:小于(<) 、大于(>) 、小于等于(<=)和大于等于(>=)返回一个布尔值。

  • 如果一个操作数是数值,则将另一个操作数转换为一个数值,然后执行数值比较。
  • 如果两个操作数都是字符串,则比较两个字符串对应的字符编码值。
  • 如果一个操作数是对象,则调用这个对象的 valueOf() 方法,用得到的结果按照前面的规则执行比较。如果对象没有 valueOf() 方法,则调用 toString() 方法,并用得到的结果执行比较。
  • 按照常理,如果一个值不小于另一个值,则一定是大于或等于那个值。然而,在与 NaN 进行比较时,这两个比较操作的结果都返回了 false

相等操作符:ECMAScript 中的相等操作符由两个等于号(==)表示,如果两个操作数相等,则返回 true。而不相等操作符由叹号后跟等于号(!=)表示。

  • 如果有一个操作数是布尔值,则在比较相等性之前先将其转换为数值。
  • 如果一个操作数是字符串,另一个操作数是数值,在比较相等性之前先将字符串转换为数值。
  • 全等操作符(===)与相等操作符的唯一区别就是比较之前不转换操作数。

条件操作符variable = boolean_expression ? true_value : false_value;

赋值操作符:简单的赋值操作符由等于号(=)表示,如果在等于号(=)前面再添加乘性操作符、加性操作符或位操作符,就可以完成复合赋值操作。

逗号操作符:逗号操作符多用于声明多个变量;但除此之外,逗号操作符还可以用于赋值。

TIP

在用于赋值时,逗号操作符总会返回表达式中的最后一项。

# 语句

ECMA-262 规定了一组语句(也称为流控制语句)。

  • if 语句if (condition) statement1 else statement2
  • do-while 语句:后测试循环语句,即只有在循环体中的代码执行之后,才会测试出口条件。
  • while 语句:前测试循环语句,也就是说,在循环体内的代码被执行之前,就会对出口条件求值。
  • for 语句:前测试循环语句,但它具有在执行循环之前初始化变量和定义循环后要执行的代码的能力。
    • for (initialization; expression; post-loop-expression) statement
  • for-in 语句:精准的迭代语句,可以用来枚举对象的属性。
  • label 语句:使用 label 语句可以在代码中添加标签,加标签的语句一般都 要与 for 语句等循环语句配合使用。
  • break 语句:立即退出循环, 强制继续执行循环后面的语句。
  • continue 语句:立即退出循环,但退出循环后会从循环的顶部继续执行。
var num = 0
outermost: for (var i = 0; i < 10; i++) {
  for (var j = 0; j < 10; j++) {
    if (i == 5 && j == 5) {
      continue outermost
    }
    num++
  }
}
console.log(num) // 95
  • with 语句:作用是将代码的作用域设置到一个特定的对象中。
// 通常我们会这样写
var qs = location.search.substring(1)
var hostName = location.hostname
var url = location.href

// 利用 with 语句重写
with (location) {
  var qs = search.substring(1)
  var hostName = hostname
  var url = href
}
  • switch 语句:其中的每一种情形(case)的含义是: “如果表达式等于这个值,则执行后面的语句” 。而 break 关键字会导致代码执行流跳出 switch 语句。
    • 如果省略 break 关键字,就会导致执行完当前 case 后,继续执行下一个 case
    • default 关键字则用于在表达式不匹配前面任何一种情形的时候,执行机动代码。
    • switch 语句中使用任何数据类型,而且每个 case 的值不一定是常量,可以是变量,甚至是表达式。

TIP

  • 由于 ECMAScript2015 之前不存在块级作用域,因此在循环内部定义的变量也可以在外部访问到。
  • switch 语句在比较值时使用的是全等操作符,因此不会发生类型转换

# 函数

ECMAScript 中的函数使用 function 关键字来声明,后跟一组参数以及函数体。

函数在定义时不必指定是否返回值,但是任何函数在任何时候都可以通过 return 语句后跟要返回的值来实现返回值。

函数会在执行完 return 语句之后停止并返回指定的值,最后立即退出。如果不带有任何返回值,函数在停止执行后将返回 undefined 值。

参数(arguments):一个类数组的对象,并不是 Array 的实例。

  • 在函数体内可以通过 arguments 对象来访问这个参数数组,从而获取传递给函数的每一个参数。
  • 命名的参数只提供便利,但不是必需的。
  • arguments 中的值与对应命名参数的值保持同步,但是它们的内存空间是独立的。
  • 参数实际上是函数的局部变量

不能给基本类型的值添加属性,尽管这样做不会导致任何错误。

  • 不能把参数命名为 evalarguments
  • 不能出现两个命名参数相同的情况。
  • 修改 arguments 中的值,与其对应命名参数的值不会改变。
  • 重写 arguments 的值会导致语法错误。

# 数据类型的差异

  • 引用类型的值是保存在内存中的对象,JavaScript 不允许直接访问内存中的位置,也就是说不能直接操作对象的内存空间。在操作对象时,实际上是在操作对象的引用而不是实际的对象。
  • 基本数据类型是按值访问的,可以操作保存在变量中的实际的值,而引用类型的值是按引用访问的。
  • 如果从一个变量向另一个变量复制基本类型的值,会在变量对象上创建一个新值,然后把该值复制到为新变量分配的位置上。
  • 当从一个变量向另一个变量复制引用类型的值时, 同样也会将存储在变量对象中的值复制一份放到为新变量分配的空间中。不同的是,这个值的副本实际上是一个指针,而这个指针指向存储在堆中的一个对象。复制操作结束后,两个变量实际上将引用同一个对象。因此,改变其中一个变量,就会影响另一个变量。

# instance 操作符

  • 用于检测被检测对象是什么类型的对象,如果使用 instanceof 操作符检测基本类型的值,则该操作符始终会返回 false

TIP

参数的传递是按值传递的,传递的本质就像是复制变量一样。

# 执行环境(execution context)

执行环境定义了变量或函数有权访问的其他数据,决定了它们各自的行为。

每个执行环境都有一个与之关联的变量对象(variable object),环境中定义的所有变量和函数都保存在这个对象中。

虽然我们编写的代码无法访问这个对象,但解析器在处理数据时会在后台使用它。

  • 全局执行环境是最外围的一个执行环境。
  • 在 Web 浏览器中,全局执行环境被认为是 window 对象,所有全局变量和函数都是作为 window 对象的属性和方法创建的。
  • 某个执行环境中的所有代码执行完毕后,该环境被销毁,保存在其中的所有变量和函数定义也随之销毁(全局执行环境直到应用程序退出——例如关闭网页或浏览器时才会被销毁)。
  • 每个函数都有自己的执行环境。当执行流进入一个函数时,函数的环境就会被推入一个环境栈中。而在函数执行之后,栈将其环境弹出,把控制权返回给之前的执行环境。
  • 当代码在一个环境中执行时,会创建变量对象的一个作用域链(scope chain)。

作用域链

  • 作用域链的用途,是保证对执行环境有权访问的所有变量和函数的有序访问。
  • 作用域链的前端,始终都是当前执行的代码所在环境的变量对象。如果这个环境是函数,则将其活动对象(activation object)作为变量对象。
  • 活动对象在最开始时只包含一个变量,即 arguments 对象(这个对象在全局环境中是不存在的)。
  • 作用域链中的下一个变量对象来自包含(外部)环境,而再下一个变量对象则来自下一个包含环境。这样,一直延续到全局执行环境;全局执行环境的变量对象始终都是作用域链中的最后一个对象。
  • 标识符解析是沿着作用域链一级一级地搜索标识符的过程。搜索过程始终从作用域链的前端开始,然后逐级地向后回溯,直至找到标识符为止(如果找不到标识符,通常会导致错误发生)。

延长作用域链:有些语句可以在作用域链的前端临时增加一个变量对象,该变量对象会在代码执行后被移除,具体来说,就是当执行流进入下列任何一个语句时,作用域链就会得到加长。

  • try-catch 语句的 catch 块。
  • with 语句。

以上两个语句都会在作用域链的前端添加一个变量对象,不同的是对 with 语句来说,会将指定的对象添加到作用域链中。对 catch 语句来说,会创建一个新的变量对象,其中包含的是被抛出的错误对象的声明。

没有块级作用域

// 对于拥有块级作用域的语言来说,在块里面申明的变量(text)只在块中可见,然而由下面代码可见在块外面却能访问到块中的变量
// 因此也验证了 JavaScript 中没有块级作用域
if (true) {
  var text = 'Hello world'
  console.log(text) // Hello world
}
console.log(text) // Hello world

# 垃圾收集

JavaScript 具有自动垃圾收集机制,执行环境会负责管理代码执行过程中使用的内存。

垃圾收集机制的原理其实很简单:找出那些不再继续使用的变量,然后释放其占用的内存,因此,垃圾收集器会按照固定的时间间隔(或代码执行中预定的收集时间),周期性地执行。

垃圾收集器必须跟踪哪个变量有用哪个变量没用,对于不再有用的变量打上标记,以备将来收回其占用的内存。目前用于标识无用变量的的策略主要包括标记清除和引用计数。

JavaScript 中最常用的垃圾收集方式是标记清除(mark-and-sweep)。当变量进入环境(例如,在函 数中声明一个变量)时,就将这个变量标记为“进入环境”,而当变量离开环境时,则将其标记为“离开环境”。垃圾收集器在运行的时候会给存储在内存中的所有变量都加上标记,然后,它会去掉环境中的变量以及被环境中的变量引用的变量的标记。而在此之后再被加上标记的变量将被视为准备删除的变量,原因是环境中的变量已经无法访问到这些变量了。最后,垃圾收集器完成内存清除工作,销毁那些带标记的值并回收它们所占用的内存空间。

另一种不太常见的垃圾收集策略叫做引用计数(reference counting)。引用计数的含义是跟踪记录每个值被引用的次数。 当声明了一个变量并将一个引用类型值赋给该变量时, 则这个值的引用次数就是 1。 如果同一个值又被赋给另一个变量,则该值的引用次数加 1。相反,如果包含对这个值引用的变量又取得了另外一个值,则这个值的引用次数减 1。当这个值的引用次数变成 0 时,则说明没有办法再访问这个值了,因而就可以将其占用的内存空间回收回来。

引用计数的策略可能会导致循环引用的问题:循环引用指的是对象 A 中包含一个指向对象 B 的指针,而对象 B 中也包含一个指向对象 A 的引用。

性能:垃圾收集器是周期性运行的,如果为变量分配的内存数量很可观,那么回收工作量也是相当大的。确定垃圾收集的时间间隔是一个非常重要的问题。在有的浏览器中可以触发垃圾收集过程,但我们不建议读者这样做。在 IE 中,调用 window.CollectGarbage() 方法会立即执行垃圾收集。在 Opera 7 及更 高版本中,调用 window.opera.collect() 也会启动垃圾收集例程。

内存:内存限制问题不仅会影响给变量分配内存,同时还会影响调用栈以及在一个线程中能够同时执行的语句数量。执行中的代码只需要保存必要的数据。一旦数据不再有用,最好通过将其值设置为 null 来释放其引用——这个做法叫做解除引用(dereferencing)。

# 参考资料

  • JavaScript 高级程序设计(第 3 版)