JavaScript 编程全解

函数JavaScript 的函数是一种对象。

对象

Javascript 中没有这样的语言结构, Javascript 中的对象是一个 名称配对的集合。这样一对儿名称和值的配对被称为属性。例如一个人的属性有:

  • 身高: 178cm
  • 体重 65kg
  • 年龄 28

所以, Javascript 对象可以定义为属性 的集合。Javascript 的对象字面量

1
2
// 对象字面量表达式的语法
{ 属性名 : 属性值,  属性名 : 属性值, ...... }

属性名可以是标识符。字符串和数值:

1
2
3
4
5
6
7
// 对象字面量表达式的例子

{ x: 2, y:1 }        // 属性名是标识符
{ "x":2, "y":1 }    // 属性名是字符串值
{ 'x':2, 'y':1 }      // 属性名是字符串值
{ 1:2, 2:1 }        // 属性名是数值
{ x:2, y:1, enable:true, color:{ r:255, g:255, b:255 } } // 各种类型的属性值

对对象字面量表达式求值所得到的结果,是所生成对象的一个引用。

1
2
3
4
// 对象字面量表达式与赋值表达式
js> var obj = { x:3, y:4 };  // 所生成对象的引用将被赋值给变量 obj
js> typeof obj;  // 通过 typeof 运算符来判别 obj 的类型,得到的结果是 object
object

属性访问

通过点语法和方括号访问属性:

1
2
3
4
5
6
7
js> var obj = {x:3, y:4}
js> typeof(obj)
"object"
js> print(obj.x)
3
js> print(obj['x'])
3

方法

可以把任意类型的值、对象或者函数赋值给对象的属性。正如前节所讲,对匿名函数表达式求值所
得到的结果是函数对象的引用,所以,也可以像下面这样来书写。

1
js> obj.fn = function (a, b) {return Number(a) + Number(b); };      // 将函数赋值给对象 obj 的属性 fn

可以像下面这样,对被赋值给属性的函数进行调用。

1
2
js> obj.fn(3, 4); // 调用函数
7

回顾一下之前章节的说明可以发现,在代码清单 2.2 之后还可以像下面这样书写。

1
2
3
4
js> obj.fn2 = sum;  // sum 是在代码清单 2.2 中定义的函数
js> obj.fn2(3, 4); // 调用函数

7

数组

Javascript 中数组中的元素可以是不同的类型:

1
2
var arr = [1, "foo", "bar", 5]
print(arr[1])     ///  foo

数据类型

像 Java 这样,变量具有数据类型的语言,被称为静态数据类型语言;而像 JavaScript 这样,变量没有类型的语言,则被称为动态数据类型语言。
在 JavaScript 中,字符串值会被隐式地转换为字符串对象类型

在 JavaScript 中书写 ‘012’.lenght 的话,(属于内建类型的)字符串值会先被隐式地转换为
字符串对象 ,然后再读取字符串对象的 length 属性。隐式类型转换也能反向进行。

1
2
3
4
var sobj = new String('abc'); // 生成字符串对象
var s = sobj + 'def';               // 将字符串对象隐式转换为了字符串值
print(s);
// abcdef

字符串值和字符串对象之间可以进行隐式类型转换。因此,一般来说并不需要在意值和对象之间的
区别。不过正因看起来非常相似,所以会存在一些陷阱。例如,在判定两者是否相等上是有差异的。对 象的相等运算,判断的是两者是否引用了同一个对象(而非两者的内容是否相同)

1
2
3
4
5
6
7
8
9
10
11
12
13
js> var sobj1 = new String('abc');
js> var sobj2 = new String('abc');
js> sobj1 == sobj2;  // 虽然字符串的内容相同,但是并非引用了同一个对象,所以结果是 false
false
js> sobj1 === sobj2; // 虽然字符串的内容相同,但是并非引用了同一个对象,所以结果是 false
false

js> sobj3 = sobj2
(new String("abc"))
js> sobj3 == sobj2
true
js> sobj3 === sobj2
true

上面的两个字符串对象,在通过 + 与空字符串值连接之
后,就会进行隐式数据类型转换而变为字符串值,从而结果也将发生变化。(比较的是两者的内容是否相同)

1
2
3
4
5
// 继续之前的代码(以下只是用于说明的代码,实际中并不推荐这样使用)
js> sobj1 + '' == sobj2 + '';
true
js> sobj1 + '' === sobj2 + '';
true

对于字符串值和字符串对象的等值判断,如果使用的是会进行隐式数据类型转换的 == 运算,则只会判定其内容是否相同,如果内容相同则结果为真。

1
2
3
4
5
6
js> var sobj = new String('abc');
js> var s = 'abc';
js> sobj == s;  // 进行数据类型转换的等值运算的结果为 true
true;
js> sobj === s;  // 不进行数据类型转换的等值运算的结果为 false
false

