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
