getify / You-Dont-Know-JS

A book series on JavaScript. @YDKJS on twitter.
Other
178.13k stars 33.42k forks source link

"this & object prototypes": cover shadowing properties #91

Closed getify closed 10 years ago

getify commented 10 years ago

Cover:

  1. Setting a shadowed property:

    var o = { a: 2 };
    var f = Object.create( o );
    f.a; // 2 -- accessed via delegation from `f` to `o`
    f.a = 5;
    f.a; // 5 -- created a shadowed property on `f`!
    o.a; // 2 -- still intact
  2. Strangely, shadowing cannot be done in the case of a property having been marked as writable:false:

    var o = { };
    Object.defineProperty( o, "a", { value:2, writable:false } );
    var f = Object.create( o );
    f.a; // 2 -- accessed via delegation from `f` to `o`
    f.a = 5;
    f.a; // 2 -- wtf!? didn't let us create a shadowed property on `f`?
    o.a; // 2 -- still intact
  3. Also strangely, a getter/setter prevents shadowing too:

    var o = { };
    Object.defineProperty( o, "a", { set: function(){}, get: function(){ return 2; } } );
    var f = Object.create( o );
    f.a; // 2 -- accessed via delegation from `f` to `o`
    f.a = 5;
    f.a; // 2 -- wtf!? didn't let us create a shadowed property on `f`?
    o.a; // 2 -- still intact

To be clear, in cases 2) and 3) above, the [[Set]] algorithm consults the descriptor of the o.a property and actually uses the o.a to attempt the = 5 assignment, which fails for 2) and has no effect for 3). In neither of those cases does shadowed f.a get created.

But in case 1) above, the descriptor for o.a isn't special, so o.a isn't used at all, and f.a = 5 creates shadowed f.a.

This WTF inconsistency between 1) and 2)/3) here is what's at issue to me.

PatAtCP commented 10 years ago

Object.create is a synonym for f = {}, f.prototype = o; In your first example, o.a is an "simple" (and, more accurately, immutable) object so a copy is passed from the prototype to the newly created object. In your second example, the a property is transformed into a fully realized property object via Object.defineProperty so all the attributes given to that property get passed on as well, including it's read only status.

setting it to writable:true will do what you'd expect it to do.

var o = { };
Object.defineProperty( o, "a", { value:2, writable:true } );
f = Object.create(o);
f.a; // 2 -- via delegation from `o`
f.a = 5;
f.a; // 5 -- holds the new value.
o.a; // 2 -- still intact
getify commented 10 years ago

Object.create is a synonym for f = {}, f.prototype = o;

This is not correct. Object.create() maps to f = {}; f.__proto__ = o; Those are entirely different things.

so a copy is passed from the prototype to the newly created object.

Uhhhh.... ummmm... what are you referring to here? Are you saying that f.a is a copy of o.a at the time of fs object creation? That's not at all an observable fact, and as far as I can tell, goes counter to what the spec says. What is your basis for that conclusion?

var o = { a: 2 };
var f = Object.create( o );

f.hasOwnProperty( "a" ); // false
Object.getOwnPropertyNames( f ); // [ ]

In no way, shape, or form is a copied from o to f at the time of fs creation. So unless I've missed something entirely, I think your assertion is incorrect.

PatAtCP commented 10 years ago

Yes, __proto__ and prototype are different beasts, but essentially similar mechinisms for adding predefined methods and attributes. I wasn't aware of which one Object.create used specifically.

As for the other issue, clearly I over simplified my wording. When you access f.a the JS engine first checks the f property tree, then looks to it's prototype and/or __proto__ and checks there, continuing on down the __proto__ chain if need be. Once o.a ( or f.__proto__.a) is found, it then passes up a copy of the property object (bound to f if need be in the case of functions), or the value of it's getter method if it has one. If you are attempting to overwrite a, the JS engine will walk down the __prorto__ chain to see if it exists first, and check to see if the property is writable first, or has a setter method, before letting you overwrite it.

getify commented 10 years ago

Yes, proto and prototype are different beasts, but essentially similar mechinisms for adding predefined methods and attributes.

