JavaScript 高级程序设计 笔记一

第1章 JavaScript 简介

JavaScript 具备与浏览器窗口及其内容等几乎所有方面交互的能力。

欧洲计算机制造商协会 ECMA European Computer Manufactures Association

ECMAScript ek-ma-script

一个完整的JavaScript实现应该由下列三个不同的部分组成:

  • 核心(ECMAScript),提供核心语言功能
  • 文档对象模型(DOM),提供访问和操作网页内容的方法和接口
  • 浏览器对象模型(BOM),提供与浏览器交互的方法和接口

ECMAScript

我们常见的 Web 浏览器知识 ECMAScript 实现可能的宿主环境之一,其他宿主环境包括Node(一种服务端JavaScript平台)和 Adobe Flash。

ECMAScript规定了这门语言的下列组成部分:

  • 语法
  • 类型
  • 语句
  • 关键字
  • 保留字
  • 操作符
  • 对象

ECMAScript 3.1 成为 ECMA-262 第5版,在2009年发布。

浏览器对象模型(BOM)

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

HTML5 致力于把很多 BOM 功能写入正式规范。

第2章 在 HTML 中使用 JavaScript

向 HTML 页面中插入 JavaScript 的主要方法,就是使用<script>元素。HTML 4.01 为<script>定义了下列6个属性:

  • async:可选,异步脚本。表示应该立即下载脚本,但不应妨碍页面中的其他操作,比如下载其他资源或等待加载其他脚本。只对外部脚本文件有效。
  • charset:可选。表示通过src属性指定的代码的字符集。由于大多数浏览器会忽略它的值,因此这个属性很少有人用。
  • defer:可选,延迟脚本表示脚本可以延迟到文档被解析和显示之后再执行。只对外部脚本文件有效。IE7 及更早版本对嵌入脚本也支持这个属性。也就是说,脚本会被延迟到整个页面都解析完毕后再运行。
  • language:已废弃。大多数浏览器会忽略这个属性,因此也没有必要再用了。
  • src:可选。表示包含要执行代码的外部文件。
  • type:可选。可以看成是language的替代属性;表示编写代码使用的脚本语言的内容类型(也成为 MIME 类型)。虽然text/javascripttext/ecmascript都已经不被推荐使用,但人们一直以来使用的都还是type/javascript。实际上,服务器在传送JavaScript 文件时使用的 MIME 类型通常是application/x-javascript,但在type中设置这个值却可能导致脚本被忽略。这个属性不是必须的,如果没有指定这个属性,其默认值仍为text/javascript

转义字符“\”

使用<script>元素的方式有两种:直接在页面中嵌入 JavaScript 代码和包含外部 JavaScript 文件。

第一种:

<script type="text/javascript">
  function sayHi(){
      alert("Hi");
  }
</script>

第二种:

<script type="text/javascript" src="example.js"></script>

外部文件example.js只需包含通常要放在开始的<script>和结束的</script>之间的那些 JavaScript 代码即可。

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

需要注意的是,带有src属性的<script>元素不应该在其<script></script>标签之间再包含额外的JavaScript 代码。如果包含了嵌入的代码,则只会下载并执行外部脚本文件,嵌入的代码会被忽略。

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

引用外部JavaScript 文件的优点

并不存在必须使用外部文件的硬性规定,但支持使用外部文件的人多会强调如下优点:

  • 可维护性:遍及不同HTML 页面的JavaScript 会造成维护问题。但把所有JavaScript 文件都放在一个文件夹中,维护起来就轻松多了。而且开发人员因此能够在不触及HTML 标记的情况下,集中精力编辑JavaScript 代码。
  • 可缓存:浏览器能够根据具体的设置缓存链接的所有外部JavaScript 文件。也就是说,如果有两个页面都使用同一个文件,那么这个文件只需下载一次。因此,最终结果就是能够加快页面加载的速度。
  • 适应未来

使用<noscript>元素可以指定在不支持脚本的浏览器中显示的替代内容。但在启用了脚本的情况下,浏览器不会显示<noscript>元素中的任何内容。

第3章 基本概念

