mirror of
https://github.com/toss/es-toolkit.git
synced 2024-11-24 11:45:26 +03:00
fix(isPlainObject): Fix isPlainObject and perf 3x+ (#692)
* test(isPlainObject): add test for symbol and anonymous classNames * test(isPlainObject): add test for runInNewContext * fix: exclude iterators, support runInNewContext and anonymous classes * refactor: use !value insteadof value === undefined (size) * test: Make tests more explicit * test: Remove checks for Symbol.toStringTag or Symbol.iterator --------- Co-authored-by: Sojin Park <raon0211@gmail.com>
This commit is contained in:
parent
faa0f2f582
commit
8ba7fbe4e8
@ -1,18 +1,102 @@
|
||||
import { describe, expect, it } from 'vitest';
|
||||
import { runInNewContext } from 'node:vm';
|
||||
import { isPlainObject } from './isPlainObject';
|
||||
|
||||
describe('isPlainObject', () => {
|
||||
it('returns true for plain objects', () => {
|
||||
it('should return true for plain objects', () => {
|
||||
const str = 'key';
|
||||
|
||||
expect(isPlainObject({})).toBe(true);
|
||||
expect(isPlainObject(Object.create(null))).toBe(true);
|
||||
expect(isPlainObject(new Object())).toBe(true);
|
||||
expect(isPlainObject(new Object({ key: 'new_object' }))).toBe(true);
|
||||
expect(isPlainObject(new Object({ key: new Date() }))).toBe(true);
|
||||
expect(isPlainObject({ 1: 'integer_key' })).toBe(true);
|
||||
expect(isPlainObject({ name: 'string_key' })).toBe(true);
|
||||
expect(isPlainObject({ [str]: 'dynamic_string_key' })).toBe(true);
|
||||
expect(isPlainObject({ [Symbol('tag')]: 'value' })).toBe(true);
|
||||
expect(
|
||||
isPlainObject({
|
||||
children: [{ key: 'deep-children' }],
|
||||
name: 'deep-plain',
|
||||
})
|
||||
).toBe(true);
|
||||
expect(
|
||||
isPlainObject({
|
||||
children: [{ key: new Date() }],
|
||||
name: 'deep-with-regular-object',
|
||||
})
|
||||
).toBe(true);
|
||||
expect(isPlainObject({ constructor: { name: 'Object2' } })).toBe(true);
|
||||
expect(isPlainObject(JSON.parse('{}'))).toBe(true);
|
||||
expect(isPlainObject(new Proxy({}, {}))).toBe(true);
|
||||
expect(isPlainObject(new Proxy({ key: 'proxied_key' }, {}))).toBe(true);
|
||||
expect(isPlainObject(JSON)).toBe(true);
|
||||
expect(isPlainObject(Math)).toBe(true);
|
||||
expect(isPlainObject(Atomics)).toBe(true);
|
||||
expect(
|
||||
isPlainObject({
|
||||
[Symbol.iterator]: function* () {
|
||||
yield 1;
|
||||
},
|
||||
})
|
||||
).toBe(true);
|
||||
expect(isPlainObject({ [Symbol.toStringTag]: 'string-tagged' })).toBe(true);
|
||||
});
|
||||
|
||||
it('returns false for non-plain objects', () => {
|
||||
it('should return false for invalid plain objects', () => {
|
||||
function fnWithProto(x: number) {
|
||||
// @ts-expect-error for the sake of testing
|
||||
this.x = x;
|
||||
}
|
||||
|
||||
function ObjectConstructor() {}
|
||||
|
||||
ObjectConstructor.prototype.constructor = Object;
|
||||
|
||||
expect(isPlainObject('hello')).toBe(false);
|
||||
expect(isPlainObject(false)).toBe(false);
|
||||
expect(isPlainObject(undefined)).toBe(false);
|
||||
expect(isPlainObject(null)).toBe(false);
|
||||
expect(isPlainObject(10)).toBe(false);
|
||||
expect(isPlainObject([])).toBe(false);
|
||||
expect(isPlainObject(Number.NaN)).toBe(false);
|
||||
expect(isPlainObject(() => 'cool')).toBe(false);
|
||||
expect(isPlainObject(new (class Cls {})())).toBe(false);
|
||||
expect(isPlainObject(new Intl.Locale('en'))).toBe(false);
|
||||
expect(isPlainObject(new (class extends Object {})())).toBe(false);
|
||||
expect(isPlainObject(fnWithProto)).toBe(false);
|
||||
expect(isPlainObject(Symbol('cool'))).toBe(false);
|
||||
expect(isPlainObject(globalThis)).toBe(false);
|
||||
expect(isPlainObject(new Date())).toBe(false);
|
||||
expect(isPlainObject(new Map())).toBe(false);
|
||||
expect(isPlainObject(Buffer.from('123123'))).toBe(false);
|
||||
expect(isPlainObject(new Error())).toBe(false);
|
||||
expect(isPlainObject(new Set())).toBe(false);
|
||||
expect(isPlainObject(new Request('http://localhost'))).toBe(false);
|
||||
expect(isPlainObject(new Promise(() => {}))).toBe(false);
|
||||
expect(isPlainObject(Promise.resolve({}))).toBe(false);
|
||||
expect(isPlainObject(Buffer.from('ABC'))).toBe(false);
|
||||
expect(isPlainObject(new Uint8Array([1, 2, 3]))).toBe(false);
|
||||
expect(isPlainObject(Object.create({}))).toBe(false);
|
||||
expect(isPlainObject(/(\d+)/)).toBe(false);
|
||||
expect(isPlainObject(new RegExp('/d+/'))).toBe(false);
|
||||
expect(isPlainObject(/d+/)).toBe(false);
|
||||
expect(isPlainObject(`cool`)).toBe(false);
|
||||
expect(isPlainObject(String.raw`rawtemplate`)).toBe(false);
|
||||
// eslint-disable-next-line
|
||||
// @ts-ignore
|
||||
expect(isPlainObject(new ObjectConstructor())).toBe(false);
|
||||
expect(
|
||||
isPlainObject(
|
||||
new Proxy(new Date(), {
|
||||
get(target) {
|
||||
return target;
|
||||
},
|
||||
})
|
||||
)
|
||||
).toBe(false);
|
||||
});
|
||||
|
||||
it('should return true for cross-realm plain objects', async () => {
|
||||
expect(isPlainObject(runInNewContext('({})'))).toBe(true);
|
||||
});
|
||||
});
|
||||
|
@ -5,34 +5,52 @@
|
||||
* @returns {value is Record<PropertyKey, any>} - True if the value is a plain object, otherwise false.
|
||||
*
|
||||
* @example
|
||||
* console.log(isPlainObject({})); // true
|
||||
* console.log(isPlainObject([])); // false
|
||||
* console.log(isPlainObject(null)); // false
|
||||
* console.log(isPlainObject(Object.create(null))); // true
|
||||
* console.log(Buffer.from('hello, world')); // false
|
||||
* ```typescript
|
||||
* // ✅👇 True
|
||||
*
|
||||
* isPlainObject({ }); // ✅
|
||||
* isPlainObject({ key: 'value' }); // ✅
|
||||
* isPlainObject({ key: new Date() }); // ✅
|
||||
* isPlainObject(new Object()); // ✅
|
||||
* isPlainObject(Object.create(null)); // ✅
|
||||
* isPlainObject({ nested: { key: true} }); // ✅
|
||||
* isPlainObject(new Proxy({}, {})); // ✅
|
||||
* isPlainObject({ [Symbol('tag')]: 'A' }); // ✅
|
||||
*
|
||||
* // ✅👇 (cross-realms, node context, workers, ...)
|
||||
* const runInNewContext = await import('node:vm').then(
|
||||
* (mod) => mod.runInNewContext
|
||||
* );
|
||||
* isPlainObject(runInNewContext('({})')); // ✅
|
||||
*
|
||||
* // ❌👇 False
|
||||
*
|
||||
* class Test { };
|
||||
* isPlainObject(new Test()) // ❌
|
||||
* isPlainObject(10); // ❌
|
||||
* isPlainObject(null); // ❌
|
||||
* isPlainObject('hello'); // ❌
|
||||
* isPlainObject([]); // ❌
|
||||
* isPlainObject(new Date()); // ❌
|
||||
* isPlainObject(new Uint8Array([1])); // ❌
|
||||
* isPlainObject(Buffer.from('ABC')); // ❌
|
||||
* isPlainObject(Promise.resolve({})); // ❌
|
||||
* isPlainObject(Object.create({})); // ❌
|
||||
* isPlainObject(new (class Cls {})); // ❌
|
||||
* isPlainObject(globalThis); // ❌,
|
||||
* ```
|
||||
*/
|
||||
export function isPlainObject(value: unknown): value is Record<PropertyKey, any> {
|
||||
if (typeof value !== 'object') {
|
||||
if (!value || typeof value !== 'object') {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (value == null) {
|
||||
return false;
|
||||
}
|
||||
const proto = Object.getPrototypeOf(value) as typeof Object.prototype | null;
|
||||
|
||||
if (Object.getPrototypeOf(value) === null) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (value.toString() !== '[object Object]') {
|
||||
return false;
|
||||
}
|
||||
|
||||
let proto = value;
|
||||
|
||||
while (Object.getPrototypeOf(proto) !== null) {
|
||||
proto = Object.getPrototypeOf(proto);
|
||||
}
|
||||
|
||||
return Object.getPrototypeOf(value) === proto;
|
||||
return (
|
||||
proto === null ||
|
||||
proto === Object.prototype ||
|
||||
// Required to support node:vm.runInNewContext({})
|
||||
Object.getPrototypeOf(proto) === null
|
||||
);
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user