Actually, I don't think that's true. __proto__ exists on objects as a public property exposing an internal [[Prototype]] linkage, which the default [[Get]] mechanism will use if it cannot resolve a property reference on the object.

prototype is a public property on functions which happens to be the place a __proto__ of an object will link to if that object was "created" by calling that function with new.

The two mechanisms are distinctly different. One is where an object is linked, one is an object that other objects will link to. It's like the semantic different between source and target. Those two mean orthagonally different things.

But anyway, I wasn't trying to belabor nitpicking. Just wanted to point it out, as in particular conflating "prototype" and "proto" is extremely common, and it's part of what I'm trying to fix with these books.

getify commented 10 years ago

[edit: removed previous comment]

getify commented 10 years ago

There's more nuance here, clearly. @PatAtCP makes a good point that it's not only writable but also set that is checked for. It doesn't change my conclusion that these are weird wtf's, but it provides more detail as to what kind of wtf it is.

PatAtCP commented 10 years ago

if you use another method to create a read only property, it reacts the same way as with Object.defineProperty

o  = {get a() {return 2} }; //o.a is now read only.
f = Object.create(o); //o === f.__proto__
f.a; //2
f.a = 5;
f.a; // 2
o.a; // 2
f.hasOwnProperty('a'); //false
getify commented 10 years ago

@PatAtCP I appreciate you illuminating more of the nuance around [[Set]]. It indeed uses [[Prototype]] more than I was aware. I will make sure to clear it up in the book.

PatAtCP commented 10 years ago

I think the behaviour is fairly consistent. as mentioned, there is a [[Prototype]] check for writability. done before any [[Set]] action. If the property is read-only or contains a setter, then the action is overriden; if not, a local copy of the property is created, so as not to pollute the prototype chain of other objects. If that local copy is removed, the prototype version is still there.

o = { a: 2 }
f = Object.create(o);
f.a = 5; 
f.a; // 5
f.hasOwnProperty('a'); //true;
o.a; // 2
delete f.a //true
f.a; // 2
f.hasOwnProperty('a'); // false;
getify commented 10 years ago

Yes but it's the WHY behind the "there is a [[Prototype]] check for writability. done before any [[Set]] action" that makes it, IMO, confusing, as you in other respects would just be adding a whole different (shadowed) property to a different object.

It makes sense that I can't change a non-writable property directly on an object. It makes very little sense why I can't shadow a non-writable property with a whole new property on a different object.

The only reasoning I can figure for this clearly intentional behavior is that it's an attempt to pretend that delegated property references behave like copied property references, to conflate and pave over the distinct differences between [[Prototype]] and classical classes.

getify commented 10 years ago

from a find by @bterlson with awb's comments, confirms my suspicions about the "reasoning" behind the writable:false part of this WTF:

http://wiki.ecmascript.org/doku.php?id=strawman:fixing_override_mistake

FTA: it goes back to, what I claim in the book is ill-advised and confusing, conflation between [[Prototype]] and "inheritance". The behavior observed here is intentional (that was never in question) and is done specifically to maintain some (I think fictional) invariant that a property that is "inherited" over [[Prototype]] pretends as if it was differentially copied to the descendant, so that the descendant has to behave by the same rules (aka, not-writable) as the ancestor.

It's clear the makers of JS like this and intend this, but I couldn't disagree more, so I intend to explain the counter-point clearly in this title.

facundocabrera commented 10 years ago

How do you prevent property resolutions via "this" if you violate the writable: false? From my perspective the idea was keep the prototype chain under control, so you can be confident that a property in an object is writable only if you allow that from the parent object in the prototype chain.

getify commented 10 years ago

@dendril

if you violate the writable: false

I didn't argue that writable:false should be overridable, per se. What I'm pointing out is that there's a clear inconsistency between the 3 cases shown above. If consistency was a goal (which clearly it is not, but I maintain it should have been), either, f.a = 5 should create a shadowed f.a in all 3 cases, or it should NOT create a shadowed f.a property in any of the three cases.

That the cases are nuanced and differently behaving just sets up the mechanism as more complicated to learn and master. Regardless of the historical "why" for these decisions, I maintain is was the worst of all the options to create inconsistency.