任何语言的核心都必然会描述这门语言的最基本的工作原理。而描述的内容通常都要涉及这门语言的语法、操作符、数据类型、内置功能等用于构建复杂解决方案的基本概念。

本章将主要按照第3版定义的ECMAScript 介绍这门语言的基本概念,并就第5版的变化给出说明。

ECMAScript 中的一切(变量、函数名和操作符)都区分大小写。

标识符

所谓标识符,就是指变量、函数、属性的名字,或者函数的参数。标识符可以是按照下列格式规则组合起来的一或多个字符:

  • 第一个字符必须是一个字母、下划线或一个美元符号
  • 其他字符可以是字母、下划线、美元符号或数字

按照惯例,ECMAScript 标识符采用驼峰大小写格式,也就是第一个字母小写,剩下的每个单词的首字母大写。

关键字和保留字

以下就是ECMAScript 的全部关键字:

以下是 ECMA-262 第3版定义的全部保留字:

在实现 ECMAScript 3 的JavaScript 引擎中使用关键字做标识符,会导致“Identifier Expected”错误。

变量

ECMAScript 的变量是松散类型的,所谓松散类型就是可以用来保存任何类型数据。换句话说,每个变量仅仅是一个用于保存值的占位符而已。定义变量时要使用var操作符(注意var是一个关键字),后跟变量名(即一个标识符)。

var操作符定义的变量将成为定义该变量的作用域中的局部变量。也就是说,如果在函数中使用var定义一个变量,那么这个变量在函数退出后就会被销毁。可以省略var操作符,从而创建一个全局变量。

虽然省略var操作符可以定义全局变量,但不推荐这种做法。因为在局部作用域中定义的全局变量很难维护,而且如果有意地忽略了var操作符,也会由于相应变量不会马上就有定义而导致不必要的混乱。如未经声明的变量赋值在严格模式下会导致抛出ReferenceError错误。

数据类型

ECMAScript 中有5种简单数据类型(基本数据类型):Undefinded、Null、Boolean、Number 和String。还有1种复杂数据类型——Object,Object 本质上是由一组无序的名值对组成的。

typeof 操作符

鉴于ECMAScript 是松散类型的,因此需要有一种手段来检测给定变量的数据类型——typeof 就是负责提供这方面信息的操作符。对一个值使用 typeof 操作符可能返回下列某个字符串:

  • ”undefined“——未定义
  • ”boolean“——布尔值
  • ”string“——字符串
  • ”number“——数值
  • ”object“——对象或null
  • ”function“——函数

注意,typeof 是一个操作符而不是函数,因此可以在其后使用圆括号,但不是必须的。

从技术角度讲,函数在ECMAScript 中是对象,不是一种数据类型。然而,函数也确实有一些特殊的属性,因此通过typeof 操作符来区分函数和其他对象是有必要的。

Undefined 类型

Undefined 类型只有一个值,即特殊的 undefined。在使用var声明变量但未对其加以初始化时,这个变量的值就是 undefined

Null 类型

Null 类型只有一个值,即特殊的 null。从逻辑角度来看,null 值表示一个空对象指针,而这也正是使用 typeof 操作符检测 null 值时会返回”object“的原因。

如果定义的变量准备在将来用于保存对象,那么最好将该变量初始化为null 而不是其他值。

Boolean 类型

Boolean 类型是ECMAScript 中使用最多的一种类型,该类型只有两个字面值:true 和 false。(区分大小写)

虽然Boolean 类型的字面值只有两个,但ECMAScript 中所有类型的值都有与这两个Boolean 值等价的值。要将一个值转换为其对应的Boolean 值,可以调用转型函数Boolean() 。下表给出了各种数据类型及其对应的转换规则:

这些转换规则对理解流控制语句自动执行相应的Boolean 转换非常重要。

Number 类型

最基本的数值字面量格式是十进制整数。

八进制字面值的第一位必须是0

十六进制字面量的前两位必须是0x

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

浮点数值

所谓浮点数值,就是该数值中必须包含一个小数点,并且小数点后面必须至少有一位数字。由于保存浮点数值需要的内存空间是保存整数值的两倍,因此ECMAScript 会不失时机地将浮点数值转换为整数值。

