本文內容:
物件基本
// 構造範例方式
const person = new Object();
person.name = 'a';
// 宣告式
const person = {
name: 'a'
};
為了標識某個標識為內部特性,ECMA-262 使用兩個中括號把特性名稱括起來。
資料屬性
修改資料屬性
Object.defineProperty()
,該方法接收3個引數:目標物件
、屬性名稱
和 描述符物件
。
const person = {};
Object.defineProperty(person, 'name', {
writable: false,
value: 'aa'
});
本例使用 Object.defineProperty()
給 person 建立了一個屬性 name
,在非嚴格模式下給這個屬性重新賦值會被忽略;在嚴格模式下,嘗試修改唯讀屬性則會報錯。
當一個屬性被定義為不可設定後,就不能再變回可設定的了。此時呼叫 Object.defineProperty()
並修改任何非 writable
屬性都會導致錯誤。
在使用 Object.defineProperty()
時,如果不在第三個引數提供 configurable
、enumerable
和 writable
。
存取器屬性
const book = {
year_: 2022,
edition: 1
};
Object.defineProperty(book, 'year_', {
get() {
return this.year_;
}
set(v) {
if (v > this.year_) {
this.year_ = v;
this.edition += v - 2022;
}
}
});
獲取函數和設定函數不是必須設定的,只設定獲取函數說明這個屬性是唯讀的。在非嚴格模式下,給唯讀屬性賦值會被忽略;在嚴格模式下則會報錯。
同時定義多個屬性
Object.defineProperties()
const book = {};
Object.defineProperties(book, {
year_: {
value: 2022
},
edition: {
value: 1
},
year: {
get() {
return this.year_;
}
set(v) {
if (v > 2022) {
this.year_ = v;
this.edition += v - 2022;
}
}
},
});
讀取屬性的特性
Object.getOwnPropertyDescriptor()
這個方法接收兩個引數:目標物件
和 目標屬性名
。
const book = {};
// 這裡呼叫上文所示的對 book 的屬性定義
// ...
const descriptor = Object.getOwnPropertyDescriptor(book, 'year');
console.log(descriptor.value); // undefined
console.log(descriptor.enumerable); // false
console.log(typeof descriptor.get); // "function"
提問:為什麼這裡的 enumerable 是 false ?(答案在「修改資料屬性「一節)
同時獲取目標物件的所有屬性的特徵
Object.getOwnPropertyDescriptors()
該靜態方法是在 ES2017 處新增的,只接收 目標物件
一個引數。
const book = {};
// 這裡呼叫上文所示的對 book 的屬性定義
// ...
console.log(Object.getOwnPropertyDescriptors(book));
/*
{
edition: {
configurable: false,
enumerable: false,
value: 1,
writable: false
},
year: {
configurable: false,
enumerable: false,
get: f(),
set: f(v),
},
year_: {
configurable: false,
enumerable: false,
value: 2022,
writable: false
},
}
*/
Object.assign()
這個方法接收一個 目標物件
和 一個或多個源物件
。
Object.assign() 執行的是淺複製。如果多個源物件有相同的屬性,則會用最後(書寫順序最靠右或最靠下)一個複製的值。
Object.assign() 會返回目標物件的參照。
const dest = {};
const res = Object.assign(dest, { id: 1, name: 'a' }, { id: 2 });
console.log(res === dest); // true
console.log(res); // { id: 2, name: 'a' }
如果在複製期間出錯,則操作會中止並退出,同時丟擲錯誤。此時 Object.assign() 只完成了部分複製,而這部分複製並不會回滾到原樣。因此 Object.assign() 只是一個盡力而為的操作。
在 ES6 以前一些棘手的情況
// === 符合預期的情況
console.log(true === 1); // false
console.log({} === {}); // false
console.log("2" === 2); // false
// 在不同的 JS 引擎中表現不同,但仍被認為相等
console.log(+0 === -0); // true
console.log(+0 === 0); // true
console.log(-0 === 0); // true
// 必須使用 isNaN() 來判定一個變數是否為 NaN
console.log(NaN === NaN); // false
console.log(isNaN(NaN)); // true
Object.is()
一定程度改善了上述棘手的情況。
console.log(Object.is(true, 1)); // false
console.log(Object.is({}, {})); // false
console.log(Object.is("2", 2)); // false
console.log(Object.is(+0, -0)); // false
console.log(Object.is(+0, 0)); // true
console.log(Object.is(-0, 0)); // false
console.log(Object.is(NaN, NaN)); // true
簡寫屬性值
const name = 'aa';
const person = { name };
console.log(person.name); // "aa"
動態命名屬性
const nameKey = 'name';
const person = { [nameKey]: 'aa' };
console.log(person); // { name: 'aa' }
在 [ ] 內可以寫任何 JS 表示式,比如說函數執行。
簡寫方法名
const functionKey = 'sayAge';
const person = {
sayName() {
console.log('aa');
}
[functionKey]() {
console.log(18);
}
};
物件解構
const person = {
name: 'aa',
age: 18
};
const {
nane,
age: personAge,
some,
hasDefault = 'default' // 不存在於源物件時使用預設值
} = person;
console.log(name); // "aa"
console.log(personAge); // 18
console.log(some); // undefined
console.log(hasDefault); // "default"
console.log(age); // Referrence Error
解構會在內部使用函數 ToObject()
( 不能在執行時環境中直接存取 ) 把後設資料解構轉換為物件。這意味著在物件解構的上下文中,原始值會被當成物件,而 null
和 undefined
不能被解構,否則會丟擲錯誤。
const { length } = 'fooBar';
console.log(length); // 6
const { constructor: c } = 4;
console.log(c === Number); // true
let { _ } = null; // TypeError
let { - } = undefined; // TypeError
基於以上認識,再來看一下 巢狀解構
const person = {
info: {
name: 'aa',
age: 18
},
};
const { info: { name, age: personAge } } = person;
console.log(name); // "aa"
console.log(age); // 18
// 當我們從一個 undefind(人為或本身沒有這個屬性) 或 一個 null 的屬性中解構時就會報錯
const { job: { desc } } = person; // TypeError
我們也可以利用物件解構這個特性給其他物件賦值
const person = {
info: {
name: 'aa',
age: 18
},
};
const copy = {};
{
info: {
name: copy.name,
age: copy.age
}
} = person;
console.log(copy); // { name: "aa", age: 18 }
此時,和 Object.assign() 類似,當解構賦值的過程中如果出現異常,這個過程是不能回滾的,就會出現部分賦值的情況。
函數引數使用解構
const person = {
name: 'aa',
age: 18
};
function print({ name, age: myAge }) {
console.log(name, myAge);
}
print(person); // "aa" 18
建構函式、原型物件和範例
建構函式和傳統的工廠函數對比有以下區別
使用 new
操作符時建構函式會執行以下操作:
在範例化時,如果不想傳引數,那麼建構函式後面的括號可加可不加。
const Person = function() {
this.name = 'aa';
this.sayName = function() {
console.log(this.name);
};
};
const person1 = new Person();
const person2 = new Person;
console.log(person1 instanceof Object); // true
console.log(person1 instanceof Person); // true
console.log(person2 instanceof Object); // true
console.log(person2 instanceof Person); // true
需要注意的是,建構函式也是函數,在呼叫一個函數而沒有明確設定 this 的情況下,this 始終指向 Global 物件 ( 在瀏覽器中就是 window 物件 )。
// 作為函數呼叫
Person('aa'); // 新增到 window 物件
window.sayName(); // "aa"
// 在另一個物件的作用域中呼叫
const o = new Object();
Person.call(o, 'aa');
o.sayName(); // "aa"
建構函式的問題
上面定義的屬性在不同範例間是不共用的,如果要維護一些公共屬性,使用上面的做法會非常不方便。
每個函數都會建立一個 prototype
屬性,這個屬性是指向一個物件的指標,該物件就是這個函數的原型物件,其包含應該由特定參照型別的範例共用的屬性和方法。
使用 原型物件
的好處是在上面定義的屬性和方法可以被物件範例共用。
假設現有一個 Person
建構函式和一個 person
範例,它們和 Object
及其原型物件的關係如下:
從這個圖可以看出
建構函式
和 原型物件
是兩個概念,它們有自己的方式彼此通訊物件
上有一個隱藏屬性 [[prototype]]
用於和自己的建構函式的原型物件進行通訊,一般情況下是不能直接存取這個屬性的,但是有的瀏覽器( Firefox、Safari 和 Chrome 等 )會暴露 __proto__
讓開發者來存取 [[prototype]]
__proto__
,因為如果程式碼執行在不支援 __proto__
的瀏覽器時,__proto__
的值就是 undefinedNumber
、String
這些建構函式都繼承自 Object 原型物件
原型方法
instanceof
判定是否包含指定建構函式的原型
console.log(person1 instanceof Person); // true
console.log(person1 instanceof Object); // true
console.log(Person.prototype instanceof Object); // true
isPrototypeOf()
console.log(Person.prototype.isPrototypeOf(person1)); // true
Object.getPrototypeOf()
返回引數內部特性 [[Prototype]] 的值
console.log(Object.getPrototypeOf(person1) === Person.prototype); // true
console.log(Object.getPrototypeOf(person1).name); // aa
Object.setPrototypeOf()
可以像範例的私有特性 [[Prototype]] 寫入一個新值以重寫一個物件的原型繼承關係
const biped = { numLegs: 2 };
const person = { name: 'aa' };
Object.setPrototypeOf(person, biped);
console.log(person.name); // aa
console.log(person.numLegs); // 2
console.log(Object.getPrototypeOf(person) === biped); // true
注意,Object.setPrototypeOf()
可能會嚴重影響程式碼效能。Mozilla 檔案說得很清楚,」在所有瀏覽器和 JS 引擎中,修改繼承關係的影響都是微妙且深遠的。這種影響並不僅是 Object.setPrototypeOf() 語句那麼簡單,而是會涉及所有存取了修改 [[Prototype]] 的物件的程式碼「。
可以通過 Object.create()
來建立一個新物件同時為其指定原型來避免 Object.setPrototypeOf()
可能造成的效能下降。
const biped = { numLegs: 2 };
const person = Object.create(biped);
person.name = 'aa';
console.log(person.name); // aa
console.log(person.numLegs); // 2
console.log(Object.getPrototypeOf(person) === biped); // true
Object.hasOwnProperty()
當屬性存在於呼叫它的物件範例上時返回 true
function Person() {
this.name = 'aa';
}
Person.sayName = function() {
console.log(this.name)
};
const p = new Person();
console.log(p.hasOwnProperty('name')); // true
console.log(p.hasOwnProperty('sayName')) // false
in 操作符 —— 當屬性存在於物件範例上或其原型鏈上時返回 true
Object.keys() —— 獲得物件上所有可列舉的範例屬性
for...in...
和 Object.keys()
的列舉順序是不確定的,取決於 JS 引擎,可能因瀏覽器而異。
物件迭代
const o = { foo: 'foo', bar: 1, baz: {} };
console.log(Object.values(o)); // ["foo", 1, {}]
console.log(Object.values(o)[2]); // true
const o = { foo: 'foo', bar: 1, baz: {} };
console.log(Object.entries(o)); // [["foo", "foo"], ["bar", 1], ["baz", {}]]
console.log(Object.entries[o][2][1] === o.baz); // true
特性
建構函式設計模式
核心思路是在子類建構函式中呼叫父類別建構函式,在呼叫的時候可以向父類別建構函式傳參。
function SuperType(name) {
this.name = name;
}
function SubType() {
// 繼承 SuperType 並傳參
SuperType.call(this, 'aa');
// 範例屬性
this.age = 18;
}
const instance = new SubType();
console.log(instance.name); // "aa"
console.log(instance.age); // 18
盜用建構函式問題
函數不能重用,因為方法必須在建構函式中定義。另外,子類不能存取父類別原型上定義的方法,因此所有型別只能使用建構函式模式。
核心思路是使用原型鏈繼承原型上的屬性和方法,通過盜用建構函式繼承範例屬性。
這樣即可把方法定義在原型上實現重用,又可以讓每個範例有自己的屬性。
function SuperType(name) {
this.name = name;
}
function SubType(name, age) {
// 繼承屬性
SuperType.call(this, name);
// 範例屬性
this.age = age;
}
// 繼承方法
SubType.prototype = new SuperType();
// 在原型鏈上定義公共方法
SubType.prototype.sayAge = function() {
console.log(this.age);
}
Object.create()
接收兩個引數:作為新物件原型的物件和給新物件定義額外屬性的物件(可選)。
第二個個引數資料格式和 Object.defineProperties()
的第二個引數一樣。
const person = {
name: 'a',
friends: ['b', 'c']
};
// person 是 anotherPerson 的原型
const anotherPerson = Object.create(person);
anotherPerson.name = 'hh';
anotherPerson.friends.push('gg');
// person 是 yetAnotherPerson 的原型
const yetAnotherPerson = Object.create(person, {
name: {
value: 'yetAnotherPerson'
}
});
yetAnotherPerson.friends.push('hh');
console.log(person.friends); // "b, c, hh, gg"
console.log(yetAnotherPerson.name); // yetAnotherPerson
核心思想是:建立一個實現繼承的函數,以某種方式增強物件,然後返回這個物件。
function createAnother(original) {
const clone = Object.create(original); // 通過呼叫函數建立一個新物件
clone.sayHi = function() { // 以某種方式增強這個物件
console.log('hi');
};
return clone; // 返回這個物件
}
const person = { name: 'aa' };
const anotherPerson = createAnother(person);
anotherPerson.sayHi(); // "hi"
// 寄生式組合繼承的基本模式
function inheritPrototype(subType, superType) {
const prototype = Object.create(superType.prototype); // 建立物件
prototype.constructor = subType; // 增強物件
subType.prototype = prototype; // 賦值物件
}
// 使用
function SuperType(name) {
this.name = name;
this.colors = ['red', 'blue', 'green'];
}
SuperType.prototype.sayName = function() {
console.log(this.name);
};
function SubType(name, age) {
SuperType.call(this, name);
this.age = age;
}
inheritPrototype(SubType, SuperType);
SubType.prototype.sayAge = function() {
console.log(this.age);
};
console.log(SuperType.sayAge); // undefined