绩效统计
章节 | 代码量(行数) |
---|---|
5.1 | 12 |
5.2 | 65 |
5.3 | 11 |
5.4 | 0 |
5.5 | 62 |
5.6 | 0 |
5.7 | 72 |
5.8 | 32 |
5.9 | 61 |
目前 | 315 |
5.1 变量的声明
变量的功能为持有某个值,或者用来表示某个对象。
如果一个变量在声明之后没有进行赋值,它的值就会是undefined。对同一个变量重复进行声明是不会引起什么问题的,原有的值也不会被清空。
var hzh1 = 7;
console.log("输出hzh1的值:");
console.log(hzh1);
var hzh1; // 即使对同一个变量重复进行声明
console.log("输出变量hzh1的值:");
console.log(hzh1); // 它的值也不会发生改变
[Running] node "e:\HMV\JavaScript\tempCodeRunnerFile.js"
输出hzh1的值:
7
输出变量hzh1的值:
7
[Done] exited with code=0 in 0.638 seconds
如果变量 a 具有某个可以被转换为 true 的值就直接使用,否则就把 7 赋值给a。
console.log("输出变量hzh2的值:");
var hzh2 = hzh2 || 7;
console.log(hzh2);
[Running] node "e:\HMV\JavaScript\JavaScript.js"
输出变量hzh2的值:
7
[Done] exited with code=0 in 0.188 seconds
在这段代码中,如果 hzh2 是一个已经被声明且赋值的变量,则不会有任何效果;而如果没有被声明过,则会在声明的同时对其进行赋值操作。
下面的代码虽然和上一段有些相像,却是有问题的。如果变量 hzh5 没有被声明过,将会引起ReferenceError 异常。不过,也不能说它绝对就是错的。这是因为,如果能确保在这条代码之前就已经对变量 hzh5 进行了声明,这段代码的作用就变为了判定变量 hzh5 的值的真假,这样就没有问题了。
var hzh5 = hzh6 || 7;
console.log("输出变量hzh5的值:");
console.log(hzh5);
[Running] node "e:\HMV\JavaScript\tempCodeRunnerFile.js"
e:\HMV\JavaScript\tempCodeRunnerFile.js:1
var hzh5 = hzh6 || 7;
^
ReferenceError: hzh6 is not defined
at Object.<anonymous> (e:\HMV\JavaScript\tempCodeRunnerFile.js:1:12)
at Module._compile (internal/modules/cjs/loader.js:999:30)
at Object.Module._extensions..js (internal/modules/cjs/loader.js:1027:10)
at Module.load (internal/modules/cjs/loader.js:863:32)
at Function.Module._load (internal/modules/cjs/loader.js:708:14)
at Function.executeUserEntryPoint [as runMain] (internal/modules/run_main.js:60:12)
at internal/main/run_main_module.js:17:47
[Done] exited with code=1 in 0.289 seconds
5.2 变量与引用
对象的概念很好地说明了变量是一种拥有名称的客体。对象本身是没有名称的,之所以使用变量,是为了通过某个名称来称呼这样一种不具有名称的对象。
var hzh = {} // 将对象赋值给变量hzh
变量又分为基本类型的变量(值型变量)与引用类型的变量。由于在 JavaScript 中,变量是不具有类型的,因此从语法标准上来看,两者并没有什么不同。不过,在 JavaScript 中仍然有对象的引用这一概念。
所谓“引用”,可以认为是一种用于指示出对象的位置的标记。如果你熟悉 C 语言,把它理解为是和指针等价的东西也没有问题。不过,引用不支持那些可以对指针进行的运算。引用这一语言功能只有指示位置信息的作用。准确地说,对象的赋值其实是将对象的引用进行赋值。
为了更好地解释引用这一概念,这里对引用类型的变量和值型变量进行比较。将基本类型的值赋值给变量的话,变量将把这个值本身保存起来。这时,可以将变量简单地理解为一个装了该值的箱子。变量本身装有所赋的这个值,所以能够将该值从变量中取出。如果在右侧写上一个变量,这一变量的值将被复制给赋值目标处(左侧)的变量。
var a = 123; // 将数值123赋值给变量a
var b = a; // 将变量a的值(数值123)赋值给变量b
像下面这样,对变量 b 进行自增操作后,变量 a 的值是不会发生改变的。图 5.1 对这一执行方式作了说明
var a = 123; // 将数值123赋值给变量a
var b = a; // 将变量a的值(数值123)赋值给变量b
console.log("第一次输出变量a的值:");
console.log(a);
console.log("第一次输出变量b的值:");
console.log(b);
b++;
console.log("");
console.log("第二次输出变量a的值:");
console.log(a);
console.log("第二次输出变量b的值:");
console.log(b);
[Running] node "e:\HMV\JavaScript\JavaScript.js"
第一次输出变量a的值:
123
第一次输出变量b的值:
123
第二次输出变量a的值:
123
第二次输出变量b的值:
124
[Done] exited with code=0 in 0.316 seconds
另一方面,如果将一个对象赋值给变量,其实是把这个对象的引用赋值给了该变量。对象本身是无法赋值给一个变量的。如果在右侧写上了这样的变量,该变量所表示的引用将被复制给赋值目标处(左侧)的变量。对象本身并不会被复制。
var a = { x:1, y:2 }; // 将对象的引用赋值给变量a
var b = a; // 将变量a的值(对象的引用)赋值给变量b
图5.1 值型变量的执行方式
图5.2 引用类型的变量的执行方式
如果像下面这样,改变了变量 b 所引用的对象,那么这一改变也会体现在变量 a 之中,这是因为这两个变量通过引用而指向了同一个对象。图 5.2 对这种执行方式进行了说明:
var a = { x:1, y:2 }; // 将对象的引用赋值给变量a
var b = a; // 将变量a的值(对象的引用)赋值给变量b
console.log("输出变量a的值:");
console.log(a);
console.log("输出变量b的值:");
console.log(b);
console.log("");
b.x++; // 改变变量b所引用的对象
console.log("输出变量b的x属性:");
console.log(b.x); // 变量b所引用的对象
console.log("输出变量a的x属性:");
console.log(a.x); // 可以发现变量a所引用的对象也被改变
[Running] node "e:\HMV\JavaScript\JavaScript.js"
输出变量a的值:
{ x: 1, y: 2 }
输出变量b的值:
{ x: 1, y: 2 }
输出变量b的x属性:
2
输出变量a的x属性:
2
[Done] exited with code=0 in 0.323 seconds
在比较了这两种赋值后,你可能会错误地认为对于值型变量而言,变量值的改变对于其他的变量来说是不可见的,而对于引用类型的变量,这一改变则是可见的。这是一种不正确的理解。对于引用类型的变量,整个过程中发生改变的其实是其引用的对象,而不是该变量的值。引用类型的变量具有的值就是引用(值),这个值将在赋值的时候被复制。请看下面的代码以及图 5.3。
var a = { x:1, y:2 };
var b = a; // 变量a与变量b引用的是同一个对象
a = { x:2, y:2 }; // 改变了变量a的值(使其引用了另一个对象)
console.log("输出变量b的x属性:");
console.log(b.x); // 变量b所引用的对象没有发生改变
[Running] node "e:\HMV\JavaScript\JavaScript.js"
输出变量b的x属性:
1
[Done] exited with code=0 in 0.267 seconds
图5.3 引用类型的变量的执行方式
在 JavaScript 中,赋值运算总是会把右侧的值复制给左侧。对于引用类型的变量来说也是一样,会将引用(用于指示对象的一种值)赋值给左侧。函数调用过程中的参数也是这样的执行方式。
5.2.1 函数的参数(值的传递)
代码清单 5.1 是一个典型的例子,hzh_no_swap 函数的代码试图交换所传递的两个参数 hzh_a 与 hzh_b 的值。然而,即使调用了这个函数,也不会对实参 hzh1 和 zero 的值造成任何影响。可以认为,在调用函数时执行了相当于 hzh_a=hzh1 以及 hzh_b=hzh2 的两次赋值操作。虽然变量 hzh1 与 hzh2 是引用类型的变量,但实际上也只是对其引用进行了复制操作。因此,并无法实现对 hzh1 和 hzh2 所引用的对象的交换。
代码清单 5.1 一个无法交换其两参数的值的函数
var hzh1 = 1;
var hzh2 = 2;
console.log("交换之前先打印hzh1和hzh2的值:");
console.log("hzh1 = " + hzh1);
console.log("hzh2 = " + hzh2);
console.log("");
function hzh_no_swap(hzh_a, hzh_b) {
var hzh_tmp = hzh_a;
hzh_a = hzh_b;
hzh_b = hzh_tmp;
console.log("hzh1 = " + hzh_a);
console.log("hzh2 = " + hzh_b)
}
console.log("调用hzh_no_swap()函数之后:");
hzh_no_swap(hzh1, hzh2);
[Running] node "e:\HMV\JavaScript\JavaScript.js"
交换之前先打印hzh1和hzh2的值:
hzh1 = 1
hzh2 = 2
调用hzh_no_swap()函数之后:
hzh1 = 2
hzh2 = 1
[Done] exited with code=0 in 0.739 seconds
【评】这里的实验结果和书上说的不一样。
在 JavaScript 中,应该把赋值运算看作将右侧的值复制给左侧的一种操作。而这一原则,对于调用函数过程中,参数对引用进行复制的情况也是成立的。这样的规则被称为按值传递(call-by-value)。
在支持对引用或指针进行运算的语言中,可以以代码清单 5.1 中函数的形式,来对实参的值进行交换。JavaScript 不支持这样的功能,所以必须通过其他方式来实现对两个参数值的交换。可以通过传递一个数组并交换其中的元素,或者通过传递一个对象并交换其属性值之类的形式来实现。代码清单 5.2 使用了 JavaScript 自带的增强功能,将交换结果设为函数的返回值,这可以说是一种最为简单的实现代码。
代码清单5.2 一个能够交换两个参数的值的函数(JavaScript 自带的增强功能)
function hzh_swap(hzh_a, hzh_b) {
return [hzh_b, hzh_a];
}
var hzh1 = 1;
var hzh2 = 2;
console.log("交换之前输出hzh1和hzh2的值:");
console.log("hzh1 = " + hzh1);
console.log("hzh2 = " + hzh2);
console.log("");
[hzh1, hzh2] = hzh_swap(hzh1, hzh2);
console.log("输出交换后的值:");
console.log("hzh1 = " + hzh1);
console.log("hzh2 = " + hzh2);
[Running] node "e:\HMV\JavaScript\JavaScript.js"
交换之前输出hzh1和hzh2的值:
hzh1 = 1
hzh2 = 2
输出交换后的值:
hzh1 = 2
hzh2 = 1
[Done] exited with code=0 in 0.36 seconds
5.2.2 字符串与引用
即使字符串型在内部是以引用类型的方式实现的,从语言规则上来看它仍然是一种值的类型。不过以字符串对象(String 类的对象实例)赋值的变量,从语言规则上来看则是一种引用类型。
5.2.3 对象与引用相关的术语总结
在将对象的引用赋值给变量 a 时,这个对象将被称作“对象a”。这种称法,会有一种(本应不具有名字的)对象其实具有 a 这样一个名称的感觉。显然这样的感觉是不正确的,因为这个对象即使在没有变量 a 的情况下,也能够独立存在。这样说的证据是,如果将变量 a 消去,或是将变量 a 指向其他的对象,原来的这个对象仍然会存在。话虽如此,每次都很准确地使用“变量 a 所引用的对象”这样的说法过于冗长,所以方便起见,还是称其为对象 a。事实上没有被任何变量引用的对象是会被内存自动回收的,不过这已经是另一个话题了。
此外,在上下文不会发生误会的情况下,可以用“对象”这一术语来指代“对象的引用”。对象是一个实体,而引用是用于指示这一实体的位置信息,两者本应是不同的。不过根据上下文可以知道,“将对象赋值给变量 a”的说法很显然是指将对象的引用赋值,所以方便起见可以直接这么说。
5.3 变量与属性
根据作用域的不同,变量可以被分为全局变量和局部变量(包括参数变量)。全局变量是在最外层代码中声明的变量。所谓最外层代码,指的是写在函数之外的代码。局部变量则是在函数内部声明的变量。全局变量和局部变量两者的本质都是属性。
var hzh1 = '黄子涵'; // 对全局变量hzh1进行赋值
console.log("访问全局变量hzh1:");
console.log(this.hzh1); // 可以通过this.hzh1进行访问
console.log("");
function hzh2() {}; // 全局函数。函数内容在此没有影响,所以留空
console.log("访问全局函数hzh2:");
console.log('hzh2' in this); // 全局对象的属性hzh2
[Running] node "e:\HMV\JavaScript\JavaScript.js"
访问全局变量hzh1:
undefined
访问全局函数hzh2:
false
[Done] exited with code=0 in 0.217 seconds
【评】这里的结果和书上的不一样,暂时不知道为什么,要做个标记。
最外层代码中的 this 引用是对全局对象的引用。因此上面代码中的 this.x,指的就是全局对象的属性 x,这也就是全局变量x。
像下面这样,在最外层代码中将 this 引用的值赋值给全局变量 global 的话,这个变量就不但是全局对象的属性,同时也是一个对全局对象的引用,从而形成了一种自己引用自己的关系,将 this 引用赋值给全局变量 global。
var global = this;
// 全局对象的属性 hzh3
console.log("访问全局变量global:");
console.log('global' in this);
[Running] node "e:\HMV\JavaScript\JavaScript.js"
访问全局变量global:
false
[Done] exited with code=0 in 0.196 seconds
【评】这里的结果和书上的不一样,暂时不知道为什么,要做个标记。
图 5.4 属性 global 具有一种自己引用了自己的关系
这种关系看起来有些混乱,在 JavaScript 中却很常见。如果是客户端 JavaScript,将会在一开始就提供一个引用了全局对象的全局变量 window。全局对象与变量 window 的关系,和之前例子中的变量 global 是相同的。
在函数内声明的变量是局部变量。作为函数参数的参数变量也是一种局部变量。局部变量(以及参数变量)是在调用函数时被隐式生成的对象的属性。被隐式生成的对象称为 Call 对象。局部变量通常在从函数被调用起至函数执行结束为止的范围内存在。
之所以说是“通常”,是因为有些局部变量在函数执行结束后仍然可以被访问。
5.4 变量的查找
从代码的角度来看,(作为右值)写出变量名以对该值进行获取的操作,或者写在赋值表达式左侧以作为赋值对象进行查询的操作,都被称为对变量名称的查找。
因此,在最外层代码中对变量名进行查找,就是查找全局对象的属性。这其实只是换了一种说法,在最外层代码中能够使用的变量与函数,只有全局变量与全局函数而已。至于对函数内的变量名的查找,是按照先查找 Call 对象的属性,再查找全局对象的属性来进行的。这相当于在函数内可以同时使用局部变量(以及参数变量)与全局变量。对于嵌套函数的情况,则会由内向外依次查找函数的 Call 对象的属性,并在最后查找全局对象的属性。
这里使用了“查找变量名”这一说法,较为抽象,而能更直观体现其意义的词则是变量的作用域。
5.5 对变量是否存在的校验
如果试图读取没有被声明的变量,则会引起 ReferenceError 异常,这是一种错误,必须对代码进行修正。避免 ReferenceError 异常的一种方法:
var hzh1 = 1;
var hzh1 = hzh1 || 7;
var hzh2;
var hzh2 = hzh2 || 2;
console.log("分别输出hzh1和hzh2的值:");
console.log("hzh1 = " + hzh1);
console.log("hzh2 = " + hzh2);var hzh1 = 1;
var hzh1 = hzh1 || 7;
console.log("输出hzh1的值:");
console.log("hzh1 = " + hzh1);
[Running] node "e:\HMV\JavaScript\JavaScript.js"
分别输出hzh1和hzh2的值:
hzh1 = 1
hzh2 = 2
[Done] exited with code=0 in 0.212 seconds
该代码利用了对已经声明的变量再次声明不会产生副作用的特性。像下面这样,分成两行并使用不同的变量,作用是一样的。
// 也可以分开两行
var hzh3;
var hzh4 = hzh3 || 4;
console.log("输出hzh4的值:");
console.log("hzh4 = " + hzh4);
[Running] node "e:\HMV\JavaScript\JavaScript.js"
输出hzh4的值:
hzh4 = 4
[Done] exited with code=0 in 0.278 seconds
准确地说,这一代码并没有判断变量 hzh3 是否已经被声明。例如在该例中,如果变量 hzh3 的值是 0 或者是 "(空字符),它在被转换为布尔型之后值就会为假,这时,代码中的变量 hzh4 则会被赋值为 4。
// 也可以分开两行
var hzh3 = 0;
var hzh4 = hzh3 || 4;
console.log("输出hzh4的值:");
console.log("hzh4 = " + hzh4);
[Running] node "e:\HMV\JavaScript\JavaScript.js"
输出hzh4的值:
hzh4 = 4
[Done] exited with code=0 in 0.197 seconds
// 也可以分开两行
var hzh3 = "";
var hzh4 = hzh3 || 4;
console.log("输出hzh4的值:");
console.log("hzh4 = " + hzh4);
[Running] node "e:\HMV\JavaScript\JavaScript.js"
输出hzh4的值:
hzh4 = 4
[Done] exited with code=0 in 0.172 seconds
接下来的代码可能有些冗长,它直接判断变量 hzh5 的值是否是 undefined 值,由此判断出变量 a 是否已声明,或者是否在声明后值为 undefined。
var hzh5;
var hzh6 = (hzh5 !== undefined) ? hzh5 : 6;
console.log("输出hzh6的值:");
console.log("hzh6 = " + hzh6);
[Running] node "e:\HMV\JavaScript\JavaScript.js"
输出hzh6的值:
hzh6 = 6
[Done] exited with code=0 in 0.193 seconds
var hzh5 = 5;
var hzh6 = (hzh5 !== undefined) ? hzh5 : 6;
console.log("输出hzh6的值:");
console.log("hzh6 = " + hzh6);
[Running] node "e:\HMV\JavaScript\JavaScript.js"
输出hzh6的值:
hzh6 = 5
[Done] exited with code=0 in 0.174 seconds
虽说对同一变量再次声明不会有副作用,但每次都要写一遍 var a 也有些麻烦。为了避免这一问题,可以通过 typeof 运算来判断是否为 undefined 值。
请看下面的例子。这个例子利用了在 JavaScript(ECMAScript) 中没有块级作用域的特性。
var hzh7 = 7;
if(typeof hzh7 !== 'undefined') {
var hzh8 = hzh7;
}else {
var hzh8 = 8;
}
console.log("输出hzh8的值:");
console.log("hzh8 = " + hzh8);
[Running] node "e:\HMV\JavaScript\JavaScript.js"
输出hzh8的值:
hzh8 = 7
[Done] exited with code=0 in 0.282 seconds
在以上这些代码中,无法区分变量 hzh7 是还没声明,还是已经声明但值为 undefined。先不论是否有必要对此加以区分,最后再介绍一种能够区分这两种情况的方法。
在读取未声明变量的值时会引起 ReferenceRrror 异常,所以不可以读取这一变量的值,但是可以仅对这一名称是否存在进行确认。为此需要使用 in 运算。
var hzh9 = 9;
if('hzh9' in this) {
var hzh10 = hzh9;
}else {
var hzh10 = 10;
}
console.log("输出hzh10的值:");
console.log("hzh10 = " + hzh10);
[Running] node "e:\HMV\JavaScript\JavaScript.js"
输出hzh10的值:
hzh10 = 10
[Done] exited with code=0 in 0.63 seconds
【评】这个实验结果和书上说的不一样,标记一下。
if('hzh9' in this) {
var hzh10 = hzh9;
}else {
var hzh10 = 10;
}
console.log("输出hzh10的值:");
console.log("hzh10 = " + hzh10);
[Running] node "e:\HMV\JavaScript\JavaScript.js"
输出hzh10的值:
hzh10 = 10
[Done] exited with code=0 in 0.192 seconds
对属性是否存在的检验
console.log(hzh1); // 访问未声明的变量会导致 ReferenceError 异常
[Running] node "e:\HMV\JavaScript\JavaScript.js"
e:\HMV\JavaScript\JavaScript.js:1
console.log(hzh1); // 访问未声明的变量会导致 ReferenceError 异常
^
ReferenceError: hzh1 is not defined
at Object.<anonymous> (e:\HMV\JavaScript\JavaScript.js:1:13)
at Module._compile (internal/modules/cjs/loader.js:999:30)
at Object.Module._extensions..js (internal/modules/cjs/loader.js:1027:10)
at Module.load (internal/modules/cjs/loader.js:863:32)
at Function.Module._load (internal/modules/cjs/loader.js:708:14)
at Function.executeUserEntryPoint [as runMain] (internal/modules/run_main.js:60:12)
at internal/main/run_main_module.js:17:47
[Done] exited with code=1 in 0.199 seconds
console.log(this.hzh); // 访问不存在的属性并不会引起错误
[Running] node "e:\HMV\JavaScript\JavaScript.js"
undefined
[Done] exited with code=0 in 0.181 seconds
var hzh = {};
console.log(hzh.x); // 读取不存在的属性仅会返回undefined,并不会引起错误
[Running] node "e:\HMV\JavaScript\JavaScript.js"
undefined
[Done] exited with code=0 in 0.199 seconds
读取不存在的属性仅会返回 undefined 值,而不会引起错误。但是如果对 undefined 值进行属性访问的话,则会像下面这样产生 TpyeError 异常。
console.log(hzh.x.y);
[Running] node "e:\HMV\JavaScript\JavaScript.js"
e:\HMV\JavaScript\JavaScript.js:1
console.log(hzh.x.y);
^
ReferenceError: hzh is not defined
at Object.<anonymous> (e:\HMV\JavaScript\JavaScript.js:1:13)
at Module._compile (internal/modules/cjs/loader.js:999:30)
at Object.Module._extensions..js (internal/modules/cjs/loader.js:1027:10)
at Module.load (internal/modules/cjs/loader.js:863:32)
at Function.Module._load (internal/modules/cjs/loader.js:708:14)
at Function.executeUserEntryPoint [as runMain] (internal/modules/run_main.js:60:12)
at internal/main/run_main_module.js:17:47
[Done] exited with code=1 in 0.171 seconds
为了避免产生 TypeError 异常,一般会使用下面的方法。
obj.x && Object.x.y
但如果是为了检测对象内是否存在某一属性,还请使用 in 运算符。
5.6 对象的定义
5.6.1 抽象数据类型与面向对象
如果从形式上来定义 JavaScript 的对象,它就是一种属性的集合。所谓属性,即名称与值的配对。属性值可以被指定为任意类型的值,包括数组或其他的对象,都没有问题。
对于对象有一种很常见的定义,即它是一种数据和操作(子程序)的结合。这一定义可以理解为,将面向对象看作一种抽象数据类型的表现形式。
面向对象的 3 要素,即封装、继承与多态吧。如果这样理解的话,面向对象程序设计的焦点就在于对象的执行方式,并将执行方式的共性定义为一种类型。
在这一语境中,常常使用类这一术语来表达类型的含义。也有些语言会把执行方式与其实现分开,将执行方式定义为接口。接口的实例(实体)被称为对象,可以对其进行指定的操作。
5.6.2 实例间的协作关系与面向对象
另一种面向对象程序设计的观点认为,与其考虑执行方式之间的共性,更应该关注实例之间的协作关系,即所谓的对象是进行消息收发的实体。对象收到消息之后将会对其作出响应。从实现的角度来看,消息的实质就是通过对方法(函数)进行调用,将对消息的响应分派给方法来处理。从本质上来说,面向对象这一术语只不过是一种在高于内部实现的语境中所使用的、较为抽象的概念而已。打个比方,可以把消息当作一种通信协议,把对象当作一个 Web 应用。
5.6.3 JavaScript 的对象
JavaScript 语言所支持的面向对象与后者的理解更为相近。在JavaScript 中,一切都是对象。对象之间的协作(消息收发)通过属性访问(以及方法的调用)来实现。而对象之间的共性,则是通过继承同一个对象的性质的方式来实现。JavaScript通过基于原型的形式来实现继承。
一旦要对面向对象的概念进行说明,事情就会变得很抽象。如果只考虑具体该如何使用 JavaScript 的对象,就不必考虑那么多复杂的问题。只需要考虑最核心的内容,将其理解为在程序中可以进行操作的数据的一种扩充即可。此外,还可以通过函数方法的形式来表示对数据进行操作的子程序。这种想法的核心就是将对象的功能进行拆分并分别进行处理。分割本身也只不过是一种手段。毕竟,面向对象方法的最终目的是降低程序的复杂程度。
5.7 对象的生成
5.7.1 对象字面量
下面列举了一些可以使用对象字面量的情况。请注意这里并没有作严格的分类。
作为 singleton 模式的用法
在设计模式中有一种 singleton 模式。在基于类的开发过程中,这种模式可以将类的实例数限定为 1 个。
JavaScript 可以实现基于类的程序设计,不过通常会作如下约定:若只需一个对象实例,则不会去设计一个类,而是会使用对象字面量。对类(构造函数)进行设计以实现 singleton 模式的想法完全是一种基于类的思考方式,在 JavaScript 中我们只需直接使用对象字面量即可。
作为多值数据的用法
可以通过对象字面量来实现多值数据。这种用法与作为关联数组的对象是相通的。例如,在代码清单 5.3 中有一个需要三个参数的函数,对参数是否为数值型的判断已被省略。
代码清单 5.3 接受多个参数的函数
function hzh(x, y, z) {
return Math.sqrt(x * x + y * y + z * z);
}
console.log("调用hzh函数:");
console.log(hzh(3, 2, 2));
[Running] node "e:\HMV\JavaScript\JavaScript.js"
调用hzh函数:
4.123105625617661
[Done] exited with code=0 in 2.023 seconds
代码清单 5.4 接受对象的函数
function hzh(pos) {
return Math.sqrt(pos.x * pos.x + pos.y * pos.y + pos.z * pos.z);
}
console.log("调用hzh函数:");
console.log(hzh({x:3, y:2, z:2}));
[Running] node "e:\HMV\JavaScript\JavaScript.js"
调用hzh函数:
4.123105625617661
[Done] exited with code=0 in 0.233 seconds
很难说哪一种方法更好,两者各有千秋。参数的数量为 3 个的情况有些微妙,或许认为代码清单 5.3 中的方法更为简单的读者会更多一些。
不过,当参数的数量越来越多时,代码清单 5.4 中的方法的优势就会体现出来。如果用代码清单 5.3 中的方法,参数数量增加之后,弄错实参的排列顺序的可能性也会上升,而 JavaScript 这样的动态程序设计语言对参数类型的检测很弱。如果像代码清单 5.4 这样使用对象作为参数,实参以对象字面量的方式传递,就不需要考虑排列的顺序,只需要使用名称即可。在其他一些程序设计语言中,支持对参数进行命名的功能,这种功能也具有类似的优点。
在 JavaScript 中,有一种模拟出默认参数的效果的习惯用法(代码清单 5.5)。这种方法需要与使用对象作为参数的方式结合使用才能发挥效果。所谓默认参数,是指在调用函数时如果没有实参,或是传递了null,则会传递一个指定的值。JavaScript 并不支持默认参数这一功能,但可以通过代码清单 5.5 这样的形式来实现。
通过 || 运算可以将参数作为布尔型来判断真假,其中利用了若调用函数时没有实参参数的值则为undefined 这一特性。通常来说,在函数内对参数进行赋值不是一种好习惯(不仅是 JavaScript,所有的程序语言都是如此),不过下面的做法被当作了一种习惯用法。
代码清单 5.5 模拟了默认参数的效果的习惯用法
function hzh(pos) {
pos = pos || { x:0, y:0, z:0 }; // 如果没有收到参数pos的话,则使用默认值
return Math.sqrt(pos.x * pos.x + pos.y * pos.y + pos.z * pos.z);
}
console.log("调用hzh函数:");
console.log(hzh({x:3, y:2, z:2}));
function hzh(pos) {
pos = pos || { x:0, y:0, z:0 }; // 如果没有收到参数pos的话,则使用默认值
return Math.sqrt(pos.x * pos.x + pos.y * pos.y + pos.z * pos.z);
}
console.log("调用hzh函数:");
console.log(hzh({x:3, y:2, z:2}));
代码清单 5.6 返回多值数据的函数
function hzh(pos) {
// 省略
return { x:3, y:2, z:2};
}
var pos = hzh();
console.log("输出返回值:");
console.log(pos.x, pos.y, pos.z);
[Running] node "e:\HMV\JavaScript\JavaScript.js"
调用hzh函数:
3 2 2
[Done] exited with code=0 in 0.191 seconds
用于替代构造函数来生成对象
最后我们介绍一下通过对象字面量来实现一个用于替代构造函数的函数的用法。该函数的功能是生成一个对象,所以需要以对象字面量作为返回值,从形式上来说,它和返回多值数据的函数是相同的。根据狭义的面向对象的定义,多值数据与对象的区别仅在于是否具有特定的执行方式。
代码清单 5.7 用于生成对象的函数(还有改进的余地)
function hzh() {
return { x:3, y:2, z:2,
huangzihan: function() {
return Math.sqrt(this.x * this.x +
this.y * this.y + this.z * this.z);
}
};
}
var obj = hzh();
console.log("调用obj对象的方法:");
console.log(obj.huangzihan());
[Running] node "e:\HMV\JavaScript\JavaScript.js"
调用obj对象的方法:
4.123105625617661
[Done] exited with code=0 in 0.26 seconds
专栏
JavaScript中用于函数返回多个值的增强功能
function hzh() {
return [1,9,1,2,4,8,9,6,0,1,7];
}
var a,b,c,d,e,f,g,h,i,j,k;
[a,b,c,d,e,f,g,h,i,j,k] = hzh();
console.log("输出数组返回值:");
console.log(a,b,c,d,e,f,g,h,i,j,k);
[Running] node "e:\HMV\JavaScript\JavaScript.js"
输出数组返回值:
1 9 1 2 4 8 9 6 0 1 7
[Done] exited with code=0 in 0.284 seconds
5.7.2 构造函数与 new 表达式
代码清单 5.8 构造函数的例子
function MyClass(x, y) {
this.x = x;
this.y = y;
}
// 对构造函数的调用
var obj = new MyClass(3, 2);
console.log("obj.x = " + obj.x);
console.log("obj.y = " + obj.y);
[Running] node "e:\HMV\JavaScript\JavaScript.js"
obj.x = 3
obj.y = 2
[Done] exited with code=0 in 0.173 seconds
new 表达式的操作
在此说明一下 new 表达式在求值时的操作。首先生成一个不具有特别的操作对象。之后通过 new 表达式调用指定的函数(即构造函数)。构造函数内的 this 引用引用了新生成的对象。执行完构造函数后,它将返回对象的引用作为 new 表达式的值。
图 5.5 构造函数的操作图
构造函数调用
构造函数总是由 new 表达式调用。为了与通常的函数调用相区别,将使用 new 表达式的调用,称为构造函数调用。构造函数与通常的函数的区别在于调用方式不同。任何函数都可以通过 new 表达式调用,因此,所有的函数都可以作为构造函数。也就是说,如果一个函数通过函数调用的方式使用,则是一个函数;如果通过构造函数调用的方式使用,则是一个构造函数。在实际开发中,通常会分别设计用于函数调用的函数与用于构造函数调用的函数,所以方便起见,将为了构造函数调用而设计的函数称为构造函数。构造函数的名称一般以大写字母开始(如 MyClass)。
构造函数在最后会隐式地执行 return this 操作。那么,如果在构造函数中显式地写有 return 语句,会发生什么情况呢?结果可能不容易理解。通过 return 返回一个对象之后,它将成为调用构造函数的 new 表达式的值。也就是说,使用 new 表达式后返回的,可能是所生成的对象以外的其他对象。然而,如果调用的构造函数中的 return 返回的是基本类型的值,则会无视这一返回值,仍然隐式地执行 return this 操作。
这种操作常常会造成混乱,我们建议不要再在构造函数内使用 return 语句。
5.7.3 构造函数与类的定义
代码清单 5.9 模拟类定义(尚有改进的余地)
// 相当于类的定义
function Huangzihan(x, y) {
// 相当于域
this.x = x;
this.y = y;
// 相当于方法
this.show = function() {
console.log(this.x, this.y);
}
}
// 对构造函数的调用(实例生成)
var hzh = new Huangzihan(3, 2);
console.log("访问obj对象的show方法:");
console.log(hzh.show());
[Running] node "e:\HMV\JavaScript\JavaScript.js"
访问obj对象的show方法:
3 2
undefined
[Done] exited with code=0 in 0.329 seconds
只要按照代码清单 5.9,就能够从形式上实现 JavaScript 的类定义。不过,代码清单 5.9 作为类的定义还存在以下两个问题。前者可以通过原型继承来解决,而后者可以通过闭包来解决
5.8 属性的访问
生成的对象可以通过属性来访问。对于对象的引用可以使用点运算符(.)或中括号运算符([])来访问其属性。需要注意的是,在点运算符之后书写的属性名会被认为是标识符,而中括号运算符内的则是被转为字符串值的式子。请看下面的例子:
var hzh1 = { x:3, y:4 };
console.log("输出hzh对象的x属性:");
console.log("hzh1.x = " + hzh1.x); // 属性x
console.log("hzh1[x] = " + hzh1['x']); // 属性x
var hzh2 = 'x';
console.log("hzh1[hzh2] = " + hzh1[hzh2]); // 属性x(而非属性key)
[Running] node "e:\HMV\JavaScript\JavaScript.js"
输出hzh对象的x属性:
hzh1.x = 3
hzh1[x] = 3
hzh1[hzh2] = 3
[Done] exited with code=0 in 0.181 seconds
不过,对于对象字面量的属性名来说,下面这样的标识符或字符字面量形式的表示,都没问题。请注意不要与上面的规则混淆。
var hzh1 = 'x';
var hzh2 = { hzh1:3 }; // 属性hzh1(而非属性x)
var hzh2 = { 'x':3 }; // 属性x
这里需要多提一句,属性访问的运算对象并不是变量,而是对象的引用。这一点,可以从以下直接 对对象字面量进行运算的示例中得到确认:
console.log("确认属性访问的运算对象是对象的引用:");
console.log({x:3, y:4}.x); // 属性x
console.log({x:3, y:4}['x']); // 属性x
[Running] node "e:\HMV\JavaScript\JavaScript.js"
确认属性访问的运算对象是对象的引用:
3
3
[Done] exited with code=0 in 0.183 seconds
5.8.1 属性值的更新
在赋值表达式的左侧书写属性访问表达式能够实现对属性值的改写。如果指定的是不存在的属性名,则会新增该属性。下面将不再使用右侧或左侧的说法,而改用属性读取,以及属性写入这样的术语。
可以使用 delete 运算表达式来删除属性。这里需要注意的是,很难区分不存在的属性与属性值为undefined 值的属性。
5.8.2 点运算符与中括号运算符在使用上的区别
有时选择用于访问对象属性的这两个运算符只凭偏好。点运算符的表述较为简洁,所以通常都会选用点运算符。不过,中括号运算符的通用性更高。
能使用点运算符的情况一定也可以使用中括号运算符,反之未必成立。但也无需因此全都使用中括号运算符。通常默认使用表述简洁的点运算符,只有在不得不使用中括号运算符的情况下,才使用中括号运算符。
只能使用中括号运算符的情况分为以下几种。
包含数值或横杠(-)的字符串不能作为标识符使用。无法作为标识符使用的字符串,不能用于点运算符的属性名,且对于保留字,也有这样的限制。不过,原本就不应该将保留字作为属性名使用,所以这里不再赘述。
// 含有横杠的属性名
var hzh = { 'huang-zihan':5 };
console.log(hzh.huang-zihan); // 将解释为hzh.huang减去zihan,从而造成错误
[Running] node "e:\HMV\JavaScript\JavaScript.js"
e:\HMV\JavaScript\JavaScript.js:3
console.log(hzh.huang-zihan); // 将解释为hzh.huang减去zihan,从而造成错误
^
ReferenceError: zihan is not defined
at Object.<anonymous> (e:\HMV\JavaScript\JavaScript.js:3:23)
at Module._compile (internal/modules/cjs/loader.js:999:30)
at Object.Module._extensions..js (internal/modules/cjs/loader.js:1027:10)
at Module.load (internal/modules/cjs/loader.js:863:32)
at Function.Module._load (internal/modules/cjs/loader.js:708:14)
at Function.executeUserEntryPoint [as runMain] (internal/modules/run_main.js:60:12)
at internal/main/run_main_module.js:17:47
[Done] exited with code=1 in 0.413 seconds
// 含有横杠的属性名
var hzh = { 'huang-zihan':5 };
console.log(hzh['huang-zihan']); // 使用[]运算以字符串值指定了一个属性名。可以正常执行
[Running] node "e:\HMV\JavaScript\JavaScript.js"
5
[Done] exited with code=0 in 0.773 seconds
数值也是如此。数组对象的属性名都是数值。由于点运算符无法使用数值,因此只能使用中括号运算符。而且很多程序设计语言都是通过中括号运算符来访问数组的元素,所以可读性也随之提高。
var hzh1 = { x:3, y:4 };
console.log("输出hzh对象的x属性:");
console.log("hzh1.x = " + hzh1.x); // 属性x
console.log("hzh1[x] = " + hzh1['x']); // 属性x
var hzh2 = 'x';
console.log("hzh1[hzh2] = " + hzh1[hzh2]); // 属性x(而非属性key)
[Running] node "e:\HMV\JavaScript\JavaScript.js"
输出hzh对象的x属性:
hzh1.x = 3
hzh1[x] = 3
hzh1[hzh2] = 3
[Done] exited with code=0 in 0.181 seconds
如果表达式的求值结果是字符串,可以直接用中括号运算符通过该表达式指定属性名。下面引用出自《JavaScript 语言精粹》一书的一个具有一定技巧性的例子。
这段代码会根据数值的符号而选择调用不同的方法。方法调用一词会让人觉得要使用的是点运算符,不过事实上中括号运算符也能被调用。
// 引用自《JavaScript语言精粹》一书
// 仅读取数值的整数部分的处理
Math[this < 0 ? 'ceiling' : 'floor'](this));
5.8.3 属性的枚举
可以通过 for in 语句对属性名进行枚举(代码清单 5.10)。通过在 for in 语句中使用中括号运算符,可以间接地实现对属性值的枚举。使用 for each in 语句可以直接枚举属性值。
代码清单 5.10 属性的枚举
var hzh1 = { x:'黄子涵是帅哥!', y:'黄子涵是靓仔!', z:'黄子涵真聪明!' };
for(var key in hzh1) {
console.log('key = ', key); // 属性名的枚举
console.log('val = ', hzh1[key]); // 属性值的枚举
}
[Running] node "e:\HMV\JavaScript\JavaScript.js"
key = x
val = 黄子涵是帅哥!
key = y
val = 黄子涵是靓仔!
key = z
val = 黄子涵真聪明!
[Done] exited with code=0 in 0.262 seconds
5.9 作为关联数组的对象
JavaScript 的对象和 Java 的映射(Map)类似。
如果将 JavaScript 对象的属性名看作键,属性值看作值,我们会发现它与 Java 中的映射非常相似。JavaScript 的对象还具有 Java 的映射所不具备的附加功能(例如方法或原型继承等),但也可以不理会这些功能,直接将其作为映射来使用。
5.9.1 关联数组
首先对与关联数组相关的术语进行整理。将数值作为键的值的数据结构通常称为数组。数组是绝大多数程序设计语言都支持的一种基本的数据结构。
由于数组的键是连续的数值,因此可以将其看作具有顺序的值的集合。除了数值以外大多都会使用字符串作为键值。不过键的类型也可以不限于字符串,对任意类型的键与值的集合进行操作的数据结构称为关联数组。在有些语言中,关联数组也被称为映射或字典。也有根据内部实现而将其称为散列的语言。虽然用词不同,但其数据结构是相同的,使用何种称法都可以。
关联数组最主要的用途是执行通过键来读取值的操作。在其他程序设计语言,特别是一些脚本语言中,关联数组被设计为一种语言本身的功能,不过在 JavaScript 中,必须通过对象来实现关联数组。
请注意,并没有专门用于关联数组的对象,这仅仅是对对象的一种不同的用法。
关联数组的操作方式
关联数组是元素的集合,其元素为键与值的配对。关联数组的基本操作有通过键来获取值、元素的设定、元素的删除这 3 种。由于其实体是 JavaScript 的对象,所以这里的元素只不过是属性的另一种说法,而键与值分别是属性名与属性值的别称。
可以通过点运算符或中括号运算符来实现按键取值。严格地说,是将该值作为右值来使用。
对于元素的设定,可以将点运算符或是中括号运算符作为左值写入赋值表达式。
// 删除关联数组的元素的例子(属性的删除)
var hzh = { x:3, y:4 };
console.log("删除前,输出hzh关联数组的x键值:");
console.log(hzh.x);
console.log("");
console.log("如果删除成功,则返回true:");
console.log(delete hzh.x); // 也可以使用delete hzh['x']
console.log("");
console.log("删除后,输出hzh关联数组的x键值:");
console.log(hzh.x);
[Running] node "e:\HMV\Babel\hzh.js"
删除前,输出hzh关联数组的x键值:
3
如果删除成功,则返回true:
true
删除后,输出hzh关联数组的x键值:
undefined
[Done] exited with code=0 in 0.172 seconds
在 C++ 语言中也有 delete 这个关键字,不过其功能却全然不同。在 C++ 中 delete 的功能是释放所引用的对象的内存,而在 JavaScript 中 delete 只用于删除对象中的属性。用映射中的术语来说就是,仅仅从映射中删除键,使其对应的值(对于对象来说也就是属性值)与该键不再有对应关系。虽然失去了引
用的对象最终可能会因为垃圾回收机制而消失,不过这并不是 delete 运算的直接功能。
对不存在的元素进行访问得到的结果是 undefined 型。需要注意的是,这与 Java 中映射返回的 null 是不同的。由于可以显式地将值设定为 undefined 值,因此无法通过将键与 undefined 值作等值比较来实现对键是否存在的检验。
5.9.2 作为关联数组的对象的注意点
作为关联数组的对象有一些和原型继承相关的注意点。原型继承指的是一种对象继承其他对象的属性并将其作为自身的属性一样来使用的做法。
function huangzihan() {}
huangzihan.prototype.z = 5; // 在原型链上设定属性z
var hzh = new huangzihan(); // 属性z继承了原型
console.log(hzh.z);
[Running] node "e:\HMV\Babel\hzh.js"
5
[Done] exited with code=0 in 0.868 seconds
for in 语句将枚举通过原型继承而得到的属性。
function huangzihan() {}
huangzihan.prototype.z = 5; // 在原型链上设定属性z
var hzh = new huangzihan(); // 属性z继承了原型
console.log(hzh.z);
for (var key in hzh) {
console.log(key); // for in 语句也会被枚举通过原型继承得到的属性
}
[Running] node "e:\HMV\Babel\hzh.js"
5
z
[Done] exited with code=0 in 0.261 seconds
请注意,通过原型继承而得到的属性无法被 delete。
function huangzihan() {}
huangzihan.prototype.z = 5; // 在原型链上设定属性z
var hzh = new huangzihan();
console.log("属性z继承了原型:");
console.log(hzh.z);
console.log("");
console.log("for in 语句也会被枚举通过原型继承得到的属性:");
for (var key in hzh) {
console.log(key);
}
console.log("");
console.log("尽管没有被delete,但还是会返回true......");
console.log(delete hzh.z);
console.log("尝试把关联数组hzh的属性z输出:");
console.log(hzh.z); //无法 delete 通过原型继承而得到的属性
[Running] node "e:\HMV\Babel\hzh.js"
属性z继承了原型:
5
for in 语句也会被枚举通过原型继承得到的属性:
z
尽管没有被delete,但还是会返回true......
true
尝试把关联数组hzh的属性z输出:
5
[Done] exited with code=0 in 0.852 seconds
在将对象作为关联数组使用时,通常都会使用对象字面量来生成。不过需要注意的是,即使视图通过使用空的对象字面量以创建一个没有元素的关联数组,也仍然会从 Object 类中继承原型的属性。可以通过 in 运算对此进行检验。
var hzh = {}; // 通过空的对象字面量生成关联数组
console.log("看看是否在Object类中原型继承了属性toString:");
console.log('toString' in hzh);
[Running] node "e:\HMV\Babel\hzh.js"
看看是否在Object类中原型继承了属性toString:
true
[Done] exited with code=0 in 0.214 seconds
var hzh = {}; // 通过空的对象字面量生成关联数组
console.log("看看是否在Object类中原型继承了属性toString:");
console.log('toString' in hzh);
for(var key in hzh) {
console.log("在key前面设置标志位");
console.log(key);
console.log("在key后面设置标志位");
}
// 没有元素会被枚举
[Running] node "e:\HMV\Babel\hzh.js"
看看是否在Object类中原型继承了属性toString:
true
[Done] exited with code=0 in 0.174 seconds
通过 in 运算符检测关联数组的键是否存在,就会发生与原型继承而来的属性相关的问题。因此,像下面这样通过 hasOwnProperty 来对其进行检测,是一种更安全的做法。
var hzh = {};
console.log(hzh.hasOwnProperty('toString')); // 由于toString不是直接属性,因此结果是 false
console.log("");
hzh['toString'] = 1;
console.log(hzh.hasOwnProperty('toString'));
console.log("");
delete hzh['toString'];
console.log(hzh.hasOwnProperty('toString'));
[Running] node "e:\HMV\Babel\hzh.js"
false
true
false
[Done] exited with code=0 in 0.244 seconds