要尽量避免显式地使用 new Stirng 生成字符串对象。尽情享受隐式转换就好了。

1
2
3
4
5
js> var s = 'abc';  // 返回字符串值下标为 1 的字符
js> s.charAt(1);
b
js> 'abc'.charAt(1); // 对于字符串字面量也能像这样进行方法调用
b

调用 String 函数进行显式数据类型转换:

1
2
3
4
5
6
7
8
js> var s = String('abc');
js> typeof s; // 变量 s 的值是字符串型
string
js> var s = String(47);  // 由数值类型向字符串值类型的显式数据类型变换
js> print(s);
47
js> typeof s; // 变量 s 的值是字符串型
string

String 类的函数以及构造函数调用

函数或是构造函数
说明

String([value])
将参数 value 转换为字符串值类型

new String([value])
生成 String 类的实例

String类的属性

属性名
说明

fromCharCode([char0[,char1,…]])
将参数 value 转换为字符串值类型

length
值为1

prototype
原型链

String.prototype 对象所具有的属性

属性名
说明

charAt(pos)
返回下标 pos 位置字符长度为 1 的字符串值。下标从 0 开始。如果下标超界则返回空字符串值

charCodeAt(pos)
返回下标 pos 位置处字符的字符编码。如果超过了下标的范围, 则返回 NaN

concat([string0, string1,…])
和参数字符串相连接后返回新的字符串值

constructor
引用一个 String 类对象

indexOf(searchString[, pos])
返回在字符串中第一个遇到的字符串值 searchString 的下标值。可以通过第二个可选参数指定搜索的起始位置。如果没有找到符合条件的结果, 则返回 -1

localeCompare(that)
比较和本地运行环境相关的字符串。根据比较的结果分别返回正数、0 或者负数

match(regexp)
返回匹配正则表达式 regexp 的结果

quote()
Javascript 自定义的增强功能。在字符串外加上双引号之后返回这一新的字符串值

replace(searchValue, replaceValue)
将 searchValue (正则表达式活字符串值) 替换为 replaceValue (字符串或函数) 后返回经过替换后的字符串

search(regexp)
返回匹配正则表达式 regexp 的位置的下标

slice(start, end)
将参数 start 开始至 end 结束的字符串部分作为新的字符串返回。 如果 start 和 end 是负数, 则返回从末尾逆向起数的下标值

split(separator, limit)
根据字符串或正则表达式形式的参数 separator 将字符串分割, 返回相应的字符串值数组

substring(start, end)
将参数 start 开始至 end 结束的字符串部分作为新的字符串返回。其作用和 slice 相同, 但是不支持以负数作为参数

toLocaleLowerCase()
将字符串中所有字符转换为和本地环境相应的小写字符

toLocaleUpperCase()
将字符串中所有字符转换为和本地环境相应的大写字符

toLowerCase()
将字符串中所有字符转换为小写字符

toSource()
Javascript 自定义的增强功能。返回用于生成 String 实例的字符串(即源代码)

toString()
将 Stirng 实例转换为字符串值(并返回)

toUpperCase()
将字符串中的所有字符转换为大写字符

trim()
去除字符串前后的空白符

trimLeft()
Javascript 自定义的增强功能。去除字符串左侧(头部) 的空白符

trimRight()
Javascript 自定义的增强功能。去除字符串右侧(尾部) 的空白符

valueOf()
将 String 实例转换为字符串值并返回

还可以像下面这样,通过数值属性获取指定下标的字符(不过这是 JavaScript 自定义的增强功能)。其返回值是一个 String 对象。

1
2
3
4
5
js> var s = new String('abc');
js> print(s[1]); // 下标为 1 的字符
b
js> print('abc'[2]); // 由于有隐式数据类型转换,所以对字符串值也能进行这样的操作
c

数值

Javascript 中, 大部分情况下浮点数只能表达数值的近似值。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
js> 0.1 + 0.2;  // 0.1 与 0.2 的和并不是 0.3。
0.30000000000000004
js> (0.1 + 0.2) == 0.3  // 两者不一致。
false
js> (0.1 + 0.2) === 0.3  // 两者不一致。
false
js> 1/3 // 1 除以 3 之后的近似结果。
0.333333333333333
js> 10/3 – 3; // 这同样是近似值。
0.333333333333333
js> (10/3 – 3) == (1/3);  // 这两个近似值是不一致的。
false
js> (10/3 – 3) === (1/3);
false

而 Perl 6 就不会出现这种情况。数值也存在数值对象:

1
2
3
4
5
6
7
8
9
10
js> var nobj = new Number(1);
js> var nobj1 = new Number(1);
js> nobj == nobj1;  // 虽然值是相同的,但是所引用的对象不同,因而结果为 false
false
js> nobj === nobj1;  // 虽然值是相同的,但是所引用的对象不同,因而结果为 false
false
js> nobj == 1;  // 会进行数据类型转换的等值运算结果为 true
true
js> nobj === 1;  // 不会进行数据类型转换的等值运算结果为 false
false

