其他分享
首页 > 其他分享> > 第5章 变量与对象

第5章 变量与对象

作者:互联网

绩效统计

章节 代码量(行数)
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 值型变量的执行方式

image

图5.2 引用类型的变量的执行方式

image

如果像下面这样,改变了变量 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 引用类型的变量的执行方式

image

在 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 变量与属性

其实,在 JavaScript 中变量就是属性,两者何止是相似,本身就是同一个概念。

根据作用域的不同,变量可以被分为全局变量和局部变量(包括参数变量)。全局变量是在最外层代码中声明的变量。所谓最外层代码,指的是写在函数之外的代码。局部变量则是在函数内部声明的变量。全局变量和局部变量两者的本质都是属性。

全局变量(以及全局函数名)是全局对象的属性。全局对象是从程序运行一开始就存在的对象

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 具有一种自己引用了自己的关系

image

这种关系看起来有些混乱,在 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 运算。

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

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 对象字面量

在 JavaScript 程序中,如果要使用对象,就需要首先生成该对象。其中一种方法是通过对象字面量来实现对象的生成。

下面列举了一些可以使用对象字面量的情况。请注意这里并没有作严格的分类。

  • 作为 singleton 模式的用法。
  • 作为多值数据的用法(函数的参数或返回值等)。
  • 用于替代构造函数来生成对象。

作为 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.6 一样,直接在代码中书写数值没有什么意义,这里仅仅是作为一个例子用于说明而已(代码清单 5.7)。

代码清单 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

使用返回对象字面量的函数,与通过 new 表达式来调用构造函数,是两种不同风格的生成对象的手段。

专栏

JavaScript中用于函数返回多个值的增强功能

通过 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)。

可以直观地将代码清单 5.8 理解为 MyClass 类的类定义。在调用时通过 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 表达式的操作

在此说明一下 new 表达式在求值时的操作。首先生成一个不具有特别的操作对象。之后通过 new 表达式调用指定的函数(即构造函数)。构造函数内的 this 引用引用了新生成的对象。执行完构造函数后,它将返回对象的引用作为 new 表达式的值。

图 5.5 构造函数的操作图

image

构造函数调用

构造函数总是由 new 表达式调用。为了与通常的函数调用相区别,将使用 new 表达式的调用,称为构造函数调用。构造函数与通常的函数的区别在于调用方式不同。任何函数都可以通过 new 表达式调用,因此,所有的函数都可以作为构造函数。也就是说,如果一个函数通过函数调用的方式使用,则是一个函数;如果通过构造函数调用的方式使用,则是一个构造函数。在实际开发中,通常会分别设计用于函数调用的函数与用于构造函数调用的函数,所以方便起见,将为了构造函数调用而设计的函数称为构造函数。构造函数的名称一般以大写字母开始(如 MyClass)。

构造函数在最后会隐式地执行 return this 操作。那么,如果在构造函数中显式地写有 return 语句,会发生什么情况呢?结果可能不容易理解。通过 return 返回一个对象之后,它将成为调用构造函数的 new 表达式的值。也就是说,使用 new 表达式后返回的,可能是所生成的对象以外的其他对象。然而,如果调用的构造函数中的 return 返回的是基本类型的值,则会无视这一返回值,仍然隐式地执行 return this 操作。

这种操作常常会造成混乱,我们建议不要再在构造函数内使用 return 语句。

5.7.3 构造函数与类的定义

通过 new 表达式调用普通的函数并生成一个对象,是一种不容易理解的语言特性。不过,这已经满足了类定义所必需的功能。

代码清单 5.9 是一个实现了定义一个具有域与方法的类的构造函数的例子。

代码清单 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 作为类的定义还存在以下两个问题。前者可以通过原型继承来解决,而后者可以通过闭包来解决

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

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

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

5.9 作为关联数组的对象

JavaScript 的对象和 Java 的映射(Map)类似。