对于那些极大或极小的数值,可以用e 表示法(即科学计数法)表示的浮点数值表示。

浮点数值的最高精读是17位小数,但在进行算术计算时其精确度远远不如整数。例如,0.1加0.2的结果不是0.3,而是0.30000000000000004。因此,永远不要测试某个特定的浮点数值

数值范围

最小数值:Number.MIN_VALUE

最大数值:Number.MAX_VALUE

如果某次计算的结果得到了一个超出JavaScript 数值范围的值,那么这个数值将会被自动转换成特殊的Infinity 数值。要想确定一个数值是不是位于最大和最小的数值之间,可以使用isFinite()函数。这个函数在参数位于最大与最小数值之间时会返回true

NaN

NaN,即非数值(Not a Number)是一个特殊的数值,这个数值用于表示一个本来要返回数值的操作数未返回数值的情况(这样就不会抛出错误了)。例如,在其他编程语言中,任何数值除以非数值都会导致错误,从而停止执行代码。但在ECMAScript 中,任何数值除以非数值会返回NaN ,因此不会影响其他代码的执行。

NaN 本身有两个非同寻常的特点:

  • 任何涉及NaN的操作(例如NaN/10)都会返回NaN
  • NaN与任何值都不相等,包括NaN本身

针对NaN的这两个特点,ECMAScript 定义了isNaN()函数。这个函数接受一个参数,该参数可以是任何类型,而函数会帮我们确定这个参数是否”不是数值“。任何不能被转换为数值的值都会导致这个函数返回true。

isNaN()也适用于对象。

数值转换

有三个函数可以把非数值转换为数值:

  • Number()
  • parseInt()
  • parseFloat()

Number() 函数的转换规则如下:

  • 如果是Boolean 值,true 和 false 将分别被转换为1和0
  • 如果是数字值,只是简单的传入和返回
  • 如果是null 值,返回0
  • 如果是undefined,返回NaN
  • 字符串转换规则如下
  • 如果是对象,则调用对象的valueOf()方法,然后依照前面的规则转换返回的值。如果转换的结果是NaN,则调用对象的toString()方法,然后再次依照前面的规则转换返回的字符串值

字符串的Number() 转换规则:

  • 如果字符串中只包含数字,则将其转换为十进制数值
  • 如果字符串中包含有效的浮点格式,将其转换为对应的浮点数值
  • 如果字符串中包含有效的十六进制格式,将其转换为相同大小的十进制整数格式
  • 如果字符串是空的,将其转换为0
  • 如果字符串中包含除上述格式之外的字符,将其转换为NaN

示例:

一元加操作符的操作与Number() 函数相同

处理整数时更常用的是parseInt()函数。转换时,它会忽略字符串前面的空格,直至找到第一个非空格字符。如果第一个字符不是数字字符或者符号,parseInt()就会返回NaN。也许考虑到八进制和十六进制的问题。示例:

建议提供此函数第二个参数,表示转换时使用的基数(即多少进制)。多数情况下,我们要解析的都是十进制数值,因此始终将10作为第二个参数是非常有必要的。

parseInt()函数类似,parseFloat()也是从第一个字符开始解析每个字符。不同的是,parseFloat()只解析十进制数,因此不需要第二个参数。示例:

String 类型

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

与PHP中的双引号和单引号会影响对字符串的解释方式不同,ECMAScript 中的这两种方式并没有什么区别。

字符字面量

String 数据类型包含一些特殊的字符字面量,也叫转义序列,用于表示非打印字符,或者具有其他用途的字符。

这些字符字面量可以出现在字符串的任意位置,将被作为一个字符来解析

字符串的特点

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

转换为字符串

要把一个值转换为字符串有两种方式:

  • toString()——例如a.toString()
  • String()——例如String(a)

区别在于转换的数值为null 或undefined 的情况下,后者可以转换为相应的字面量。

要把某个值转换为字符串,可以使用加号操作符把它与一个字符串加在一起。

Object 类型

