I find that when I write javascript I encounter a situation where I am forced to write ugly code. This is due to my inability to reconcile the following two criteria:
1) Define data using shorthand, e.g. var data = { a: 1, b: { c: 2, d: 3 ... } }
2) Use a primitive operator to check for property existence
For example consider a function which returns the intersection of two Object
instances' keys as an Array
of the intersecting keys:
var intersection = function(obj1, obj2) {
var result = [];
for (var k in obj1) if (k in obj2) result.push(k);
return result;
};
That code looks quite nice. But unfortunately, it doesn't always work as expected! This is due to the inconsistency between for (x in y)
, and if (x in y)
: Using for (x in y)
will only iterate over "own" properties (properties which return true for hasOwnProperty
), while if (x in y)
while apply for "own" and "non-own" properties!
If I call intersection
like so:
var obj1 = { toString: 'hahaha' };
var obj2 = {};
var intersectingKeys = intersection(obj1, obj2);
I will wind up with intersectingKeys === [ 'toString' ];
Obviously this is not correct: an intersection
operation involving an empty set (as obj2
appears to be) must return an empty set. While {}
is clearly intended to be "empty", our problem is that ('toString' in {}) === true
. This also applies to terms such as 'constructor'
, 'valueOf'
, as well as any new properties that are introduced to Object.prototype
in the future.
In my opinion, if a native operator can provide iteration over keys, a native operator should be able to verify whether a key will appear in an iteration. It feels inconsistent and ugly, to me, to use a native operator for one, but a function call for the other. For that reason I dislike this fix:
var safeIntersection = function(obj1, obj2) {
var result = [];
for (var k in obj1) if (obj2.hasOwnProperty(k)) result.push(k);
return result;
};
If if (x in y)
must be used, I only see one other possible solution: Ensure that the parameters passed to intersection
have no properties at all, apart from properties explicitly defined by our code. In other words, ensure that we only work with prototype-less objects:
var obj1 = Object.create(null, {
toString: {
configurable: true,
enumerable: true,
writable: true,
value: 'hahaha'
}
});
var obj2 = Object.create(null, {});
var intersectingKeys = intersection(obj1, obj2);
Note that this code uses intersection
, not safeIntersection
, and still works because obj1
and obj2
are prototype-less. But the problem is, now data definition is really, really clunky! Look at how much code it takes to define an object with a single "toString" property. This approach prevents us from using javascript's beautiful object-shorthand. Even if we write a utility function to encompass prototype-less object creation, the definition of nested objects is still incredibly clunky:
// Utility function for prototype-less object definition
var obj = function(props) {
return Object.create(null, props.map(function(v) {
return {
writable: true,
configurable: true,
enumerable: true,
value: v
};
}));
};
// Now defining `obj1` looks ok...
var obj1 = obj({ toString: 'hahaha' });
// But for large, nested object definitions it's sooper ugly:
var big = obj({
a: 'a value',
b: 'b value',
moreProps: obj({
wheee: 'yay',
evenMoreProps: obj({
prop: 'propMeUp'
/* ... */
})
/* ... */
})
});
Javascript's object-definition shorthand is a huge perk of the language, and throwing it away by being forced to wrap all { ... }
instances in a function call seems like a tremendous pity.
My ideal solution to this problem would involve converting the shorthand object constructor to produce prototype-less objects. Perhaps a global setting:
// Perhaps along with other global settings such as:
'use strict';
Error.stackTraceLimit = Infinity;
// We could also have:
Object.shorthandIncludesPrototype = false;
Although even if this solutions were available it would break tons and tons of pre-existing libraries. :(
How do I reconcile the following criteria???:
1) Write code that works
2) Use the primitive in
operator to check for property existence
3) Define objects using typical shorthand
Perhaps it's impossible to meet all these criteria simultaneously. In that case, what are some of the next-best approaches for keeping code clean in these cases?
As jfriend00 already wrote, the question proposes constraints that are fundamentally impossible in the current versions of JavaScript. The best we can do is write some abstractions within the limitations of the language.
We've already looked at some possibilities, such as using a wrapper function (like obj()
in the question). Because "universal support" is not a criterion in the question, I'll propose one solution using ES6 Proxies. We can use the has()
handler method of a Proxy
to alter the behavior of the in
operator so that it only considers own properties:
Object.prototype.own = function () {
return new Proxy(this, {
has: function (target, propertyName) {
return target.hasOwnProperty(propertyName);
}
});
};
document.writeln('toString' in {});
document.writeln('toString' in {}.own());
document.writeln('foo' in { foo: 1 });
document.writeln('foo' in { foo: 1 }.own());
This requires a modern browser (no IE; Edge OK) or Node 6.4.0+. Of course, adding members to the prototype of Object
is a risky move. We can add additional handlers to the Proxy
as needed, or rewrite the implementation for ES5 and below (but this requires significantly more code).
Compatibility concerns aside, this approach satisfies the requirements of the question: we can use object literals and the in
operator, and the code is concise and readable. We just need to remember to call own()
when we need it.
We can rewrite the intersection()
method from the question using this convention:
Object.prototype.own = function () {
return new Proxy(this, {
has: function (target, propertyName) {
return target.hasOwnProperty(propertyName);
}
});
};
var intersection = function(obj1, obj2) {
var result = [];
for (var k in obj1.own()) if (k in obj2.own()) result.push(k);
return result;
};
document.writeln(intersection({ a: 1, b: 2, c: 3 }, { b: 2, c: 3, d: 4}));