Value-In-Array and Value-In-Object Checks
"Is X in this array?" and "is X anywhere in this object?" sound like the same question but call for different tools. Arrays have `includes` (which handles `NaN`), `indexOf` (which does not), and `Set.has` for hot paths. Objects need `Object.values` for a flat scan and a recursive walk for nested structures. This snippet covers both, with the gotchas that bite: `NaN`, hot-path scans, and shared object identity.
606 views
13
// includes(): SameValueZero comparison, treats NaN as equal to NaN.
console.log([1, 2, 3].includes(2)); // true
console.log([1, NaN, 3].includes(NaN)); // true (this is the modern win)
console.log(['a', 'b'].includes('A')); // false (case-sensitive)
// indexOf(): strict equality (===), so NaN is never found.
console.log([1, NaN, 3].indexOf(NaN)); // -1
console.log([1, 2, 3].indexOf(2)); // 1
console.log([1, 2, 3].indexOf(99)); // -1
// Set.has(): O(1) average lookup. Build the Set once, query many times.
const banned = new Set(['admin', 'root', 'system']);
console.log(banned.has('admin')); // true
console.log(banned.has('user')); // false
// Object identity: arrays compare by reference, NOT by content.
const items = [{ id: 1 }, { id: 2 }];
console.log(items.includes({ id: 1 })); // false (different object)
console.log(items.some((x) => x.id === 1)); // true (predicate compares by id)includes is the right default for primitives: it handles NaN correctly and reads as a yes/no question. indexOf !== -1 is the older idiom and fails the NaN case because it uses strict equality. For a hot loop or membership check inside a render path, build a Set once with new Set(values) and call .has(x) per lookup; that turns repeated linear scans into average O(1). The biggest gotcha is OBJECT membership: [{id:1}].includes({id:1}) is false because the two object literals are distinct references. For "find by field value", reach for .some(predicate) or .find(predicate) instead.
// Flat scan: Object.values + includes.
const user = { id: 1, name: 'Ada', role: 'admin' };
console.log(Object.values(user).includes('admin')); // true
console.log(Object.values(user).includes('Ada')); // true
console.log(Object.values(user).includes('guest')); // false
// Recursive scan for nested objects.
const hasValueDeep = (obj, target) => {
if (obj === target) return true;
if (obj === null || typeof obj !== 'object') return false;
for (const v of Object.values(obj)) {
if (hasValueDeep(v, target)) return true;
}
return false;
};
const doc = {
title: 'Hello',
meta: { author: { name: 'Ada', email: '[email protected]' } },
tags: ['draft', 'review']
};
console.log(hasValueDeep(doc, 'Ada')); // true (nested in meta.author.name)
console.log(hasValueDeep(doc, 'review')); // true (inside tags array)
console.log(hasValueDeep(doc, 'missing')); // false
// Beware: hasValueDeep does not handle cyclic references. Pass a Set of
// visited nodes if your data graph might contain cycles.For a flat object, Object.values(obj).includes(x) is the one-liner answer. For nested data (object inside object inside array), the recursive walker above checks the current node, recurses into every value (which works for arrays since Object.values(['a', 'b']) returns the items), and short-circuits on the first match. The walker uses strict equality at the leaf, so it finds primitives and the same-reference object but not a structurally equivalent copy; for that use js-object-deep-equal instead. Add a seen Set of visited nodes if your input might have cycles, otherwise the recursion will overflow.