ECMAScript 中的对象其实就是一组数据和功能的集合。

在ECMAScript 中,Object 类型是所有它的实例的基础。换句话说,Object 类型所具有的任何属性和方法也同样存在于更具体的对象中。

Object 的每个实例都具有下列属性和方法:

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

由于在ECMAScript 中Object 是所有对象的基础,因此所有对象都具有这些基本的属性和方法。

操作符

一元操作符

只能操作一个值的操作符叫做一元操作符。一元操作符是ECMAScript 中最简单的操作符。

递增和递减操作符

分为前置型和后置型。

执行前置递增和递减操作时,变量的值都是在语句被求值以前改变的。而后置的是在包含它们的语句被求值之后才执行的。

所有这4个操作符对任何值都适用,规则如下:

  • 在应用于一个包含有效数字字符的字符串时,先将其转换为数字值,再执行加减1的操作。字符串变量变成数值变量。
  • 在应用于一个不包含有效数字字符的字符串时,将变量的值设置为NaN。字符串变量变成数值变量。
  • 在应用于布尔值时,false转换为0,true转换为1,之后再执行加减1的操作。布尔值变量变成数值变量。
  • 在应用于浮点数值时,执行加减1操作。
  • 在应用于对象时,先调用对象的valueOf() 方法以取得一个可供操作的值。然后对该值应用前述规则。如果结果是NaN,则再调用toString() 方法后再应用前述规则。对象变量变为数值变量。

示例:

一元加和减操作符

一元加和减操作符主要用于基本的算术运算。

对非数值应用一元加或减操作符时,该操作符会像Number() 转型函数一样对这个值执行转换。

位操作符

位操作符用在最基本的层次上,即按内存中表示数值的位来操作数值。速度更快

在ECMAScript 中,当对数值应用位操作符时,后台会发生如下转换过程:64位的数值被转换为32位数值,然后执行位操作,最后再将32位的结果转换回64位数值。

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

按位非(NOT)

按位非操作符由一个波浪线(~)表示,执行按位非的结果就是返回数值的反码。示例:

按位非操作的本质:操作数的负值减1

按位与(AND)

按位与操作符由一个和号字符(&)表示,它有两个操作符数。按位与操作只在两个数值对应位都是1时才返回1,任何一位是0,结果都是0。

按位或(OR)

按位或操作符由一条竖线符号(|)表示,有两个操作数。按位或操作在有一个位是1的情况下就返回1,而只有在两个位都是0的情况下才返回0。

按位异或(XOR)

按位异或操作符由一个插入符号(^)表示,有两个操作数。这个操作在两个数值对应位上只有一个1是才返回1,如果对应的两位都是1或都是0,则返回0。

左移

左移操作符由两个小于号(<<)表示,这个操作符会将数值的所有位向左移动指定的位数。

注意,左移不会影响操作数的符号位。

有符号的右移

有符号的右移操作符由两个大于号(>>)表示,这个操作符会将数值向右移动,但保留符号位(即正负号标记)。

在移位的过程中,原数值会出现空位。只不过这次的空位出现在原数值的左侧、符号位的右侧。而此时ECMAScript 会用符号位的值来填充所有空位,以便得到一个完整的值。

无符号右移

无符号右移操作符由3个大于号(>>>)表示,这个操作符会将数值的所有32位都向右移动,然后以0来填充空位。所以,对正数的无符号右移与有符号右移结果相同,但对负数的结果就不一样了。

无符号右移操作符会把负数的二进制码当成正数的二进制码。而且,由于负数以其绝对值的二进制补码形式表示,因此就会导致无符号右移后的结果非常之大。

布尔操作符

布尔操作符一共有3个:

  • 非(NOT)
  • 与(AND)
  • 或(OR)

逻辑非

逻辑非操作符由一个叹号(!)表示。同时使用两个逻辑非操作符,实际上就会模拟Boolean() 转型函数的行为。

逻辑与

逻辑与操作符由两个和号(&&)表示,有两个操作数。逻辑与操作属于短路操作,即如果第一个操作数能够决定结果,那么就不会再对第二个操作数求值。

