For AI agents: the complete documentation index is available at /tc39-atlas/llms.txt, the full documentation bundle is available at /tc39-atlas/llms-full.txt, and this page is available as Markdown at /tc39-atlas/proposals/proposal-object-pick-or-omit.md.
  • 简体中文
  • Object pick/omit S1

    提案概览
    提案速览

    该提案引入静态方法 Object.pick 和 Object.omit,基于键列表或谓词函数方便地选择或排除对象属性,弥补了解构赋值在动态属性或原型属性访问方面的人体工程学不足。API 接受可选的键数组或谓词,以及 thisArg,返回新的普通对象,包括对 Symbol 键和原型属性的处理。

    Note

    以下 README 来自上游仓库,其中的阶段或状态标注可能滞后;当前信息以提案概览为准。

    Object.{pick, omit}

    ECMAScript 提案,针对 Object.pickObject.omit 的规范与参考实现。

    作者: @Aleen && Hemanth HM

    倡导者: @js-choi

    目前该提案在进程中处于阶段 1

    动机

    让我们考虑几个现实场景,来理解本提案试图解决的问题。

    • MouseEvent 上,我们只关心 'ctrlKey'、'shiftKey'、'altKey'、'metaKey' 事件。
    • 我们有一个 configObject,需要从中获取 ['dependencies'、'devDependencies'、'peerDependencies']
    • 我们有一个 optionsBag,并允许其中包含 ['shell'、'env'、'extendEnv'、'uid'、'gid']
    • req.body 中,我们想要提取 ['name'、'company'、'email'、'password']
    • 通过从 props 中提取 compareKeys 并与 prevProps 比较,检查组件是否应 shouldReload
    • 假设我们有一个 depsObject,需要忽略其中所有 @internal/packages
    • 我们有 props,需要从中移除 ['_csrf'、'_method']
    • 我们需要通过从 ({ ...state.models, ...action.update }) 中移除 action.deleted 来构造 newModelData
    • 当过滤列表由 CLI 参数给出时,过滤配置对象。

    你看,生活就是关于 挑选(pick)我们想要的,以及 省略(omit)我们不想要的!

    如果语言提供了便捷的方法来帮助处理类似场景,生活是否会变得更容易?

    有人可能会争论说,我们可以如下实现 pickomit

    const pick = (obj, keys) => Object.fromEntries(
        keys.map(k => obj.hasOwnProperty(k) && [k, obj[k]]).filter(x => x)
    );
    
    /*
    我们也可以使用解构赋值
    const { authKey, ...toLog } = userInfo;
    */
    const omit = (obj, keys) => Object.fromEntries(
        keys.map(k => !obj.hasOwnProperty(k) && [k, obj[k]]).filter(x => x)
    );

    上述实现的主要挑战:

    • 不符合人体工程学!
    • 如果选择解构方式,它对 pick 根本不起作用,也无法用于动态值的 omit
    • 解构不能像 Object.pick 那样克隆新对象。
    • 解构不能像 Object.pick 那样从原型挑选属性。
    • 解构无法动态挑选属性,而 Object.pick 可以。
    • 解构无法省略某些属性,而在没有此提案的情况下只能克隆然后删除

    我们可以在下面的 es.discourse 中阅读更多关于此类用例和挑战的信息:

    考虑到这些,如果我们有 Object.pickObject.omit 静态方法岂不更容易?!

    现在让我们讨论一下这种有用方法的 API 是什么?

    语法

    Object.pick(obj[, pickedKeys | predictedFunction(currentValue[, key[, object]])[, thisArg])
    Object.omit(obj[, omittedKeys | predictedFunction(currentValue[, key[, object]])[, thisArg])
    参数
    • obj:你想要从中挑选或省略的对象。
    • pickedKeys可选):你想要从对象中挑选的属性的键。默认值为空数组。
    • omittedKeys可选):你想要从对象中省略的属性的键。默认值为空数组。
    • predictedFunction可选):用于预测属性是否应被挑选或省略的函数。默认值为恒等函数:x => x
      • currentValue:对象中当前处理的值。
      • key:对象中currentValue的键。
      • object:调用 pick 的对象。
    • thisArg可选):在预测函数内部用作 this 的对象。
    返回值
    • 返回一个新对象,其中包含从对象中挑选或省略的属性。

    用法

    // 默认
    Object.pick({a : 1}); // => {}
    Object.omit({a : 1}); // => {a: 1}
    Object.pick({a : 0, b : 1}, v => v); // => {b: 1}
    Object.pick({a : 0, b : 1}, v => !v); // => {a: 0}
    Object.pick({}, function () { console.log(this) }); // => 对象本身
    Object.pick({}, function () { console.log(this) }, window); // => Window
    
    Object.pick({a : 1, b : 2}, ['a']); // => {a: 1}
    Object.omit({a : 1, b : 2}, ['b']); // => {a: 1}
    
    Object.pick({a : 1, b : 2}, ['c']); // => {}
    Object.omit({a : 1, b : 2}, ['c']); // => {a: 1, b: 2}
    
    Object.pick([], [Symbol.iterator]); // => {Symbol(Symbol.iterator): f}
    Object.pick([], ['length']); // => {length: 0}
    
    Object.pick({a : 1, b : 2}, v => v === 1); // => {a: 1}
    Object.pick({a : 1, b : 2}, v => v !== 2); // => {a: 1}
    Object.pick({a : 1, b : 2}, (v, k) => k === 'a'); // => {a: 1}
    Object.pick({a : 1, b : 2}, (v, k) => k !== 'b'); // => {a: 1}

    愿景

    1. 在挑选情况下的一种语法糖:

      为了扩展本提案的动机,可能有一些语法表示法作为从对象中挑选属性的替代,比如提案 proposal-slice-notation

      关于如何包装挑选键有两个想法:

      • 方括号:

        ({a : 1, b : 2, c : 3}).['a', 'b']; // => {a : 1, b : 2}
        
        const keys = ['a', 'b'];
        ({a : 1, b : 2, c : 3}).[keys[0], keys[1]]; // => {a : 1, b : 2}
        ({a : 1, b : 2, c : 3}).[...keys]; // => {a : 1, b : 2}
      • 花括号:

        ({a : 1, b : 2, c : 3}).{a, b} // => {a : 1, b : 2}
        
        const keys = ['a', 'b'];
        ({a : 1, b : 2, c : 3}).{[keys[0]], b}; // => {a : 1}
        ({a : 1, b : 2, c : 3}).{[...keys]}; // => {a : 1, b : 2}
        
        // 类似于解构
        ({a : 1, b : 2, c : 3}).{a, b : B}; // => {a : 1, B : 2}

        目前,关于是否应该挑选具有默认赋值值的属性存在分歧。

        // 如果考虑挑选的含义,初始值没有意义
        ({a : 1, b : 2, c : 3}).{a, d = 2}; // => {a : 1}
        
        // 如果视为“重构”,简写有理由挑选
        ({a : 1, b : 2, c : 3}).{a, d = 2}; // => {a : 1, d : 2}

      尽管如此,这只是一个简单的愿景,欢迎讨论。

    常见问题

    1. 当涉及到对象的原型链时,方法应该挑选还是省略? (答案可能改变)

      答:Lodash 的 _.pick_.omit 的实现已考虑了链。为了保持规则,我们可以挑选原型上的属性,但不能省略它们:

      Object.pick({a : 1}, ['toString']); // => {toString: f}
      Object.omit({a : 1}, ['toString']).toString; // => ƒ toString() { [native code] }

      同样的规则适用于 __proto__,即使它已被弃用,因为提案应足够纯粹,不应指定特殊逻辑来消除弃用属性:

      Object.pick({}, ['__proto__']); // => {__proto__: {...}}
      Object.omit({}, ['__proto__']).__proto__; // => {...} 

      在某些观点中,从原型链挑选或省略属性应使方法更具扩展性:

      const pickOwn = (obj, keys) => Object.pick(obj, keys.filter(key => obj.hasOwnProperty(key)));
      const omitOwn = (obj, keys) => Object.omit(obj, keys.filter(key => obj.hasOwnProperty(key)));
    2. 返回值的类型是什么?

      答:所有这些方法应返回普通对象:

      Object.pick([]); // => {} 
      Object.omit([]); // => {}
      Object.pick(new Map()); // => {} 
      Object.omit(new Map()); // => {}
    3. 如何处理 Symbol

      答:Symbol 应仅被视为具有 Symbol 键的属性,并应遵守上述规则:

      Object.pick([], [Symbol.iterator]); // => {Symbol(Symbol.iterator): f}, 从原型挑选
      Object.omit([], [Symbol.iterator]); // => {}, 普通对象
      
      const symbol = Symbol('key');
      Object.omit({a : 1, [symbol]: 2}, [symbol]); // => {a : 1}
      
      Object.prototype[symbol] = 'test'; // 覆盖原型
      Object.pick({}, [symbol]); // => {Symbol(key): "test"}, 从原型挑选
      Object.omit({}, [symbol])[symbol]; // => "test", 不能从原型省略属性
    4. 如果对象的某些属性不可访问,比如抛出错误,Object.pickObject.omit 能否操作这样的对象?

      答:我建议抛出由 Object.pickObject.omit 包装的错误,但这不是最终选择

      Object.pick(Object.defineProperty({}, 'key', {
         get() { throw new Error() }
      }), ['key']);

      错误堆栈将如下所示:

      Uncaught Error
          at Object.get (<anonymous>:2:20)
          at Object.pick (<anonymous>:2:10)
          at <anonymous>:1:8 
    5. proposal-shorthand-improvements 相比,何时应使用这两种方法?

      答:多个属性。假设我们需要确保一个对象没有除 key1key2 之外的任何副作用键:

      postData({[key1] : o[key1], [key2] : o[key2]});
      postData(Object.pick(o, [key1, key2])); 
    6. 为什么不能直接在 Object.prototype 上定义?

      答:由于 Object 特别基础,两者都会与其他任何对象的属性冲突。简而言之,如果定义了,任何继承自 Object 的对象,如果其原型中定义了 pickomit,都会破坏。

    7. 为什么不定义与 Lodash 中的 pickByomitBy 对应的过滤方法?

      答:将方法加倍是不必要的,因为可以将其合并到参数中:

      此外,传递的过滤方法可以很容易地以相同的含义反转,这意味着 omitBy 可以轻松定义为 pickBy 的逆操作。

      Object.pick({a : 1, b : 2}, v => v);
      
      // 相当于以下内容:
      Object.omitBy({a: 1, b : 2}, v => !v);

    注意:您对本提案有任何建议或想法吗?欢迎通过 issues 进行讨论。