feat(has): Implement has

This commit is contained in:
Sojin Park 2024-08-11 12:56:15 +09:00
parent be4162dc19
commit 01cf3a6c9a
14 changed files with 530 additions and 6 deletions

View File

@ -1,5 +1,5 @@
import { bench, describe } from 'vitest';
import { get as getToolkit } from 'es-toolkit';
import { get as getToolkit } from 'es-toolkit/compat';
import { get as getLodash } from 'lodash';
describe('get with string', () => {

View File

@ -0,0 +1,23 @@
import { bench, describe } from 'vitest';
import { has as hasToolkit } from 'es-toolkit/compat';
import { has as hasLodash } from 'lodash';
describe('has with string', () => {
bench('es-toolkit/has', () => {
hasToolkit({ a: { b: 3 } }, 'a.b');
});
bench('lodash/has', () => {
hasLodash({ a: { b: 3 } }, 'a.b');
});
});
describe('has with string array', () => {
bench('es-toolkit/has', () => {
hasToolkit({ a: { b: 3 } }, ['a', 'b']);
});
bench('lodash/has', () => {
hasLodash({ a: { b: 3 } }, ['a', 'b']);
});
});

View File

@ -160,6 +160,7 @@ function sidebar(): DefaultTheme.Sidebar {
{ text: 'pickBy', link: '/reference/object/pickBy' },
{ text: 'get (compat)', link: '/reference/compat/object/get' },
{ text: 'set (compat)', link: '/reference/compat/object/set' },
{ text: 'has (compat)', link: '/reference/compat/object/has' },
{ text: 'property (compat)', link: '/reference/compat/object/property' },
],
},

View File

@ -169,6 +169,7 @@ function sidebar(): DefaultTheme.Sidebar {
{ text: 'pickBy', link: '/ko/reference/object/pickBy' },
{ text: 'get (호환성)', link: '/ko/reference/compat/object/get' },
{ text: 'set (호환성)', link: '/ko/reference/compat/object/set' },
{ text: 'has (호환성)', link: '/ko/reference/compat/object/has' },
{ text: 'property (호환성)', link: '/ko/reference/compat/object/property' },
],
},

View File

@ -156,6 +156,7 @@ function sidebar(): DefaultTheme.Sidebar {
{ text: 'pickBy', link: '/zh_hans/reference/object/pickBy' },
{ text: 'get (兼容性)', link: '/zh_hans/reference/compat/object/get' },
{ text: 'set (兼容性)', link: '/zh_hans/reference/compat/object/set' },
{ text: 'has (兼容性)', link: '/zh_hans/reference/compat/object/has' },
{ text: 'property (兼容性)', link: '/zh_hans/reference/compat/object/property' },
],
},

View File

@ -0,0 +1,60 @@
# has
::: info
이 함수는 호환성을 위한 `es-toolkit/compat` 에서만 가져올 수 있어요. 대체할 수 있는 네이티브 JavaScript API가 있거나, 아직 충분히 최적화되지 않았기 때문이에요.
`es-toolkit/compat`에서 이 함수를 가져오면, [lodash와 완전히 똑같이 동작](../../../compatibility.md)해요.
:::
객체가 주어진 경로에 해당하는 프로퍼티를 가지는지 확인해요.
경로로는 객체의 프로퍼티 이름, 프로퍼티 이름의 배열, 또는 깊은 경로를 나타내는 문자열을 쓸 수 있어요.
만약에 경로가 인덱스를 나타내고, 객체가 배열 또는 `arguments` 객체라면, 그 인덱스가 유효한지 (0 이상이고 길이 미만인지) 확인해요. 그래서 모든 인덱스가 정의되어 있지 않은 희소 배열(Sparse array)에도 쓸 수 있어요.
## 인터페이스
```typescript
function has(object: unknown, path: string | number | symbol | Array<string | number | symbol>): boolean;
```
### 파라미터
- `object` (`unknown`): 프로퍼티가 있는지 확인할 객체.
- `path` (`string`, `number`, `symbol`, `Array<string | number | symbol>`): 프로퍼티가 있는지 확인할 경로. 프로퍼티 이름, 프로퍼티 이름의 배열, 또는 깊은 경로를 나타내는 문자열을 쓸 수 있어요.
### 반환 값
(`boolean`): 객체가 경로에 값을 가지고 있으면 `true`, 아니면 `false`.
## 예시
```typescript
import { has } from 'es-toolkit/compat';
const obj = { a: { b: { c: 3 } } };
has(obj, 'a'); // true
has(obj, ['a', 'b']); // true
has(obj, ['a', 'b', 'c']); // true
has(obj, 'a.b.c'); // true
has(obj, 'a.b.d'); // false
has(obj, ['a', 'b', 'c', 'd']); // false
has([], 0); // false
has([1, 2, 3], 2); // true
has([1, 2, 3], 5); // false
```
## 데모
::: sandpack
```ts index.ts
import { has } from 'es-toolkit/compat';
const obj = { a: { b: { c: 3 } } };
console.log(has(obj, 'a.b.c'));
```
:::

