有没有办法冻结 ES6 地图?

Is there a way to freeze an ES6 Map?

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

Object.freeze and Object.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 的 freeze 冻结属性不是 objects 或者这可能是一个错误/尚未实现?

是的,我知道,我可能应该使用 Immutable.js,但是有什么方法可以用原生 ES6 地图做到这一点吗?

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

在支持扩展内置函数的真实 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 制作一个包装器 class 而不是扩展 Map class.

@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);
}

抱歉,我无法发表评论。我只想添加我的打字稿变体

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;
}

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

class FreezeMap extends Map {
    /**
     * @param {Map<number, any>} OriginalMap
     * @return {Map<number, any>}
     */
    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`);
    };
}

由于 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),
    });
}

关于这种方法需要记住的几件事:

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

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

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版本:

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(); }
}

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

如果有人正在寻找已接受答案的 TypeScript 版本:

export type ReadonlyMap<K,V> = Omit<Map<K,V>, "set"| "delete"| "clear">

export function freeze<K, V>(map: Map<K, V>): ReadonlyMap<K, V> {
  if (map instanceof Map) {
    map.set = (key: K) => {
      throw new Error(`Can't set property ${key}, map is not extensible`);
    };

    map.delete = (key: K) => {
      throw new Error(`Can't delete property ${key}, map is not extensible`);
    };

    map.clear = () => {
      throw new Error("Can't clear map, map is frozen");
    };
  }

  return Object.freeze(map);
}