让 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.

posted @ 2021-04-21 00:50  yuan_bao_er  阅读(1171)  评论(0编辑  收藏  举报