JavaScript 語言核心(16)檢驗物件



因為 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
如果想要取得物件本身的特性名稱,無論 `` 是否設為 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] 等,這也可作為判斷型態的依據,基於對標準的支持,現在一些程式庫多使用這個來作判斷。
注意!在 Internet Explorer 中,alertconfirm 等內建函式,或是某些物件上的方法,typeof 不會正確地回報為 'function',使用 instanceof 看看是否為 Function 實例,結果也是 falsetoString 傳回的也不一定是 [object Function]