JavaScript物件導向—物件的建立和操作

MomentYY發表於2022-03-11

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

相關文章