feat(bind): implement bind (#337)

* feat(bind): implement bind

* fix: redundant prototype copy
This commit is contained in:
D-Sketon 2024-08-04 20:33:43 +08:00 committed by GitHub
parent b0011fa038
commit f198dfd9d4
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
8 changed files with 365 additions and 0 deletions

View 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);
});
});

View File

@ -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' },
],
},
{

View File

@ -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' },
],
},
{

View 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!'
```

View 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
View 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
View 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');

View File

@ -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';