Understanding the 'in' Operator and Property Enumeration in JavaScript
The in operator in JavaScript offers two primary use cases: standalone checks and iteration with in for-in loops. When used independently, in returns true if a specified property is accessible through an object, regardless of whether that property resides directly on the instance or is inherited from its prototype chain.
Consider the following example:
function Person() {
this.instanceProp = "Instance Value";
}
Person.prototype.inheritedProp = "Prototype Value";
Person.prototype.showProp = function() {
console.log(this.instanceProp || this.inheritedProp);
};
let personInstance1 = new Person();
let personInstance2 = new Person();
// Initially, 'inheritedProp' is only on the prototype
console.log(personInstance1.hasOwnProperty("inheritedProp")); // false
console.log("inheritedProp" in personInstance1); // true
// Adding 'inheritedProp' to the instance
personInstance1.inheritedProp = "Overridden Value";
console.log(personInstance1.inheritedProp); // "Overridden Value" (from instance)
console.log(personInstance1.hasOwnProperty("inheritedProp")); // true
console.log("inheritedProp" in personInstance1); // true
// 'personInstance2' still accesses 'inheritedProp' from the prototype
console.log(personInstance2.inheritedProp); // "Prototype Value"
console.log(personInstance2.hasOwnProperty("inheritedProp")); // false
console.log("inheritedProp" in personInstance2); // true
// Removing the instance property allows access to the prototype property again
delete personInstance1.inheritedProp;
console.log(personInstance1.inheritedProp); // "Prototype Value" (from prototype)
console.log(personInstance1.hasOwnProperty("inheritedProp")); // false
console.log("inheritedProp" in personInstance1); // true
In the scenario above, the inheritedProp is always accessible via personInstance1, either directly or through inheritance, hence "inheritedProp" in personInstance1 consistently returns true. To specifically identify properties located on the prototype chain, you can combine hasOwnProperty() with the in operator.
A helper function can determine if a property exists solely on the prototype:
function hasPrototypeProperty(object, propertyName) {
return !object.hasOwnProperty(propertyName) && (propertyName in object);
}
The in operator returns true for any accessible property, while hasOwnProperty() returns true only for properties defined directly on the instance. Therefore, if in evaluates to true and hasOwnProperty() returns false, the property is confirmed to be an inherited one.
// Using the Person constructor and its prototype from the previous example
let person = new Person();
console.log(hasPrototypeProperty(person, "inheritedProp")); // true (initially)
person.inheritedProp = "Greg"; // Overriding the prototype property
console.log(hasPrototypeProperty(person, "inheritedProp")); // false (now it's an instance property)
Initially, "inheritedProp" exists only on the prototype, so hasPrototypeProperty returns true. After overriding it on the instance, the instance property takes precedence, causing hasPrototypeProperty to return false, even though the prototype still holds the property.
When used in a for-in loop, the in operator enumerates all property accessible through the object that are also enumerable, including both instance and prototype properties. Instance properties that shadow non-enumerable prototype properties will also be included in the for-in loop, as developer-defined properties are typically enumerable by default.
To retrieve only the enumerable own properties of an object, the Object.keys() method is useful. It accepts an object and returns an array of strings representing the names of its enumerable own properties.
// Using the Person constructor and its prototype
let prototypeProperties = Object.keys(Person.prototype);
console.log(prototypeProperties); // ["inheritedProp", "showProp"] (order might vary)
let p1 = new Person();
p1.name = "Rob"; // Add an instance property
p1.age = 31; // Add another instance property
let p1InstanceKeys = Object.keys(p1);
console.log(p1InstanceKeys); // ["name", "age"] (order might vary)
In this example, prototypeProperties contains an array of the enumerable properties of Person.prototype. When called on an instance p1, Object.keys(p1) returns only the enumerable properties directly defined on p1.
For a comprehensive list of all own properties, regardless of enumerability, use Object.getOwnPropertyNames():
// Continuing with Person.prototype
let allPrototypeProps = Object.getOwnPropertyNames(Person.prototype);
console.log(allPrototypeProps); // ["constructor", "inheritedProp", "showProp"] (constructor is non-enumerable)
Note that the result includes the non-enumerable constructor property. Both Object.keys() and Object.getOwnPropertyNames() serve as effective alternatives to for-in loops in specific scenarios.
With the introduction of Symbol types in ECMAScript 6, a companion method to Object.getOwnPropertyNames() was needed for properties keyed by Symbols, as these lack a traditional name. This led to the creation of Object.getOwnPropertySymbols().
let sym1 = Symbol('key1');
let sym2 = Symbol('key2');
let objWithSymbols = {
[sym1]: 'Symbol Value 1',
[sym2]: 'Symbol Value 2',
regularProp: 'Regular Value'
};
console.log(Object.getOwnPropertySymbols(objWithSymbols));
// Expected output: [Symbol(key1), Symbol(key2)]
Property Enumeration Order
There are significant differences in property enumeration order among for-in, Object.keys(), Object.getOwnPropertyNames(), Object.getOwnPropertySymbols(), and Object.assign(). The enumeration order for for-in and Object.keys() is generally not guaranteed and can vary across JavaScript engines and browsers.
Conversely, Object.getOwnPropertyNames(), Object.getOwnPropertySymbols(), and Object.assign() adhere to a deterministic order: numeric keys are enumerated in ascending order first, followed by string keys and Symbol keys in the order they were inserted. For object literals, keys are inserted in the order they appear, separated by commas.
let symA = Symbol('symA');
let symB = Symbol('symB');
let obj = {
10: 'ten',
'beta': 'b',
[symA]: 'symbol A',
'alpha': 'a',
0: 'zero',
[symB]: 'symbol B',
2: 'two',
'gamma': 'g'
};
obj[1] = 'one'; // Inserted based on insertion order
console.log(Object.getOwnPropertyNames(obj));
// Expected output: ["0", "1", "2", "10", "alpha", "beta", "gamma"]
console.log(Object.getOwnPropertySymbols(obj));
// Expected output: [Symbol(symA), Symbol(symB)] (order depends on insertion)