逻辑或

逻辑或操作符由两个竖线符号(||)表示,有两个操作数。也属于短路操作符

我们可以利用逻辑或的属性为变量赋值,以避免null 或 undefined 值。

var myObject = preferredObject || backupObject;

ECMAScript 程序的赋值语句经常会使用这种模式。

乘性操作符

ECMAScript 定义了3个乘性操作符:乘法*、除法/和求模%。如果参与乘性计算的某个操作数不是数值,后台会先使用Number() 转型函数将其转换为数值。也就是说,空字符串会被当作0,布尔值true 将被当作1。

关系操作符

  • 小于<
  • 大于>
  • 小于等于<=
  • 大于等于>=

一些规则:

  • 如果两个操作数都是数值,则执行数值比较
  • 如果两个操作数都是字符串,则比较两个字符串对应的字符编码值
  • 如果一个操作数是数值,则将另一个操作数转换为一个数值,然后执行数值比较
  • 如果一个操作数是对象,则调用这个对象的valueof() 方法,用得到的结果按照前面的规则执行比较。如果对象没有valueof() 方法,则调用toString() 方法,并用得到的结果根据前面的规则执行比较。
  • 如果一个操作数是布尔值,则先将其转换为数值,然后再执行比较。

相等操作符

相等和不相等

  • 相等==
  • 不相等!=

这两个操作符都会先转换操作数(强制转型),然后再比较它们的相等性。

在转换不同的数据类型时,相等和不相等操作符遵循下列基本规则:

  • 如果有一个操作数是布尔值,则在比较相等性之前先将其转换为数值
  • 如果一个操作数是字符串,另一个操作数是数值,在比较相等性之前先将字符串转换为数值
  • 如果一个操作数是对象,另一个操作数不是,则调用对象的valueOf() 方法,用得到的基本类型值按照前面的规则进行比较

进行比较时遵循下列规则:

  • null 和undefined 是相等的
  • 要比较相等性之前,不能将null 和undefined 转换成其他任何值
  • 如果有一个操作数是NaN,则相等操作符返回false,而不相等操作符返回true。重要提示:即使两个操作数都是NaN,相等操作符也返回false;因为按照规则,NaN不等于NaN。
  • 如果两个操作数都是对象,则比较它们是不是同一个对象。如果两个操作数都指向同一个对象,则相等操作符返回true;否则,返回false。

下表列出一些特殊情况及比较结果:

全等和不全等

除了在比较之前不转换操作数之外,全等和不全等操作符没有什么区别。全等操作符由3个等于号(===)表示,它只在两个操作数未经转换就相等的情况下返回true。

不全等操作符由一个叹号后跟两个等于号(!==)表示,它在两个操作数未经转换就不相等的情况下返回true。

记住:null == undefined 会返回true,因为它们是类似的值;但null === undefined会返回false,因为它们是不同类型的值。

由于相等和不相等操作符存在类型转换问题,而为了保持代码中数据类型的完整性,我们推荐使用全等和不全等操作符。

条件操作符

variable = boolean_expression ? true_value : false_value;

例如:

var max = (num1 > num2) ? num1 : num2;

赋值操作符

简单的赋值操作符由等于号(=)表示,其作用就是把右侧的值赋给左侧的变量。

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

设计复合赋值操作符的主要目的是简化赋值操作,并不会带来任何性能的提升。

逗号操作符

使用逗号操作符可以在一条语句中执行多个操作。

语句

if 语句

语法:

if (condition) statement1 else statement2

其中的condition (条件)可以是任意表达式;而且对这个表达式求值的结果不一定是布尔值。ECMAScript 会自动调用Boolean() 转换函数将这个表达式的结果转换为一个布尔值。

do-while 语句

do-while 语句是一种后测试循环语句,即只有在循环体中的代码执行之后,才会测试出口条件。换句话说,在对条件表达式求值之前,循环体内的代码至少会被执行一次。语法:

do {
  statement
} while (expression)

while 语句

while 语句属于前测试循环语句,也就是说,在循环体内的代码被执行之前,就会对出口条件求值。因此,循环体内的代码有可能永远不会被执行。语法:

