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/stage/unstaged/proposal-function-helpers.md.
  • 简体中文
  • Function helpers ?

    中文标题:函数辅助工具

    提案概览
    提案速览

    该提案定义了一组常见的 JavaScript 函数辅助方法,包括 Function.flow、Function.pipe、Function.constant、Function.identity 和 Function.noop,以及诸如 Function.prototype.once 和 Function.prototype.debounce 等实例方法。将这些工具标准化旨在减少对外部库的依赖,改善开发者使用体验,并通过为常用操作提供统一的标准名称来提高代码清晰度。

    Note

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

    JavaScript 函数辅助工具

    已撤回的 ECMAScript Stage-0 提案。J. S. Choi,2021。

    2021-10,proposal-function-helpers 被提交至委员会全体会议 以寻求 Stage 1。委员会因该提案过于宽泛而予以拒绝,并要求将其拆分为多个提案。因此,本提案被撤回并拆分为 proposal-function-pipe-flowproposal-function-un-this

    原始提案

    许多有用且常见的辅助函数被定义、下载并被大量使用。 我们至少应该将其中一部分标准化。 本提案正在寻求委员会对 Stage 1 的共识: 即至少将部分 Function 辅助函数标准化是“值得研究的”。 它并非要标准化所有能想到的辅助函数: 而只是选定的少数常用函数。 选择标准化哪些函数将是 Stage 2 的细枝末节的争论。 或者,委员会也可以要求将该提案拆分为多个提案。

    这些便捷函数很简单,并且可以在用户空间轻松重新实现。 那么为什么要将它们标准化?因为:

    1. 这些辅助函数常用且普遍有用。 尽管它们易于重新实现,但每个函数都频繁地从 NPM 下载。 这在意料之中: 毕竟,每个 JavaScript 开发者都需要操作回调, 但他们往往不希望自己编写这些工具函数。
    2. 标准化将改善开发者的使用体验。 如果我们在 REPL 或脚本中需要这些函数, 无需下载外部包或将定义粘贴到自己的代码中, 只需解构 Function 对象即可。
    3. 标准化将提高代码清晰度。 每个函数将有一个标准名称, 而不是来自不同库的、指向同一事物的各种名称。

    与新语法不同,标准化的辅助函数是改善所有开发者体验的相对轻量级的方式。 这些辅助函数是已被广泛走出的路径, 每一个都值得考虑进行标准化。

    以下函数仅仅是可能性。 选择标准化哪些函数将是 Stage 2 的细枝末节的争论。

    Function.flow

    Function.flow 静态方法通过组合多个回调来创建一个新函数。

    Function.flow(...fns);
    
    const { flow } = Function;
    
    const f = flow(f0, f1, f2);
    f(5, 7); // f2(f1(f0(5, 7))).
    
    const g = flow(g0);
    g(5, 7); // g0(5, 7).
    
    const h = flow();
    h(5, 7); // 5.

    以下真实世界示例最初使用了 lodash.flow

    // From gatsby@3.14.3/packages/gatsby-plugin-sharp/src/plugin-options.js:
    flow(
      mapUserLinkHeaders(pluginData),
      applySecurityHeaders(pluginOptions),
      applyCachingHeaders(pluginData, pluginOptions),
      mapUserLinkAllPageHeaders(pluginData, pluginOptions),
      applyLinkHeaders(pluginData, pluginOptions),
      applyTransfromHeaders(pluginOptions),
      saveHeaders(pluginData)
    )
    
    // From strapi@3.6.8
    // packages/strapi-admin/services/permission/permissions-manager/query-builers.js:
    const transform = flow(flattenDeep, cleanupUnwantedProperties);
    
    // From semantic-ui-react@v2.0.4/docs/static/utils/getInfoForSeeTags.js:
    const getInfoForSeeTags = flow(
      _.get('docblock.tags'),
      _.filter((tag) => tag.title === 'see'),
      _.map((tag) => {
      }),
    )

    Function.flow 创建的任何函数都会将自己的参数应用于最左边的回调。 然后将该结果应用于下一个回调。 换句话说,函数组合从左到右发生。

    最左边的回调可以具有任意元数, 但任何后续回调都应为一元函数。

    如果 Function.flow 没有收到参数,则默认返回 Function.identity(本提案稍后定义)。

    先例包括:

    Function.flowAsync

    Function.flowAsync 静态方法通过组合多个可能异步的回调来创建一个新函数;所创建的函数将始终返回一个 Promise。

    Function.flowAsync(...fns);
    
    const { flowAsync } = Function;
    
    // (...args) => Promise.resolve(x).then(f0).then(f1).then(f2).
    flowAsync(f0, f1, f2);
    
    const f = flowAsync(f0, f1, f2);
    await f(5, 7); // await f2(await f1(await f0(5, 7))).
    
    const g = flowAsync(g0);
    await g(5, 7); // await g0(5, 7).
    
    const h = flowAsync();
    await h(5, 7); // await 5.

    Function.flowAsync 创建的任何函数都会将自己的参数应用于最左边的回调。 然后该结果在被应用于下一个回调之前会被 await。 换句话说,异步函数组合从左到右发生。

    最左边的回调可以具有任意元数, 但任何后续回调都应为一元函数。

    如果 Function.flowAsync 没有收到参数,则默认返回 Promise.resolve

    名称“flow”源自 lodash.flow。 (名称 compose 会与其他语言的从右到左函数组合相混淆。)

    Function.pipe

    Function.pipe 静态方法将一系列回调应用于给定的输入值,并返回最后一个回调的结果。

    Function.pipe(input, ...fns);
    
    const { pipe } = Function;
    
    // f2(f1(f0(5))).
    pipe(5, f0, f1, f2);
    
    // 5.
    pipe(5);
    
    // undefined.
    pipe();

    以下真实世界示例最初使用了 fp-tspipe 函数。

    // From @gripeless/pico@1.0.1/source/inline.ts:
    return pipe(
      download(absoluteURL),
      mapRej(downloadErrorToDetailedError),
      chainFluture(responseToBlob),
      chainFluture(blobToDataURL),
      mapFluture(dataURL => `url(${dataURL})`)
    )
    
    // From StoplightIO Prism v4.5.0 packages/http/src/validator/validators/body.ts:
    return pipe(
      specs,
      A.findFirst(spec => !!typeIs(mediaType, [spec.mediaType])),
      O.alt(() => A.head(specs)),
      O.map(content => ({ mediaType, content }))
    );

    第一个回调被应用于 input, 然后第二个回调被应用于第一个回调的结果, 依此类推。 换句话说,函数管道从左到右发生。

    每个回调都应是一元函数。

    如果 Function.pipe 只收到一个参数,则默认返回 input
    如果 Function.pipe 没有收到参数,则返回 undefined

    先例包括:

    • fp-tsimport { pipe } from 'fp-ts/function';

    F# 管道运算符怎么了?

    F#、Haskell 以及其他基于自动柯里化一元函数的语言 都有一个隐式一元函数应用运算符。 管道提案推进小组曾两次向 TC39 提交 F# 管道的 Stage 2 提案, 但均未成功, 原因是多位 TC39 代表提出了反对意见, 包括内存性能方面的担忧、关于 await 的语法担忧, 以及对鼓励生态系统分叉的担忧。 (更多信息,请参阅管道提案的 HISTORY.md。)

    鉴于这一现实,TC39 更有可能通过 Function.pipe 辅助函数,而不是类似的语法运算符。

    将辅助函数标准化并不排除 之后将等效运算符标准化。 例如,即使 Math.pow 已经存在,TC39 仍然将二元 ** 标准化。

    未来,我们可能会尝试提出 F# 管道运算符, 但我们希望首先尝试提出 Function.pipe, 以便尽快将其益处带给更广泛的 JavaScript 社区。

    Function.pipeAsync

    Function.pipeAsync 静态方法将一系列可能异步的回调应用于给定的输入值,并返回一个 Promise。该 Promise 将解析为最后一个回调的结果。

    Function.pipeAsync(input, ...fns);
    
    const { pipeAsync } = Function;
    
    // Promise.resolve(5).then(f0).then(f1).then(f2).
    pipeAsync(5, f0, f1, f2);
    
    // Promise.resolve(5).
    pipeAsync(5);
    
    // Promise.resolve(undefined).
    pipeAsync();

    首先对输入进行 await。 然后将第一个回调应用于 input,并对其进行 await, 然后将第二个回调应用于第一个回调的结果,并对其进行 await, 依此类推。 换句话说,函数管道从左到右发生。

    每个回调都应是一元函数。

    如果任何回调返回一个 Promise,并且该 Promise 随后因错误而拒绝, 那么 Function.pipeAsync 返回的 Promise 将以相同的错误拒绝。

    如果 Function.pipeAsync 只收到一个参数, 则默认返回 Promise.resolve(input)
    如果 Function.pipeAsync 没有收到参数, 则返回 Promise.resolve(undefined)

    Function.constant

    Function.constant 静态方法根据一个常量值创建一个新函数。该新函数无论收到什么参数,都将始终返回该值。

    Function.constant(value);
    
    const { constant } = Function;
    
    const f = constant(5);
    f(11, 0, 3); // 5.
    
    const g = constant();
    g(11, 0, 3); // undefined.

    以下真实世界示例最初使用了 lodash.constant

    // From cypress@8.6.0/packages/net-stubbing/lib/server/util.ts:
    setDefaultHeader('access-control-expose-headers', constant('*'))
    
    // From cypress@8.6.0/packages/driver/src/cypress/utils.ts:
    return [fn, constant(type)]
    
    // From Odoo v15.0 addons/pad/static/src/js/pad.js:
    url.toJSON = constant(this.url);
    
    // From ng-table@3.0.1/test/specs/settings.spec.ts:
    const newSettings: = {
      filterOptions: _.mapValues(allSettings.filterOptions, constant(undefined)),
      dataOptions: _.mapValues(allSettings.dataOptions, constant(undefined)),
      groupOptions: _.mapValues(allSettings.groupOptions, constant(undefined))
    };
    
    // From Elastic Kibana v7.15.1
    // src/plugins/vis_types/vislib/public/fixtures/mock_data/histogram/_slices.js.
    {
      name: 0,
      size: 378611,
      aggConfig: {
        type: 'histogram',
        schema: 'segment',
        fieldFormatter: constant(String),
        params: {
          interval: 1000,
          extended_bounds: { /* … */ },
        },
      },
      /* … */
    },
    
    // From Yhat Rodeo v2.5.2 src/node/services/files.test.js:
    fs.lstat.onCall(0).yields(null, {isDirectory: constant(true)});

    先例包括:

    • lodashlodash.constant 每周从 NPM 单独下载约 81,000 次
    • stdlibimport constantFunction from '@stdlib/utils-constant-function';
    • fp-tsimport { constant } from 'fp-ts/function';
    • Ramdaimport { always } from 'ramda/src/always';

    Function.identity

    Function.identity 静态方法始终返回其第一个参数。

    Function.identity(value);
    
    const { identity } = Function;
    
    identity(5); // 5.
    identity(); // undefined.

    以下真实世界示例最初使用了 lodash.identity

    // From cypress@8.6.0/packages/driver/src/cypress/runner.ts:
    // “Iterates over a suite's tests (including nested suites)
    // and will return as soon as the callback is true”.
    const findTestInSuite = (suite, fn = identity) => {
      for (const test of suite.tests) {
        if (fn(test)) {
          return test
        } /* … */
      }
    }
    
    // From gatsby@3.14.3/packages/gatsby-plugin-sharp/src/plugin-options.js:
    // “Get all non falsey values”.
    return _.pickBy(options, identity)
    
    // From gatsby@3.14.3/packages/gatsby-plugin-gatsby-cloud/src/constants.js:
    export const DEFAULT_OPTIONS = {
      // “Optional transform for manipulating headers for sorting, etc”.
      transformHeaders: identity,
      /* … */
    }
    
    // From ghost@4.19.0/core/frontend/helpers/img_url.js:
    // “only make paths relative if we didn't get a request for an absolute url”.
    const maybeEnsureRelativePath = !absoluteUrlRequested ? ensureRelativePath : _.identity;
    
    // From Meteor v2.5.0 tools/cordova/builder.js:
    const boilerplate = new Boilerplate(CORDOVA_ARCH, manifest, {
      urlMapper: identity,
      /* … */
    });

    先例包括:

    Function.noop

    Function.noop 静态方法始终返回 undefined。 Function.noop 等价于 () => {}。 它也等价于 constant()

    这个函数已经可以从 jQuery 和 Lodash 中获得并被频繁使用,通常用于填充必需的回调参数或禁用某个回调属性。

    const { noop } = Function;
    [ 0, 1 ].map(noop)
    // [ undefined, undefined ].

    以下真实世界示例最初使用了 jQuery 的 $.nooplodash.noop

    // From Wordpress v5.1.11:
    { /* … */
      defaultExpandedArguments: {
        duration: 'fast',
        completeCallback: noop }
      /* … */ }
    
    // From three@0.133.1/test/benchmark/benchmark.js:
    SuiteUI.prototype.run = function() {
      this.runButton.click = noop;
      this.runButton.innerText = "Running..."
      this.suite.run({ async: true });
    }
    
    // From typeahead.js@0.11.1/src/typeahead/dataset.js:
    this.cancel = function cancel() {
      canceled = true;
      that.cancel = noop;
      that.async &&
        that.trigger('asyncCanceled', query);
    };
    
    // From typeahead.js@0.11.1/src/bloodhound/bloodhound.js:
    // “if max size is less than 0, provide a noop cache”.
    sync = sync || noop;
    async = async || noop;
    sync(this.remote ? local.slice() : local);
    
    // From typeahead.js@0.11.1/src/bloodhound/lru_cache.js:
    // “if max size is less than 0, provide a noop cache”.
    if (this.maxSize <= 0) {
        this.set = this.get = $.noop;
    }
    
    // From verdaccio@5.1.6/packages/middleware/src/middleware.ts:
    errorReportingMiddleware(req, res, noop);
    
    // From Odoo v15.0 addons/bus/static/src/js/services/bus_service.js:
    Promise.resolve(this._audio.play()).catch(noop);
    
    // From ClickHouse v21.10.2.15-stable website/js/docsearch.js:
    if (this.$hint.length === 0) {
      this.setHint = this.getHint = this.clearHint = this.clearHintIfInvalid = noop;
    }

    先例包括:

    Function.prototype.once

    Function.prototype.once 方法创建一个新函数,该新函数最多调用一次原函数,无论新函数被调用多少次。

    fn.once();
    
    const fn = console.log.once();
    fn(5); // Prints 5.
    fn(5); // Does not print anything.
    fn(5); // Does not print anything.
    
    const initialize = createApplication.once();
    initialize();
    initialize();
    // createApplication is invoked only once.

    以下真实世界示例最初使用了 lodash.once

    // From Meteor v2.2.1:
    // “Are we running Meteor from a git checkout?”
    export const inCheckout = (function () {
      try { /* … */ } catch (e) { console.log(e); }
      return false;
    }).once();
    
    // From cypress@8.6.0:
    cy.on('command:retry', _.after(2, (() => {
      button.remove() /* … */
    }).once()))
    
    // From Jitsi Meet v6482:
    this._hangup = (() => {
     sendAnalytics(createToolbarEvent('hangup'));
     /* … */
    }).once()

    先例包括:

    Function.prototype.debounce

    Function.prototype.debounce 方法创建一个新函数,该新函数最多调用一次原函数,无论新函数被调用多少次。

    fn.debounce(numOfMilliseconds);

    许多图形应用程序使用 debounce。 在此示例中,日志记录发生在来自 inputEl 的 keyup 事件上,但仅当用户停止输入至少 250 毫秒之后:

    inputEl.addEventListener('keyup',
      console.log.debounce(250));

    此方法可能带有选项,这些选项可能会在 Stage 1 中进行细枝末节的争论。

    先例包括:

    Function.prototype.throttle

    Function.prototype.throttle 方法创建一个新函数,当被调用时,它会调用原函数——但在给定的时间长度内最多调用一次。

    fn.throttle(numOfMilliseconds);

    许多图形应用程序使用 throttle。 在此示例中,日志记录发生在窗口滚动时,但每 250 毫秒不超过一次:

    inputEl.addEventListener('keyup',
      console.log.throttle(250));

    此方法可能带有选项,这些选项可能会在 Stage 1 中进行细枝末节的争论。

    先例包括:

    Function.prototype.aside

    Function.prototype.aside 方法创建一个新的一元函数,该函数在返回原始参数之前对其参数应用某个回调。

    fn.aside();
    
    const { aside } = Function;
    
    console.log.aside(5); // Prints 5 before returning 5.
    
    arr.map(console.log.aside).map(f);
    // Prints each item from `arr` before passing them to `f`.
    
    const data = await Promise.resolve('intro.txt')
      .then(Deno.open)
      .then(Deno.readAll)
      .then(console.log.aside())
      .then(data => new TextDecoder('utf-8').decode(data));

    以下真实世界示例最初使用了 lodash.aside 和 lodash/fp 的 pipe。

    // From IBM/report-toolkit v0.6.1 packages/common/src/config.js:
    export function filterEnabledRules(config) {
      return pipe(
        config,
        _.getOr({}, 'rules'),
        _.toPairs,
        _.reduce(
          (enabledRules, [ruleName, ruleConfig]) =>
            (_.isObject(ruleConfig) && _.get('enabled', ruleConfig)) ||
            (_.isBoolean(ruleConfig) && ruleConfig)
              ? [ruleName, ...enabledRules]
              : enabledRules,
          []
        ),
        (ruleIds => {
          debug('found %d enabled rule(s)', ruleIds.length);
        }).aside();
    }

    先例包括:

    • lodash_.tap
    • Ramdaimport { tap } from 'ramda/src/tap';

    Function.prototype.unThis

    Function.prototype.unThis 方法创建一个新函数,该新函数调用原函数,将其第一个参数作为原函数的 this 接收者,并将其余参数作为原函数的普通参数。

    这对于将基于 this 的函数转换为非基于 this 的函数非常有用。

    fn.unThis();
    
    const $slice = Array.prototype.slice.unThis();
    $slice([ 0, 1, 2 ], 1); // [ 1, 2 ].

    这不能替代 bind-this 语法,该语法允许开发者更改函数的接收者而无需创建包装函数。

    fn.unThis() 等价于
    Function.prototype.call.bind(fn),也等价于
    Function.prototype.bind.bind(Function.prototype.call)(fn)

    因此,fn.unThis()(thisArg, ...restArgs) 等价于 fn.call(thisArg, ...restArgs)

    以下真实世界示例最初使用了 call-bind 或手动创建的类似函数。

    // From chrome-devtools-frontend@1.0.934332
    // node_modules/array-includes/test/implementation.js.
    runTests(implementation.unThis(), t);
    
    // From string.prototype.trimstart@1.0.4/index.js:
    var bound = getPolyfill().unThis();
    
    // From andreasgal/dom.js (84b7ab6) src/snapshot.js.
    const /* … */
      join = A.join || Array.prototype.join.unThis(),
      map = A.map || Array.prototype.map.unThis(),
      push = A.push || Array.prototype.push.unThis(),
      /* … */;

    先例包括: