地图默认值

Map default value

我正在寻找类似地图默认值的内容。

m = new Map();
//m.setDefVal([]); -- how to write this line???
console.log(m[whatever]);

现在结果是 Undefined 但我想得到空数组 [].

首先回答关于标准 Map 的问题:ECMAScript 2015 中提出的 Javascript Map 不包括 setter 作为默认值。但是,这并不限制您自己实现该功能。

如果你只想打印一个列表,当 m[whatever] 未定义时,你可以: console.log(m.get('whatever') || []); 正如 Li357 在他的评论中指出的那样。

如果你想重用这个功能,你也可以把它封装成一个函数,比如:

function getMapValue(map, key) {
    return map.get(key) || [];
}

// And use it like:
const m = new Map();
console.log(getMapValue(m, 'whatever'));

但是,如果这不能满足您的需求,并且您真的想要一个具有默认值的地图,您可以为它编写自己的地图 class,例如:

class MapWithDefault extends Map {
  get(key) {
    if (!this.has(key)) {
      this.set(key, this.default());
    }
    return super.get(key);
  }
  
  constructor(defaultFunction, entries) {
    super(entries);
    this.default = defaultFunction;
  }
}

// And use it like:
const m = new MapWithDefault(() => []);
m.get('whatever').push('you');
m.get('whatever').push('want');
console.log(m.get('whatever')); // ['you', 'want']

截至 2022 年,Map.prototype.emplace 已达到 stage 2

正如提案页面上所说,core-js 库中提供了一个 polyfill。

出于我的目的,我认为使用 DefaultMap class 扩展普通 Map 并添加其他方法会更清楚。这真的很好,因为它会导致更多的声明性代码。也就是说,当你声明一个新的Map时,不仅要声明键的类型和值的类型,还要声明默认值。

举个简单的例子:

// Using a primitive as a default value
const myMap1 = new DefaultMap<string, number>(123);
const myMap1Value = myMap1.getAndSetDefault("some_key");

// Using a factory function to generate a default value
const myMap2 = new DefaultMap<string, number, [foo: Foo]>((_key, foo) => foo.bar);
const foo = new Foo();
const myMap2Value = myMap2.getAndSetDefault("some_key", foo);

代码如下:

type FactoryFunction<K, V, A extends unknown[]> = (k: K, ...extraArgs: A) => V;
type FirstArg<K, V, A extends unknown[]> =
  | Iterable<[K, V]>
  | V
  | FactoryFunction<K, V, A>;
type SecondArg<K, V, A extends unknown[]> = V | FactoryFunction<K, V, A>;

interface ParsedArgs<K, V, A extends unknown[]> {
  iterable: Iterable<[K, V]> | undefined;
  defaultValue: V | undefined;
  defaultValueFactory: FactoryFunction<K, V, A> | undefined;
}

/**
 * An extended Map with some new methods:
 *
 * - `getAndSetDefault` - If the key exists, this will return the same thing as the `get` method.
 *   Otherwise, it will set a default value to the key, and then return the default value.
 * - `getDefaultValue` - Returns the default value to be used for a new key. (If a factory function
 *   was provided during instantiation, this will execute the factory function.)
 * - `getConstructorArg` - Helper method for cloning the map. Returns either the default value or
 *   the reference to the factory function.
 *
 * When instantiating a new DefaultMap, you must specify either a default value or a function that
 * returns a default value.
 *
 * Example:
 * ```ts
 * // Initializes a new empty DefaultMap with a default value of "foo"
 * const defaultMapWithPrimitive = new DefaultMap<string, string>("foo");
 *
 * // Initializes a new empty DefaultMap with a default value of a new Map
 * const defaultMapWithFactory = new DefaultMap<string, Map<string, string>>(() => {
 *   return new Map();
 * })
 *
 * // Initializes a DefaultMap with some initial values and a default value of "bar"
 * const defaultMapWithInitialValues = new DefaultMap<string, string>([
 *   ["a1", "a2"],
 *   ["b1", "b2"],
 * ], "bar");
 * ```
 *
 * If specified, the first argument of a factory function must always be equal to the key:
 *
 * ```ts
 * const defaultMapWithConditionalDefaultValue = new DefaultMap<number, number>((key: number) => {
 *   return isOdd(key) ? 0 : 1;
 * });
 * ```
 *
 * You can also specify a factory function that takes a generic amount of arguments beyond the
 * first:
 *
 * ```ts
 * const factoryFunction = (_key: string, arg2: boolean) => arg2 ? 0 : 1;
 * const defaultMapWithExtraArgs = new DefaultMap<string, string, [arg2: boolean]>(factoryFunction);
 * ```
 */