while (expression) statement

for 语句

for 语句也是一种前测试循环语句,但它具有在执行循环之前初始化变量和定义循环后要执行的代码的能力。

for (initialization; expression; post-loop-expression) statement

使用while 循环做不到的,使用 for 循环同样也做不到。也就是说,for 循环知识把与循环有关的代码集中在了一个位置。

由于ECMAScript 中不存在块级作用域,因此在循环内部定义的变量也可以在外部访问到。

for 语句中的初始化表达式、控制表达式和循环后表达式都是可选的。将这三个表达式全部省略,就会创建一个无限循环。而只给出控制表达式实际上就把for 循环换成了while 循环。

for-in 语句

for-in 语句是一种精准的迭代语句,可以用来枚举对象的属性。枚举的是属性而非值。

for (property in expression) statement

示例:

for (var propName in window){
  document.write(propName);
}

在这个例子中,使用for-in 循环来显示BOM 中window 对象的所有属性。

ECMAScript 对象的属性没有顺序。因此,通过for-in 循环输出的属性名的顺序是不可预测的。具体来讲,所有属性都会被返回一次,但返回的先后次序可能会因浏览器而异。

为了保证最大限度的兼容性,建议在使用for-in 循环之前,先检测确认该对象的值不是null 或 undefined。

label 语句

使用label 语句可以在代码中添加标签,以便将来使用。语法:

label: statement

定义的标签由break 或 continue 语句引用。加标签的语句一般都要与for 语句等循环语句配合使用。

break 和 continue 语句

break 和 continue 语句用于在循环中精确地控制代码的执行。其中,break 语句会立即退出循环,强制继续执行循环后面的语句。而 continue 语句虽然也是立即退出循环,但退出循环后会从循环的顶部继续执行。

break 和 continue 语句都可以与 label 语句联合使用,从而返回代码中特定的位置。这种联合使用的情况多发生在循环嵌套的情况下。

with 语句

with 语句的作用是将代码的作用域设置到一个特定的对象中。语法:

with (expression) statement;

严格模式下不允许使用with 语句,否则将视为语法错误。

由于大量使用with 语句会导致性能下降,同时也会给调试代码造成困难,因此在开发大型应用程序时,不建议使用with 语句。

switch 语句

switch (expression){
  case value: statement
    break;
  case value: statement
    break;
  default: statement
}

ECMAScript 中switch 语句中可以使用任何数据类型,无论是字符串,还是对象都没有问题。且每个case 的值不一定是常量,可以使变量,甚至是表达式。

switch 语句在比较值时使用的是全等操作符,因此不会发生类型转换。

函数

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

function functionName(arg0, arg1,...,argN){
  statements
}

函数中,可通过return 语句后跟要返回的值来实现返回值。位于return 语句之后的任何代码永远都不会执行。

理解参数

ECMAScript 函数的参数与大多数其他语言中函数的参数有所不同。ECMAScript 函数不介意传递进来多少个参数,也不在乎传进来参数是什么数据类型。也就是说,即便你定义的函数只接收两个参数,在调用这个函数时也未必一定要传递两个参数。可以传递一个、三个甚至不传递参数,而解析器永远不会有什么怨言。之所以会这样,原因是ECMAScript 中的参数在内部是用一个数组来表示的。函数接收到的始终是这个数组,而不关心数组中包含哪些参数。实际上,在函数体内可以通过arguments 对象来访问这个参数数组,从而获得传递给函数的每一个参数。

ECMAScript 函数的一个重要特点:命名的参数只提供便利,但不是必需的。

通过访问arguments 对象的length 属性可以获知有多少个参数传递给了函数。

arguments 对象可以与命名参数一起使用,且它的值永远与对应命名参数的值保持同步。

没有传递值的命名参数将自动被赋予undefined 值。

没有重载

ECMAScript 中定义了两个名字相同的函数,则该名字只属于后定义的函数。

通过检查传入参数中函数的类型和数量并作出不同的反应,可以模仿方法的重载。

例如:

