diff --git a/benchmarks/performance/spread.bench.ts b/benchmarks/performance/spread.bench.ts new file mode 100644 index 00000000..7d3dc6ac --- /dev/null +++ b/benchmarks/performance/spread.bench.ts @@ -0,0 +1,24 @@ +import { bench, describe } from 'vitest'; +import { spread as spreadToolkit } from 'es-toolkit'; +import { spread as spreadCompat } from 'es-toolkit/compat'; +import { spread as spreadLodash } from 'lodash'; + +describe('spread', () => { + // eslint-disable-next-line @typescript-eslint/no-unused-vars + function fn(_a: unknown, _b: unknown, _c: unknown) { + // eslint-disable-next-line prefer-rest-params + return Array.from(arguments); + } + + bench('es-toolkit/spread', () => { + spreadToolkit(fn, 1); + }); + + bench('es-toolkit/compat/spread', () => { + spreadCompat(fn, 1); + }); + + bench('lodash/spread', () => { + spreadLodash(fn, 1); + }); +}); diff --git a/docs/.vitepress/en.mts b/docs/.vitepress/en.mts index f497986f..9f3f09bc 100644 --- a/docs/.vitepress/en.mts +++ b/docs/.vitepress/en.mts @@ -134,6 +134,7 @@ function sidebar(): DefaultTheme.Sidebar { { text: 'partial', link: '/reference/function/partial' }, { text: 'partialRight', link: '/reference/function/partialRight' }, { text: 'rest', link: '/reference/function/rest' }, + { text: 'spread', link: '/reference/function/spread' }, ], }, { diff --git a/docs/.vitepress/zh_hans.mts b/docs/.vitepress/zh_hans.mts index f85fa26d..2d646ee8 100644 --- a/docs/.vitepress/zh_hans.mts +++ b/docs/.vitepress/zh_hans.mts @@ -131,6 +131,7 @@ function sidebar(): DefaultTheme.Sidebar { { text: 'partial', link: '/zh_hans/reference/function/partial' }, { text: 'partialRight', link: '/zh_hans/reference/function/partialRight' }, { text: 'rest', link: '/zh_hans/reference/function/rest' }, + { text: 'spread', link: '/zh_hans/reference/function/spread' }, ], }, { diff --git a/docs/reference/function/spread.md b/docs/reference/function/spread.md new file mode 100644 index 00000000..ef2bf5e8 --- /dev/null +++ b/docs/reference/function/spread.md @@ -0,0 +1,51 @@ +# spread + +Creates a function that invokes `func` with the `this` binding of the create function and an array of arguments much like [`Function#apply`](https://www.ecma-international.org/ecma-262/6.0/#sec-function.prototype.apply). + +## Signature + +```typescript +function spread any>(func: F, startIndex: number = 0): (...args: any[]) => ReturnType; +``` + +### Parameters + +- `func` (`F`): The function to spread arguments over. +- `startIndex` (`number`, optional): The start position of the spread. Defaults to `0`. + +### Returns + +(`(...args: any[]) => ReturnType`): A new function that invokes `func` with the spread arguments. + +## Examples + +```typescript +import { spread } from 'es-toolkit/function'; + +const say = spread(function (who, what) { + return who + ' says ' + what; +}); + +say(['fred', 'hello']); +// => 'fred says hello' +``` + +## Lodash Compatibility + +Import `spread` from `es-toolkit/compat` for full compatibility with lodash. + +- `spread` treats `startIndex` as `0` for negative or `NaN` values. +- `spread` accepts `startIndex` with fractional values, but coerces them to integers. + +```typescript +import { spread } from 'es-toolkit/compat'; + +function fn(a: unknown, b: unknown, c: unknown) { + return Array.from(arguments); +} + +spread(fn, -1)([1, 2]); // Returns [1, 2] +spread(fn, NaN)([1, 2]); // Returns [1, 2] +spread(fn, 'a')([1, 2]); // Returns [1, 2] +spread(fn, 1.6)(1, [2, 3]); // Returns [1, 2, 3] +``` diff --git a/docs/zh_hans/reference/array/chunk.md b/docs/zh_hans/reference/array/chunk.md index 458f6b0d..ce53ac53 100644 --- a/docs/zh_hans/reference/array/chunk.md +++ b/docs/zh_hans/reference/array/chunk.md @@ -53,10 +53,10 @@ console.log(chunk([1, 2, 3, 4, 5], 2)); ## Lodash 兼容性 -从`es-toolkit/compat`中导入`chunk`以实现与 lodash 的完全兼容。 +从 `es-toolkit/compat` 中导入 `chunk` 以实现与 lodash 的完全兼容。 -- chunk 不抛出异常,当给定的`size`小于 1 时。 -- chunk 接受分数值,这些值将被向下舍入到最近的整数。 +- `chunk` 当给定的 `size` 小于 1 时不抛出异常。 +- `chunk` 接受分数值,这些值将被向下舍入到最近的整数。 ```typescript import { chunk } from 'es-toolkit/compat'; diff --git a/docs/zh_hans/reference/function/spread.md b/docs/zh_hans/reference/function/spread.md new file mode 100644 index 00000000..44bd84e1 --- /dev/null +++ b/docs/zh_hans/reference/function/spread.md @@ -0,0 +1,51 @@ +# spread + +创建一个函数,该函数调用 `func`,使用创建函数的 `this` 绑定和一个类似于 [`Function#apply`](https://www.ecma-international.org/ecma-262/6.0/#sec-function.prototype.apply) 的参数数组。 + +## 签名 + +```typescript +function spread any>(func: F, startIndex: number = 0): (...args: any[]) => ReturnType; +``` + +### 参数 + +- `func` (`F`): 用于展开参数的函数。 +- `startIndex` (`number`, 可选): 参数的开始位置,默认为 `0`。 + +### 返回值 + +(`(...args: any[]) => ReturnType`): 一个新的函数,该函数使用展开的参数调用 `func`。 + +## 示例 + +```typescript +import { spread } from 'es-toolkit/function'; + +const say = spread(function (who, what) { + return who + ' says ' + what; +}); + +say(['fred', 'hello']); +// => 'fred says hello' +``` + +## Lodash 兼容性 + +从 `es-toolkit/compat` 中导入 `spread` 以实现与 lodash 的完全兼容。 + +- `spread` 当给定的 `startIndex` 小于 0 或为 `NaN` 时将其视为 `0`。 +- `spread` 接受分数值的 `startIndex`,但将其强制转换为整数。 + +```typescript +import { spread } from 'es-toolkit/compat'; + +function fn(a: unknown, b: unknown, c: unknown) { + return Array.from(arguments); +} + +spread(fn, -1)([1, 2]); // Returns [1, 2] +spread(fn, NaN)([1, 2]); // Returns [1, 2] +spread(fn, 'a')([1, 2]); // Returns [1, 2] +spread(fn, 1.6)(1, [2, 3]); // Returns [1, 2, 3] +``` diff --git a/src/compat/function/spread.spec.ts b/src/compat/function/spread.spec.ts new file mode 100644 index 00000000..3050542d --- /dev/null +++ b/src/compat/function/spread.spec.ts @@ -0,0 +1,59 @@ +import { describe, it, expect } from 'vitest'; +import { spread } from './spread'; + +describe('spread', () => { + // eslint-disable-next-line @typescript-eslint/no-unused-vars + function fn(_a: unknown, _b: unknown, _c: unknown) { + // eslint-disable-next-line prefer-rest-params + return Array.from(arguments); + } + + it('should spread arguments to `func`', () => { + const spreadFn = spread(fn); + const expected = [1, 2]; + + expect(spreadFn([1, 2])).toEqual(expected); + expect(spreadFn([1, 2], 3)).toEqual(expected); + }); + + it('should accept a falsey `array`', () => { + const falsey = [null, undefined, false, 0, NaN, '']; + const spreadFn = spread(() => true); + const expected = falsey.map(() => true); + + const actual = falsey.map(array => { + return spreadFn(array); + }); + + expect(actual).toEqual(expected); + }); + + it('should work with `startIndex`', () => { + const spreadFn = spread(fn, 1); + const expected = [1, 2, 3]; + + expect(spreadFn(1, [2, 3])).toEqual(expected); + expect(spreadFn(1, [2, 3], 4)).toEqual(expected); + }); + + it('should treat `start` as `0` for negative or `NaN` values', () => { + const values = [-1, NaN, 'a']; + const expected = values.map(() => [1, 2]); + + const actual = values.map(value => { + // @ts-expect-error - spreadFn is not being called with the correct arguments + const spreadFn = spread(fn, value); + return spreadFn([1, 2]); + }); + + expect(actual).toEqual(expected); + }); + + it('should coerce `startIndex` to an integer', () => { + const spreadFn = spread(fn, 1.6); + const expected = [1, 2, 3]; + + expect(spreadFn(1, [2, 3])).toEqual(expected); + expect(spreadFn(1, [2, 3], 4)).toEqual(expected); + }); +}); diff --git a/src/compat/function/spread.ts b/src/compat/function/spread.ts new file mode 100644 index 00000000..377f9212 --- /dev/null +++ b/src/compat/function/spread.ts @@ -0,0 +1,22 @@ +import { spread as spreadToolkit } from '../../function/spread.ts'; + +/** + * Creates a function that invokes `func` with the `this` binding of the create function and an array of arguments much like `Function#apply`. + * + * @template F The type of the function to spread arguments over. + * @param {F} func The function to spread arguments over. + * @param {number} startIndex The start position of the spread. + * @returns {(...args: any[]) => ReturnType} A new function that invokes `func` with the spread arguments. + */ +export function spread any>( + func: F, + startIndex: number = 0 +): (...args: any[]) => ReturnType { + startIndex = Number.parseInt(startIndex as any, 10); + + if (Number.isNaN(startIndex) || startIndex < 0) { + startIndex = 0; + } + + return spreadToolkit(func, startIndex); +} diff --git a/src/compat/index.ts b/src/compat/index.ts index 00a8027f..ea88a0cb 100644 --- a/src/compat/index.ts +++ b/src/compat/index.ts @@ -42,6 +42,7 @@ export { indexOf } from './array/indexOf.ts'; export { ary } from './function/ary.ts'; export { bind } from './function/bind.ts'; export { rest } from './function/rest.ts'; +export { spread } from './function/spread.ts'; export { get } from './object/get.ts'; export { set } from './object/set.ts'; diff --git a/src/function/index.ts b/src/function/index.ts index 62db4737..a4f3f914 100644 --- a/src/function/index.ts +++ b/src/function/index.ts @@ -11,3 +11,4 @@ export { unary } from './unary.ts'; export { partial } from './partial.ts'; export { partialRight } from './partialRight.ts'; export { rest } from './rest.ts'; +export { spread } from './spread.ts'; diff --git a/src/function/spread.spec.ts b/src/function/spread.spec.ts new file mode 100644 index 00000000..51a65bea --- /dev/null +++ b/src/function/spread.spec.ts @@ -0,0 +1,38 @@ +import { describe, it, expect } from 'vitest'; +import { spread } from './spread'; + +describe('spread', () => { + // eslint-disable-next-line @typescript-eslint/no-unused-vars + function fn(_a: unknown, _b: unknown, _c: unknown) { + // eslint-disable-next-line prefer-rest-params + return Array.from(arguments); + } + + it('should spread arguments to `func`', () => { + const spreadFn = spread(fn); + const expected = [1, 2]; + + expect(spreadFn([1, 2])).toEqual(expected); + expect(spreadFn([1, 2], 3)).toEqual(expected); + }); + + it('should accept a falsey `array`', () => { + const falsey = [null, undefined, false, 0, NaN, '']; + const spreadFn = spread(() => true); + const expected = falsey.map(() => true); + + const actual = falsey.map(array => { + return spreadFn(array); + }); + + expect(actual).toEqual(expected); + }); + + it('should work with `startIndex`', () => { + const spreadFn = spread(fn, 1); + const expected = [1, 2, 3]; + + expect(spreadFn(1, [2, 3])).toEqual(expected); + expect(spreadFn(1, [2, 3], 4)).toEqual(expected); + }); +}); diff --git a/src/function/spread.ts b/src/function/spread.ts new file mode 100644 index 00000000..e973c3c8 --- /dev/null +++ b/src/function/spread.ts @@ -0,0 +1,22 @@ +/** + * Creates a function that invokes `func` with the `this` binding of the create function and an array of arguments much like `Function#apply`. + * + * @template F The type of the function to spread arguments over. + * @param {F} func The function to spread arguments over. + * @param {number} startIndex The start position of the spread. + * @returns {(...args: any[]) => ReturnType} A new function that invokes `func` with the spread arguments. + */ +export function spread any>( + func: F, + startIndex: number = 0 +): (...args: any[]) => ReturnType { + return function (this: any, ...args: any[]) { + const array = args[startIndex]; + const params = args.slice(0, startIndex); + + if (array) { + params.push(...array); + } + return func.apply(this, params); + }; +}