1.原始值和引用值

在 ECMAScript 中,变量可以存在两种类型的值,即原始值和引用值。

  • 原始值

  • 存储在栈(stack)中的简单数据段,也就是说,它们的值直接存储在变量访问的位置。

  • 引用值

  • 存储在堆(heap)中的对象,也就是说,存储在变量处的值是一个指针(point),指向存储对象的内存处。

为变量赋值时,ECMAScript 的解释程序必须判断该值是原始类型,还是引用类型。要实现这一点,解释程序则需尝试判断该值是否为 ECMAScript 的原始类型之一,即 Undefined、Null、Boolean、Number 和 String 型。由于这些原始类型占据的空间是固定的,所以可将他们存储在较小的内存区域 - 栈中。这样存储便于迅速查寻变量的值。

在许多语言中,字符串都被看作引用类型,而非原始类型,因为字符串的长度是可变的。ECMAScript 打破了这一传统。

如果一个值是引用类型的,那么它的存储空间将从堆中分配。由于引用值的大小会改变,所以不能把它放在栈中,否则会降低变量查寻的速度。相反,放在变量的栈空间中的值是该对象存储在堆中的地址。地址的大小是固定的,所以把它存储在栈中对变量性能无任何负面影响。如下图所示:

存储在堆和栈中的原始值和引用值

原始类型

如前所述,ECMAScript 有 5 种原始类型(primitive type),即 Undefined、Null、Boolean、Number 和 String。ECMA-262 把术语类型(type)定义为值的一个集合,每种原始类型定义了它包含的值的范围及其字面量表示形式。

ECMAScript 提供了 typeof 运算符来判断一个值是否在某种类型的范围内。可以用这种运算符判断一个值是否表示一种原始类型:如果它是原始类型,还可以判断它表示哪种原始类型。

在稍后的章节,我们将为您深入讲解 ECMAScript 的原始类型和引用类型

2.值 undefined 并不同于未定义的值。但是,typeof 运算符并不真正区分这两种值。考虑下面的代码:

var oTemp;

alert(typeof oTemp);  //输出 "undefined"alert(typeof oTemp2);  //输出 "undefined"

前面的代码对两个变量输出的都是 "undefined",即使只有变量 oTemp2 从未被声明过。如果对 oTemp2 使用除 typeof 之外的其他运算符的话,会引起错误,因为其他运算符只能用于已声明的变量上。

例如,下面的代码将引发错误:

var oTemp;
alert(oTemp2 == undefined);

当函数无明确返回值时,返回的也是值 "undefined",如下所示:

function testFunc() {
}

alert(testFunc() == undefined);  //输出 "true"

3.ECMA-262 中定义的最特殊的类型是 Number 类型。这种类型既可以表示 32 位的整数,还可以表示 64 位的浮点数。对于浮点字面量的有趣之处在于,用它进行计算前,真正存储的是字符串。

特殊的 Number 值

几个特殊值也被定义为 Number 类型。前两个是 Number.MAX_VALUE 和 Number.MIN_VALUE,它们定义了 Number 值集合的外边界。所有 ECMAScript 数都必须在这两个值之间。不过计算生成的数值结果可以不落在这两个值之间。

当计算生成的数大于 Number.MAX_VALUE 时,它将被赋予值 Number.POSITIVE_INFINITY,意味着不再有数字值。同样,生成的数值小于 Number.MIN_VALUE 的计算也会被赋予值 Number.NEGATIVE_INFINITY,也意味着不再有数字值。如果计算返回的是无穷大值,那么生成的结果不能再用于其他计算。

事实上,有专门的值表示无穷大,(如你猜到的)即 Infinity。Number.POSITIVE_INFINITY 的值为 Infinity。Number.NEGATIVE_INFINITY 的值为 -Infinity。

由于无穷大数可以是正数也可以是负数,所以可用一个方法判断一个数是否是有穷的(而不是单独测试每个无穷数)。可以对任何数调用 isFinite() 方法,以确保该数不是无穷大。例如:

var iResult = iNum * some_really_large_number;if (isFinite(iResult)) {
    alert("finite");
}

else {
    alert("infinite");
}

