JavaScript物件導向—物件的建立和操作
前言
雖然說在JavaScript程式語言中,函式是第一公民,但是JavaScript不僅支援函數語言程式設計,也支援物件導向程式設計。JavaScript物件設計成了一組屬性的無序集合,由key和value組成,key為一個識別符號名稱,而value可以是任意型別的值,當函式作為物件的屬性值時,這個函式就可以稱之為物件的方法。下面就來看看JavaScript的物件導向吧。
1.JavaScript建立物件的方式
一般地,常用於建立物件的方式有兩種,早期經常使用Object類,通過new關鍵字來建立一個物件,有點類似於Java中建立物件,後來為了方便就直接使用物件字面量的方式來建立物件了,用法更為簡潔。
-
使用
Object
類建立物件;const obj = new Object() // 建立一個空物件 // 往物件中新增屬性 obj.name = 'curry' obj.age = 30
-
使用物件字面量建立物件;
// 直接往{}新增鍵值對 const obj = { name: 'curry', age: 30 }
2.物件屬性操作的控制
物件建立出來後,如何對該物件進行操作控制呢?這裡涉及到一個很重要的方法:Object.defineProperty()。
2.1.Object.defineProperty()
該方法可以在物件上定義一個新的屬性,也可修改物件現有屬性,並將該物件返回。
Object.defineProperty(obj, prop, descriptor)
接收三個引數:
- obj:指定操作的物件;
- prop:指定需要定義或修改的屬性名稱;
- description:定義或修改的屬性描述符;
2.2.屬性描述符的分類
什麼是屬性描述符?顧名思義就是對物件中的屬性進行描述,簡單來說就是給物件某個屬性指定一些規則。屬性描述符主要分為資料屬性描述符和存取屬性描述符兩種型別。
對於屬性描述符中的屬性是否兩者都可以設定呢?其實資料和存取屬性描述符兩者是有區別,下面的表格統計了兩者可用和不可用的屬性:
屬性 | configurable | enumerable | value | writable | get | set |
---|---|---|---|---|---|---|
資料屬性描述符 | 可以 | 可以 | 可以 | 可以 | 不可以 | 不可以 |
存取屬性描述符 | 可以 | 可以 | 不可以 | 不可以 | 可以 | 可以 |
那麼為什麼有些屬性可以用,有些屬性又不能用呢?因為資料屬性描述符和存取屬性描述符所擔任的角色不一樣,下面就來詳細介紹一下,它們兩者的區別。
2.3.資料屬性描述符
從上面的表格可以知道,資料屬性描述符可以使用configurable、enumerable、value、writable。而這就是資料屬性描述符的四個特性。
- Configurable:表示是否可以通過delete刪除物件屬性,是否可以修改它的特性,或者是否可以將它修改為存取屬性描述符。當通過
new Object()
或者字面量的方式建立物件時,其中的屬性的configurable
預設為true
,當通過屬性描述符定義一個屬性時,其屬性的configurable
預設為false
。 - Enumerable:表示是否可以通過for-in或者Object.keys()返回該屬性。當通過
new Object()
或者字面量的方式建立物件時,其中的屬性的enumerable
預設為true
,當通過屬性描述符定義一個屬性時,其屬性的enumerable
預設為false
。 - Writable:表示是否可以修改屬性的值。當通過
new Object()
或者字面量的方式建立物件時,其中的屬性的writable
性描述符定義一個屬性時,其屬性的writable
預設為false
。 - Value:屬性的value值,讀取屬性時會返回該值,修改屬性時會對其進行修改。(預設:undefined)
const obj = {
name: 'curry'
}
Object.defineProperty(obj, 'age', {
configurable: false, // age屬性是否可以刪除,預設false
enumerable: false, // age屬性是否可以列舉,預設false
writable: false, // age屬性是否可以寫入(修改),預設false
value: 30 // age屬性的值,預設undefined
})
// 當configurable為false,age屬性是不可被刪除的
delete obj.age
console.log(obj) // { name: 'curry', age: 30 }
// 當writable為false,age屬性的值是不可被修改的
obj.age = 18
console.log(obj) // { name: 'curry', age: 30 }
// 如果將enumerable修改為false,age屬性是不可以被遍歷出來的
for (const key in obj) {
console.log(key) // name
}
2.4.存取屬性描述符
存取屬性描述符可以使用configurable、enumerable、get、set。在獲取物件某個屬性值時,可以通過get來攔截,在設定物件某個屬性值時,可以通過set來攔截。configurable和enumerable的用法和特性跟資料屬性描述符一樣。
- Get:獲取屬性時會執行的函式。(預設undefined)
- Set:設定屬性時會執行的函式。(預設undefined)
get和set的使用場景:
-
隱藏某一個私有屬性,不希望直接被外界使用和賦值。如下程式碼
_age
表示不想直接被外界使用,外界就可以通過使用age
的set和get來訪問設定_age
了。 -
如果希望截獲某一個屬性它訪問和設定值的過程。(Vue2的響應式原理就在這)
const obj = { name: 'curry', _age: 30 } // 注意:這裡的this是指向obj物件的 Object.defineProperty(obj, 'age', { configurable: true, enumerable: true, get: function() { console.log('age屬性被訪問了') return this._age }, set: function(newValue) { console.log('age屬性被設定了') this._age = newValue } }) obj.age // age屬性被訪問了 obj.age = 18 // age屬性被設定了
2.5.同時給多個屬性定義屬性描述符
上面使用Object.defineProperty()方法都是給單個屬性進行定義描述符,想要一次性定義多個屬性,那麼就可以使用Object.defineProperties()方法了。寫法如下:
Object.defineProperties(obj, {
name: {
configurable: true,
enumerable: true,
writable: true,
value: 'curry'
},
age: {
configurable: false,
enumerable: false,
get: function() {
return this._age
},
set: function(newValue) {
this._age = newValue
}
}
})
3.Object中常用的方法
上面介紹了Object中defineProperty和defineProperties兩個方法。其實Object中還有很多方法,下面介紹一些常用的。
-
獲取物件的屬性描述符:
- 獲取單個屬性:
Object.getOwnPropertyDescriptor
; - 獲取所有屬性:
Object.getOwnPropertyDescriptors
;
const obj = { name: 'curry', age: 30 } console.log(Object.getOwnPropertyDescriptor(obj, 'age')) // { value: 30, writable: true, enumerable: true, configurable: true } console.log(Object.getOwnPropertyDescriptors(obj)) /* { name: { value: 'curry', writable: true, enumerable: true, configurable: true }, age: { value: 30, writable: true, enumerable: true, configurable: true } } */
- 獲取單個屬性:
-
Object.preventExtensions()
:禁止物件擴充套件新屬性,給一個物件新增新的屬性會失敗(在嚴格模式下會報錯)。 -
Object.seal()
:將物件密封起來,不允許配置和刪除屬性。(實際還是呼叫preventExtensions
,並且將現有屬性的configurable
設定為false
) -
Object.freeze()
:將物件凍結起來,不允許修改物件現有屬性。(實際上是呼叫seal,並且將現有屬性的writable
設定為false
)
4.JavaScript建立多個物件
上面提到的建立物件的方式僅適用於建立單個物件適用,如果有多個物件比較類似,那麼一個個建立必然是很麻煩的,如何批量建立物件呢?JavaScript也給我們提供了一些方案。
4.1.方案一:工廠函式
如果我們不想在建立物件時做重複的工作,那麼就可以定義一個函式為我們去做這些重複性的工作,我們只需要將屬性對應的值傳入函式即可。
function createObj(name, age) {
// 建立一個空物件
const obj = {}
// 設定對應屬性值
obj.name = name
obj.age = age
// 公共方法共用
obj.sayHello = function() {
console.log(`My name is ${this.namename}, I'm ${this.age} years old.`)
}
// 將物件返回
return obj
}
const obj1 = createObj('curry', 30)
const obj2 = createObj('kobe', 24)
console.log(obj1) // { name: 'curry', age: 30, sayHello: [Function (anonymous)] }
console.log(obj2) // { name: 'kobe', age: 24, sayHello: [Function (anonymous)] }
obj1.sayHello() // My name is undefined, I'm 30 years old.
obj2.sayHello() // My name is undefined, I'm 24 years old.
缺點:建立出來的物件全是通過字面量建立的,獲取不到物件真實的型別。
4.2.方案二:建構函式
(1)什麼是建構函式?
- 建構函式也稱之為構造器(constructor),通常是我們在建立物件時會呼叫的函式;
- 在其他物件導向的程式語言裡面,建構函式是存在於類中的一個方法,稱之為構造方法;
- 如果一個普通的函式被使用new操作符來呼叫了,那麼這個函式就稱之為是一個建構函式;
- 一般規定建構函式的函式名首字母大寫;
(2)new操作符呼叫函式的作用
當一個函式被new操作符呼叫了,預設會進行如下幾部操作:
- 在記憶體中建立一個新的物件(空物件);
- 這個物件內部的[[prototype]]屬性會被賦值為該建構函式的prototype屬性;
- 建構函式內部的this,會指向建立出來的新物件;
- 執行函式的內部程式碼(函式體程式碼);
- 如果建構函式沒有返回物件,則預設返回建立出來的新物件。
(3)建構函式建立物件的過程
- 通過建構函式建立的物件就真實的型別了,如下所示的Person型別;
function Person(name, age) {
this.name = name
this.age = age
this.sayHello = function() {
console.log(`My name is ${this.name}, I'm ${this.age} years old.`)
}
}
const p1 = new Person('curry', 30)
const p2 = new Person('kobe', 24)
console.log(p1) // Person { name: 'curry', age: 30, sayHello: [Function (anonymous)] }
console.log(p2) // Person { name: 'kobe', age: 24, sayHello: [Function (anonymous)] }
缺點:在每次使用new建立新物件時,會重新給每個物件建立新的屬性,包括物件中方法,實際上,物件中的方法是可以共用的,消耗了不必要的記憶體。
console.log(p1.sayHello === p2.sayHello) // false
4.3.方案三:原型+建構函式
在瞭解該方案之前,需要先簡單的認識一下何為原型。
(1)物件的原型
JavaScript中每個物件都有一個特殊的內建屬性[[prototype]](我們稱之為隱式原型),這個特殊的屬性指向另外一個物件。那麼這個屬性有什麼用呢?
- 前面介紹了,當我們通過物件的key來獲取對應的value時,會觸發物件的get操作;
- 首先,get操作會先檢視該物件自身是否有對應的屬性,如果有就找到並返回其值;
- 如果在物件自身沒有找到該屬性就會去物件的[[prototype]]這個內建屬性中查詢;
那麼物件的[[prototype]]屬性怎麼獲取呢?主要有兩種方法:
- 通過物件的
__proto__
屬性訪問; - 通過
Object.getPrototypeOf()
方法獲取;
const obj = {
name: 'curry',
age: 30
}
console.log(obj.__proto__)
console.log(Object.getPrototypeOf(obj))
(2)函式的原型
所有的函式都有一個prototype屬性,並且只有函式才有這個屬性。前面提到了new操作符是如何在記憶體中建立一個物件,並給我們返回建立出來的物件,其中第二步這個物件內部的[[prototype]]屬性會被賦值為該建構函式的prototype屬性。將程式碼與圖結合,來看一下具體的過程。
示例程式碼:
function Person(name, age) {
this.name = name
this.age = age
}
const p1 = new Person('curry', 30)
const p2 = new Person('kobe', 24)
// 驗證:物件(p1\p2)內部的[[prototype]]屬性(__proto__)會被賦值為該建構函式(Person)的prototype屬性;
console.log(p1.__proto__ === Person.prototype) // true
console.log(p2.__proto__ === Person.prototype) // true
記憶體表現:
- p1和p2的原型都指向Person函式的prototype原型;
- 其中還有一個constructor屬性,預設原型上都會有這個屬性,並且指向當前的函式物件;
(3)結合物件和函式的原型,建立物件
先簡單的總結一下:
- 前面使用建構函式建立物件的缺點是物件中的方法不能共用;
- 物件的屬性可以通過[[prototype]]隱式原型進行查詢;
- 建構函式建立出來的物件[[prototype]]與建構函式prototype指向同一個物件(同一個地址空間);
- 那麼我們可以將普通的屬性放在建構函式的內部,將方法放在建構函式的原型上,當查詢方法時,就都會去到建構函式的原型上,從而實現方法共用;
function Person(name, age) {
this.name = name
this.age = age
}
Person.prototype.sayHello = function() {
console.log(`My name is ${this.name}, I'm ${this.age} years old.`)
}
const p1 = new Person('curry', 30)
const p2 = new Person('kobe', 24)
console.log(p1.sayHello === p2.sayHello) // true