有没有办法冻结ES6地图?

时间:2016-03-02 12:30:52

标签: javascript hashmap ecmascript-6 immutability

我正在寻找一种冻结原生ES6地图的方法。

Object.freezeObject.seal似乎不起作用:

let myMap = new Map([["key1", "value1"]]);
// Map { 'key1' => 'value1' }

Object.freeze(myMap);
Object.seal(myMap);

myMap.set("key2", "value2");
// Map { 'key1' => 'value1', 'key2' => 'value2' }

这是预期的行为,因为objectsmaps的冻结属性不是objects,或者这可能是错误/尚未实现?

是的,我知道,我应该使用Immutable.js,但有没有办法用原生ES6地图做到这一点?

5 个答案:

答案 0 :(得分:9)

没有,你可以写一个包装来做到这一点。 Object.freeze锁定对象的属性,但Map实例是对象,它们存储的值不是属性,因此冻结对它们没有影响,就像任何其他隐藏了内部状态的类一样。 / p>

在真正的ES6环境中,支持扩展内置版(非Babel ),您可以这样做:

class FreezableMap extends Map {
    set(...args){
        if (Object.isFrozen(this)) return this;

        return super.set(...args);
    }
    delete(...args){
        if (Object.isFrozen(this)) return false;

        return super.delete(...args);
    }
    clear(){
        if (Object.isFrozen(this)) return;

        return super.clear();
    }
}

如果您需要在ES5环境中工作,您可以轻松地为Map创建一个包装类,而不是扩展Map类。

答案 1 :(得分:6)

@loganfsmyth,你的回答给了我一个想法,那是怎么回事:

function freezeMap(myMap){

  if(myMap instanceof Map) {

    myMap.set = function(key){
      throw('Can\'t add property ' + key + ', map is not extensible');
    };

    myMap.delete = function(key){
      throw('Can\'t delete property ' + key + ', map is frozen');
    };

    myMap.clear = function(){
      throw('Can\'t clear map, map is frozen');
    };
  }

  Object.freeze(myMap);
}

这对我来说很完美:)

在评论中更新了@Bergi中的积分:

var mapSet = function(key){
  throw('Can\'t add property ' + key + ', map is not extensible');
};

var mapDelete = function(key){
  throw('Can\'t delete property ' + key + ', map is frozen');
};

var mapClear = function(){
  throw('Can\'t clear map, map is frozen');
};

function freezeMap(myMap){

  myMap.set = mapSet;
  myMap.delete = mapDelete;
  myMap.clear = mapClear;

  Object.freeze(myMap);
}

答案 2 :(得分:1)

由于 Map 和 Set 对象将其元素存储在内部插槽中,因此冻结它们不会使它们不可变。 无论用于扩展或修改 Map 对象的语法如何,其内部插槽仍然可以通过 Map.prototype.set 可变。因此,保护​​地图的唯一方法是不要将其直接暴露给不受信任的代码。

解决方案 A:为您的地图创建只读视图

您可以创建一个新的类似 Map 的对象,以显示您的 Map 的只读视图。例如:

function mapView (map) {
    return Object.freeze({
        get size () { return map.size; },
        [Symbol.iterator]: map[Symbol.iterator].bind(map),
        clear () { throw new TypeError("Cannot mutate a map view"); } ,
        delete () { throw new TypeError("Cannot mutate a map view"); },
        entries: map.entries.bind(map),
        forEach (callbackFn, thisArg) {
            map.forEach((value, key) => {
                callbackFn.call(thisArg, value, key, this);
            });
        },
        get: map.get.bind(map),
        has: map.has.bind(map),
        keys: map.keys.bind(map),
        set () { throw new TypeError("Cannot mutate a map view"); },
        values: map.values.bind(map),
    });
}

关于这种方法需要记住以下几点:

  • 此函数返回的视图对象是实时的:原始 Map 中的更改将反映在视图中。如果您不在代码中保留对原始地图的任何引用,这无关紧要,否则您可能希望将地图副本传递给 mapView 函数。
  • 需要类似 Map 的对象的算法应该适用于地图视图,前提是它们从未尝试对其应用 Map.prototype 方法。由于该对象不是具有内部插槽的实际 Map,因此对其应用 Map 方法会抛出异常。
  • 无法在开发工具中轻松检查 mapView 的内容。

或者,可以将 MapView 定义为具有私有 #map 字段的类。这使得调试更容易,因为开发工具可以让您检查地图的内容。

class MapView {
    #map;

    constructor (map) {
        this.#map = map;
        Object.freeze(this);
    }