View File

@ -0,0 +1,62 @@
# has
::: info
This function is only available in `es-toolkit/compat` for compatibility reasons. It either has alternative native JavaScript APIs or isnt fully optimized yet.
When imported from `es-toolkit/compat`, it behaves exactly like lodash and provides the same functionalities, as detailed [here](../../../compatibility.md).
:::
Checks if a given path exists within an object.
You can provide the path as a single property key, an array of property keys,
or a string representing a deep path.
If the path is an index and the object is an array or an arguments object,
the function will verify if the index is valid and within the bounds of the array
or arguments object, even if the array or arguments object is sparse
(i.e., not all indexes are defined).
## Signature
function has(object: unknown, path: string | number | symbol | Array<string | number | symbol>): boolean;
### Parameters
- `object` (`unknown`): The object to query.
- `path` (`string` or `number` or `symbol` or `Array<string | number | symbol>`): The path to check. This can be a single property key, an array of property keys, or a string representing a deep path.
### Returns
(`boolean`): Returns `true` if the path exists in the object, `false` otherwise.
## Examples
```typescript
import { has } from 'es-toolkit/compat';
const obj = { a: { b: { c: 3 } } };
has(obj, 'a'); // true
has(obj, ['a', 'b']); // true
has(obj, ['a', 'b', 'c']); // true
has(obj, 'a.b.c'); // true
has(obj, 'a.b.d'); // false
has(obj, ['a', 'b', 'c', 'd']); // false
has([], 0); // false
has([1, 2, 3], 2); // true
has([1, 2, 3], 5); // false
```
## 데모
::: sandpack
```typescript
import { has } from 'es-toolkit/compat';
const obj = { a: { b: { c: 3 } } };
console.log(has(obj, 'a.b.c'));
```
:::

View File

@ -0,0 +1,60 @@
# has
::: info
出于兼容性原因,此函数仅在 `es-toolkit/compat` 中提供。它可能具有替代的原生 JavaScript API或者尚未完全优化。
`es-toolkit/compat` 导入时,它的行为与 lodash 完全一致,并提供相同的功能,详情请见 [这里](../../../compatibility.md)。
:::
检查给定路径在对象中是否存在。
您可以提供路径作为单个属性键、属性键数组,或表示深层路径的字符串。
如果路径是索引,且对象是数组或参数对象,函数将验证索引是否有效,并且在数组或参数对象的范围内,
即使数组或参数对象是稀疏的(即,并非所有索引都被定义)。
## 签名
function has(object: unknown, path: string | number | symbol | Array<string | number | symbol>): boolean;
### 参数
- `object` (`unknown`): 要查询的对象。
- `path` (`string` 或 `number``symbol``Array<string | number | symbol>`): 要检查的路径。这可以是单个属性键、属性键数组或表示深层路径的字符串。
### 返回值
(`boolean`): 如果路径在对象中存在,则返回 `true`,否则返回 `false`
## 示例
```typescript
import { has } from 'es-toolkit/compat';
const obj = { a: { b: { c: 3 } } };
has(obj, 'a'); // true
has(obj, ['a', 'b']); // true
has(obj, ['a', 'b', 'c']); // true
has(obj, 'a.b.c'); // true
has(obj, 'a.b.d'); // false
has(obj, ['a', 'b', 'c', 'd']); // false
has([], 0); // false
has([1, 2, 3], 2); // true
has([1, 2, 3], 5); // false
```
## 演示
::: sandpack
```ts index.ts
import { has } from 'es-toolkit/compat';
const obj = { a: { b: { c: 3 } } };
console.log(has(obj, 'a.b.c'));
```
:::

View File

@ -0,0 +1,22 @@
import { describe, expect, it } from 'vitest';
import { toKey } from './toKey';
describe('toKey', () => {
it('converts strings to strings', () => {
expect(toKey('asd')).toBe('asd');
});
it('converts symbols to symbols', () => {
const symbol = Symbol('a');
expect(toKey(symbol)).toBe(symbol);
});
it("converts 0 to '0'", () => {
expect(toKey(0)).toBe('0');
});
it("converts -0 to '-0'", () => {
expect(toKey(-0)).toBe('-0');
expect(toKey(Object(-0))).toBe('-0');
});
});

