Proposal: Symbol.inObject well-known symbol to customize the "in" operator's behavior
Le 9 mai 2019 à 20:52, Tom Barrasso <tom at barrasso.me> a écrit :
Like Symbol.hasInstance but for the "in" operator. This symbol would work for both native and user-defined objects.
Example implementation prototyping native object:
String.prototype[Symbol.inObject] = function(searchString) { return this.includes(searchString) }
Example implementation for user-defined object:
function range(min, max) => ({ [Symbol.inObject]: (prop) => { return (prop >= min && prop <= max) } })
Example usage:
("foo" in "food") // true (14 in range(1, 25)) // true
Those two examples seem to give to the in
operator a meaning that it was not intended to have. The in
operator is specifically meant to check whether a given property exists in a given object.
Also, there already exists a way to customise the behaviour of the in
operator, namely by using a Proxy.
Thanks interesting, I hadn’t realized it was possible to “trap” the in operator using Proxy. I may be wrong, but I don’t think Proxy is capable of operating on the prototype chain. Specifically, I don’t think you can change the behavior of the in operator for all Strings (which I’m sure many would prefer).
If this Symbol were seriously considered I believe it would expand the meaning of the in operator as you’re correct, this is definitely not it’s current intention.
Tom
There's several ways proxies can change the result of in
:
- Directly via
handler.has(target, key)
(returns boolean) - Indirectly via
handler.getOwnPropertyDescriptor(target, key)
(returns existing descriptor orundefined
if missing) - Indirectly via `handler.getPrototypeOf(target)' (returns relevant prototype)
The proxy trap has
is almost a direct substitute for your
Symbol.inObject
in most cases, provided you're okay with keys getting
stringified.
handler.has(target, key)
appears to be the most direct way to accomplish
this, although as mentioned in requires dealing with string parsing.
Here's a quick example I put together using Proxy
to accomplish the
range
function.
function range(min, max) {
return new Proxy(Object.create(null), {
has (target, key) {
return (Number(key) >= min && Number(key) <= max)
}
})
}
However, the same cannot be achieved easily for Strings. I consider using Strings directly, but got a "Uncaught TypeError: Cannot create proxy with a non-object as target or handler." I also considered using template tags as well, but that seemed like more of a novel hack and not advantageous compared to a simple function. This is the best I could come up with.
function q(str) {
const S = class extends String{}
return new Proxy(new S(str), {
has (target, searchString) {
return str.includes(searchString)
}
})
}
A class that extends String is sufficient where a String itself is not,
although this has ramifications elsewhere like being passed by reference,
not value, through functions.
Based on the stringification and inability to work with native object
prototype, I'm not fully convinced that Proxy
is a complete substitute
for Symbol.inObject
.
Tom
"" in q("food") // true
Is the purpose of the code to return a boolean for string and integer input?
What is the significance of in
operator usage to output?
Le 9 mai 2019 à 23:17, Tom Barrasso <tom at barrasso.me> a écrit :
If this Symbol were seriously considered I believe it would expand the meaning of the in operator as you’re correct, this is definitely not it’s current intention.
The in
operator has a well-defined meaning, that by design you can’t ignore even with the fanciest proxy. (And no, there is no hope for introducing a new mechanism that allows to overcome those limitations, since they are by design.)
Consider for example the following proxy around a String object:
function conflateInAndIncludes(str) {
return new Proxy(Object(str), {
has(target, key) { return str.includes(key) }
})
}
var FrankensteinFood = conflateInAndIncludes("food");
"foo" in FrankensteinFood // true, yeah!
"bar" in FrankensteinFood // false, yeah!
"length" in FrankensteinFood // TypeError: proxy can't report a non-configurable own property '"length"' as non-existent
There are obvious limitations given the masking/ overwriting of the default
implementation, but are these limitations not also true for other symbols
like Symbol.hasInstance
? If overwritten (including in a prototype chain),
it then masks the original behavior of the instanceof
operator. Symbols
are one way to do this more explicitly and without the caveats of needing
to use a Proxy everywhere.
Any symbol that allows for reconfiguring/ overloading an operator risks
breaking the assumptions about how that operator is supposed to behave.
Perhaps I’m not understanding some way this is different/ more significant
for overloading the in
operator than say instanceof
or the spread
operator?
Perhaps a more reasonable use case than prototyping String would be a
user-defined, iterable data structure like an unrolled linked list?
Even with Proxy
this is not possible unless we wrapped all constructor
calls.
class UnrolledLinkedList {
constructor(nodeCapacity = 8) {
// ...
}
[Symbol.inObject](index) {
return (index >= 0 && index <= this.length)
}
let list = new UnrolledLinkedList();
list.add(1);
list.add(2);
list.add(3);
if (1 in list) {
// ...
}
A developer may want to mimic the behavior of Array
, but using a Proxy
would require wrapping all constructor calls or calls to the in
operator.
The other alternative is to set numeric properties on the list, but doing
so isn't always possible for every data structure.
Tom
Still not gathering the significance or purpose of using in
to
generate a boolean output. What appears to be the requirement can be
achieved by using Set
or Map
let m = new Map([[0, "a"],[ 1, 1], [2, {}]]);
m.has(0); // true
m.get(2) // {}
This example could be fixed by passing {}
as the proxy target.
Like Symbol.hasInstance but for the "in" operator. This symbol would work for both native and user-defined objects.
Example implementation prototyping native object:
String.prototype[Symbol.inObject] = function(searchString) { return this.includes(searchString) }
Example implementation for user-defined object:
function range(min, max) => ({ [Symbol.inObject]: (prop) => { return (prop >= min && prop <= max) } })
Example usage:
("foo" in "food") // true (14 in range(1, 25)) // true