export class DefaultMap<K, V, A extends unknown[] = []> extends Map<K, V> {
  private defaultValue: V | undefined;
  private defaultValueFactory: FactoryFunction<K, V, A> | undefined;

  /**
   * See the DefaultMap documentation:
   * [insert link here]
   */
  constructor(
    iterableOrDefaultValueOrDefaultValueFactory: FirstArg<K, V, A>,
    defaultValueOrDefaultValueFactory?: SecondArg<K, V, A>,
  ) {
    const { iterable, defaultValue, defaultValueFactory } = parseArguments(
      iterableOrDefaultValueOrDefaultValueFactory,
      defaultValueOrDefaultValueFactory,
    );

    if (defaultValue === undefined && defaultValueFactory === undefined) {
      error(
        "A DefaultMap must be instantiated with either a default value or a function that returns a default value.",
      );
    }

    if (iterable === undefined) {
      super();
    } else {
      super(iterable);
    }

    this.defaultValue = defaultValue;
    this.defaultValueFactory = defaultValueFactory;
  }

  /**
   * If the key exists, this will return the same thing as the `get` method. Otherwise, it will set
   * a default value to the key, and then return the default value.
   */
  getAndSetDefault(key: K, ...extraArgs: A): V {
    const value = this.get(key);
    if (value !== undefined) {
      return value;
    }

    const defaultValue = this.getDefaultValue(key, ...extraArgs);
    this.set(key, defaultValue);
    return defaultValue;
  }

  /**
   * Returns the default value to be used for a new key. (If a factory function was provided during
   * instantiation, this will execute the factory function.)
   */
  getDefaultValue(key: K, ...extraArgs: A): V {
    if (this.defaultValue !== undefined) {
      return this.defaultValue;
    }

    if (this.defaultValueFactory !== undefined) {
      return this.defaultValueFactory(key, ...extraArgs);
    }

    return error("A DefaultMap was incorrectly instantiated.");
  }

  /**
   * Helper method for cloning the map. Returns either the default value or a reference to the
   * factory function.
   */
  getConstructorArg(): V | FactoryFunction<K, V, A> {
    if (this.defaultValue !== undefined) {
      return this.defaultValue;
    }

    if (this.defaultValueFactory !== undefined) {
      return this.defaultValueFactory;
    }

    return error("A DefaultMap was incorrectly instantiated.");
  }
}

function parseArguments<K, V, A extends unknown[]>(
  firstArg: FirstArg<K, V, A>,
  secondArg?: SecondArg<K, V, A>,
): ParsedArgs<K, V, A> {
  return secondArg === undefined
    ? parseArgumentsOne(firstArg)
    : parseArgumentsTwo(firstArg, secondArg);
}

function parseArgumentsOne<K, V, A extends unknown[]>(
  firstArg: FirstArg<K, V, A>,
): ParsedArgs<K, V, A> {
  const arg = firstArg as SecondArg<K, V, A>;
  const { defaultValue, defaultValueFactory } =
    parseDefaultValueOrDefaultValueFactory(arg);
  return {
    iterable: undefined,
    defaultValue,
    defaultValueFactory,
  };
}

function parseArgumentsTwo<K, V, A extends unknown[]>(
  firstArg: FirstArg<K, V, A>,
  secondArg: SecondArg<K, V, A>,
): ParsedArgs<K, V, A> {
  const firstArgType = type(firstArg);
  if (firstArgType !== "table") {
    error(
      "A DefaultMap constructor with two arguments must have the first argument be the initializer list.",
    );
  }

  const { defaultValue, defaultValueFactory } =
    parseDefaultValueOrDefaultValueFactory(secondArg);
  return {
    iterable: firstArg as Iterable<[K, V]>,
    defaultValue,
    defaultValueFactory,
  };
}

function parseDefaultValueOrDefaultValueFactory<K, V, A extends unknown[]>(
  arg: SecondArg<K, V, A>,
): {
  defaultValue: V | undefined;
  defaultValueFactory: FactoryFunction<K, V, A> | undefined;
} {
  if (typeof arg === "function") {
    return {
      defaultValue: undefined,
      defaultValueFactory: arg as FactoryFunction<K, V, A>,
    };
  }

  if (
    typeof arg === "boolean" ||
    typeof arg === "number" ||
    typeof arg === "string"
  ) {
    return {
      defaultValue: arg as V,
      defaultValueFactory: undefined,
    };
  }

  return error(
    `A DefaultMap was instantiated with an unknown type of: ${typeof arg}`,
  );
}