JavaScript 中的延遲載入屬性模式

飛奔的龜龜發表於2021-06-18

傳統上,開發人員在 JavaScript 類中為例項中可能需要的任何資料建立屬性。對於在建構函式中隨時可用的小塊資料來說,這不是問題。但是,如果在例項中可用之前需要計算某些資料,您可能不想預先支付該費用。例如,考慮這個類:

class MyClass {
    constructor() {
        this.data = someExpensiveComputation();
    }
}

在這裡,data屬性是作為執行一些昂貴計算的結果而建立的。如果您不確定是否會使用該屬性,則預先執行該計算可能效率不高。幸運的是,有幾種方法可以將這些操作推遲到以後。

按需屬性模式

優化執行昂貴操作的最簡單方法是等到需要資料後再進行計算。例如,您可以使用帶有 getter 的訪問器屬性來按需進行計算,如下所示:

class MyClass {
    get data() {
        return someExpensiveComputation();
    }
}

 在這種情況下,直到有人第一次讀取該data屬性時,您的昂貴計算才會發生,這是一種改進。但是,每次data讀取屬性時都會執行相同的昂貴計算,這比之前的示例更糟糕,其中至少只執行了一次計算。這不是一個好的解決方案,但您可以在此基礎上建立一個更好的解決方案。

凌亂的延遲載入屬性模式

只有在訪問屬性時才執行計算是一個好的開始。您真正需要的是在該點之後快取資訊並僅使用快取版本。但是您將這些資訊快取在哪裡以便於訪問?最簡單的方法是定義一個具有相同名稱的屬性並將其值設定為計算資料,如下所示:

class MyClass {
    get data() {
        const actualData = someExpensiveComputation();

        Object.defineProperty(this, "data", {
            value: actualData,
            writable: false,
            configurable: false,
            enumerable: false
        });

        return actualData;
    }
}

 在這裡,該data屬性再次定義為類上的 getter,但這次它快取了結果。呼叫Object.defineProperty()建立一個名為的新屬性data,該屬性具有固定值actualData,並且設定為不可寫、可配置和不可列舉(以匹配 getter)。之後,返回值本身。下次data訪問屬性時,它將從新建立的屬性中讀取而不是呼叫 getter:

const object = new MyClass();

// calls the getter
const data1 = object.data;

// reads from the data property
const data2 = object.data;

 

實際上,所有計算僅在第一次data讀取屬性時完成對該data屬性的每次後續讀取都返回快取的版本。

這種模式的一個缺點是data屬性開始是不可列舉的原型屬性,最終是不可列舉的自己的屬性:

const object = new MyClass();
console.log(object.hasOwnProperty("data"));     // false

const data = object.data;
console.log(object.hasOwnProperty("data"));     // true

 雖然這種區別在很多情況下並不重要,但理解這種模式很重要,因為它在傳遞物件時可能會導致微妙的問題。幸運的是,使用更新的模式很容易解決這個問題。

類的唯一自己的延遲載入屬性模式

如果您有一個用例,其中延遲載入的屬性始終存在於例項中很重要,那麼您可以使用Object.defineProperty()在類建構函式中建立屬性。它比前面的例子有點混亂,但它會確保該屬性只存在於例項上。下面是一個例子:

class MyClass {
    constructor() {

        Object.defineProperty(this, "data", {
            get() {
                const actualData = someExpensiveComputation();

                Object.defineProperty(this, "data", {
                    value: actualData,
                    writable: false,
                    configurable: false
                });

                return actualData;
            },
            configurable: true,
            enumerable: true
        });

    }
}

在這裡,建構函式data使用Object.defineProperty()該屬性是在例項上建立的(通過使用this)並定義一個 getter 並指定該屬性為可列舉和可配置的(典型的自己的屬性)。data屬性設定為可配置特別重要,以便您可以Object.defineProperty()再次呼叫它。

然後 getter 函式進行計算並再次呼叫Object.defineProperty()data屬性現在被重新定義為具有特定值的資料屬性,並且不可寫和不可配置以保護最終資料。然後,計算資料從 getter 返回。下次data讀取屬性時,它將從儲存的值中讀取。作為獎勵,該data財產現在僅作為自己的財產存在,並且在第一次閱讀之前和之後的行為都相同:

const object = new MyClass();
console.log(object.hasOwnProperty("data"));     // true

const data = object.data;
console.log(object.hasOwnProperty("data"));     // true

 對於類,這很可能是您要使用的模式;另一方面,物件文字可以使用更簡單的方法。

物件字面量的延遲載入屬性模式

如果您使用物件字面量而不是類,則過程要簡單得多,因為在物件字面量上定義的 getter 被定義為可列舉的自身屬性(而不是原型屬性),就像資料屬性一樣。這意味著您可以對類使用凌亂的延遲載入屬性模式而對於物件來說不會凌亂:

const object = {
    get data() {
        const actualData = someExpensiveComputation();

        Object.defineProperty(this, "data", {
            value: actualData,
            writable: false,
            configurable: false,
            enumerable: false
        });

        return actualData;
    }
};

console.log(object.hasOwnProperty("data"));     // true

const data = object.data;
console.log(object.hasOwnProperty("data"));     // true

 

結論

在 JavaScript 中重新定義物件屬性的能力提供了一個獨特的機會來快取可能計算成本很高的資訊。通過從重新定義為資料屬性的訪問器屬性開始,您可以將計算推遲到第一次讀取屬性時,然後快取結果以供以後使用。這種方法既適用於類,也適用於物件字面量,並且在物件字面量中更簡單一些,因為您不必擔心您的 getter 會在原型上結束。

提高效能的最佳方法之一是避免重複執行相同的工作,因此任何時候您可以快取結果以供以後使用,都可以加快程式的執行速度。延遲載入屬性模式等技術允許任何屬性成為快取層以提高效能。

 

相關文章