如果将 JavaScript 对象的属性名看作键,属性值看作值,我们会发现它与 Java 中的映射非常相似。JavaScript 的对象还具有 Java 的映射所不具备的附加功能(例如方法或原型继承等),但也可以不理会这些功能,直接将其作为映射来使用。

5.9.1 关联数组

首先对与关联数组相关的术语进行整理。将数值作为键的值的数据结构通常称为数组。数组是绝大多数程序设计语言都支持的一种基本的数据结构。

由于数组的键是连续的数值,因此可以将其看作具有顺序的值的集合。除了数值以外大多都会使用字符串作为键值。不过键的类型也可以不限于字符串,对任意类型的键与值的集合进行操作的数据结构称为关联数组。在有些语言中,关联数组也被称为映射或字典。也有根据内部实现而将其称为散列的语言。虽然用词不同,但其数据结构是相同的,使用何种称法都可以。

关联数组最主要的用途是执行通过键来读取值的操作。在其他程序设计语言,特别是一些脚本语言中,关联数组被设计为一种语言本身的功能,不过在 JavaScript 中,必须通过对象来实现关联数组。

请注意,并没有专门用于关联数组的对象,这仅仅是对对象的一种不同的用法。

关联数组的操作方式

关联数组是元素的集合,其元素为键与值的配对。关联数组的基本操作有通过键来获取值、元素的设定、元素的删除这 3 种。由于其实体是 JavaScript 的对象,所以这里的元素只不过是属性的另一种说法,而键与值分别是属性名与属性值的别称。

可以通过点运算符或中括号运算符来实现按键取值。严格地说,是将该值作为右值来使用。

对于元素的设定,可以将点运算符或是中括号运算符作为左值写入赋值表达式。

对象的删除可以通过 delete 运算符。用对象的术语来说就是删除属性。使用方法如下。

// 删除关联数组的元素的例子(属性的删除)
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 作为关联数组的对象的注意点

作为关联数组的对象有一些和原型继承相关的注意点。原型继承指的是一种对象继承其他对象的属性并将其作为自身的属性一样来使用的做法。

如下所示,从形式上来说,对象 obj 的属性并不是其直接属性,而是通过原型继承而得到的属性。

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

但是,通过 for in 语句对元素进行枚举不会有任何效果。这是由于 enumerable 属性的缘故。

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

5.10 属性的属性

虽然说起来有些绕口,不过属性也是有其属性的。表 5.1 总结了 ECMAScript 第 5 版中定义了的属性 。

在 ECMAScript 中,属性值被定位为“值属性”这样一种属性。使用这一定义的话,属性就成为了名称(属性名)和多个属性的集合。

表 5.1 属性的属性

属性的属性名 含义
writable 可以改写属性值
enumerable 可以通过for in 语句枚举
configurable 可以改变属性的属性。可以删除属性。
get 可以指定属性值的 getter 函数
set 可以指定属性值的 setter 函数

在表 5.1 的属性中,enumerable 是在 ECAMScript 第 5 版之前就被广泛使用的属性。在标准的对象中有一部分属性的 enumerable 属性为假而无法通过 for in 语句枚举。其中一个很容易理解的例子是数列的 length 属性。

虽然 ECMAScript 第 5 版对属性读写方法进行了标准化处理,不过在实际的 JavaScript 开发中,我们一般也不会用到对属性的读写。而 enumerable 也是标准对象所具有的属性,所以通常也不需要对自己生成的对象的属性显式地进行修改。不过属性本身确实有助于使代码更为健壮,或许随着 ECMAScript 第 5 版的普及,变更属性的情况也会变得越来越常见。

5.11 垃圾回收

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

由 JavaScript 有着客户端程序大多运行时间很短这一历史原因,因此与其他程序设计语言相比,开发者并不太关心对象的存在生命周期。如果整个程序的生命周期就很短,相对来说就没有必要对每个对象的生命周期太过在意。

