From 984d56728e3208324996a960b7d6cf9e618df613 Mon Sep 17 00:00:00 2001 From: D-Sketon <2055272094@qq.com> Date: Wed, 18 Dec 2024 14:08:52 +0800 Subject: [PATCH] feat(compat): implement methodOf (#907) * feat(compat): implement methodOf * make lint happy * Update docs/ko/reference/compat/util/methodOf.md * Update docs/ja/reference/compat/util/methodOf.md * Update docs/ko/reference/compat/util/methodOf.md --------- Co-authored-by: Sojin Park Co-authored-by: Sojin Park --- benchmarks/performance/method.bench.ts | 24 +++ benchmarks/performance/methodOf.bench.ts | 24 +++ docs/ja/reference/compat/util/methodOf.md | 39 +++++ docs/ko/reference/compat/util/methodOf.md | 39 +++++ docs/reference/compat/util/methodOf.md | 39 +++++ .../zh_hans/reference/compat/util/methodOf.md | 39 +++++ src/compat/_internal/stubFour.ts | 3 + src/compat/_internal/stubThree.ts | 3 + src/compat/_internal/stubTwo.ts | 3 + src/compat/index.ts | 1 + src/compat/util/method.ts | 1 - src/compat/util/methodOf.spec.ts | 154 ++++++++++++++++++ src/compat/util/methodOf.ts | 26 +++ 13 files changed, 394 insertions(+), 1 deletion(-) create mode 100644 benchmarks/performance/method.bench.ts create mode 100644 benchmarks/performance/methodOf.bench.ts create mode 100644 docs/ja/reference/compat/util/methodOf.md create mode 100644 docs/ko/reference/compat/util/methodOf.md create mode 100644 docs/reference/compat/util/methodOf.md create mode 100644 docs/zh_hans/reference/compat/util/methodOf.md create mode 100644 src/compat/_internal/stubFour.ts create mode 100644 src/compat/_internal/stubThree.ts create mode 100644 src/compat/_internal/stubTwo.ts create mode 100644 src/compat/util/methodOf.spec.ts create mode 100644 src/compat/util/methodOf.ts diff --git a/benchmarks/performance/method.bench.ts b/benchmarks/performance/method.bench.ts new file mode 100644 index 00000000..4a9c1166 --- /dev/null +++ b/benchmarks/performance/method.bench.ts @@ -0,0 +1,24 @@ +import { bench, describe } from 'vitest'; +import { method as methodToolkit_ } from 'es-toolkit/compat'; +import { method as methodLodash_ } from 'lodash'; + +const methodToolkit = methodToolkit_; +const methodLodash = methodLodash_; + +const object = { + a: { + b: function (x: number, y: number) { + return x + y; + }, + }, +}; + +describe('method', () => { + bench('es-toolkit/compat', () => { + methodToolkit('a.b', 1, 2)(object); + }); + + bench('lodash', () => { + methodLodash('a.b', 1, 2)(object); + }); +}); diff --git a/benchmarks/performance/methodOf.bench.ts b/benchmarks/performance/methodOf.bench.ts new file mode 100644 index 00000000..fd167ccb --- /dev/null +++ b/benchmarks/performance/methodOf.bench.ts @@ -0,0 +1,24 @@ +import { bench, describe } from 'vitest'; +import { methodOf as methodOfToolkit_ } from 'es-toolkit/compat'; +import { methodOf as methodOfLodash_ } from 'lodash'; + +const methodOfToolkit = methodOfToolkit_; +const methodOfLodash = methodOfLodash_; + +const object = { + a: { + b: function (x: number, y: number) { + return x + y; + }, + }, +}; + +describe('methodOf', () => { + bench('es-toolkit/compat', () => { + methodOfToolkit(object, 1, 2)('a.b'); + }); + + bench('lodash', () => { + methodOfLodash(object, 1, 2)('a.b'); + }); +}); diff --git a/docs/ja/reference/compat/util/methodOf.md b/docs/ja/reference/compat/util/methodOf.md new file mode 100644 index 00000000..94acddf5 --- /dev/null +++ b/docs/ja/reference/compat/util/methodOf.md @@ -0,0 +1,39 @@ +# methodOf + +::: info +この関数は互換性のために `es-toolkit/compat` からのみインポートできます。代替可能なネイティブ JavaScript API があるか、まだ十分に最適化されていないためです。 + +`es-toolkit/compat` からこの関数をインポートすると、[lodash と完全に同じように動作](../../../compatibility.md)します。 +::: + +指定された`object`のパスにあるメソッドを、提供された引数で呼び出す関数を作成します。 + +## インターフェース + +```typescript +function methodOf(object: object, ...args: any[]): (path: PropertyKey | PropertyKey[]) => any; +``` + +### パラメータ + +- `object` (`object`): 要検索のオブジェクト。 +- `args` (`...any`): メソッドを呼び出す際に使用する引数。 + +### 戻り値 + +(`(path: PropertyKey | PropertyKey[]) => any`): パスを受け取り、`object` の `path` で `args` でメソッドを呼び出す新しい関数。 + +## 例 + +```typescript +const object = { + a: { + b: function (x, y) { + return x + y; + }, + }, +}; + +const add = methodOf(object, 1, 2); +console.log(add('a.b')); // => 3 +``` diff --git a/docs/ko/reference/compat/util/methodOf.md b/docs/ko/reference/compat/util/methodOf.md new file mode 100644 index 00000000..0ce82d1d --- /dev/null +++ b/docs/ko/reference/compat/util/methodOf.md @@ -0,0 +1,39 @@ +# methodOf + +::: info +이 함수는 호환성을 위한 `es-toolkit/compat` 에서만 가져올 수 있어요. 대체할 수 있는 네이티브 JavaScript API가 있거나, 아직 충분히 최적화되지 않았기 때문이에요. + +`es-toolkit/compat`에서 이 함수를 가져오면, [lodash와 완전히 똑같이 동작](../../../compatibility.md)해요. +::: + +주어진 경로에 있는 객체의 메서드를 제공된 파라미터로 호출하는 함수를 만들어요. + +## 인터페이스 + +```typescript +function methodOf(object: object, ...args: any[]): (path: PropertyKey | PropertyKey[]) => any; +``` + +### 파라미터 + +- `object` (`object`): 조회할 객체. +- `args` (`...any`): 메서드를 호출할 때 사용할 인수. + +### 반환 값 + +(`(path: PropertyKey | PropertyKey[]) => any`): 경로를 받아 `object`의 `path`에서 `args`로 메서드를 호출하는 새로운 함수. + +## 예시 + +```typescript +const object = { + a: { + b: function (x, y) { + return x + y; + }, + }, +}; + +const add = methodOf(object, 1, 2); +console.log(add('a.b')); // => 3 +``` diff --git a/docs/reference/compat/util/methodOf.md b/docs/reference/compat/util/methodOf.md new file mode 100644 index 00000000..405a819c --- /dev/null +++ b/docs/reference/compat/util/methodOf.md @@ -0,0 +1,39 @@ +# methodOf + +::: info +This function is only available in `es-toolkit/compat` for compatibility reasons. It either has alternative native JavaScript APIs or isn’t fully optimized yet. + +When imported from `es-toolkit/compat`, it behaves exactly like lodash and provides the same functionalities, as detailed [here](../../../compatibility.md). +::: + +Creates a function that invokes the method at a given path of `object` with the provided arguments. + +## Signature + +```typescript +function methodOf(object: object, ...args: any[]): (path: PropertyKey | PropertyKey[]) => any; +``` + +### Parameters + +- `object` (`object`): The object to query. +- `args` (`...any`): The arguments to invoke the method with. + +### Returns + +(`(path: PropertyKey | PropertyKey[]) => any`): Returns a new function that takes a path and invokes the method at `path` of `object` with `args`. + +## Examples + +```typescript +const object = { + a: { + b: function (x, y) { + return x + y; + }, + }, +}; + +const add = methodOf(object, 1, 2); +console.log(add('a.b')); // => 3 +``` diff --git a/docs/zh_hans/reference/compat/util/methodOf.md b/docs/zh_hans/reference/compat/util/methodOf.md new file mode 100644 index 00000000..98971230 --- /dev/null +++ b/docs/zh_hans/reference/compat/util/methodOf.md @@ -0,0 +1,39 @@ +# methodOf + +::: info +出于兼容性原因,此函数仅在 `es-toolkit/compat` 中提供。它可能具有替代的原生 JavaScript API,或者尚未完全优化。 + +从 `es-toolkit/compat` 导入时,它的行为与 lodash 完全一致,并提供相同的功能,详情请见 [这里](../../../compatibility.md)。 +::: + +创建一个函数,该函数使用提供的参数调用指定`object`路径上的方法。 + +## 签名 + +```typescript +function methodOf(object: object, ...args: any[]): (path: PropertyKey | PropertyKey[]) => any; +``` + +### 参数 + +- `object` (`object`): 要查询的对象。 +- `args` (`...any`): 用来调用方法的参数。 + +### 返回值 + +(`(path: PropertyKey | PropertyKey[]) => any`): 返回一个新函数,该函数接受一个路径,并用`args`在`object`的`path`调用方法。 + +## 示例 + +```typescript +const object = { + a: { + b: function (x, y) { + return x + y; + }, + }, +}; + +const add = methodOf(object, 1, 2); +console.log(add('a.b')); // => 3 +``` diff --git a/src/compat/_internal/stubFour.ts b/src/compat/_internal/stubFour.ts new file mode 100644 index 00000000..921617fd --- /dev/null +++ b/src/compat/_internal/stubFour.ts @@ -0,0 +1,3 @@ +export const stubFour = function () { + return 4; +}; diff --git a/src/compat/_internal/stubThree.ts b/src/compat/_internal/stubThree.ts new file mode 100644 index 00000000..6335709b --- /dev/null +++ b/src/compat/_internal/stubThree.ts @@ -0,0 +1,3 @@ +export const stubThree = function () { + return 3; +}; diff --git a/src/compat/_internal/stubTwo.ts b/src/compat/_internal/stubTwo.ts new file mode 100644 index 00000000..e76aaaf8 --- /dev/null +++ b/src/compat/_internal/stubTwo.ts @@ -0,0 +1,3 @@ +export const stubTwo = function () { + return 2; +}; diff --git a/src/compat/index.ts b/src/compat/index.ts index 4b6092b0..d85416bf 100644 --- a/src/compat/index.ts +++ b/src/compat/index.ts @@ -208,6 +208,7 @@ export { iteratee } from './util/iteratee.ts'; export { lt } from './util/lt.ts'; export { lte } from './util/lte.ts'; export { method } from './util/method.ts'; +export { methodOf } from './util/methodOf.ts'; export { now } from './util/now.ts'; export { stubArray } from './util/stubArray.ts'; export { stubFalse } from './util/stubFalse.ts'; diff --git a/src/compat/util/method.ts b/src/compat/util/method.ts index 72c790d3..c4b67485 100644 --- a/src/compat/util/method.ts +++ b/src/compat/util/method.ts @@ -1,5 +1,4 @@ import { invoke } from './invoke.ts'; -import { flatten } from '../array/flatten.ts'; /** * Creates a function that invokes the method at `path` of a given object with the provided arguments. diff --git a/src/compat/util/methodOf.spec.ts b/src/compat/util/methodOf.spec.ts new file mode 100644 index 00000000..209b8831 --- /dev/null +++ b/src/compat/util/methodOf.spec.ts @@ -0,0 +1,154 @@ +import { describe, expect, it } from 'vitest'; +import { constant, each, map, noop } from '..'; +import { methodOf as methodOfToolkit } from './methodOf'; +import { times } from './times'; +import { stubFour } from '../_internal/stubFour'; +import { stubOne } from '../_internal/stubOne'; +import { stubThree } from '../_internal/stubThree'; +import { stubTwo } from '../_internal/stubTwo'; + +describe('methodOf', () => { + it('should create a function that calls a method of a given key', () => { + const object = { a: stubOne }; + + each(['a', ['a']], path => { + const methodOf = methodOfToolkit(object); + expect(methodOf.length).toBe(1); + expect(methodOf(path)).toBe(1); + }); + }); + + it('should work with deep property values', () => { + const object = { a: { b: stubTwo } }; + + each(['a.b', ['a', 'b']], path => { + const methodOf = methodOfToolkit(object); + expect(methodOf(path)).toBe(2); + }); + }); + + it('should work with a non-string `path`', () => { + const array = times(3, constant); + + each([1, [1]], path => { + const methodOf = methodOfToolkit(array); + expect(methodOf(path)).toBe(1); + }); + }); + + it('should coerce `path` to a string', () => { + function fn() {} + fn.toString = constant('fn'); + + const expected = [1, 2, 3, 4]; + const object = { + null: stubOne, + undefined: stubTwo, + fn: stubThree, + '[object Object]': stubFour, + }; + const paths = [null, undefined, fn, {}]; + + times(2, index => { + const actual = map(paths, path => { + const methodOf = methodOfToolkit(object); + // @ts-expect-error - methodOf should handle nullish values + return methodOf(index ? [path] : path); + }); + + expect(actual).toEqual(expected); + }); + }); + + it('should work with inherited property values', () => { + function Foo() {} + Foo.prototype.a = stubOne; + + each(['a', ['a']], path => { + // @ts-expect-error - Foo is a constructor + const methodOf = methodOfToolkit(new Foo()); + expect(methodOf(path)).toBe(1); + }); + }); + + it('should use a key over a path', () => { + const object = { 'a.b': stubOne, a: { b: stubTwo } }; + + each(['a.b', ['a.b']], path => { + const methodOf = methodOfToolkit(object); + expect(methodOf(path)).toBe(1); + }); + }); + + it('should return `undefined` when `object` is nullish', () => { + // eslint-disable-next-line no-sparse-arrays + const values = [, null, undefined]; + const expected = map(values, noop); + + each(['constructor', ['constructor']], path => { + const actual = map(values, (value, index) => { + // @ts-expect-error - methodOf should handle nullish values + const methodOf = index ? methodOfToolkit() : methodOfToolkit(value); + return methodOf(path); + }); + + expect(actual).toEqual(expected); + }); + }); + + it('should return `undefined` for deep paths when `object` is nullish', () => { + // eslint-disable-next-line no-sparse-arrays + const values = [, null, undefined]; + const expected = map(values, noop); + + each(['constructor.prototype.valueOf', ['constructor', 'prototype', 'valueOf']], path => { + const actual = map(values, (value, index) => { + // @ts-expect-error - methodOf should handle nullish values + const methodOf = index ? methodOfToolkit() : methodOfToolkit(value); + return methodOf(path); + }); + + expect(actual).toEqual(expected); + }); + }); + + it('should return `undefined` if parts of `path` are missing', () => { + const object = {}; + const methodOf = methodOfToolkit(object); + + each(['a', 'a[1].b.c', ['a'], ['a', '1', 'b', 'c']], path => { + expect(methodOf(path)).toBe(undefined); + }); + }); + + it('should apply partial arguments to function', () => { + const object = { + fn: function () { + // eslint-disable-next-line prefer-rest-params + return Array.prototype.slice.call(arguments); + }, + }; + + const methodOf = methodOfToolkit(object, 1, 2, 3); + + each(['fn', ['fn']], path => { + expect(methodOf(path)).toEqual([1, 2, 3]); + }); + }); + + it('should invoke deep property methods with the correct `this` binding', () => { + const object = { + a: { + b: function () { + return this.c; + }, + c: 1, + }, + }; + const methodOf = methodOfToolkit(object); + + each(['a.b', ['a', 'b']], path => { + expect(methodOf(path)).toBe(1); + }); + }); +}); diff --git a/src/compat/util/methodOf.ts b/src/compat/util/methodOf.ts new file mode 100644 index 00000000..86c1818c --- /dev/null +++ b/src/compat/util/methodOf.ts @@ -0,0 +1,26 @@ +import { invoke } from './invoke.ts'; + +/** + * Creates a function that invokes the method at a given path of `object` with the provided arguments. + * + * @param {object} object - The object to query. + * @param {...any} args - The arguments to invoke the method with. + * @returns {(path: PropertyKey | PropertyKey[]) => any} - Returns a new function that takes a path and invokes the method at `path` with `args`. + * + * @example + * const object = { + * a: { + * b: function (x, y) { + * return x + y; + * } + * } + * }; + * + * const add = methodOf(object, 1, 2); + * console.log(add('a.b')); // => 3 + */ +export function methodOf(object: object, ...args: any[]): (path: PropertyKey | PropertyKey[]) => any { + return function (path: PropertyKey | PropertyKey[]) { + return invoke(object, path, args); + }; +}