JavaScript面向對象(1)—對象的創建和操作


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


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM