檢驗物件

July 28, 2022

JavaScript 是動態定型語言,對於物件的操作,僅要求是否具備所需行為,而不在意所謂的類型。

對於確認物件的行為,物件的特性偵測絕大多數情況下就足夠了。例如:

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

特性不存在的話,會傳回 undefined,而在判斷式中會被作為 false,若存在,則會傳回物件,在判斷式中會被作為 true,這就是物件特性偵測的基本原理。

如果真得確認物件的型態,有許多方式,但這些方式基本上不是提供的資訊有限,就是不能完全信任。

typeof/constructor

例如,許多場合最常看到的 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

ES13 以後,可以使用 hasOwn 取代 hasOwnProperty

enumerable

在物件上直接使用 .[] 新建的特性可以用 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] 等,這也可作為判斷型態的依據,基於對標準的支持,現在一些程式庫多使用這個來作判斷。

分享到 LinkedIn 分享到 Facebook 分享到 Twitter