View File

@ -0,0 +1,20 @@
import { isSymbol } from '../predicate/isSymbol';
/**
* Converts `value` to a string key if it's not a string or symbol.
*
* @private
* @param {*} value The value to inspect.
* @returns {string|symbol} Returns the key.
*/
export function toKey(value: unknown) {
if (typeof value === 'string' || isSymbol(value)) {
return value;
}
if (Object.is(value?.valueOf(), -0)) {
return '-0';
}
return `${value}`;
}

View File

@ -41,6 +41,7 @@ export { bind } from './function/bind.ts';
export { get } from './object/get.ts';
export { set } from './object/set.ts';
export { has } from './object/has.ts';
export { property } from './object/property.ts';
export { mapKeys } from './object/mapKeys.ts';
export { mapValues } from './object/mapValues.ts';

View File

@ -1,4 +1,5 @@
import { isDeepKey } from '../_internal/isDeepKey.ts';
import { toKey } from '../_internal/toKey.ts';
import { toPath } from '../_internal/toPath.ts';
import type { Get } from './get.types.ts';
@ -308,11 +309,7 @@ export function get(object: any, path: PropertyKey | readonly PropertyKey[], def
let index;
for (index = 0; index < resolvedPath.length && current != null; index++) {
let key = resolvedPath[index];
if (Object.is(key.valueOf(), -0)) {
key = '-0';
}
let key = toKey(resolvedPath[index]);
current = current[key];
}

View File

@ -0,0 +1,206 @@
import { describe, expect, it } from 'vitest';
import { toArgs } from '../_internal/toArgs';
import { has } from './has';
import { stubTrue } from '../_internal/stubTrue';
import { range } from '../../math/range';
import { args } from '../_internal/args';
import { symbol } from '../_internal/symbol';
import { stubFalse } from '../_internal/stubFalse';
describe('has', () => {
it(`should check for own properties`, () => {
const object = { a: 1 };
['a', ['a']].forEach(path => {
expect(has(object, path)).toBe(true);
});
});
it(`should not use the \`hasOwnProperty\` method of \`object\``, () => {
const object = { hasOwnProperty: null, a: 1 };
expect(has(object, 'a')).toBe(true);
});
it(`should support deep paths`, () => {
const object = { a: { b: 2 } };
['a.b', ['a', 'b']].forEach(path => {
expect(has(object, path)).toBe(true);
});
['a.a', ['a', 'a']].forEach(path => {
expect(has(object, path)).toBe(false);
});
});
it(`should coerce \`path\` to a string`, () => {
function fn() {}
fn.toString = () => 'fn';
const object = { null: 1, undefined: 2, fn: 3, '[object Object]': 4 };
const paths: any[] = [null, undefined, fn, {}];
const expected = paths.map(stubTrue);
range(2).forEach(index => {
const actual = paths.map(path => has(object, index ? [path] : path));
expect(actual).toEqual(expected);
});
});
it(`should work with \`arguments\` objects`, () => {
expect(has(args, 1)).toBe(true);
});
it(`should work with a non-string \`path\``, () => {
const array = [1, 2, 3];
[1, [1]].forEach(path => {
expect(has(array, path)).toBe(true);
});
});
it(`should preserve the sign of \`0\``, () => {
const object = { '-0': 'a', 0: 'b' };
const props = [-0, Object(-0), 0, Object(0)];
const expected = props.map(stubTrue);
const actual = props.map(key => has(object, key));
expect(actual).toEqual(expected);
});
it(`should work with a symbol \`path\``, () => {
function Foo() {}
// eslint-disable-next-line
// @ts-ignore
Foo.prototype[symbol] = 1;
const symbol2 = Symbol('b');
Object.defineProperty(Foo.prototype, symbol2, {
configurable: true,
enumerable: false,
writable: true,
value: 2,
});
const object = Foo.prototype;
// eslint-disable-next-line
// @ts-ignore
expect(has(object, symbol)).toBe(true);
expect(has(object, symbol2)).toBe(true);
});
it(`has should check for a key over a path`, () => {
const object = { 'a.b': 1 };
['a.b', ['a.b']].forEach(path => {
expect(has(object, path)).toBe(true);
});
});
it(`should return \`true\` for indexes of sparse values`, () => {
const sparseArgs = toArgs([1]);
const sparseArray = Array(1);
const sparseString = Object('a');
delete sparseArgs[0];
const values = [sparseArgs, sparseArray, sparseString];
const expected = values.map(stubTrue);
const actual = values.map(value => has(value, 0));
expect(actual).toEqual(expected);
});
it(`should return \`true\` for indexes of sparse values with deep paths`, () => {
const sparseArgs = toArgs([1]);
const sparseArray = Array(1);
const sparseString = Object('a');
delete sparseArgs[0];
const values = [sparseArgs, sparseArray, sparseString];
const expected = values.map(() => [true, true]);
const actual = values.map(value => ['a[0]', ['a', '0']].map(path => has({ a: value }, path)));
expect(actual).toEqual(expected);
});
it(`should return false for inherited properties`, () => {
function Foo() {}
Foo.prototype.a = 1;
['a', ['a']].forEach(path => {
// eslint-disable-next-line
// @ts-ignore
expect(has(new Foo(), path)).toBe(false);
});
});
it(`should return false for nested inherited properties`, () => {
function Foo() {}
Foo.prototype.a = { b: 1 };
['a.b', ['a', 'b']].forEach(path => {
// eslint-disable-next-line
// @ts-ignore
expect(has(new Foo(), path)).toBe(false);
});
});
it(`should return \`false\` when \`object\` is nullish`, () => {
const values = [null, undefined];
const expected = values.map(stubFalse);
['constructor', ['constructor']].forEach(path => {
const actual = values.map(value => has(value, path));
expect(actual).toEqual(expected);
});
});
it(`should return \`false\` for deep paths when \`object\` is nullish`, () => {
const values = [null, undefined];
const expected = values.map(stubFalse);
['constructor.prototype.valueOf', ['constructor', 'prototype', 'valueOf']].forEach(path => {
const actual = values.map(value => has(value, path));
expect(actual).toEqual(expected);
});
});
it(`should return \`false\` for nullish values of nested objects`, () => {
const values = [, null, undefined];
const expected = values.map(stubFalse);
['a.b', ['a', 'b']].forEach(path => {
const actual = values.map((value, index) => {
const object = index ? { a: value } : {};
return has(object, path);
});
expect(actual).toEqual(expected);
});
});
it(`should return \`false\` over sparse values of deep paths`, () => {
const sparseArgs = toArgs([1]);
const sparseArray = Array(1);
const sparseString = Object('a');
delete sparseArgs[0];
const values = [sparseArgs, sparseArray, sparseString];
const expected = values.map(() => [false, false]);
const actual = values.map(value => ['a[0].b', ['a', '0', 'b']].map(path => has({ a: value }, path)));
expect(actual).toEqual(expected);
});
});

70
src/compat/object/has.ts Normal file
View File

@ -0,0 +1,70 @@
import { isDeepKey } from '../_internal/isDeepKey.ts';
import { isIndex } from '../_internal/isIndex.ts';
import { toPath } from '../_internal/toPath.ts';
import { isArguments } from '../predicate/isArguments.ts';
/**
* Checks if a given path exists within an object.
*
* You can provide the path as a single property key, an array of property keys,
* or a string representing a deep path.
*
* If the path is an index and the object is an array or an arguments object, the function will verify
* if the index is valid and within the bounds of the array or arguments object, even if the array or
* arguments object is sparse (i.e., not all indexes are defined).
*
* @param {Object} object - The object to query.
* @param {PropertyKey | PropertyKey[]} path - The path to check. This can be a single property key,
* an array of property keys, or a string representing a deep path.
* @returns {boolean} Returns `true` if the path exists in the object, `false` otherwise.
*
* @example
*
* const obj = { a: { b: { c: 3 } } };
*
* has(obj, 'a'); // true
* has(obj, ['a', 'b']); // true
* has(obj, ['a', 'b', 'c']); // true
* has(obj, 'a.b.c'); // true
* has(obj, 'a.b.d'); // false
* has(obj, ['a', 'b', 'c', 'd']); // false
* has([], 0); // false
* has([1, 2, 3], 2); // true
* has([1, 2, 3], 5); // false
*/
export function has(object: unknown, path: PropertyKey | readonly PropertyKey[]): boolean;
export function has(object: any, path: PropertyKey | readonly PropertyKey[]): boolean {
let resolvedPath;
if (Array.isArray(path)) {
resolvedPath = path;
} else if (typeof path === 'string' && isDeepKey(path) && object?.[path] == null) {
resolvedPath = toPath(path);
} else {
resolvedPath = [path];
}
if (resolvedPath.length === 0) {
return false;
}
let current = object;
for (let i = 0; i < resolvedPath.length; i++) {
const key = resolvedPath[i];
// Check if the current key is a direct property of the current object
if (current == null || !Object.prototype.hasOwnProperty.call(current, key)) {
const isSparseIndex = (Array.isArray(current) || isArguments(current)) && isIndex(key) && key < current.length;
if (!isSparseIndex) {
return false;
}
}
current = current[key];
}
return true;
}