讓 JavaScript 對象完全只讀不可以被修改


在 JavaScript 中, 如何讓一個對象是不可變的? 即 immutable, 讓這個對象只讀, 不可以被修改, 被覆蓋.

使用場景
為什么有這樣的需求呢?

假象一下這樣的場景, 我們寫了一個 JS, 在其中定義了一個對象, 會開放出來給第三方使用. 如果想讓這個對象安全的被第三方使用, 需要避免這個對象被下鈎子(hook), 也就是要避免這個對象被覆蓋重寫.

例如

    window.openApi = {
        payMoney: function() {
            alert('payMoney');
        }
    };

這樣定義的對象就很容易被下鈎子, 造成安全問題, 因為對象的屬性是很容易被覆寫的.

例如

    var originalPayMoney = window.openApi.payMoney;
    // 覆寫方法, 相當於攔截原來的邏輯, 注入新的邏輯
    window.openApi.payMoney = function() {
        alert('hook');
        originalPayMoney();
    };

這樣當下次再調用 window.openApi.payMoney 時, 就會執行被注入的邏輯, 存在安全問題.

解決辦法
那么有沒有辦法讓一個對象的屬性是不可以被修改的? 

也許你聽過 immutable-js, 但回想一下 ES5 好像提供了這樣一個方法: Object.freeze()

讓我們看看 MDN 上是如何說明這個方法的

The Object.freeze() method freezes an object: that is, 
prevents new properties from being added to it;
prevents existing properties from being removed;
and prevents existing properties, or their enumerability, configurability, or writability, from being changed. (Note however that child objects may still change - calling .freeze() does not make the object immutable.)
Values cannot be changed for data properties. Note that values that are objects can still be modified, unless they are also frozen.
即通過 Object.freeze 來操作一個對象后, 就會使這個對象不可以新增屬性, 刪除屬性, 對於已經存在的屬性也不可以重新配置(The descriptor for the property), 或者被修改.

但也提到了一點, 如果屬性是一個對象值的, 那么這個屬性還是可以被修改的, 除非再次讓這個屬性為 freeze.

MDN 上有詳細的示例說明 Object.freeze 的用法和作用, 而且有 example shows that object values in a frozen object can be mutated (freeze is shallow), 即 deepFreeze, To make obj immutable, freeze each object in obj.

freeze 還不夠
讓我們回到上面那個例子, 我們讓 openApi 為 freeze, 就可以讓對象 immutable, 防止這個對象的屬性被修改了.

    'use strict';
    window.openApi = {
        payMoney: function() {
            alert('payMoney');
        }
    };
    Object.freeze(window.openApi);

    try {
        window.openApi.payMoney = function() {
            alert('freeze');
        };
    } catch (error) {
        // TypeError: Cannot assign to read only property 'payMoney' of object '#<Object>'
        console.log(error);
    }

    // {writable: false, enumerable: true, configurable: false}
    console.log(Object.getOwnPropertyDescriptor(window.openApi, 'payMoney'));
    // {writable: true,  enumerable: true, configurable: true}
    console.log(Object.getOwnPropertyDescriptor(window, 'openApi'));

但這只能避免 openApi 的 payMoney 屬性被重新賦值修改, 避免不了 window 的 openApi 屬性被重新定義或者被重新賦值(即完全重寫), 因為 freeze 操作只是針對於 window.openApi 對象本身, 而非針對 window 對象本身, 所以你仍舊可以重新定義 window 的 openApi 屬性.

相當於

    var obj = {
        payMoney: function() {
            alert('payMoney');
        }
    };
    Object.freeze(obj);
    window.openApi = obj;

即 freeze 只針對於操作的 obj 對象本身, 它肯定是不會管 window 這個對象的.

例如: freeze 后, 可以通過重新定義屬性來打破 immutable

    'use strict';
    window.openApi = {
        payMoney: function() {
            alert('payMoney');
        }
    };
    Object.freeze(window.openApi);

    // 重新定義
    Object.defineProperty(window, 'openApi', {
        value: {
            payMoney: 'redefine'
        }
    });
    // Object {payMoney: "redefine"}
    console.log(window.openApi);

例如: freeze 后, 可以通過重新賦值來打破 immutable 

    'use strict';
    window.openApi = {
        payMoney: function() {
            alert('payMoney');
        }
    };
    Object.freeze(window.openApi);

    // 重新賦值
    window.openApi = {
        payMoney: 'reassignment'
    };
    // Object {payMoney: "reassignment"}
    console.log(window.openApi);

最終方案
因此我們還必須讓 openApi 不可以被重新定義(property descriptor can not be changed), 不可以被重新賦值.

這個可以通過 Object.defineProperty 分別設置其 configurable 和 writable 為 false.

configurable: if the type of this property descriptor may be changed and if the property may be deleted from the corresponding object.
writable: if the value associated with the property may be changed with an assignment operator.
    /* ---------------------------------------------------------- */
    'use strict';
    window.openApi = {
        payMoney: function() {
            alert('payMoney');
        }
    };
    /* ---------------------------------------------------------- */
    Object.freeze(window.openApi);
    Object.defineProperty(window, 'openApi', {
        configurable: false,
        writable: false
    });
    console.log(Object.getOwnPropertyDescriptor(window, 'openApi'));
    /* ---------------------------------------------------------- */
    // freeze 讓對象只讀, 防止屬性被直接修改
    try {
        window.openApi.payMoney = function() {
            alert('freeze');
        };
    } catch (error) {
        // TypeError: Cannot assign to read only property 'payMoney' of object '#<Object>'
        console.log(error);
    }

    // configurable 防止屬性被重新定義
    try {
        Object.defineProperty(window, 'openApi', {
            value: {
                payMoney: 'redefine'
            }
        });
    } catch (error) {
        // TypeError: Cannot redefine property: openApi
        console.log(error);
    }

    // writable 防止屬性被重新賦值
    try {
        window.openApi = {
            payMoney: 'reassignment'
        };
    } catch (error) {
        // TypeError: Cannot assign to read only property 'openApi' of object '#<Window>'
        console.log(error);
    }

    console.log(window.openApi);
    window.openApi.payMoney();

最后不經感慨, 經過那么多折騰后, window.openApi 還是純潔的... (๑•̀ㅂ•́)و✧

發散一下
* Object.seal()

The Object.seal() method seals an object,
preventing new properties from being added to it
and marking all existing properties as non-configurable.
Values of present properties can still be changed as long as they are writable.


免責聲明!

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



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