Open RReverser opened 5 years ago
@alexcrichton I've also just noticed that current implementation of Object::try_from
is also wrong due to this issue (it assumes that any object inherits from Object
prototype, but it's not true).
I'll admit that I don't think I know enough about JS to know what to do about this issue, but a PR would be most welcome to fix the state of affairs!
I guess the hardest part of this would be to figure out naming in a way that wouldn't be too confusing (in JS it's object
as a value type vs Object
as a class).
I believe the correct check for whether something is an object or not is Object(foo) === foo
(note the lack of new
, which is very important!)
That performs unnecessary boxing of primitives and I'm not sure what you're gaining over current JsValue::is_object
.
My suggestion was only about splitting interfaces, but keeping the checks as they already are.
@RReverser There's some things which are objects but aren't typeof "object"
, such as regexps (in some browsers), and functions.
So at the very least, it would need to be changed to a blacklist (anything that isn't a primitive is assumed to be an object).
But that has its own share of issues, such as if ECMAScript adds in a new primitive (which they did, with Symbol
).
So Object(foo) === foo
is the most reliable way of detecting objects, since it actually uses the ToObject
primitive.
If your concern is about the boxing, it can do a typeof
check first, and only do the Object(foo)
check if it isn't a primitive.
Come to think of it, we may even want to allow for primitives, because doing things like Object.keys("foo")
is perfectly valid. So in that case we should actually only be excluding null
and undefined
.
My suggestion was only about splitting interfaces, but keeping the checks as they already are.
Yes, I understood that, I wasn't replying to your most recent post, but to the concept overall.
The point of this issue is to expand the support for using the Object.foo
methods. So making sure that the "is it an object?" check is correct is important for that (otherwise we'll just end up in the same situation again later of "I want to use Object
methods but it doesn't work")
There's some things which are objects but aren't typeof "object", such as regexps (in some browsers), and functions.
I think the last time this happened was in IE5 (6?) and we already don't support these browsers due to other APIs we rely on. I'd rather target spec-compliant implementations.
Come to think of it, we may even want to allow for primitives, because doing things like Object.keys("foo") is perfectly valid.
This is a good point. But given that JsString
/ Number
/ Boolean
all extend from Object
(that is, automatically Deref
to it), this should still work as it does now?
I think the last time this happened was in IE5 (6?) and we already don't support these browsers due to other APIs we rely on. I'd rather target spec-compliant implementations.
That's a fair point, though my point about functions still stands.
This is a good point. But given that JsString / Number / Boolean all extend from Object (that is, automatically Deref to it), this should still work as it does now?
Yes, but it's rather strange: they claim to inherit from Object
, and you can use Object
methods on them, and you can cast them to Object
with into()
, but you can't use dyn_into()
, and instanceof
returns false
!
That's a fair point, though my point about functions still stands.
I guess functions are similar to other primitives mentioned below in this regard (in that they are not objects, but they inherit from Object
).
but you can't use dyn_into(), and instanceof returns false!
This is true. I'm not sure what would be the best way to go about this yet.
For the "base prototype-less object" type the is_type_of
check could be as simple as checking that the value is not null
/undefined
- then it's guaranteed to either be an object
or a primitive that inherits an Object
and so all the methods that don't care about prototypes like Object::keys
would work as expected.
However, is_type_of
on the actual Object
as a class might be a bit more complicated for the reasons you mentioned above...
but you can't use dyn_into(), and instanceof returns false!
This is true. I'm not sure what would be the best way to go about this yet.
On the other hand, same applies to working with these values in JavaScript itself too, so maybe it's not a big deal?
I guess functions are similar to other primitives mentioned below in this regard (in that they are not objects, but they inherit from Object).
Actually, unlike the primitives, functions are true objects: you can set properties on them, and they retain them:
function foo() {}
foo.x = 5;
var bar = "bar";
bar.x = 5;
console.log(foo.x, bar.x);
On the other hand, same applies to working with these values in JavaScript itself too, so maybe it's not a big deal?
Not quite, since JS doesn't need dyn_into
, since it's dynamically typed. So this is really a Rust-specific problem. (However I agree that type checking is pretty crazy/inconsistent in JS in general)
I'm not sure what would be the best way to go about this yet.
I'm not entirely sure either, but given the existence of the primitives and things like Object.create(null)
, my thinking is that we should treat Object
as being similar to JsString
.
In other words, JsString
doesn't actually care about or use the String
prototype, so Object
shouldn't care about the prototype either.
So Object
would basically just mean "something which quacks like an object", as opposed to being strictly tied to the Object
class.
Currently Object::try_from(val)
checks JsValue::is_object(val)
which checks typeof val == "object" && val !== null
,
while val.dyn_ref::<Object>()
checks <Object as JsCast>::is_type_of(val)
which checks val instanceof Object
.
This causes different behaviors for the following types:
functions | objects that doesn't inherit Object |
|
---|---|---|
dyn_ref::<Object> |
✅ | ❌ |
Object::try_from |
❌ | ✅ |
Examples of objects that doesn't inherit
Object
Object.create(null)
Object.create(Object.create(null))
.
js_sys::Object
has some methods that rely on Object.prototype
, e.g. obj.constructor()
.
I think we should align behaviors of Object::try_from
and <Object as JsCast>::is_type_of
so that they both accepts js functions but rejects js objects that doesn't inherit Object
(which is, every js value that val instanceof Object
is true).
Futhermore, we could:
introduce a structural type TypeOfObjectOrFunction
which covers every type that typeof val === "object" || typeof val === "function"
(including null
);
introduce a structural type NonNullTypeOfObjectOrFunction
which extends = TypeOfObjectOrFunction
but excludes null
make Object
extends = NonNullTypeOfObjectOrFunction
;
make static methods of Object
that doesn't rely on instanceof Object
accept &NonNullTypeOfObjectOrFunction
, for example, Object::has_own
;
Note that Object::create(prototype: &Object) -> Object
is still valid because as long as the prototype inherits Object
, the return object also inherits Object
.
We can add a static method for NonNullTypeOfObjectOrFunction
: fn create(prototype: &TypeOfObjectOrFunction) -> NonNullTypeOfObjectOrFunction
.
implement instance methods of Object
that can be called with Object.prototype.method
as final static methods for NonNullTypeOfObjectOrFunction
.
For example, in addition to Object::is_prototype_of(&self, value: &JsValue)
,
impl NonNullTypeOfObjectOrFunction {
pub fn is_prototype_of(instance: &NonNullTypeOfObjectOrFunction, value: &JsValue) {
call_js_code!(
Object.prototype.isPrototypeOf.call(instance, value)
)
}
}
As for JsString
,
there are two kinds of inconsistency:
JsString
extends Object
but JsString
accepts primitive strings (String()
) while Object
rejects them.
<JsString as JsCast>::is_type_of(val)
is implemented with JsValue::is_string
which checks typeof val === "string"
.
JsString
doesn't accept object strings (new String()
). However when calling instance methods, primitive strings have the same behaviors as object strings.
To make things consistent, we could:
JsString
structural. Document that structural extends = Object
only inherits instance methods and properties but not instanceof
, JsCast::is_type_of
checks and it doesn't correspond to prototype chains.PrimitiveString
. extends = JsString
. Only allows typeof val === "string"
. This type is actually structural because it is not an object.ObjectString
. extends = JsString
extends = Object
. Only allows val instanceof String
. Document that static methods of String
are implemented for JsString
.The above solutions assume wasm_bindgen
allows extends = <structural type>
but I don't know whether it's allowed. The wasm-bindgen book documents extends = Class as in the JS class hierarchy sense. Maybe wasm_bindgen
could introduce structural extends with a new attribute implements = Interface
.
This would be a breaking change, but, I think,
Object.keys
,Object.entries
,Object.setPrototypeOf
etc. need to be changed to somehow support prototype-less objects.In JavaScript it's common to have object values without prototype used as dictionaries created by either
Object.create(null)
or a corresponding literal form{ __proto__: null, ... }
.These don't have
Object
in their prototype chain, so you can't call methods fromObject.prototype
on them (e.g..to_string()
won't work), anddyn_ref
fails as expected.However, you should still be able to call
Object.keys
,Object.values
,Object.seal
and other static methods, because they support such objects as well as any others.So we need to somehow separate these two types to allow arbitrary objects in static methods, and only prototyped objects in methods that accept
&self
/&this
.