檢驗物件
July 28, 2022JavaScript 是動態定型語言,對於物件的操作,僅要求是否具備所需行為,而不在意所謂的類型。
對於確認物件的行為,物件的特性偵測絕大多數情況下就足夠了。例如:
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
也是根據物件的原型物件來判斷 true
或 false
的。例如:
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]
等,這也可作為判斷型態的依據,基於對標準的支持,現在一些程式庫多使用這個來作判斷。