站長阿川

站長阿川私房教材:
學 JavaScript 前端,帶作品集去面試!

站長精心設計,帶你實作 63 個小專案,得到作品集!

立即開始免費試讀!

妙啊!Js的對象屬性居然还能用這麼寫

image

Hi,我是石小石~


靜態屬性獲取的缺陷

前段時間在做專案國際化時,遇到一個比較隱蔽的問題:

我們在定義列舉常量時,直接調用了 i18n 的翻譯方法:

export const OverdueStatus: any = {
  ABOUT_TO_OVERDUE: {
    value: 'ABOUT_TO_OVERDUE',
    name: i18n.global.t('common.about_to_overdue'),
    color: '#ad0000',
    bgColor: '#ffe1e1'
  },
}

結果發現翻譯始終不生效。排查後才發現原因很簡單 —— OverdueStatus 物件的初始化早於 i18n 實例的生成,因此取得的翻譯結果是空的。

雖然最後我通過封裝自定義 Vue 插件的方式徹底解決了問題,但排查過程中其實還有一個可選思路。

當時我想到的最直接辦法是:讓 name 在被訪問時再去執行 i18n.global.t,而不是在物件定義時就執行。比如把 OverdueStatus 定義為函式:

export const OverdueStatus = () => ({
  ABOUT_TO_OVERDUE: {
    value: 'ABOUT_TO_OVERDUE',
    name: i18n.global.t('common.about_to_overdue'),
    color: '#ad0000',
    bgColor: '#ffe1e1'
  },
})

這樣在調用時:

OverdueStatus().ABOUT_TO_OVERDUE.name

就能確保翻譯邏輯在 i18n 實例創建完成之後再執行,從而避免初始化順序的問題。不過,這種方式也有明顯的缺點:所有類似的枚舉都要改成函式,調用時也得多加一層執行,整體程式碼會變得不夠簡潔。

如何優雅地實現“動態獲取屬性”?

上面提到的“把枚舉改成函式返回”雖然能解決問題,但在實際業務中顯得有些笨拙。是否有更優雅的方式,讓屬性本身就支持 動態計算 呢?

其實,JavaScript 本身就為我們提供了解決方案 —— getter

舉個例子,我們可以把枚舉物件改寫成這樣:

export const OverdueStatus: any = {
  ABOUT_TO_OVERDUE: {
    value: 'ABOUT_TO_OVERDUE',
    get name() {
      return i18n.global.t('common.about_to_overdue')
    },
    color: '#ad0000',
    bgColor: '#ffe1e1'
  },
}

這樣一來,在訪問 name 屬性時,才會真正執行 i18n.global.t,確保翻譯邏輯在 i18n 實例創建完成後才生效,完美解決問題。

訪問器屬性的原理

在 JavaScript 規範裡,get 定義的屬性叫 訪問器屬性,區別於普通的 數據屬性 (Data Property)。簡單來說 getter 其實就是物件屬性的一種特殊定義方式。

當我們寫:

const obj = {
  get foo() {
    return "bar"
  }
}

等價於用 Object.defineProperty

const obj = {}
Object.defineProperty(obj, "foo", {
  get: function() {
    return "bar"
  }
})

所以訪問 obj.foo 時,其實是觸發了這個 get 函式,而不是讀取一個固定的值。

類比 Vue 的 computed

在 Vue 裡,我們經常寫 computed 計算屬性,其實就是 getter 的思想。

import { computed, ref } from "vue"

const firstName = ref("Tom")
const lastName = ref("Hanks")

const fullName = computed(() => `${firstName.value} ${lastName.value}`)

computed 內部其實就是包裝了一個 getter 函式。

注意點

  • getter 不能跟屬性值同時存在:
const obj = {
  get name() { return "石小石" },
  name: "石小石Orz" // 會報錯
}
  • getter 是只讀的,如果你想支持賦值,需要配合 setter
const obj = {
  _age: 18,
  get age() { return this._age },
  set age(val) { this._age = val }
}

obj.age = 20
console.log(obj.age) // 20

其他實用場景

延遲計算

有些值計算比較複雜,但只有在真正使用時才去算,可以提升性能。

const user = {
  firstName: "石",
  lastName: "小石",
  get fullName() {
    // 類比一個計算,在開發中,一個很複雜的計算才使用此方法
    console.log("計算了一次 fullName")
    return `${this.firstName} ${this.lastName}`
  }
}

console.log(user.fullName) // "石小石"

這種寫法讓 API 看起來更自然,不需要調用函式 user.getFullName(),而是 user.fullName

資料封裝與保護

有些屬性可能並不是一個固定欄位,而是基於內部狀態計算出來的:

const cart = {
  items: [100, 200, 300],
  get total() {
    return this.items.reduce((sum, price) => sum + price, 0)
  }
}

console.log(cart.total) // 600

這樣 cart.total 永遠是最新的,不用擔心手動維護,你也不用寫一個函式專門去更新這個值。


原文出處:https://juejin.cn/post/7543300730116325403


共有 0 則留言


精選技術文章翻譯,幫助開發者持續吸收新知。
站長阿川

站長阿川私房教材:
學 JavaScript 前端,帶作品集去面試!

站長精心設計,帶你實作 63 個小專案,得到作品集!

立即開始免費試讀!