检验对象


因为 JavaScript 是动态语言,通常很少直接确认对象的类型,对于对象的操作,仅要求是否具备所需特性,而不在意所谓的类型,对象的特性侦测绝大多数情况下就足够了。例如:

if(obj.someProperty) { 
    // 特性存在时作某些事
}

因为特性不存在的话,会返回undefined,而在判断式中会被作为false,若存在,则会返回对象,在判断式中会被作为true,这就是对象特性侦测的基本原理。

如果真得确认对象的类型,有许多方式,但这些方式基本上不是提供的信息有限,就是不能完全信任。

例如,许多场合最常看到的typeof运算符,返回值是字符串,对于基本数据类型,数值会返回'number'、字符串会返回'string'、布尔会返回'boolean'、对于Function实例会返回'function'、对于undefined会返回'undefined'、对于其他对象一律返回'object',包括null也是返回'object',所以使用typeof,只要是非函数实例的对象,基本上无从辨别真正类型。

你可以从对象的constructor特性来确认对象的构造函数为何,因为如〈函数 prototype 特性〉有谈过,每个函数的实例,其prototype会有个constructor特性,参考至实例化对象时的函数,这是确认对象类型的方式之一,只不过,constructor是个可修改的特性,虽然没什么人会去修改constructor特性,但是如果是在原型链的情况下:

function Car() {}
Car.prototype.wheels = 4;

function SportsCar() {}
SportsCar.prototype = new Car();
SportsCar.prototype.doors = 2;

var sportsCar = new SportsCar();
console.log(sportsCar.doors);        // 2
console.log(sportsCar.wheels);       // 4
console.log(sportsCar.constructor);  // [Function: Car]

上面这个例子,是经常见到利用原型链查找机制,实现出继承的效果。由于SportsCar.prototype设定为Car的实例,所以在查找wheels特性时,sportsCar参考的对象本身没有,就到原型对象上找,也就是SportsCar.prototype所参考的对象上找,这个对象是Car的实例,本身也没有wheels特性,所以就到Car实例的原型寻找,也就是Car.prototype参考的对象,此时就找到了。

然而,在查找constructor时,依同样的机制,找到的其实是Car.prototype.constructor特性,上例中应该再加一行才会比较正确:

SportsCar.prototype.constructor = SportsCar;

如果忘了作这个动作,试图透过constructor识别对象的类型,得到的就会是不正确的结果。

关于使用new创建实例,〈函数 prototype 特性〉中谈过,使用new关键字时,JavaScript 会先创建一个空对象实例,接着设定实例的原型对象为函数的prototype特性参考的对象,然后调用构造函数并将创建的实例设为this

注意,实例的原型对象是在创建实例之后就确立下来的,原型链查找特性时,是根据实例上的原型对象,而不是函数上的prototype。例如,你可以看看以下为何无法获取特性:

function Car() {
    Car.prototype.wheels = 4;
}

function SportsCar() {
    SportsCar.prototype = new Car();
    SportsCar.prototype.doors = 2;
}

var sportsCar = new SportsCar();
console.log(sportsCar.doors);    // undefined
console.log(sportsCar.wheels);   // undefined

这是初学者常犯的错误。注意,对象的原型是在创建对象之后就确立下来的,所以在这行:

var sportsCar = new SportsCar();

sportsCar参考的实例就被指定了原型对象,也就是当时的SportsCar.prototype参考的对象,默认就是具有一个constructor特性的Object实例,之后你在SportsCar函数中将SportsCar.prototype指定为Car的实例,对sportsCar的原型对象根本没有影响,sportsCar的原型对象仍是Object实例,而不是Car实例,自然就找不到doors特性,更别说是wheels特性了。

再来用实际的程序示范会更清楚,这次用非标准的Object.getPrototypeOf来验证:

function Car() {
    Car.prototype.wheels = 4;
}

function SportsCar() {
    SportsCar.prototype = new Car();
    SportsCar.prototype.doors = 2;
}

var sportsCar = new SportsCar();

console.log(
    Object.getPrototypeOf(sportsCar) === SportsCar.prototype
); // false

从上例中可以看到,创建实侧时就设定了原型对象,而实例上的原型对象最后跟SportsCar.prototype根本就不是同一个对象了。

事实上,instanceof也是根据对象的原型对象来判断truefalse的。例如:

function Car() {}
function SportsCar() {}
SportsCar.prototype = new Car();

var sportsCar = new SportsCar();
console.log(sportsCar instanceof SportsCar);  // true
console.log(sportsCar instanceof Car);        // true
console.log(sportsCar instanceof Object);     // true

简单地说,instanceof是根据原型链来查找。明白这个机制,就可以用Object.create来创建一个类数组对象,并令instanceof Array检验结果为true

var arrayLike = Object.create(Array.prototype, {
    '0'    : {value : 10},
    '1'    : {value : 20},
    '2'    : {value : 30},
    length : {value : 3}
});

console.log(arrayLike instanceof Array);  // true

根据〈函数 prototype 特性〉中对Object.create的介绍,上例中创建的对象,并不是直接从Array构造而来,不过,最后的结果依然显示为true

如果你想要检验对象原型,除了使用Object.getPrototypeOf获取原型对象外,也可以使用isPrototypeOf方法。例如:

console.log(Array.prototype.isPrototypeOf([]));              // true
console.log(Function.prototype.isPrototypeOf(Array));         // true
console.log(Object.prototype.isPrototypeOf(Array.prototype)); // true

isPrototypeOf的作用与instanceof类似,都是透过原型链来确认:

console.log(Array.prototype.isPrototypeOf([]));  // true
console.log(Object.prototype.isPrototypeOf([])); // true

在获取一个对象的特性时会寻找原型链,如果想确认特性是对象本身所拥有,或是其原型上的特性,可透过对象都具有的hasOwnProperty方法(当然,这是Object.prototype上的一个特性)。例如:

var o = {x : 10};
console.log(o.hasOwnProperty('x'));        // true
console.log(o.hasOwnProperty('toString')); // false
console.log(o.hasOwnProperty('xyz')); // false

如果特性不是对象本身拥有,而是原型链上可获取,则会返回false,寻找不到特性也是返回false

在对象上直接使用.[]新建的特性可以用for in枚举,有些内置特性或特性的enumerable被设为false时无法枚举,想要知道特性是不是可用for in枚举,则可以使用propertyIsEnumerable方法。例如:

var o = {x : 10};
console.log(o.propertyIsEnumerable('x'));        // true
console.log(o.propertyIsEnumerable('toString')); // false
console.log(o.propertyIsEnumerable('xyz'));      // false

当然,特性不存在时就无法枚举,所以会返回false

ECMAScript 5 中,想要一次获取对象上可枚举的特性名称,可以使用Object.keys,例如:

console.log(Object.keys({x : 10, y : 20}).join(', ')); // x, y

如果想要获取对象本身的特性名称,无论enumerable是否设为false,可以使用Object.getOwnPropertyNames,例如:

var obj = {};

Object.defineProperties(obj, {
    'name': {
         value      : 'John',
         enumerable : true
     },
     'age': {
         value      : 39,
         enumerable : false
     },
});

console.log(Object.keys(obj).join(', '));                // name
console.log(Object.getOwnPropertyNames(obj).join(', ')); // name, age

另外,ECMAScript 规格要求Object默认的toString要返回'[object class]'格式的字符串。JavaScript 的内置类型基本上都会遵守这样的规定,例如Object实例会返回[object Object]、数组会返回[object Array]、函数会返回[object Function]等,这也可作为判断类型的依据,基于对标准的支持,现在一些程序库多使用这个来作判断。


展开阅读全文