function doAdd(){
  if(arguments.length == 1){
    alert(arguments[0] + 10);
  } else if(arguments.length == 2){
    alert(arguments[0] + arguments[1])
  }
}

第4章 变量、作用域和内存问题

基本类型和引用类型的值

ECMAScript 变量可能包含两种不同数据类型的值:基本类型值和引用类型值。基本类型值指的是简单的数据段,而引用类型值指那些可能由多个值构成的对象。

动态的属性

定义基本类型值和引用类型值的方式是类似的:创建一个变量并为该变量赋值。对于引用类型的值,我们可以为其添加属性和方法,也可以改变和删除其属性和方法。而基本类型值不能被添加。

复制变量值

除了保存的方式不同之外,在从一个变量向另一个变量复制基本类型值和引用类型值时,也存在不同。

如果从一个变量向另一个变量复制基本类型的值,会在变量对象上创建一个新值,然后把该值复制到为新变量分配的位置上。示例:

var num1 = 5;
var num2 = num1;

图示:

当从一个变量向另一个变量复制引用类型的值时,同样也会将存储在变量对象中的值复制一份放到为新变量分配的空间中。不同的是,这个值的副本实际上是一个指针,而这个指针指向存储在堆中的另一个对象。复制操作结束后,两个变量实际上将引用同一个对象。因此,改变其中一个变量,就会影响另一个变量,示例如下:

var obj1 = new Object();
var obj2 = obj1;
obj1.name = "Nicholas";
alert(obj2.name); //"Nicholas"

图示:

传递参数

ECMAScript 中所有函数的参数都是按值传递的。也就是说,把函数外部的值复制给函数内部的参数,就和把值从一个变量复制到另一个变量一样。基本类型值的传递如同基本类型变量的复制一样,而引用类型值的传递,则如同引用类型变量的复制一样。

在向参数传递基本类型的值时,被传递的值会被复制给一个局部变量(即命名参数,或者用ECMAScript 的概念来说,就是arguments 对象的一个元素)。在向参数传递引用类型的值时,会把这个值在内存中的地址复制给一个局部变量,因此这个局部变量的变化会反映在函数的外部

检测类型

typeof 操作符是确定一个变量是字符串、数值、布尔值,还是undefined 的最佳工具。如果变量的值是一个对象或null,则typeof 操作符会返回”object“。

检测是什么类型的对象,可以使用instanceof 操作符,语法如下:

result = variable instanceof constructor

如果变量是给定引用类型的实例,那么instanceof 操作符就会返回true。

根据规定,所有引用类型的值都是Object 的实例。因此,在检测一个引用类型值和Object 构造函数时,instanceof 操作符始终会返回true。当然,如果使用instanceof 操作符检测基本类型的值,则该操作符会返回false,因为基本类型不是对象。

小结

  • 基本类型值在内存中占据固定大小的空间,因此被保存在栈内存
  • 从一个变量向另一个变量复制基本类型的值,会创建这个值的一个副本
  • 引用类型的值是对象,保存在堆内存
  • 包含引用类型值的变量实际上包含的并不是对象本身,而是一个指向该对象的指针
  • 从一个变量向另一个变量复制引用类型的值,复制的其实是指针,因此两个变量最终都指向同一个对象
  • 确定一个值是哪种基本类型可以使用typeof 操作符,而确定一个值是哪种引用类型可以使用instanceof 操作符。

执行环境及作用域

执行环境定义了变量或函数有权访问的其他数据,决定了它们各自的行为。每个执行环境都有一个与之关联的变量对象,环境中定义的所有变量和函数都保存在这个对象中。虽然我们编写的代码无法访问这个对象,但解析器在处理数据时会在后台使用它。

全局执行环境是最外围的一个执行环境。在Web 浏览器中,全局执行环境被认为是window 对象,因此所有全局变量和函数都是作为window 对象的属性和方法创建的。

每个函数都有自己的执行环境。当执行流进入一个函数时,函数的环境就会被推入一个环境栈中。而在函数执行之后,栈将其环境弹出,把控制权返回给之前的执行环境。

