From f198dfd9d4be293377c29243274f180a947ac483 Mon Sep 17 00:00:00 2001 From: D-Sketon <2055272094@qq.com> Date: Sun, 4 Aug 2024 20:33:43 +0800 Subject: [PATCH] feat(bind): implement bind (#337) * feat(bind): implement bind * fix: redundant prototype copy --- benchmarks/performance/bind.bench.ts | 31 ++++ docs/.vitepress/en.mts | 1 + docs/.vitepress/zh_hans.mts | 1 + docs/reference/function/bind.md | 47 ++++++ docs/zh_hans/reference/function/bind.md | 47 ++++++ src/function/bind.spec.ts | 189 ++++++++++++++++++++++++ src/function/bind.ts | 48 ++++++ src/function/index.ts | 1 + 8 files changed, 365 insertions(+) create mode 100644 benchmarks/performance/bind.bench.ts create mode 100644 docs/reference/function/bind.md create mode 100644 docs/zh_hans/reference/function/bind.md create mode 100644 src/function/bind.spec.ts create mode 100644 src/function/bind.ts diff --git a/benchmarks/performance/bind.bench.ts b/benchmarks/performance/bind.bench.ts new file mode 100644 index 00000000..f31c93cb --- /dev/null +++ b/benchmarks/performance/bind.bench.ts @@ -0,0 +1,31 @@ +import { bench, describe } from 'vitest'; +import { bind as bindToolkit } from 'es-toolkit'; +import { bind as bindLodash } from 'lodash'; + +function fn(this: any) { + const result = [this]; + // eslint-disable-next-line prefer-rest-params + return result.concat(Array.from(arguments)); +} + +describe('bind', () => { + bench('es-toolkit/bind - without placeholder', () => { + const object = {}; + bindToolkit(fn, object, 'a'); + }); + + bench('lodash/bind - without placeholder', () => { + const object = {}; + bindLodash(fn, object, 'a'); + }); + + bench('es-toolkit/bind - with placeholder', () => { + const object = {}; + bindToolkit(fn, object, 'a', bindToolkit.placeholder); + }); + + bench('lodash/bind - with placeholder', () => { + const object = {}; + bindLodash(fn, object, 'a', bindLodash.placeholder); + }); +}); diff --git a/docs/.vitepress/en.mts b/docs/.vitepress/en.mts index 7322a735..5c9130a0 100644 --- a/docs/.vitepress/en.mts +++ b/docs/.vitepress/en.mts @@ -120,6 +120,7 @@ function sidebar(): DefaultTheme.Sidebar { { text: 'negate', link: '/reference/function/negate' }, { text: 'once', link: '/reference/function/once' }, { text: 'noop', link: '/reference/function/noop' }, + { text: 'bind', link: '/reference/function/bind' }, ], }, { diff --git a/docs/.vitepress/zh_hans.mts b/docs/.vitepress/zh_hans.mts index 4895211f..7b56d4e1 100644 --- a/docs/.vitepress/zh_hans.mts +++ b/docs/.vitepress/zh_hans.mts @@ -114,6 +114,7 @@ function sidebar(): DefaultTheme.Sidebar { { text: 'negate', link: '/zh_hans/reference/function/negate' }, { text: 'once', link: '/zh_hans/reference/function/once' }, { text: 'noop', link: '/zh_hans/reference/function/noop' }, + { text: 'bind', link: '/zh_hans/reference/function/bind' }, ], }, { diff --git a/docs/reference/function/bind.md b/docs/reference/function/bind.md new file mode 100644 index 00000000..ea7734b1 --- /dev/null +++ b/docs/reference/function/bind.md @@ -0,0 +1,47 @@ +# bind + +Creates a function that invokes `func` with the `this` binding of `thisArg` and `partials` prepended to the arguments it receives. + +The `bind.placeholder` value, which defaults to a `symbol`, may be used as a placeholder for partially applied arguments. + +**Note:** Unlike native `Function#bind`, this method doesn't set the `length` property of bound functions. + +## Signature + +```typescript +function bind(func: (...args: any[]) => any, thisArg?: any, ...partials: any[]): (...args: any[]) => any; +namespace bind { + placeholder: symbol; +} +``` + +### Parameters + +- `fn` (`(...args: any[]) => any`): The function to bind. +- `thisArg` (`any`, optional): The `this` binding of `func`. +- `partials` (`any[]`): The arguments to be partially applied. + +### Returns + +(`(...args: any[]) => any`): Returns the new bound function. + +## Examples + +```typescript +import { bind } from 'es-toolkit/function'; + +function greet(greeting, punctuation) { + return greeting + ' ' + this.user + punctuation; +} + +const object = { user: 'fred' }; + +let bound = bind(greet, object, 'hi'); +bound('!'); +// => 'hi fred!' + +// Bound with placeholders. +bound = bind(greet, object, bind.placeholder, '!'); +bound('hi'); +// => 'hi fred!' +``` diff --git a/docs/zh_hans/reference/function/bind.md b/docs/zh_hans/reference/function/bind.md new file mode 100644 index 00000000..edb2a03a --- /dev/null +++ b/docs/zh_hans/reference/function/bind.md @@ -0,0 +1,47 @@ +# bind + +创建一个调用 `func` 的函数,`thisArg` 绑定 `func` 函数中的 `this`,并且 `func` 函数会接收 `partials` 附加参数。 + +`bind.placeholder` 的值默认是一个 `symbol`,可以用作附加的部分参数的占位符。 + +**注意:** 不同于原生的 `Function#bind`,这个方法不会设置绑定函数的 `length` 属性。 + +## 签名 + +```typescript +function bind(func: (...args: any[]) => any, thisArg?: any, ...partials: any[]): (...args: any[]) => any; +namespace bind { + placeholder: symbol; +} +``` + +### 参数 + +- `fn` (`(...args: any[]) => any`): 绑定的函数。 +- `thisArg` (`any`, optional): `func` 绑定的 `this` 对象。 +- `partials` (`any[]`): 附加的部分参数。 + +### Returns + +(`(...args: any[]) => any`): 返回新的绑定函数。 + +## 示例 + +```typescript +import { bind } from 'es-toolkit/function'; + +function greet(greeting, punctuation) { + return greeting + ' ' + this.user + punctuation; +} + +const object = { user: 'fred' }; + +let bound = bind(greet, object, 'hi'); +bound('!'); +// => 'hi fred!' + +// Bound with placeholders. +bound = bind(greet, object, bind.placeholder, '!'); +bound('hi'); +// => 'hi fred!' +``` diff --git a/src/function/bind.spec.ts b/src/function/bind.spec.ts new file mode 100644 index 00000000..1d51c6c9 --- /dev/null +++ b/src/function/bind.spec.ts @@ -0,0 +1,189 @@ +import { describe, it, expect } from 'vitest'; +import { bind } from './bind'; +import { isEqual } from '../predicate'; + +function fn(this: any) { + const result = [this]; + // eslint-disable-next-line prefer-rest-params + return result.concat(Array.from(arguments)); +} + +describe('bind', () => { + it('should bind a function to an object', () => { + const object = {}, + bound = bind(fn, object); + + expect(bound('a')).toEqual([object, 'a']); + }); + + it('should accept a falsey `thisArg`', () => { + const values = [false, 0, '', NaN, null, undefined]; + const expected = values.map(value => [value]); + + const actual = values.map(value => { + const bound = bind(fn, value); + return bound(); + }); + + expect( + actual.every((value, index) => { + return isEqual(value, expected[index]); + }) + ).toBe(true); + }); + + it('should bind a function to nullish values', () => { + const bound = bind(fn, null); + const actual = bound('a'); + + expect(actual[0] === null); + expect(actual[1]).toBe('a'); + + const bound2 = bind(fn, undefined); + const actual2 = bound2('b'); + + expect(actual2[0] === undefined); + expect(actual2[1]).toBe('b'); + + const bound3 = bind(fn); + const actual3 = bound3('b'); + + expect(actual3[0] === undefined); + expect(actual3[1]).toBe('b'); + }); + + it('should partially apply arguments ', () => { + const object = {}; + let bound = bind(fn, object, 'a'); + + expect(bound()).toEqual([object, 'a']); + + bound = bind(fn, object, 'a'); + expect(bound('b')).toEqual([object, 'a', 'b']); + + bound = bind(fn, object, 'a', 'b'); + expect(bound()).toEqual([object, 'a', 'b']); + expect(bound('c', 'd')).toEqual([object, 'a', 'b', 'c', 'd']); + }); + + it('should support placeholders', () => { + const object = {}; + const ph = bind.placeholder; + const bound = bind(fn, object, ph, 'b', ph); + + expect(bound('a', 'c')).toEqual([object, 'a', 'b', 'c']); + expect(bound('a')).toEqual([object, 'a', 'b', undefined]); + expect(bound('a', 'c', 'd')).toEqual([object, 'a', 'b', 'c', 'd']); + expect(bound()).toEqual([object, undefined, 'b', undefined]); + }); + + it('should create a function with a `length` of `0`', () => { + // eslint-disable-next-line @typescript-eslint/no-unused-vars + const fn = function (_a: unknown, _b: unknown, _c: unknown) {}; + let bound = bind(fn, {}); + + expect(bound.length).toBe(0); + + bound = bind(fn, {}, 1); + expect(bound.length).toBe(0); + }); + + it('should ignore binding when called with the `new` operator', () => { + function Foo(this: any) { + return this; + } + + const bound = bind(Foo, { a: 1 }); + // @ts-expect-error - bound is a constructor + const newBound = new bound(); + + expect(bound().a).toBe(1); + expect(newBound.a).toBe(undefined); + expect(newBound instanceof Foo); + }); + + it('should handle a number of arguments when called with the `new` operator', () => { + function Foo(this: any) { + return this; + } + + function Bar() {} + + const thisArg = { a: 1 }; + const boundFoo = bind(Foo, thisArg) as any; + const boundBar = bind(Bar, thisArg) as any; + expect([new boundFoo().a, new boundBar().a]).toEqual([undefined, undefined]); + expect([new boundFoo(1).a, new boundBar(1).a]).toEqual([undefined, undefined]); + expect([new boundFoo(1, 2).a, new boundBar(1, 2).a]).toEqual([undefined, undefined]); + expect([new boundFoo(1, 2, 3).a, new boundBar(1, 2, 3).a]).toEqual([undefined, undefined]); + expect([new boundFoo(1, 2, 3, 4).a, new boundBar(1, 2, 3, 4).a]).toEqual([undefined, undefined]); + expect([new boundFoo(1, 2, 3, 4, 5).a, new boundBar(1, 2, 3, 4, 5).a]).toEqual([undefined, undefined]); + expect([new boundFoo(1, 2, 3, 4, 5, 6).a, new boundBar(1, 2, 3, 4, 5, 6).a]).toEqual([undefined, undefined]); + expect([new boundFoo(1, 2, 3, 4, 5, 6, 7).a, new boundBar(1, 2, 3, 4, 5, 6, 7).a]).toEqual([undefined, undefined]); + expect([new boundFoo(1, 2, 3, 4, 5, 6, 7, 8).a, new boundBar(1, 2, 3, 4, 5, 6, 7, 8).a]).toEqual([ + undefined, + undefined, + ]); + }); + + it('should ensure `new bound` is an instance of `func`', () => { + function Foo(value: any) { + return value && object; + } + + const bound = bind(Foo) as any; + const object = {}; + + expect(new bound() instanceof Foo); + expect(new bound(true)).toBe(object); + }); + + it('should append array arguments to partially applied arguments', () => { + const object = {}, + bound = bind(fn, object, 'a'); + + expect(bound(['b'], 'c')).toEqual([object, 'a', ['b'], 'c']); + }); + + it('should not rebind functions', () => { + const object1 = {}, + object2 = {}, + object3 = {}; + + const bound1 = bind(fn, object1), + bound2 = bind(bound1, object2, 'a'), + bound3 = bind(bound1, object3, 'b'); + + expect(bound1()).toEqual([object1]); + expect(bound2()).toEqual([object1, 'a']); + expect(bound3()).toEqual([object1, 'b']); + }); + + it('should not error when instantiating bound built-ins', () => { + let Ctor = bind(Date, null) as any; + + const expected = new Date(2012, 4, 23, 0, 0, 0, 0); + let actual = new Ctor(2012, 4, 23, 0, 0, 0, 0); + expect(actual).toEqual(expected); + + Ctor = bind(Date, null, 2012, 4, 23); + actual = new Ctor(0, 0, 0, 0); + expect(actual).toEqual(expected); + }); + + it('should not error when calling bound class constructors with the `new` operator', () => { + const createCtor: any = function () { + return class A {}; + }; + + const bound = bind(createCtor()) as any; + expect(Boolean(new bound())).toBe(true); + expect(Boolean(new bound(1))).toBe(true); + expect(Boolean(new bound(1, 2))).toBe(true); + expect(Boolean(new bound(1, 2, 3))).toBe(true); + expect(Boolean(new bound(1, 2, 3, 4))).toBe(true); + expect(Boolean(new bound(1, 2, 3, 4, 5))).toBe(true); + expect(Boolean(new bound(1, 2, 3, 4, 5, 6))).toBe(true); + expect(Boolean(new bound(1, 2, 3, 4, 5, 6, 7))).toBe(true); + }); +}); diff --git a/src/function/bind.ts b/src/function/bind.ts new file mode 100644 index 00000000..bea0da37 --- /dev/null +++ b/src/function/bind.ts @@ -0,0 +1,48 @@ +/** + * + * Creates a function that invokes `func` with the `this` binding of `thisArg` and `partials` prepended to the arguments it receives. + * + * The `bind.placeholder` value, which defaults to a `symbol`, may be used as a placeholder for partially applied arguments. + * + * **Note:** Unlike native `Function#bind`, this method doesn't set the `length` property of bound functions. + * + * @param {(...args: any[]) => any} func The function to bind. + * @param {any} thisArg The `this` binding of `func`. + * @param {any[]} partials The arguments to be partially applied. + * @returns {(...args: any[]) => any} Returns the new bound function. + * + * @example + * function greet(greeting, punctuation) { + * return greeting + ' ' + this.user + punctuation; + * } + * const object = { user: 'fred' }; + * let bound = bind(greet, object, 'hi'); + * bound('!'); + * // => 'hi fred!' + * + * bound = bind(greet, object, bind.placeholder, '!'); + * bound('hi'); + * // => 'hi fred!' + */ +export function bind(func: (...args: any[]) => any, thisArg?: any, ...partials: any[]): (...args: any[]) => any { + const wrapper = function (this: any, ...args: any[]) { + let index = 0; + const result = partials.map(bindArg => { + if (bindArg === bind.placeholder) { + return args[index++]; + } + return bindArg; + }); + for (let i = index; i < args.length; i++) { + result.push(args[i]); + } + if (this instanceof wrapper) { + // @ts-expect-error - fn is a constructor + return new func(...result); + } + return func.apply(thisArg, result); + }; + return wrapper; +} + +bind.placeholder = Symbol('bind.placeholder'); diff --git a/src/function/index.ts b/src/function/index.ts index 4fb6cde4..c19867ad 100644 --- a/src/function/index.ts +++ b/src/function/index.ts @@ -5,3 +5,4 @@ export { noop } from './noop.ts'; export { once } from './once.ts'; export { throttle } from './throttle.ts'; export { negate } from './negate.ts'; +export { bind } from './bind.ts';