调用 Number 函数:

1
2
3
4
5
6
7
8
js> var n1 = Number(1);
js> typeof n1;  // 变量 n1 的值为数值
number
js> n1 == 1;
true
js> n1 === 1;
true
js> var n = Number('1');  // 从字符串值至数值型的显式数据类型转换

第四章

Javascript 标识符区分大小写。JavaScript(准确地说是 ECMAScript)的代码块中的变量并不存在块级作用域这样
的概念。

1
2
3
4
// 变量声明的例子
var foo;
var foo, bar; // 同时声明多个变量
var foo = 'FOO', bar = 'BAR'; // 在声明变量的同时进行初始化

Javascript 中的 switch/case

1
2
3
4
5
6
7
8
9
10
11
12
var x = 0;
switch (x) {
case 0:
    print("0");
case 1:
    print("1");
case 2:
    print("2");
default:
    print("default");
    break;
}

使用 switch 语句时,等值比较表达式可以被隐藏起来,所以与使用了等值比较的 if-else 语句相比表达
上更为简洁。需要注意的是,switch 语句所隐藏的等值比较运算是不会对数据类型进行转换的 === 运算。
如果原来的 if-else 语句中的表达式使用的是 == 运算的话,就可能会在执行上有一些细微的差别

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
var s = 'foo';
switch (s) { // 可以在 switch 表达式中使用字符串值。
// 可以在 case 表达式中使用和 switch 表达式类型不同的值。
// s === 0 的值为假,所以将继续进行比较。
case 0:
    print('not here');
    break;
// 可以在 case 表达式中使用含有变量的表达式。
// s === s.length 的值为假,所以将继续进行比较。
case s.length:
    print('not here');
    break;
// 可以在 case 表达式中使用方法调用表达式。
// s === (0).toString() 的值为假,所以将继续进行比较。
case (0).toString();
    print('not here');
    break;
// 还可以在 case 表达式中书写这样的表达式。
// s === 'f' + 'o' + 'o' 为真,所以将执行以下的代码。
case 'f' + 'o' + 'o':
    print('here');
    break;
// 如果所有的 case 表达式在等值运算(===)后得到的结果都为假,则执行以下的代码。
default:
    print('not here');
    break;
}

标签:

1
2
3
4
5
6
7
8
9
// 使用标签来同时跳出嵌套的循环
outer_loop:
while (true) {
    print("outer loop");
    while (true) {
        print("inner loop");
        break outer_loop;
    }
}

外层循环被标以 outer_loop 的标签(此前提到过,请再回想一下,while 循环以及相应的代码块共同组成了一句语句)。

异常:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// try-catch-finally 结构的语法
try {
    语句
    语句
    ……
} catch ( 变量名 ) { // 该变量是一个引用了所捕捉到的异常对象的局部变量
    语句
    语句
    ……
} finally {
    语句
    语句
    ……
}

在 try 语句之外,或者没有 catch 子句的 try 语句,都是无法捕捉异常的。这时函数会中断并返回至调用该函数之处。

finally 子句必定会在跳出 try 语句之时被执行。即使没有产生异常,finally 子句也会被执行。也就是说,如果没有产生异常的话,在执行完 try 子句之后会继续执行 finally 子句的代码;如果产生了异常,则会在执行 finally 子句之前首先执行 catch 子句。对于没有 catch 子句的 try 语句来说,异常将会被直接传
递至上一层,但 finally 子句仍然会被执行。

代码清单 4.9 try 语句的执行示例

1
2
3
4
5
6
7
8
9
try {
    print('1');
    null.x; // 在此处强制产生一个 TypeError 异常
    print('not here');  // 这条语句不会被执行
} catch (e) { // 对象 e 是 TypeError 对象的一个引用
    print('2');
} finally {
    print('3');
}
1
2
3
4
// 代码清单 4.9 的运行结果
1
2
3

with 表达式:

with 语句用于临时改变名称(变量名或是函数名)的查找范围。with 语句中使用的表达式是 Object
类型的。如果使用了其他类型的值,则会被转换为 Object 类型。在 with 语句内对变量名进行查找时,将
会从所指定对象的属性开始寻找。

1
2
3
4
5
6
7
// with 语句的例子
js> var x = 1;  // 全局变量
js> var obj = { x:7, y:8 };
js> with (obj) {
    print(x);  // 如果要查找变量 x,则会在查找全局变量 x 之前先查找到 obj.x
}
7

注释:

1
2
// 单行注释
/* 注释 */

运算符和操作数在英语中分别称为 operator 和 operand。