    get size () { return this.#map.size; }
    [Symbol.iterator] () { return this.#map[Symbol.iterator](); }
    clear () { throw new TypeError("Cannot mutate a map view"); }
    delete () { throw new TypeError("Cannot mutate a map view"); }
    entries () { return this.#map.entries(); }
    forEach (callbackFn, thisArg) {
        this.#map.forEach((value, key) => {
            callbackFn.call(thisArg, value, key, this);
        });
    }
    get (key) { return this.#map.get(key); }
    has (key) { return this.#map.has(key); }
    keys () { return this.#map.keys(); }
    set () { throw new TypeError("Cannot mutate a map view"); }
    values () { return this.#map.values(); }
}

解决方案 B:创建自定义 FreezableMap

与其简单地允许创建只读视图,我们还可以创建自己的 FreezableMap 类型,其 setdeleteclear 方法仅有效如果对象没有被冻结。它需要做更多的工作,但结果更加灵活,因为它还让我们支持 Object.sealObject.preventExtensions

关闭版本:

function freezableMap(...args) {
    const map = new Map(...args);

    return {
        get size () { return map.size; },
        [Symbol.iterator]: map[Symbol.iterator].bind(map),
        clear () {
            if (Object.isSealed(this)) {
                throw new TypeError("Cannot clear a sealed map");
            }
            map.clear();
        },
        delete (key) {
            if (Object.isSealed(this)) {
                throw new TypeError("Cannot remove an entry from a sealed map");
            }
            return map.delete(key);
        },
        entries: map.entries.bind(map),
        forEach (callbackFn, thisArg) {
            map.forEach((value, key) => {
                callbackFn.call(thisArg, value, key, this);
            });
        },
        get: map.get.bind(map),
        has: map.has.bind(map),
        keys: map.keys.bind(map),
        set (key, value) {
            if (Object.isFrozen(this)) {
                throw new TypeError("Cannot mutate a frozen map");
            }
            if (!Object.isExtensible(this) && !map.has(key)) {
                throw new TypeError("Cannot add an entry to a non-extensible map");
            }
            map.set(key, value);
            return this;
        },
        values: map.values.bind(map),
    };
}

课程版本:

class FreezableMap {
    #map;

    constructor (...args) {
        this.#map = new Map(...args);
    }

    get size () { return this.#map.size; }
    [Symbol.iterator] () { return this.#map[Symbol.iterator](); }
    clear () {
        if (Object.isSealed(this)) {
            throw new TypeError("Cannot clear a sealed map");
        }
        this.#map.clear();
    }
    delete (key) {
        if (Object.isSealed(this)) {
            throw new TypeError("Cannot remove an entry from a sealed map");
        }
        return this.#map.delete(key);
    }
    entries () { return this.#map.entries(); }
    forEach (callbackFn, thisArg) {
        this.#map.forEach((value, key) => {
            callbackFn.call(thisArg, value, key, this);
        });
    }
    get (key) { return this.#map.get(key); }
    has (key) { return this.#map.has(key); }
    keys () { return this.#map.keys(); }
    set (key, value) {
        if (Object.isFrozen(this)) {
            throw new TypeError("Cannot mutate a frozen map");
        }
        if (!Object.isExtensible(this) && !this.#map.has(key)) {
            throw new TypeError("Cannot add an entry to a non-extensible map");
        }
        this.#map.set(key, value);
        return this;
    }
    values () { return this.#map.values(); }
}

我特此将此代码发布到公共领域。 请注意,它没有经过太多测试,并且没有保修。 快乐复制粘贴。

答案 3 :(得分:0)

对不起,我无法发表评论。我只想添加我的打字稿变体

const mapSet = function (key: unknown) {
  throw "Can't add property " + key + ', map is not extensible';
};

const mapDelete = function (key: unknown) {
  throw "Can't delete property " + key + ', map is frozen';
};

const mapClear = function () {
  throw 'Can\'t clear map, map is frozen';
};

function freezeMap<T extends Map<K, V>, K, V>(myMap: T) {
  myMap.set = mapSet;
  myMap.delete = mapDelete;
  myMap.clear = mapClear;

  Object.freeze(myMap);

  return myMap;
}

答案 4 :(得分:0)

所以应用 ES6 让代码看起来更清晰。从我的角度来看:)

class FreezeMap extends Map {
    /**
     * @param {Map<number, ErrorInstance>} OriginalMap
     * @return {Map<number, ErrorInstance>}
     */
    constructor(OriginalMap) {
        super();
        OriginalMap.set = this.set.bind(OriginalMap);
        OriginalMap.delete = this.delete.bind(OriginalMap);
        OriginalMap.clear = this.clear.bind(OriginalMap);
        Object.freeze(OriginalMap);
        return OriginalMap;
    };

    set(key) {
        throw new Error(`Can't add property ${key}, map is not extensible`);
    };

    delete(key) {
        throw new Error(`Can't delete property ${key}, map is frozen`);
    };

    clear() {
        throw new Error(`Can't clear map, map is frozen`);
    };
}