不过随着最近各种 Web 应用以及服务器端 JavaScript 程序的发展,情况发生了变化。现在已经有必要像其他的程序设计语言那样,考虑对象的生命周期问题了。垃圾回收的目的是,使开发者不必为对象的生命周期管理花费太多精力。因此通常只考虑代码即可,具体的 JavaScript 实现会帮忙解决那些麻烦的问题。虽说通过 delete 来删除不再使用的属性是一个不错的习惯,但只要不会造成内存泄漏,就没有必要在这方面花太多的心思。

不过,即使有垃圾回收功能,仍然有可能发生内存泄漏。有些是由于垃圾回收机制的实现存在问题,更多的是因为发生了循环引用的情况而造成了内存泄漏。

所谓循环引用,指的是对象通过属性相互引用而导致它们不会被判定为不再使用的状态。对于客户端 JavaScript 来说,存在几种常见的可能导致循环引用的情况,因此建议使用内存泄漏检测工具来检测。

5.12 不可变对象

5.12.1 不可变对象的定义

所谓不可变对象,指的是在被生成之后状态不能再被改变的对象。由于对象的状态是由其各个属性的值所决定的,因此从形式上来说也是指无法改变属性的值的对象。也有观点认为,在对象引用了另一个对象的情况下,只有当那个被引用的对象也是不可变的时候,引用了它的对象才能被称为不可变对象。

从广义上来说,不可变对象指的是不去改变状态的对象。而从狭义上来说,只有既没有改变,也无法改变状态的对象,即为了禁止改变而专门设计的对象,才被称为不可变对象。JavaScript 中的一种典型的不可变对象就是字符串对象。

5.12.2 不可变对象的作用

灵活运用不可变对象有助于提高程序的健壮性。这是因为,程序中的很多错误都是由于非法改变了对象的状态而造成的。例如,将对象传递给方法的参数时,存在方法会改写对象内容的隐患。如果那是一个不可变对象,则不用担心这一问题。不清楚对象的内部构造就改写很容易引起错误,在排除了这种情况之后,就可以减少花在这个问题上的精力。

虽然不可变对象是一种便利的程序设计技巧,但其实在 JavaScript 开发中并没有被大量使用。其中最主要的一个原因就是花销的取舍。为了确保对象的不可变,不得不增加一些和主要功能无关的代码。对于一直使用小规模代码的 JavaScript 来说,需要权衡花销。

5.12.3 实现不可变对象的方式

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

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

JavaScript 中的对象没有像 private 属性这样的显式访问控制功能。为了将属性隐藏,可以使用一种被称为闭包的方法。

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

表 5.2 ECMAScript 第 5 版中用于支持对象的不可变化的函数
方法名 属性新增 属性删除 属性变更 确认方法
preventExtension × Object.isExtensible
seal × × Object.isSealed
freeze × × × Object.isFrozen

代码清单 5.6 ~ 代码清单 5.8 是各个方法的具体示例。Object.keys方法用于对属性枚举。

5.13 方法

5.14 引用

5.14.1 this 引用的规则

5.14.2 this 引用的注意点

5.15 apply与call

5.16 原型继承

5.16.1 原型链

5.16.2 原型链的具体示例

5.16.3 原型继承与类

5.16.4 对于原型链的常见误解以及 proto 属性

5.16.5 原型对象

5.16.6 ECMAScript 第 5 版与原型对象

5.17 对象与数据类型

5.17.1 数据类型判定(constructor 属性)

5.17.2 constructor 属性的注意点

5.17.3 数据类型判定(instance 运算与 isPrototypeOf 方法)

5.17.4 数据类型判定(鸭子类型)

5.17.5 属性的枚举(原型继承的相关问题)

5.18 ECMScript第5版中的Object类

5.18.1 属性对象

5.18.2 访问器的属性

5.19 标准对象

5.20 Object类

5.21 全局对象

5.21.1 全局对象与全局变量

5.21.2 Math 对象

5.21.3 Error 对象

标签:console,变量,对象,JavaScript,hzh,log,属性
来源: https://www.cnblogs.com/Huang-zihan/p/16281678.html