1
2
3
4
5
// 赋值表达式的结合律为右结合
x = y = z = 0;
将以
x = (y = (z = 0);
的方式被求值

在 JavaScript 所有算术运算中的数值都是浮点小数。

ECMAScript 中,=== 被称为 Strict Equals 运算符,而 == 则被称为 Equals 运算符。
将 Strict Equals 运算符(===)称为全等运算符,而将 Equals 运算符(==)称为相等运算符。两者的区别在于,是否会在进行相等判定时进行数据类型转换。
全等运算不会进行数据类型转换,因此数据类型是否一致也是判断是否相等的内容之一。而相等运算 (==)会先进行数据类型转换,在数据类型相同后再进行相等判断。两种运算符的运算结果都是布尔值。

下面总结了全等运算的一些特性。

  • 1. x 与 y 如果数据类型不相符,则结果为假。
  • 2. 两者都是 undefined 值或两者都是 null 值的情况,结果为真。
  • 3.  两者都是数值,但有一方为 NaN,或者两者都是 NaN 的情况,结果为假。否则,如果数值相等则结果为真,不相等则为假。
  • 4. 两者都是字符串的情况下,如果内容一致则结果为真,否则结果为假。
  • 5. 两者都是布尔值的情况下,如果值一致则结果为真,否则结果为假。
  • 6. 两者都是对象引用的情况下,如果引用的是同一个对象则结果为真,否则结果为假。

相等运算 == 由于会进行隐式数据类型转换,所以其执行方式更为复杂。下面是对其运算规则的总结。

  ● x 与 y 的数据类型相同时,与全等运算的结果相同。
  ● x 与 y 的数据类型不同时,判定规则如下。
  (1)一方为 null 值,另一方为 undefined 值的情况,结果为真。
  (2)一方为数值,另一方为字符串值的情况,将字符串值转换为数值之后对数值进行比较。
  (3)一方为布尔值,另一方为数值的情况,将布尔值转换为数值之后对数值进行比较。
  (4)一方为布尔值,另一方为字符串值的情况,将两者都转换为数值后对数值进行比较。
  (5)一方为数值,另一方为对象引用的情况,将对象引用转换为数值后对数值进行比较。
  (6)一方为字符串值,另一方为对象引用的情况,将对象引用转换为字符串值后对字符串的内容进行比较。
  (7)以上 6 种情况之外的运算结果都为假

void 运算符:

无论向其传递什么操作数,其运算结果都会是 undefined 值。下面是一个具体的例子。

1
2
3
4
5
6
7
8
9
js> print(void 0);  // 操作数为数值
undefined
js> print(void 'x'); // 操作数为字符串值
undefined
js> var x = 0;
js> void x++; // 由于会先对操作数进行求值,所以 x 将自增
js> print(x);
1
js> void(x); // 常常会把操作数通过括号包围起来

在客户端 JavaScript 中有不少相关的习惯用法。下面是一个在 HTML 中点击了标签 a 之后发送表单内容的 JavaScript 代码的例子。

1
<a href="javascript:void(document.form.submit())"> 发送 HTML 表单数据但不跳转页面 </a>

hred 属性中所写的表达式如果具有值的话,则会被标签 a 认为是 URL 并跳转至该页面。为了阻止标签 a 的这一行为,需要将 href 属性中表达式的值强制设为 undefined 值。对此最为简单的惯用方法就是通过 void 运算来实现。

逗号运算符(,)是一个双目运算符,其作用为依次对其左操作数与右操作数求值。逗号运算符的运
算结果是其右操作数的值,也就是说其结果的类型取决于所使用的操作数。下面是一个具体的例子。

1
2
3
4
js> print((x = 1, y = 2)); // 请注意,如果不在真个参数外加括号的话,其含义就会变为参数的数量是两个
2
js> print((x = 1, ++x, ++x)); // 由于是左结合,相当于 ((x = 1, ++x), ++x)
3

字符 .(点)称为点运算符,中括号 [] 称为中括号运算符,它们都是用于访问属性的运算符。虽然这
两个运算符不太显眼,却有着很重要的作用。
其左操作数为对象引用,右操作数为属性名。如果左操作数不是对象引用的话,则会被转换为
Object 类型。点运算符的右操作数是一个用于表示属性名的标识符,而中括号运算符的右操作数为字符
串型或是可以被转换为字符串型的值。

第五章 变量与对象

1
var a = a || 7;  // 一种习惯用法。如果变量 a 已经具有某个值(严格来说是具有某个可以被转换为 true 的值)就直接使用,否则就把 7 赋值给 a

准确地说,对象的赋值其实是将对象的引用进行赋值。变量有值类型和引用类型。将基本类型的值赋值给变量的话,变量将把这个值本身保存起来。这时,可以将变量简单地理解为一个装了该值的箱子。变量本身装有所赋的这个值,所以能够将该值从变量中取出。

如果将一个对象赋值给变量,其实是把这个对象的引用赋值给了该变量。对象本身是无法赋值给一个变量的。如果在右侧写上了这样的变量,该变量所表示的引用将被复制给赋值目标处(左侧)的变量。对象本身并不会被复制。 var a = {x:2, y:3} “变量 a 所引用的对象”.

在上下文不会发生误会的情况下,可以用“对象”这一术语来指代“对象的引用”。对象是一个实体,而引用是用于指示这一实体的位置信息,两者本应是不同的。不过根据上下文可以知
道,“将对象赋值给变量 a”的说法很显然是指将对象的引用赋值,所以方便起见可以直接这么说。

变量和属性

很多读者都会觉得对象的属性和变量非常相似吧。两者都可以通过其名字(变量名或属性名)来获取其值,也都可以作为赋值对象,而写在赋值表达式的左侧。其实,在 JavaScript 中变量就是属性,两者何止是相似,本身就是同一个概念。全局变量和局部变量两者的本质都是属性。全局变量(以及全局函数名)是全局对象的属性。全局对象是从程序运行一开始就存在的对象。下面的代码证明,全局变量即为全局对象的属性。

1
2
3
4
5
6
7
8
js> var x = 'foo';  // 对全局变量 x 进行赋值
js> print(this.x);  // 可以通过 this.x 进行访问
foo
js> function fn() {return "functon"}; // 全局函数。
js> 'fn' in this; // 全局对象的属性 fn
true

js>this.fn() // "function"

最外层代码中的 this 引用是对全局对象的引用。因此上面代码中的 this.x,指的就是全局对象的属性 x,这也就是全局变量 x。

像下面这样,在最外层代码中将 this 引用的值赋值给全局变量 global 的话,这个变量就不但是全局对象的属性,同时也是一个对全局对象的引用,从而形成了一种自己引用自己的关系.

1
2
3
js> var global = this;  // 将 this 引用赋值给全局变量 global
js> 'global' in this; // 全局对象的属性 global
true

在最外层代码中对变量名进行查找,就是查找全局对象的属性。这其实只是换了一种说法,在最外层代码中能够使用的变量与函数,只有全局变量与全局函数而已。

至于对函数内的变量名的查找,前一节中已经介绍过,是按照先查找 Call 对象的属性,再查找全局对象的属性来进行的。这相当于在函数内可以同时使用局部变量(以及参数变量)与全局变量。对于
嵌套函数的情况,则会由内向外依次查找函数的 Call 对象的属性,并在最后查找全局对象的属性。

对变量是否存在的检验

1
var a = a || 7;  // 一种习惯用法。如果变量 a 已经具有某值,则使用变量 a 的值
1
2
3
// 如果变量 a 已经具有某值,则使用变量 a 的值。代码示例(1)
var a;
var b = a || 7;
1
2
3
// 如果变量 a 已经具有某值,则使用变量 a 的值。代码示例(2)
var a;
var b = a !== undefined ? a : 7;
1
2
3
4
5
6
7
8
// 如果变量 a 已经具有某值,则使用变量 a 的值。代码示例(3)
// (不使用 var a 的版本)
if (typeof a !== 'undefined') {
    var b = a;
} else {
    var b = 7;
}
// 从这里开始可以使用变量 b, 因为 Javascript 中没有块级作用域。

可以在最外层代码中,像下面这样来判断在全局对象中是否存在属性 a,也就是说,可以用来检测
全局变量 a 是否存在。

1
2
3
4
5
6
7
// 用于判断变量 a 是否已经被声明的代码
if ('a' in this) {
    var b = a;
} else {
    var b = 7;
}
// 从这里开始可以使用变量 b

对属性是否存在的检验

变量与属性实质上是一样的。不过,如果变量或属性本身不存在,处理方式则会有所不同。请看下面的例子:

1
2
3
4
5
6
7
js> print(x); // 访问未声明的变量会导致 ReferenceError 异常
ReferenceError: x is not defined
js> print(this.x);  // 访问不存在的属性并不会引起错误
undefined
js> var obj = {};
js> print(obj.x); // 读取不存在的属性仅会返回 undefined 值,并不会引起错误
undefined

读取不存在的属性仅会返回 undefined 值,而不会引起错误。但是如果对 undefined 值进行属性访问的话,则会像下面这样产生 TpyeError 异常。

1
2
js> print(obj.x.y);
TypeError: obj.x is undefined

为了避免产生 TypeError 异常,一般会使用下面的方法。

1
obj.x && obj.x.y

但如果是为了检测对象内是否存在某一属性,还请使用 in 运算符。

构造函数与 new 表达式

构造函数是用于生成对象的函数。可以直观地将代码清单 5.8 理解为 MyClass 类的类定义。在调用时通过 new 来生成一个对象实例。

1
2
3
4
5
6
7
8
9
10
// 构造函数(类的定义)
function MyClass(x, y) {
    this.x = x;
    this.y = y;
}

// 对代码清单 5.8 的构造函数的调用
js> var obj = new MyClass(3, 2);
js> print(obj.x, obj.y);
3 2

从形式上来看,构造函数的调用方式如下。
  ● 构造函数本身和普通的函数声明形式相同。
  ● 构造函数通过 new 表达式来调用。
  ● 调用构造函数的 new 表达式的值是(被新生成的)对象的引用。
  ● 通过 new 表达式调用的构造函数内的 this 引用引用了(被新生成的)对象

  • new 表达式的操作

在此说明一下 new 表达式在求值时的操作。首先生成一个不具有特别的操作对象。之后通过 new 表达式调用指定的函数(即构造函数)。构造函数内的 this 引用引用了新生成的对象。执行完构造函数后,它将返回对象的引用作为 new 表达式的值。new 表达式的操作就是以上这些。实际上其中还含有一个和原型链有关的问题,将会在之后进行说明。

img

构造函数总是由 new 表达式调用。为了和普通的函数调用区别开, 将使用 new 表达式的调用称为构造函数的调用。构造函数的名称一般以大写字母开始。(例如 MyClass)。
构造函数在最后会隐式地执行 return this 操作。所以构造函数最终会返回新生成的这个对象。

构造函数与类的定义

1
2
3
4
5
6
7
8
9
10
11
12
// 模拟类定义(尚有改进的余地)

// 相当于类的定义
function MyClass(x, y) {
    // 相当于域
    this.x = x;
    this.y = y;
    // 相当于方法
    this.show = function() {
        print(this.x, this.y);
    }
}
1
2
3
4
// 对代码清单 5.9 中的构造函数的调用(实例生成)
js> var obj = new MyClass(3, 2);
js> obj.show();
3 2

这段代码简单地实现了Javascript 的定义, 但是有两个问题:

● 由于所有的实例都是复制了同一个方法所定义的实体,所以效率(内存效率与执行效率)低下。
● 无法对属性值进行访问控制(private 或 public 等)。

前者可以通过原型继承来解决,后者可以通过闭包来解决。

属性的访问

通过点运算符和方括号运算符来访问对象的属性。在点运算符之后书写的属性名会被认为是标识符,而中括号运算符内的则是被转为字符串值的表达式。

1
2
3
4
5
6
7
8
js> var obj = { x:3, y:4 };
js> print(obj.x); // 属性 x
3
js> print(obj['x']); // 属性 x
3
js> var key = 'x';
js> print(obj[key]); // 属性 x(而非属性 key)
3

属性访问的运算对象并不是变量,而是对象的引用

1
2
3
4
5
js> ({x:3, y:4}).x;  // 属性 x
3

js> ({x:3, y:4})['x'];  // 属性 x
3

现实中几乎不会对对象字面量进行运算。不过当这种运算对象不是一个变量时,倒是常常会以方法链之类的形式出现。

点运算符与中括号运算符在使用上的区别

只能使用中括号运算符的情况分为以下几种。
  ● 使用了不能作为标识符的属性名的情况。
  ● 将变量的值作为属性名使用的情况。
  ● 将表达式的求值结果作为属性名使用的情况。

1
2
3
4
// 含有横杠的属性名
js> obj = { 'foo-bar':5 };
js> obj.foo-bar; // 将解释为 obj.foo 减去 bar,从而造成错误
ReferenceError: bar is not defined

无法作为标识符被使用的字符串,仍可以在中括号运算符中使用。

1
2
js> obj['foo-bar'];  // 使用 [] 运算以字符串值指定了一个属性名。可以正常执行
5

属性的枚举

可以通过 for in 语句对属性名进行枚举(代码清单 5.10)。通过在 for in 语句中使用中括号运算符,可以间接地实现对属性值的枚举。使用 for each in 语句可以直接枚举属性值。

1
2
3
4
5
6
7
8
9
10
11
12
13
var obj = { x:3, y:4, z:5 };
for (var key in obj) {
    print('key = ', key); // 属性名的枚举
    print('val = ', obj[key]); // 属性值的枚举
}

// 代码清单 5.10 的运行结果
key = x
val = 3
key = y
val = 4
key = z
val = 5

属性可以分为直接属性以及继承于原型的属性。for in 语句和 for each in 语句都会枚举继承于原型的属性。

作为关联数组的对象

在 JavaScript 中,必须通过对象来实现关联数组(字典、散列)

简单说来,原型继承指的是一种对象继承其他对象的属性并将其作为自身的属性一样来使用的做法。如下所示,从形式上来说,对象 obj 的属性并不是其直接属性,而是通过原型继承而得到的属性。

1
2
3
4
5
js> function MyClass() {}
js> MyClass.prototype.z = 5;  // 在原型链上设定属性 z
js> var obj = new MyClass();  // 属性 z 继承了原型
js> print(obj.z);
5

for in 语句将枚举通过原型继承而得到的属性。

1
2
3
// 接之前的代码
js> for (var key in obj) { print(key); }  // for in 语句也会枚举通过原型继承得到的属性
z

请注意,通过原型继承而得到的属性无法被 delete。继续接之前的代码。

1
2
3
4
5
6
// 接之前的代码
js> delete obj.z; // 尽管没有被 delete,但还是会返回 true……

true
js> print(obj.z); // 无法 delete 通过原型继承而得到的属性
5

即使通过使用空的对象字面量创建一个没有元素的空的关联数组, 也仍然会从 Object 类中继承原型的属性。 可以通过 in 运算对此进行检验。

1
2
3
js> var map = { };         // 通过空的对象字面量生成关联数组
js> 'toString' in map;    // map 所引用的对象从 Object 类中继承了属性 toString
true

但是,通过 for in 语句对元素进行枚举不会有任何效果。这是由于 enumerable 属性的缘故,将在之后的小节中说明。

1
2
3
4
5
// 接之前的代码
js> for (var key in map) {
   print(key);
}
// 没有元素会被枚举

通过 in 运算符检测关联数组的键是否存在,就会发生与原型继承而来的属性相关的问题。因此,像下面这样通过 hasOwnProperty 来对其进行检测,是一种更安全的做法。

1
2
3
4
5
6
7
8
9
js> var map = {};
js> map.hasOwnProperty('toString');      // 由于 toString 不是直接属性,因此结果为 false
false
js> map['toString'] = 1;
js> map.hasOwnProperty('toString');
true
js> delete map['toString'];
js> map.hasOwnProperty('toString');
false

属性的属性

在标准的对象中有一部分属性的 enumerable 属性为假而无法通过 for in 语句枚举。其中一个很容易理解的例子是数列的 length 属性。

属性的属性名
含义

writable
可以改写属性值

enumerable
可以通过 for in 语句枚举

configurable
可以改变属性的属性。可以删除属性

get
可以指定属性值的 getter 函数

set
可以指定属性值的 setter 函数

垃圾回收

不再使用的对象的内存将会自动回收,这种功能称作垃圾回收。所谓不再使用的对象,指的是没有被任何一个属性(变量)引用的对象。

循环引用会造成内存泄漏。所谓循环引用,指的是对象通过属性相互引用而导致它们不会被判定为不再使用的状态。

不可变对象

所谓不可变对象,指的是在被生成之后状态不能再被改变的对象。由于对象的状态是由其各个属性的值所决定的,因此从形式上来说也是指无法改变属性的值的对象

JavaScript 中的一种典型的不可变对象就是字符串对象。

在 JavaScript 中可以通过以下方式实现对象的不可变。

  ● 将属性(状态)隐藏,不提供变更操作。
  ● 灵活运用 ECMAScript 第 5 版中提供的函数。
  ● 灵活运用 writable 属性、configurable 属性以及 setter 和 getter。

为了将属性隐藏,可以使用一种被称为闭包的方法。

在 ECMAScript 第 5 版中有一些用于支持对象的不可变化的函数(表 5.2)。seal 可以向下兼容 preventExtensions,freeze 可以向下兼容 seal。这里的向下兼容,指的是比后者有更为严格的限制。

ECMAScript 第 5 版中用于支持对象的不可变化的函数

方法名
属性新增
属性删除
属性值变更
确认方法

preventExtensions
X
O
O
Object.isExtensible

seal
X
X
O
Object.isSealed

freeze
X
X
X
Object.isFrozen

Object.preventExtensions 的例子

1
2
3
4
5
6
7
8
9
10
11
12
13
14
js> var obj = { x:2, y:3 };
js> Object.preventExtensions(obj);
// 无法新增属性
js> obj.z = 4;
js> Object.keys(obj);
["x", "y"]
// 可以删除属性
js> delete obj.y;
js> Object.keys(obj);
["x"]
// 可以更改属性值
js> obj.x = 20;
js> print(obj.x);
20

Object.seal 的例子

1
2
3
4
5
6
7
8
9
10
js> var obj = { x:2, y:3 };
js> Object.seal(obj);
// 无法删除属性
js> delete obj.y; // 将返回 false
js> Object.keys(obj);
["x", "y"]
// 可以更改属性值
js> obj.x = 20;
js> print(obj.x);
20

Object.freeze 的例子

1
2
3
4
5
6
js> var obj = { x:2, y:3 };
js> Object.freeze(obj);
// 无法更改属性值
js> obj.x = 20;
js> print(obj.x);
2

● 一旦更改就无法还原。
● 如果想让原型继承中的被继承方也不可变化,需要对其进行显式的操作。

方法

我们将作为对象属性的函数称为方法。那些使用了 this 引用来调用并访问了对象的属性的函数,被称为方法。

this 引用

this 引用有着会根据代码的上下文语境自动改变其引用对象的特性。

在此,总结一下 this 引用的规则。

● 在最外层代码中,this 引用引用的是全局对象。
● 在函数内,this 引用根据函数调用方式的不同而有所不同(参见表 5.3)。

对于函数内部的情况,this 引用的引用对象并不是根据函数的内容或声明方式而改变的,而是根据其调用方式而改变。也就是说,即使是同一个函数,如果调用方式不同,this 引用的引用对象也会有所不同。

函数内部的 this 引用

函数的调用方式
this 引用的引用对象

构造函数调用
所生成的对象

方法调用
接收方对象

apply 或是 call 调用
由 apply 或 call 的参数指定的对象

其它方式的调用
全局对象

对于构造函数调用的情况,this 引用的引用对象是所生成的对象。 上表中的方法调用的说明中的接收方对象是这样一种对象: ● 通过点运算符或中括号运算符调用对象的方法时,在运算符左侧所指定的对象。

方法是对象的属性所引用的函数。下面是一个关于方法和接收方对象的具体例子。

1
2
3
4
5
6
7
8
9
// 对象定义
js> var obj = {
    x:3,
    doit: function() { print('method is called.' + this.x ); }
};
js> obj.doit();  // 对象 obj 是接收方对象。doit 是方法。
method is called. 3
js> obj['doit']();  // 对象 obj 是接收方对象。doit 是方法。
method is called. 3

现在说明上面的例子。首先是将对象的引用赋值给了变量 obj。这个对象有两个属性。属性 x 的值为数值 3,属性 doit 的值是一个函数。将该函数称为方法 doit。可以通过点运算符或中括号运算符对 obj 调用方法 doit。这时,方法调用的目标对象被称为接收方对象(也就是说,obj 所引用的对象是一个接收方对象)。被调用的方法内的 this 引用引用了该接收方对象。

this 引用注意点

在 Java 中, this 所引用的接收方对象始终是该类的实例, 而在 Javascript 中却不一定总是如此。Javascript 的 this 引用的引用对象会随着方法调用方式的不同而改变。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
var obj = { 
    x : 3;
    doit: function() { print('method is called.' + this.x ); }
}

var fn = obj.doit;       // 将 ojb.doit 引用的 Function 对象赋值给全局变量
fn();                          // 函数内的 this 引用引用了全局对象, 现在全局变量中还没有定义变量 x, 所以下面会打印 undefined
method is called. undefined
var x = 5;                 // 确认 this 引用确实引用了全局对象
fn();
method is called. 5
var obj2 = { x:4, doit2:fn };  //  将obj的方法(Function对象的引用)赋值给了另一个对象obj2的属性
obj2.doit2(); // 方法内的 this 引用引用了对象 obj2
method is called. 4

在方法内部调用方法的情况

1
2
3
4
5
6
7
8
9
// 从 doit 方法内调用 doit2 方法时,必须通过 this 引用,以 this.doit2() 的方式实现
js> var obj = {
x:3,
doit: function() { print('doit is called.' + this.x ); this.doit2(); },
doit2: function() { print('doit2 is called.' + this.x); }
};
js> obj.doit();
doit is called. 3
doit2 is called. 3

apply 与 call

在 Function 对象中包含 apply 与 call 这两种方法,通过它们调用的函数的 this 引用,可以指向任意特定的对象。也就是说,可以理解为它们能够显式地指定接收方对象。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
js> function f() { print(this.x); }
js> var obj = { x:4 };
js> f.apply(obj); // 通过 apply 调用函数 f。函数内的 this 引用引用了对象 obj
4
js> f.call(obj); // 通过 call 调用函数 f。函数内的 this 引用引用了对象 obj
4
// 将接收方对象指定为另一个对象并进行方法调用
js> var obj = {
    x:3,
    doit: function() { print('method is called.' + this.x ); }
};
js> var obj2 = { x:4 };
js> obj.doit.apply(obj2);  // 通过 apply 调用 obj.doit 方法。方法内的 this 引用引用了对象 obj2

method is called. 4

apply 与 call 之间的不同之处在于两者对其他参数的传递方式。对于 apply 来说,剩余的参数将通过数组来传递,而 call 是直接按原样传递形参。

1
2
3
4
5
js> function f(a, b) { print('this.x = ' + this.x + ', a = ' + a + ', b = ' + b); }
js> f.apply({x:4}, [1, 2]);  // 作为第 2 个参数的数列中的元素都是函数 f 的参数
this.x = 4, a = 1, b = 2
js> f.call({x:4}, 1, 2);  // 从第 2 个参数起的参数都是函数 f 的参数
this.x = 4, a =1 , b = 2