最后一个特殊值是 NaN,表示非数(Not a Number)。NaN 是个奇怪的特殊值。一般说来,这种情况发生在类型(String、Boolean 等)转换失败时。例如,要把单词 blue 转换成数值就会失败,因为没有与之等价的数值。与无穷大一样,NaN 也不能用于算术计算。NaN 的另一个奇特之处在于,它与自身不相等,这意味着下面的代码将返回 false:

alert(NaN == NaN);  //输出 "false"

出于这个原因,不推荐使用 NaN 值本身。函数 isNaN() 会做得相当好:

alert(isNaN("blue"));  //输出 "true"alert(isNaN("666"));  //输出 "false"

3.

String 类型的独特之处在于,它是唯一没有固定大小的原始类型。可以用字符串存储 0 或更多的 Unicode 字符,有 16 位整数表示(Unicode 是一种国际字符集,本教程后面将讨论它)。

字符串中每个字符都有特定的位置,首字符从位置 0 开始,第二个字符在位置 1,依此类推。这意味着字符串中的最后一个字符的位置一定是字符串的长度减 1:

字符串的长度和字符的位置

字符串字面量是由双引号(")或单引号(')声明的。而 Java 则是用双引号声明字符串,用单引号声明字符。但是由于 ECMAScript 没有字符类型,所以可使用这两种表示法中的任何一种。例如,下面的两行代码都有效:

var sColor1 = "red";
var sColor2 = 'red';

String 类型还包括几种字符字面量,Java、C 和 Perl 的开发者应该对此非常熟悉。

下面列出了 ECMAScript 的字符字面量:

字面量含义
\n换行
\t制表符
\b空格
\r回车
\f换页符
\\反斜杠
\'单引号
\"双引号
\0nnn八进制代码 nnn 表示的字符(n 是 0 到 7 中的一个八进制数字)
\xnn十六进制代码 nn 表示的字符(n 是 0 到 F 中的一个十六进制数字)
\unnnn十六进制代码 nnnn 表示的 Unicode 字符(n 是 0 到 F 中的一个十六进制数字)

4.Boolean 对象

Boolean 对象是 Boolean 原始类型的引用类型。

要创建 Boolean 对象,只需要传递 Boolean 值作为参数:

var oBooleanObject = new Boolean(true);

Boolean 对象将覆盖 Object 对象的 ValueOf() 方法,返回原始值,即 true 和 false。ToString() 方法也会被覆盖,返回字符串 "true" 或 "false"。

遗憾的是,在 ECMAScript 中很少使用 Boolean 对象,即使使用,也不易理解。

问题通常出现在 Boolean 表达式中使用 Boolean 对象时。例如:

var oFalseObject = new Boolean(false);
var bResult = oFalseObject && true;	//输出 true

在这段代码中,用 false 值创建 Boolean 对象。然后用这个值与原始值 true 进行 AND 操作。在 Boolean 运算中,false 和 true 进行 AND 操作的结果是 false。不过,在这行代码中,计算的是 oFalseObject,而不是它的值 false。

正如前面讨论过的,在 Boolean 表达式中,所有对象都会被自动转换为 true,所以 oFalseObject 的值是 true。然后 true 再与 true 进行 AND 操作,结果为 true。

注意:虽然你应该了解 Boolean 对象的可用性,不过最好还是使用 Boolean 原始值,避免发生这一节提到的问题。

5.void

void 运算符对任何值返回 undefined。该运算符通常用于避免输出不应该输出的值,例如,从 HTML 的 <a> 元素调用 JavaScript 函数时。要正确做到这一点,函数不能返回有效值,否则浏览器将清空页面,只显示函数的结果。例如:

<a href="javascript:window.open('about:blank')">Click me</a>

如果把这行代码放入 HTML 页面,点击其中的链接,即可看到屏幕上显示 "[object]"。TIY

这是因为 window.open() 方法返回了新打开的窗口的引用。然后该对象将被转换成要显示的字符串。

要避免这种效果,可以用 void 运算符调用 window.open() 函数:

<a href="javascript:void(window.open('about:blank'))">Click me</a>

这使 window.open() 调用返回 undefined,它不是有效值,不会显示在浏览器窗口中。

提示:请记住,没有返回值的函数真正返回的都是 undefined。

6.在使用前缀式运算符时,注意增量和减量运算符都发生在计算表达式之前。考虑下面的例子:

var iNum = 10;
--iNum;
alert(iNum);	//输出 "9"
alert(--iNum);	//输出 "8"
alert(iNum);	//输出 "8"

在算术表达式中,前增量和前减量运算符的优先级是相同的,因此要按照从左到右的顺序计算之。例如:

var iNum1 = 2;
var iNum2 = 20;
var iNum3 = --iNum1 + ++iNum2;	//等于 "22"var iNum4 = iNum1 + iNum2;		//等于 "22"

在前面的代码中,iNum3 等于 22,因为表达式要计算的是 1 + 21。变量 iNum4 也等于 22,也是 1 + 21。


与前缀式运算符不同的是,后缀式运算符是在计算过包含它们的表达式后才进行增量或减量运算的。考虑以下的例子:

var iNum = 10;
iNum--;
alert(iNum);	//输出 "9"
alert(iNum--);	//输出 "9"
alert(iNum);	//输出 "8"

在算术表达式中,后增量和后减量运算符的优先级是相同的,因此要按照从左到右的顺序计算之。例如:

var iNum1 = 2;
var iNum2 = 20;
var iNum3 = iNum1-- + iNum2++;	//等于 "22"var iNum4 = iNum1 + iNum2;		//等于 "22"

在前面的代码中,iNum3 等于 22,因为表达式要计算的是 2 + 20。变量 iNum4 也等于 22,不过它计算的是 1 + 21,因为增量和减量运算都在给 iNum3 赋值后才发生。

7.比较数字和字符串

另一种棘手的状况发生在比较两个字符串形式的数字时,比如:

var bResult = "25" < "3";
alert(bResult);	//输出 "true"

上面这段代码比较的是字符串 "25" 和 "3"。两个运算数都是字符串,所以比较的是它们的字符代码("2" 的字符代码是 50,"3" 的字符代码是 51)。

不过,如果把某个运算数该为数字,那么结果就有趣了:

var bResult = "25" < 3;
alert(bResult);	//输出 "false"

这里,字符串 "25" 将被转换成数字 25,然后与数字 3 进行比较,结果不出所料。

无论何时比较一个数字和一个字符串,ECMAScript 都会把字符串转换成数字,然后按照数字顺序比较它们。

不过,如果字符串不能转换成数字又该如何呢?考虑下面的例子:

var bResult = "a" < 3;
alert(bResult);

你能预料到这段代码输出什么吗?字母 "a" 不能转换成有意义的数字。不过,如果对它调用 parseInt() 方法,返回的是 NaN。根据规则,任何包含 NaN 的关系运算符都要返回 false,因此这段代码也输出 false:

var bResult = "a" >= 3;
alert(bResult);

通常,如果小于运算的两个值返回 false,那么大于等于运算必须返回 true,不过如果某个数字是 NaN,情况则非如此。

8.判断两个变量是否相等是程序设计中非常重要的运算。在处理原始值时,这种运算相当简单,但涉及对象,任务就稍有点复杂。

ECMAScript 提供了两套等性运算符:等号和非等号用于处理原始值,全等号和非全等号用于处理对象。

http://www.w3school.com.cn/js/pro_js_operators_equality.asp

9.检测参数个数

还可以用 arguments 对象检测函数的参数个数,引用属性 arguments.length 即可。

下面的代码将输出每次调用函数使用的参数个数:

function howManyArgs() {
  alert(arguments.length);
}

howManyArgs("string", 45);
howManyArgs();
howManyArgs(12);

上面这段代码将依次显示 "2"、"0" 和 "1"。

注释:与其他程序设计语言不同,ECMAScript 不会验证传递给函数的参数个数是否等于函数定义的参数个数。开发者定义的函数都可以接受任意个数的参数(根据 Netscape 的文档,最多可接受 255 个),而不会引发任何错误。任何遗漏的参数都会以 undefined 传递给函数,多余的函数将忽略。

10.Function 对象(类)

ECMAScript 最令人感兴趣的可能莫过于函数实际上是功能完整的对象。

Function 类可以表示开发者定义的任何函数。

用 Function 类直接创建函数的语法如下:

var function_name = new function(arg1, arg2, ..., argN, function_body)

在上面的形式中,每个 arg 都是一个参数,最后一个参数是函数主体(要执行的代码)。这些参数必须是字符串。

记得下面这个函数吗?

function sayHi(sName, sMessage) {
  alert("Hello " + sName + sMessage);
}

还可以这样定义它:

var sayHi 
= 
new Function("sName", "sMessage", "alert(\"Hello \" + sName + sMessage);");

虽然由于字符串的关系,这种形式写起来有些困难,但有助于理解函数只不过是一种引用类型,它们的行为与用 Function 类明确创建的函数行为是相同的。

11.函数重载

请看下面这个例子:

function doAdd(iNum) {
  alert(iNum + 20);
}

function doAdd(iNum) {
  alert(iNum + 10);
}

doAdd(10);	//输出 "20"

如你所知,第二个函数重载了第一个函数,使 doAdd(10) 输出了 "20",而不是 "30"。

如果以下面的形式重写该代码块,这个概念就清楚了:

var doAdd = new Function("iNum", "alert(iNum + 20)");
var doAdd = new Function("iNum", "alert(iNum + 10)");
doAdd(10);

请观察这段代码,很显然,doAdd 的值被改成了指向不同对象的指针。函数名只是指向函数对象的引用值,行为就像其他对象一样。甚至可以使两个变量指向同一个函数:

var doAdd = new Function("iNum", "alert(iNum + 10)");
var alsodoAdd = doAdd;
doAdd(10);	//输出 "20"alsodoAdd(10);	//输出 "20"

在这里,变量 doAdd 被定义为函数,然后 alsodoAdd 被声明为指向同一个函数的指针。用这两个变量都可以执行该函数的代码,并输出相同的结果 - "20"。因此,如果函数名只是指向函数的变量,那么可以把函数作为参数传递给另一个函数吗?回答是肯定的!

function callAnotherFunc(fnFunction, vArgument) {
  fnFunction(vArgument);
}

var doAdd = new Function("iNum", "alert(iNum + 10)");

callAnotherFunc(doAdd, 10);	//输出 "20"

在上面的例子中,callAnotherFunc() 有两个参数 - 要调用的函数和传递给该函数的参数。这段代码把 doAdd() 传递给 callAnotherFunc() 函数,参数是 10,输出 "20"。

注意:尽管可以使用 Function 构造函数创建函数,但最好不要使用它,因为用它定义函数比用传统方式要慢得多。不过,所有函数都应看作 Function 类的实例。

12.Function 对象的 length 属性

如前所述,函数属于引用类型,所以它们也有属性和方法。

ECMAScript 定义的属性 length 声明了函数期望的参数个数。例如:

function doAdd(iNum) {
  alert(iNum + 10);
}

function sayHi() {
  alert("Hi");
}

alert(doAdd.length);	//输出 "1"alert(sayHi.length);	//输出 "0"

函数 doAdd() 定义了一个参数,因此它的 length 是 1;sayHi() 没有定义参数,所以 length 是 0。

记住,无论定义了几个参数,ECMAScript 可以接受任意多个参数(最多 255 个),这一点在《函数概述》这一章中讲解过。属性 length 只是为查看默认情况下预期的参数个数提供了一种简便方式。

Function 对象的方法

Function 对象也有与所有对象共享的 valueOf() 方法和 toString() 方法。这两个方法返回的都是函数的源代码,在调试时尤其有用。例如:

function doAdd(iNum) {
  alert(iNum + 10);
}

document.write(doAdd.toString());//function doAdd(iNum) { alert(iNum + 10); }
document.write(doAdd.valueOf());//function doAdd(iNum) { alert(iNum + 10); }

13.ECMAScript 最易让人误解的一点是,它支持闭包(closure)。

闭包,指的是词法表示包括不被计算的变量的函数,也就是说,函数可以使用函数之外定义的变量。

简单的闭包实例

在 ECMAScript 中使用全局变量是一个简单的闭包实例。请思考下面这段代码:

var sMessage = "hello world";

function sayHelloWorld() {
  alert(sMessage);
}

sayHelloWorld();

在上面这段代码中,脚本被载入内存后,并没有为函数 sayHelloWorld() 计算变量 sMessage 的值。该函数捕获 sMessage 的值只是为了以后的使用,也就是说,解释程序知道在调用该函数时要检查 sMessage 的值。sMessage 将在函数调用 sayHelloWorld() 时(最后一行)被赋值,显示消息 "hello world"。

复杂的闭包实例

在一个函数中定义另一个会使闭包变得更加复杂。例如:

var iBaseNum = 10;

function addNum(iNum1, iNum2) {
  function doAdd() {
    return iNum1 + iNum2 + iBaseNum;
  }
  return doAdd();
}

这里,函数 addNum() 包括函数 doAdd() (闭包)。内部函数是一个闭包,因为它将获取外部函数的参数 iNum1 和 iNum2 以及全局变量 iBaseNum 的值。 addNum() 的最后一步调用了 doAdd(),把两个参数和全局变量相加,并返回它们的和。

这里要掌握的重要概念是,doAdd() 函数根本不接受参数,它使用的值是从执行环境中获取的。

可以看到,闭包是 ECMAScript 中非常强大多用的一部分,可用于执行复杂的计算。

提示:就像使用任何高级函数一样,使用闭包要小心,因为它们可能会变得非常复杂。

14.面向对象语言的要求

一种面向对象语言需要向开发者提供四种基本能力:

  1. 封装 - 把相关的信息(无论数据或方法)存储在对象中的能力

  2. 聚集 - 把一个对象存储在另一个对象内的能力

  3. 继承 - 由另一个类(或多个类)得来类的属性和方法的能力

  4. 多态 - 编写能以多种方法运行的函数或方法的能力

ECMAScript 支持这些要求,因此可被是看做面向对象的。

14.对象废除

ECMAScript 拥有无用存储单元收集程序(garbage collection routine),意味着不必专门销毁对象来释放内存。当再没有对对象的引用时,称该对象被废除(dereference)了。运行无用存储单元收集程序时,所有废除的对象都被销毁。每当函数执行完它的代码,无用存储单元收集程序都会运行,释放所有的局部变量,还有在一些其他不可预知的情况下,无用存储单元收集程序也会运行。

把对象的所有引用都设置为 null,可以强制性地废除对象。例如:

var oObject = new Object;// do something with the object hereoObject = null;

当变量 oObject 设置为 null 后,对第一个创建的对象的引用就不存在了。这意味着下次运行无用存储单元收集程序时,该对象将被销毁。

每用完一个对象后,就将其废除,来释放内存,这是个好习惯。这样还确保不再使用已经不能访问的对象,从而防止程序设计错误的出现。此外,旧的浏览器(如 IE/MAC)没有全面的无用存储单元收集程序,所以在卸载页面时,对象可能不能被正确销毁。废除对象和它的所有特性是确保内存使用正确的最好方法。

注意:废除对象的所有引用时要当心。如果一个对象有两个或更多引用,则要正确废除该对象,必须将其所有引用都设置为 null。

15.早绑定和晚绑定

所谓绑定(binding),即把对象的接口与对象实例结合在一起的方法。

早绑定(early binding)是指在实例化对象之前定义它的属性和方法,这样编译器或解释程序就能够提前转换机器代码。在 Java 和 Visual Basic 这样的语言中,有了早绑定,就可以在开发环境中使用 IntelliSense(即给开发者提供对象中属性和方法列表的功能)。ECMAScript 不是强类型语言,所以不支持早绑定。

另一方面,晚绑定(late binding)指的是编译器或解释程序在运行前,不知道对象的类型。使用晚绑定,无需检查对象的类型,只需检查对象是否支持属性和方法即可。ECMAScript 中的所有变量都采用晚绑定方法。这样就允许执行大量的对象操作,而无任何惩罚。

16.在 ECMAScript 中,所有对象并非同等创建的。

一般来说,可以创建并使用的对象有三种:本地对象、内置对象和宿主对象。

17.ECMAScript 对象作用域

作用域指的是变量的适用范围。

公用、私有和受保护作用域

概念

在传统的面向对象程序设计中,主要关注于公用和私有作用域。公用作用域中的对象属性可以从对象外部访问,即开发者创建对象的实例后,就可使用它的公用属性。而私有作用域中的属性只能在对象内部访问,即对于外部世界来说,这些属性并不存在。这意味着如果类定义了私有属性和方法,则它的子类也不能访问这些属性和方法。

受保护作用域也是用于定义私有的属性和方法,只是这些属性和方法还能被其子类访问。

ECMAScript 只有公用作用域

对 ECMAScript 讨论上面这些作用域几乎毫无意义,因为 ECMAScript 中只存在一种作用域 - 公用作用域。ECMAScript 中的所有对象的所有属性和方法都是公用的。因此,定义自己的类和对象时,必须格外小心。记住,所有属性和方法默认都是公用的!

建议性的解决方法

许多开发者都在网上提出了有效的属性作用域模式,解决了 ECMAScript 的这种问题。

由于缺少私有作用域,开发者确定了一个规约,说明哪些属性和方法应该被看做私有的。这种规约规定在属性前后加下划线:

obj._color_ = "blue";

这段代码中,属性 color 是私有的。注意,下划线并不改变属性是公用属性的事实,它只是告诉其他开发者,应该把该属性看作私有的。

有些开发者还喜欢用单下划线说明私有成员,例如:obj._color。

静态作用域

静态作用域定义的属性和方法任何时候都能从同一位置访问。在 Java 中,类可具有属性和方法,无需实例化该类的对象,即可访问这些属性和方法,例如 java.net.URLEncoder 类,它的函数 encode() 就是静态方法。

ECMAScript 没有静态作用域

严格来说,ECMAScript 并没有静态作用域。不过,它可以给构造函数提供属性和方法。还记得吗,构造函数只是函数。函数是对象,对象可以有属性和方法。例如:

function sayHello() {
  alert("hello");
}

sayHello.alternate = function() {
  alert("hi");
}

sayHello();		//输出 "hello"sayHello.alternate();	//输出 "hi"

这里,方法 alternate() 实际上是函数 sayHello 的方法。可以像调用常规函数一样调用 sayHello() 输出 "hello",也可以调用 sayHello.alternate() 输出 "hi"。即使如此,alternate() 也是 sayHello() 公用作用域中的方法,而不是静态方法。

关键字 this

this 的功能

在 ECMAScript 中,要掌握的最重要的概念之一是关键字 this 的用法,它用在对象的方法中。关键字 this 总是指向调用该方法的对象,例如:

var oCar = new Object;
oCar.color = "red";
oCar.showColor = function() {
  alert(this.color);
};

oCar.showColor();		//输出 "red"

TIY

在上面的代码中,关键字 this 用在对象的 showColor() 方法中。在此环境中,this 等于 oCar。下面的代码与上面的代码的功能相同:

var oCar = new Object;
oCar.color = "red";
oCar.showColor = function() {
  alert(oCar.color);
};

oCar.showColor();		//输出 "red"

TIY

使用 this 的原因

为什么使用 this 呢?因为在实例化对象时,总是不能确定开发者会使用什么样的变量名。使用 this,即可在任何多个地方重用同一个函数。请思考下面的例子:

function showColor() {
  alert(this.color);
};

var oCar1 = new Object;
oCar1.color = "red";
oCar1.showColor = showColor;

var oCar2 = new Object;
oCar2.color = "blue";
oCar2.showColor = showColor;

oCar1.showColor();		//输出 "red"oCar2.showColor();		//输出 "blue"

TIY

在上面的代码中,首先用 this 定义函数 showColor(),然后创建两个对象(oCar1 和 oCar2),一个对象的 color 属性被设置为 "red",另一个对象的 color 属性被设置为 "blue"。两个对象都被赋予了属性 showColor,指向原始的 showColor () 函数(注意这里不存在命名问题,因为一个是全局函数,而另一个是对象的属性)。调用每个对象的 showColor(),oCar1 输出是 "red",而 oCar2 的输出是 "blue"。这是因为调用 oCar1.showColor() 时,函数中的 this 关键字等于 oCar1。调用 oCar2.showColor() 时,函数中的 this 关键字等于 oCar2。

注意,引用对象的属性时,必须使用 this 关键字。例如,如果采用下面的代码,showColor() 方法不能运行:

function showColor() {
  alert(color);
};

如果不用对象或 this 关键字引用变量,ECMAScript 就会把它看作局部变量或全局变量。然后该函数将查找名为 color 的局部或全局变量,但是不会找到。结果如何呢?该函数将在警告中显示 "null"。

18.ECMAScript 定义类或对象

http://www.w3school.com.cn/js/pro_js_object_defining.asp