fix(isPlainObject): Fix isPlainObject and perf 3x+ (#692)
Some checks are pending
CI / codecov (push) Waiting to run
Release / release (push) Waiting to run

* 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:
Sébastien Vanvelthem 2024-10-09 05:47:27 +02:00 committed by GitHub
parent faa0f2f582
commit 8ba7fbe4e8
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
2 changed files with 130 additions and 28 deletions

View File

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

View File

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