键值聚合体的对象


在 JavaScript 中,对象是Object的实例。你可以如下创建一个新的对象:

var obj = new Object();

实际上,现在已经很少人这么编写了,使用对象字面量(Object literal)语法就可以创建一个对象:

var obj = {};

两者的作用相同,对象字面量写法显然较有效率。在 JavaScript 中,每个对象都可以是独一无二,不一定是由其构造函数来规范,这能力称为对象个性化(Object individuation),你可以随时为对象新增特性(Properties),也可以随时用delete运算符来删除特性。例如:

> var obj = {};
undefined
> obj.x = 10;
10
> obj.x;
10
> delete obj.x;
true
> obj.x;
undefined
>

若删除成功,delete会返回true,并非所有特性都可被delete删除,不可配置的特性无法被删除(之后文件会看到如何定义对象特性是否为可配置),举例来说,Array实例有个length特性,你无法删除它,这会引发TypeError

> var arr = [];
undefined
> arr.length;
0
> delete arr.length;
TypeError: Cannot delete property 'length' of [object Array]
    at repl:1:1
    at ContextifyScript.Script.runInThisContext (vm.js:50:33)
    at REPLServer.defaultEval (repl.js:240:29)
    at bound (domain.js:301:14)
    at REPLServer.runBound [as eval] (domain.js:314:12)
    at REPLServer.onLine (repl.js:441:10)
    at emitOne (events.js:121:20)
    at REPLServer.emit (events.js:211:7)
    at REPLServer.Interface._onLine (readline.js:282:10)
    at REPLServer.Interface._line (readline.js:631:8)
>

附带一提的是,delete作用在字面量(Literal)上时会返回true,例如delete 1

如果事先知道对象的特性,可以使用对象字面量一并创建。例如:

> var obj = {
...     x : 10,
...     y: 20
... };
undefined
> obj.x;
10
> obj.y;
20
>

想要知道对象上有哪些自定义特性,可以使用for in语法,逐一取出对象的特性名称。例如:

> for(var prop in obj) {
...     console.log(prop);
...     console.log(typeof prop);
... }
x
string
y
string
undefined
> 'x' in obj;
true
>

由以上也可以得知,每个特性名称其实是字符串类型,这也说明了,如果想用in测试某特性时,特性名称必须以字符串指定。

事实上,点运算符(.)只是访问对象特性的一种方式。你也可以使用[]运算符来访问特性。例如:

> var obj = {};
undefined
> obj['x'] = 10;
10
> obj.x;
10
> obj['x'];
10
>

JavaScript 的对象本质上,其实是个特性与值的群集(Collection),要比喻的话,有点像是 Java 中的Map对象。如果你要使用for in获取对象上的特性与值,则可以如下:

> var obj = {
...     x : 10,
...     y : 20
... };
undefined
> for(var prop in obj) {
...     console.log(prop + ': ', obj[prop]);
... }
x:  10
y:  20
undefined
>

使用[]运算符的场合之一,就是当你的特性会包括空白、.字符等时。例如:

> var obj = {
...     'openhome.cc': 'OpenHome',
... };
undefined
> obj.openhome.cc;
TypeError: Cannot read property 'cc' of undefined
    at repl:1:13
    at REPLServer.self.eval (repl.js:110:21)
    at repl.js:249:20
    at REPLServer.self.eval (repl.js:122:7)
    at Interface.<anonymous> (repl.js:239:12)
    at Interface.EventEmitter.emit (events.js:95:17)
    at Interface._onLine (readline.js:202:10)
    at Interface._line (readline.js:531:8)
    at Interface._ttyWrite (readline.js:760:14)
    at ReadStream.onkeypress (readline.js:99:10)
> obj['openhome.cc'];
'OpenHome'
> delete obj['openhome.cc'];
true
> 'openhome.cc' in obj;
false
>

除了使用in测试对象上是否存在特性之外,由于对象上不存在某个特性时,你试图访问时会返回undefined,而undefined若在判断是否成立时会被当作false,所以就有了特性侦测的作法:

> var obj = {};
undefined
> obj.x ? 'has x' : 'has no x';
'has no x'
> obj.x = 10;
10
> obj.x ? 'has x' : 'has no x';
'has x'
>

特性侦测也可以与||一同结合,用在合并对象特性。例如:

function doSome(option) {    
    return {
        x : option.x || 1,
        y : option.y || 2,
        z : option.z || 3
    };
}

function log(obj) {
    for(var p in obj) {
        console.log(p + ': ' + obj[p]);
    }
}

var processed = doSome({
    x : 10,
    y : 20
});

log(processed);

在上例中,doSome返回对象的xyz特性默认值分别是 1、2、3,如果option上有提供对应的特性,则以option提供的为主,这经常用在函数上有太多参数及默认值要提供的场合,之后说明函数时还会看到,不清楚||运算为何会有这种结果的话,请看〈不只是加减乘除的运算符〉中的说明。执行结果会显示:

x: 10
y: 20
z: 3

JavaScript 是个弱类型语言,在需要将对象转为数值的场合,会调用valueOf方法。例如:

> var obj = {
...     valueOf : function() {
.....       return 100;
.....   }
... };
undefined
> 100 + obj;
200
> obj + 200;
300
> obj > 100;
false
> obj >= 100;
true
>

在需要将对象转换为字符串的场合,则会调用toString方法。例如:

> var caterpillar = {
...     name : 'Justin Lin',
...     url  : 'openhome.cc',
...     toString : function() {
.....         return '[name: ' + this.name + ', url: ' + this.url + ']';
.....     }
... };
undefined
> 'My info: ' + caterpillar;
'My info: [name: Justin Lin, url: openhome.cc]'
>

在上例中,若透过caterpillar调用了toStringthis就是参考至caterpillar所参考的对象,之后还会详细说明this是什么。

在 JavaScript 中,===用在对象比较时,是比较参考的对象是否为同一对象,而不是对象实际内含值(==得考虑类型转换后的结果),如果你要比较两个对象实际上是否为同一对象,必须自行定义专属方法,这个方法名并没有规范。例如,也许定义个equals方法:

> var man1 = {
...     name : 'Justin Lin',
...     url  : 'openhome.cc',
...     equals : function(other) {
.....         return (this.name === other.name) &&  (this.url === other.url);
.....     }
... };
undefined
> var man2 = {
...     name : 'Justin Lin',
...     url  : 'openhome.cc',
...     equals : function(other) {
.....         return (this.name === other.name) &&  (this.url === other.url);
.....     }
... };
undefined
> man1 === man2;
false
> man1.equals(man2);
true
>

在上例中,两个对象的equals参考的函数定义重复了。如果你懂在 JavaScript 中函数是对象的观念,则可以修改如下:

function equals(other) {
    return (this.name === other.name) &&  (this.url === other.url);
}

var man1 = {
    name : 'Justin Lin',
    url  : 'openhome.cc',
    equals : equals
};

var man2 = {
    name : 'Justin Lin',
    url  : 'openhome.cc',
    equals : equals
};

var man3 = {
    name : 'Justin Lin',
    url  : 'openhome.cc',
    equals : equals
};

console.log(man1.equals(man2));  // true
console.log(man1.equals(man3));  // true

实际上,如果你知道如何使用函数定义构造函数,并了解运用原型链(Prototype chain)实现继承,上头的需求可以改为以下的方式:

function Man(name, url) {
    this.name = name;
    this.url = url;
}

Man.prototype.equals = function(other) {
    return (this.name === other.name) &&  (this.url === other.url);
};

var man1 = new Man('Justin Lin', 'openhome.cc');
var man2 = new Man('Justin Lin', 'openhome.cc');
var man3 = new Man('Justin Lin', 'openhome.cc');

console.log(man1.equals(man2));
console.log(man1.equals(man3));

至于为何可以这么做,会留在之后说明函数时再来讨论。

在严格模式中,如果想删除的特性并非可配置的(Configurable)(之后文件会看到如何定义对象特性是否为可配置),会发生TypeError。举例来说,Array实例有个length特性,在严格模式下你不能对它做delete,否则会引发TypeError


展开阅读全文