As to the overall point you're making, I don't really understand what you're saying so I can't really respond to it.

facundocabrera commented 10 years ago

Sorry @getify I was thinking in rare situation (I'm trying to create an example, for now just discard the comment).

About the shadowing, I had read the following in the book JavaScript - The definitive guide by Flanagan (page 124 = 2^0 2^1 2^2 causality?):

A attempt to set a property p of an object o fails in these circumstances (I'm just listing 1 of 3):

Seems you do not agree with the definition and I guess it's not a WTF, it's just a predefined behavior.

I have 2 questions (if could answer will be great):

  1. Why do you consider shadowing important in this case? Because writable: false means do not touch my value and eventually a library or a framework could relies on this for something special like a "read-only configuration" or something like that.
  2. Running 2) in strict mode you will get an error advising the read-only situation, and looks robust, but in the example 3) you are defining an accessor property and they do not behave as data properties (define set imply the value will be writable so you wont get a "shadowable" situation because setters are not data properties). Are you sure 3) is a valid scenario?
getify commented 10 years ago

@dendril

and I guess it's not a WTF, it's just a predefined behavior.

I'm declaring the inconsistency between the 3 above cases a WTF. "WTF" here doesn't necessarily mean "unexplained" or "inexplicable", it means "what crack were they smoking?" Not really, but you get the idea. :)

Because writable: false means do not touch my value

Of course it does, but that's not what case 1) is really about!

Think of it this way:

"use strict";

var someObject = { };
Object.defineProperty(someObject,"a",{value:2, writable:false});

// somewhere else, later in the program
var foo = Object.create(someObject);
foo.a = 3;
// error! wtf!?
// I am just wanting to add a property to my OWN object,
// why does the property on `someObject` of same name
// get to prevent me from doing that!?

// somewhere else, even later in the program
var bar = { a: 3 };
bar.__proto__ = someObject;
bar.a = 4;
someObject.a; // 2 -- still, untouched
bar.a; // 4 -- good, let me keep doing what I want to do!

// wtf, THIS works? So I have to create the property before
// I link up the prototype, and it works fine, but if I do those
// tasks in the reverse order, it doesn't work? Bizarre.

If you think about [[Prototype]] as purely about parent-child class inheritance, you can twist yourself into justifying this WTF, but if you embrace [[Prototype]] linkage as just being about linking two arbitrary peer objects together, then why does one object's property get to affect the other object's property!?

Are you sure 3) is a valid scenario?

Of course it is. See above. If I link two arbitrary objects together, and I want a different a on each, for whatever reason. I can do it, but I have to go the inconsistently more manual route of defineProperty(..).

getify commented 10 years ago

@dendril

Why do you consider shadowing important in this case?

I don't particularly like shadowing. I'm not arguing for or against shadowing, though.

What I'm really complaining about is that this case 1) is not like cases 2) and 3), all three of which use the same = assignment syntax. That's surprising. The lack of consistency is gross here. I understand what the historical record says about how we got "here". It's not in question what the facts are. Or even what their reasoning was to get us here. What I'm questioning is why they felt that losing consistency was less of an evil than some of the other tradeoffs they could have chosen.

For example, there could be all shadowing in all 3 cases when you use the = assignment operator. Tradeoffs?

  1. you wouldn't be able to implicitly call a delegated (proto) setter. Is that a bad thing? Maybe. But not that bad. Seems niche to me. I think it would have been a lesser tradeoff than consistency. There's ways you could have let people manually call the setter (syntactic or boilerplate) if we really wanted to preserve that use-case.

