mirror of
https://github.com/toss/es-toolkit.git
synced 2024-11-24 03:32:58 +03:00
feat(bind): implement bind (#337)
* feat(bind): implement bind * fix: redundant prototype copy
This commit is contained in:
parent
b0011fa038
commit
f198dfd9d4
31
benchmarks/performance/bind.bench.ts
Normal file
31
benchmarks/performance/bind.bench.ts
Normal file
@ -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);
|
||||
});
|
||||
});
|
@ -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' },
|
||||
],
|
||||
},
|
||||
{
|
||||
|
@ -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' },
|
||||
],
|
||||
},
|
||||
{
|
||||
|
47
docs/reference/function/bind.md
Normal file
47
docs/reference/function/bind.md
Normal file
@ -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!'
|
||||
```
|
47
docs/zh_hans/reference/function/bind.md
Normal file
47
docs/zh_hans/reference/function/bind.md
Normal file
@ -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!'
|
||||
```
|
189
src/function/bind.spec.ts
Normal file
189
src/function/bind.spec.ts
Normal file
@ -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);
|
||||
});
|
||||
});
|
48
src/function/bind.ts
Normal file
48
src/function/bind.ts
Normal file
@ -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');
|
@ -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';
|
||||
|
Loading…
Reference in New Issue
Block a user