当代码在一个环境中执行时,会创建变量对象的一个作用域链。作用域链的用途,是保证对执行环境有权访问的所有变量和函数的有序访问。作用域链的前端,始终都是当前执行的代码所在环境的变量对象。如果这个环境是函数,则将其活动对象作为变量对象。活动对象在最开始时只包含一个变量,即arguments 对象(这个对象在全局环境中是不存在的)。作用域链中的下一个变量对象来自包含(外部)环境,而再下一个变量对象则来自下一个包含环境。这样,一直延续到全局执行环境;全局执行环境的变量对象始终都是作用域链中的最后一个对象。

标识符解析是沿着作用域链一级一级地搜索标识符的过程。搜索过程始终从作用域链的前端开始,然后逐级地向后回溯,直至找到标识符为止(如果找不到标识符,通常会导致错误发生)。

内部环境可以通过作用域链访问所有的外部环境,但外部环境不能访问内部环境中的任何变量和函数。这些环境之间的联系是线性、有次序的。每个环境都可以向上搜索作用域链,以查询变量和函数名;但任何环境都不能通过向下搜索作用域链而进入另一个执行环境。

函数参数也被当做变量来对待,因此其访问规则与执行环境中的其他变量相同。

延长作用域链

当执行流进入下列任何一个语句时,作用域链就会得到加长:

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

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

没有块级作用域

使用var 声明的变量会自动被添加到最接近的环境中。在函数内部,最接近的环境就是函数的局部环境;在with 语句中,最接近的环境是函数环境。如果初始化变量时没有使用var 声明,该变量会自动被添加到全局环境。

当在某个环境中为了读取或写入而引用一个标识符时,必须通过搜索来确定该标识符实际代表什么。搜索过程从作用域链的前端开始,向上逐级查询与给定名字匹配的标识符。如果在局部环境中找到了该标识符,搜索过程停止,变量就绪。如果在局部环境中没有找到该变量名,则继续沿作用域链向上搜索。搜索过程将一直追溯到全局环境的变量对象。如果在全局环境中也没有找到这个标识符,则意味着该变量尚未声明。

在这个搜索过程中,如果存在一个局部的变量的定义,则搜索会自动停止,不再进入另一个变量对象。换句话说,如果局部环境中存在着同名标识符,就不会使用位于父环境中的标识符。

如果在局部环境中需要访问同名的全局变量,需添加window

小结

  • 执行环境有全局执行环境(也称为全局环境)和函数执行环境之分
  • 每次进入一个新执行环境,都会创建一个用于搜索变量和函数的作用域链
  • 函数的局部环境不仅有权访问函数作用域中的变量,而且有权访问其包含(父)环境,乃至全局环境
  • 全局环境只能访问在全局环境中定义的变量和函数,而不能直接访问局部环境中的任何数据
  • 变量的执行环境有助于确定应该何时释放内存

垃圾收集

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

  • 离开作用域的值将被自动标记为可以回收,因此将在垃圾收集期间被删除。
  • ”标记清除“是目前主流的垃圾收集算法,这种算法的思想是给当前不使用的值加上标记,然后再回收其内存。
  • 另一种垃圾收集算法是”引用计数“,这种算法的思想是跟踪记录所有值被引用的次数。JavaScript 引擎目前都不再使用这种算法。
  • 在代码中存在循环引用现象时,”引用计数“算法就会导致问题。
  • 解除变量的引用不仅有助于消除循环引用现象,而且对垃圾收集也有好处。为了确保有效地回收内存,应该及时解除不再使用的全局对象、全局对象属性以及循环应用变量的引用

确保占用最少的内存可以让页面获得更好的性能。而优化内存占用的最佳方式,就是为执行中的代码只保存必要的数据。一旦数据不再有用,最好通过将其设置为null 来释放其引用——这个做法叫做解除引用。这一做法适用于大多数全局变量和全局对象的属性。局部变量会在它们离开执行环境时自动被解除引用。

不过,解除一个值的引用并不意味着自动回收该值所占用的内存。解除引用的真正作用是让值脱离执行环境,以便垃圾收集器下次运行时将其回收。

推荐阅读更多精彩内容