By contrast, there could have been no shadowing in these 3 cases with = assignment operator. That is, all the = assignments would always traverse up the __proto__ chain and find the first existing slot and call against that (like cases 2 and 3 do, but also for 1). If it was writable, change it, if not, error. If a proto setter, use it (fixes above tradeoff!). If no existing proto slot was found, would add directly to instance. Tradeoffs?

  1. you'd have to manually use defineProperty(..) if you wanted to shadow, in all 3 cases, which you already have to do in cases 2) and 3) anyway, so that's actually a plus for consistency to make all 3 cases work the same for both shadowing and non-shadowing.

    Pain of switching? Yeah, it'd have "broken the web" because everyone relies on case 1) shadowing. Lots of pain, gnashing of teeth, etc. Eventually, some day, people would get over it. ;-)

  2. you could accidentally overwrite built-ins, like with a.toString = ..., which could overwrite the built-in Object.prototype.toString. That means you could accidentally affect all other objects' default string serializations in a really surprising and bad way. This is kinda a big negative all by itself, as it's really surprising compared to what we currently have.

    One way that could have been addressed ("fixed") would have been to make the built-in's on the Object.prototype (and the other built-in prototypes) as writable:false and configurable:true. They couldn't be overwritten accidentally (would error!), so that reduces the surprise, but if you really DID want to overwrite them, since they are still configurable, you could call defineProperty(Object.prototype,"toString",{value: ... , writable:false, configurable:true}) to change it. More "work"? Sure. But again, more consistent.

    Wouldn't have been that painful to switch over to. And once we switched, we'd have consistency. Which is objectively better.

The point? We didn't have to get where we are. It's not entirely clear if we could ever correct this ship now or not. It'd be as painful then as it would be now. Which is to say, we could, but I don't sense that anyone cares as much (enough!) about consistency as I do to ever rip that band-aid off.

facundocabrera commented 10 years ago

Got it.

I'm planning to finally switch to ES5 and Object.defineProperty() and Object.create(proto [, properties)] will be my next friends. I'm going to blacklist __proto__ and in that way I feel everything will work in a consistent way.

The bad news: AFAIK ES6 defines Object.setPrototypeOf will keep this problem alive. :(

getify commented 10 years ago

Note: apparently a similar topic was being discussed on es-discuss recently. I don't love the conclusions (or lack thereof) as a result, but it's interesting to note they are discussing it.

Hope maybe they would/could take discussion in this thread, specifically my comments here, into account at some point.

/cc @allenwb @erights @jorendorff

tsu-complete commented 8 years ago

I suppose I'm pretty late to this conversation but if you haven't already seen this, I have found a way to shadow getters.

https://gist.github.com/tsu-complete/674363fa45aac492595d

(It's in coffee, if you need js you can paste it into http://coffeescript.org/ under the "Try Coffeescript" button)

kingofwhales commented 7 years ago
var o = { };
Object.defineProperty( o, "a", { set: function(){}, get: function(){ return 2; } } );
var f = Object.create( o );
f.a; // 2 -- accessed via delegation from `f` to `o`
f.a = 5;
f.a; // 2 -- wtf!? didn't let us create a shadowed property on `f`?
o.a; // 2 -- still intact

Is case 3 actually a result of shadowing? The getter is returning a fixed value of 2, in which case we won't know if the 2 is from f or o? I have tried the following case with a different setter and getter. The result is that I am "able" to create a semi-shadowed property on f ?

let o = {}
Object.defineProperty(o,"a",{set:function(value){this._a_ = value},get:function(){return this._a_ * 2}})
let f = Object.create(o)
o.a = 100
o.a   // output 200
f.a = 200
f.a   // output 400
o.a // still output 200

f.hasOwnProperty("a")  // false
o.hasOwnProperty("a") // true

f //  {"_a_": 200}
o // {"_a_": 100}

So does this count as creating a shadowed property? a still doesn't exist as a property for f but it does return different value through the getter.

Thank you so much!

getify commented 7 years ago

The shadowed property you've created is _a_, but a is still not shadowed, there's only one of them (on o). In other words, making it look like shadowing doesn't mean it actually is shadowing.

benawhite commented 6 years ago

What I don't understand is, why is writable enforced on f, but configurable is not.

var o = {};
Object.defineProperty(o, 'a', {value: 2, writable: false, configurable: false});
var f = Object.create(o);
f.a; // 2 -- accessed via delegation from 'f' to 'o'
f.a = 5;
f.a; // 2 -- writable enforced from 'o'
Object.defineProperty(f, 'a', {value: 5, writable: false, configurable: false});
f.a; // 5 -- configurable not enforced from 'o'
getify commented 6 years ago

Probably because they were guarding against the potential accidental case of 'f.a = 5' but if you use 'defineProperty(..)' it's not likely an accident.