A place to cache linked articles (think custom and personal wayback machine)
Nelze vybrat více než 25 témat Téma musí začínat písmenem nebo číslem, může obsahovat pomlčky („-“) a může být dlouhé až 35 znaků.

index.md 27KB

title: JS Objects: De”construct”ion url: http://davidwalsh.name/javascript-objects-deconstruction hash_url: f8470a704b

JS Objects: TL;DR

JavaScript has been plagued since the beginning with misunderstanding and awkwardness around its “prototypal inheritance” system, mostly due to the fact that “inheritance” isn’t how JS works at all, and trying to do that only leads to gotchas and confusions that we have to pave over with user-land helper libs. Instead, embracing that JS has “behavior delegation” (merely delegation links between objects) fits naturally with how JS syntax works, which creates more sensible code without the need of helpers.

When you set aside distractions like mixins, polymorphism, composition, classes, constructors, and instances, and only focus on the objects that link to each other, you gain a powerful tool in behavior delegation that is easier to write, reason about, explain, and code-maintain. Simpler is better. JS is “objects-only” (OO). Leave the classes to those other languages!

Due Thanks

I’d like to thank the following amazing devs for their generous time in feedback/tech review of this article series: David Bruant, Hugh Wood, Mark Trostler, and Mark McDonnell. I am also honored that David Walsh wanted to publish these articles on his fantastic blog.

In part 1 of this article series (which you should totally go read if you haven’t yet!), I revisited an idea not original to me: JS doesn’t have “inheritance” in the traditional sense, and what it does have is more appropriately labeled “behavior delegation” -- the ability of one object to delegate a method or property access which it cannot handle over to another object which can handle it.

Then, in part 2, I addressed several distractions which I think obfuscate JS’s true object-oriented identity, including “custom types”, “mixins”, “polymorphism” (which we’ll come back to again later), and even the new “class syntax” coming in ES6. I suggested that to understand (and leverage) better the [[Prototype]], we needed to strip away the cruft. Here, I will attempt to do that.

Turtles Objects all the way down up

The key realization, the punchline to this entire article series, is that [[Prototype]] is really only about linking one object to another object, for the purposes of delegating, if the first object cannot handle a property or method access but the second can. In other words, it’s only objects, linked to other objects. That’s really all JS has.

In a sense, JS is the purest essence of a “object-oriented (OO)” language, in that it really is all about objects. In contrast to most other languages, JS is somewhat unique that you can actually create objects directly without the notion of classes or other abstractions. That’s a powerful and brilliant feature!

People often bash JavaScript, but it’s one of the few prog languages that let you directly create objects. Others: Lua, Dylan, Cecil.

— Axel Rauschmayer (@rauschmaApril 9, 2013

JavaScript legitimately is ”object-oriented”, and perhaps we shouldn’t have used that term for the other languages which imply a lot more than just ”objects”. Maybe “class-oriented” would have been more accurate, which would have freed us up to use “object-oriented” for JS. Of course, as I argued in part 1, what everybody means when they use some term, matters, so it’s far too late to redefine or bend the commonly accepted “object-oriented” to my own purposes, much as I’d like to.

I’m mildly tempted, however, to just hijack the abbreviation of “OO” to mean “objects-only” instead of “object-oriented”, but I bet that probably wouldn’t get anywhere, either. So, for our purposes here, let’s just sayJavaScript is “object-based (OB)” to clarify against “object-oriented (OO)”.

Whatever we call it, we normally tap into this object mechanism by following the “OO way”: we create a function which we use as a “constructor”, and we call that function with new so that we can “instantiate” our “class”, which we specify with the constructor function together with its subsequent .prototype additions… but all that is like a magician’s sleight of hand that dazzles you over here to distract you from what’s really going on over there.

What really matters, at the end of the trick, is that two objects end up linked to each other via the[[Prototype]] chain.

Codez Plz

Before we can derive and understand that simpler view of “objects-only” or “object-based”, we need to understand what actually gets created and linked when we build up some “inherited” objects in JavaScript. Not only are we going to see what happens by default, but what doesn’t happen.

Take this code for our main example:

function Foo(who) {

this.me = who;

} Foo.prototype.identify = function() {

return "I am " + this.me;

}; function Bar(who) {

Foo.call(this,who);

} Bar.prototype = Object.create(Foo.prototype); // NOTE: .constructor is borked here, need to fix Bar.prototype.speak = function() {

alert("Hello, " + this.identify() + ".");

}; var b1 = new Bar(“b1”); var b2 = new Bar(“b2”); b1.speak(); // alerts: “Hello, I am b1.” b2.speak(); // alerts: “Hello, I am b2.”

Note: Some people write Bar.prototype = Object.create(Foo.prototype); as Bar.prototype = new Foo();. Both approaches end up with the same linked objects, where Bar.prototype is an object linked via its[[Prototype]] to Foo.prototype. The only real difference is whether or not the Foo function is called during the creation of Bar.prototype. Depending on your circumstances and intent, you may or may not want that to happen, so let’s consider them roughly interchangable but with different purposes.

What we have is an object labeled Foo.prototype with an identify() method, and another object calledBar.prototype with a speak() method. Bar.prototype is a new empty object that is [[Prototype]]-linked to Foo.prototype. Then we have two objects b1 and b2, who each are each respectively linked via their own [[Prototype]] to Bar.prototypeb1 and b2 also have an “owned property” directly on each of them called me, which respectively holds the values “b1” and “b2”.

Let’s take a visual look at the relationships implied by the above code snippet:

Note: All the [[Prototype]] links in the diagram also mention a “.proto” propertyproto is a formerly non-standard property (which exists in most but not all JS environments) to expose the internal [[Prototype]]chain. As of ES6, however, it will be standardized.

I left a whole bunch of detail out of that diagram, intentionally, so it was even remotely digestable. But of course, since JS is all objects, all the linkages and ancestry of each item can be fully traced. We’ll come back to all the omitted parts of this diagram in a little bit.

Note in this diagram that the function constructors all have a .prototype property pointing at an object. As we’ve been suggesting, the object is what we really care about, and in this way of viewing the JS object mechanism, the way we get that object is to look at a constructor function’s .prototype. The function doesn’t really serve any particularly important role.

I know a whole bunch of you just screamed out, “sure it does! it runs constructor code to initialize the new object!” OK, you’re technically right. Foo() has some code in it which is ultimately run against b1 and b2.

But the devil’s always in the details. First, we don’t need a constructor function to run such code. That’s just one way of getting that outcome. And I’m going to suggest it’s a more distracting approach.

Secondly, unlike C++, the base-class/superclass Foo() ”constructor” doesn’t automatically get called when you run the child-class Bar() ”constructor” to make b1 and b2. So, like Java, we have to manually call theFoo() function from Bar(), but unlike Java, we have to do so with a variation of the explicit “mixin” pattern (I’d probably call it “implicit mixin” here) to make it work like we expect. That’s an ugly detail that is very easy to forget or get wrong.

So, where you’d probably argue with me that “constructor” functions are useful being automatically called at the construction of an object, I’d point out that this is true for only the immediate level, not for the entire “chain of inheritance”, which means that automatic behavior is pretty limited/shallow in utility.

Polymorphism redux

Moreover, we see here the first hint of the problems with relative polymorphism in JS: you can’t do it! I can’t tellBar() to automatically and relatively call his ancestor constructor(s), via a relative reference. I have to manually call (aka, “borrow”) the Foo() function (it’s not a constructor here, just a normal function call!) from inside ofBar(), and to make sure the this is bound correctly, I have to do the slightly more awkward .call(this)style of code. Ugh.

What may not be obvious until you go back and look closer at the diagram above is that the Foo() function isnot related in any useful/practical way to the Bar() function. The Foo() function does not even appear in the “inheritance” (aka “delegation”) chain of Bar.prototype object. The fact that there are some lines you can follow on the graph for indirect relationships doesn’t mean that those relationships are what you’d want to rely on in your code.

The problem with polymorphism we’re seeing here is not only for “constructor” functions. Any function at one level of the [[Prototype]] chain that wants to call up to an ancestor with the same name must do so via this manual implicit mixin approach just like we did inside of Bar() above. We have no effective way of making relative references up the chain.

Importantly, this means that not only do we establish the link between Bar and Foo once at “class” definition, but every single polymorphic reference also has to be hardcoded with the direct relationship. This significantly decreases the flexibility and maintainability of your code. As soon as you make a function hard-coded with implicit mixin to an “ancestor”, now your function can’t be “borrowed” as easily by other objects without those possible unintended side effects.

OK, so let’s say you agree with me at this point that polymoprhism in JS is more trouble than it’s worth. Using constructor-based coding to wire JS objects to each other forces you into the problems of polymorphism.

.constructor

Another detail that’s easy to miss is that an object’s .constructor property really doesn’t behave like we’d probably expect. It’s correct at the Foo() level of the graph, but below that, at Bar() and b1 / b2, notice that the implied linkage there shows .constructor references, strangely, still pointing at Foo.

Actually, what this means is that the only time a .constructor property is added to an object is when that object is the default .prototype attached to a declared function, as is the case of Foo(). When objects are created via new Fn() or Object.create(..) calls, those objects don’t get a .constructor added to them.

Let me say that again: an object created by a constructor doesn’t actually get a .constructor property to point to which constructor it was created by. This is an extremely common misconception.

So if you reference b1.constructor for instance, then you’re actually going to delegate up the chain a few links, to Foo.prototype. Of course, Foo.prototype has a .constructor property and it’s pointing at Foo like you’d expect.

What’s that mean? In the above snippet, right after you perform Bar.prototype = Object.create(Foo) (or even if you’d done Bar.prototype = new Foo()), if you plan to rely on the .constructor property (which many do), you need to perform an extra step, right where I put the JS “Note:” comment:

//…
Bar.prototype = Object.create(Foo.prototype);
Bar.prototype.constructor = Bar; // <-- add this line!
//…

Then b1.constructor references will delegate to that Bar.prototype level, and will “correctly” point at Bar()as you’d probably have expected. Ugh…more syntax gotchas that user-land libs always have to “fix” for us.

Furthermore, the fact that Foo.prototype has a .constructor property pointing at Foo is strange, when you think about “constructor” the way most people do. It’s nice that it gives objects created by new Foo() a way to delegate to a .constructor property access and find Foo(), but it’s bizarre where .constructor actually lives.

It implies that Foo() constructed Foo.prototype, but that’s nonsense. Foo() had nothing to do with creating the default Foo.prototypeFoo.prototype defaults to an empty object that was actually constructed by the built-in Object() constructor.

So we have to change how we think of what the .constructor property means. It does not mean “the constructor this object was created by”. It actually means “the constructor which creates any objects that end up getting [[Prototype]] linked to this object.” Subtle but super important difference to get straight.

Point? These confusions only happen/matter if you’re using constructor-style code, so it’s the choice of this style of code that opts you into the problems. You don’t have to live with that pain. There’s a better, simpler way!

The Whole Pie

Now let’s look at everything that’s actually implied by the above snippet of code. Ready for the whole messy thing?

Take a few minutes to just take all that in. Why show you such a complex diagram?

This diagram actually shows you where some of JavaScript’s functionality comes from, where before you might have just never considered how it all worked. For instance, have you wondered how all functions are able to use behavior such as call()apply()bind(), etc? You may have assumed each function has that behavior built-in, but as you can see from this diagram, functions delegate up their [[Prototype]] chain to handle those behaviors.

While the behavior delegation part is sensible and useful, consider all the implied complexity of constructor-style coding as visualized here. It’s pretty tough to trace all the different entities and diagrams and make much sense of it all. A lot of that complexity comes from the function constructors. (here’s the same complete graph but with the implied relationship lines omitted, if that helps to digest)

If you take that diagram, and remove all the functions and any associated arrows (which we’ll see in just a moment), you’re left with “objects-only”, and you’ll have a much more simplified view of the JS objects world.

Simpler: Object -> Object

Perfection is achieved, not when there is nothing more to add, but when there is nothing left to take away. --Antoine de Saint-Exupery

For refresher, the same prototype-style code from above:

function Foo(who) {

this.me = who;&#13;

} Foo.prototype.identify = function() {

return "I am " + this.me;&#13;

}; function Bar(who) {

Foo.call(this,who);&#13;

} Bar.prototype = Object.create(Foo.prototype); // NOTE: .constructor is borked here, need to fix Bar.prototype.speak = function() {

alert("Hello, " + this.identify() + ".");&#13;

}; var b1 = new Bar(“b1”); var b2 = new Bar(“b2”); b1.speak(); // alerts: “Hello, I am b1.” b2.speak(); // alerts: “Hello, I am b2.”

Now, let’s instead consider this alternative snippet of code, which accomplishes exactly the same, but it does so without any of the confusion/distraction of “constructor functions”, new.prototype, etc. It just creates several objects and links them together.

var Foo = {

init: function(who) {&#13;
    this.me = who;&#13;
},&#13;
identify: function() {&#13;
    return "I am " + this.me;&#13;
}&#13;

}; var Bar = Object.create(Foo); Bar.speak = function() {

alert("Hello, " + this.identify() + ".");&#13;

}; var b1 = Object.create(Bar); b1.init(“b1”); var b2 = Object.create(Bar); b2.init(“b2”); b1.speak(); // alerts: “Hello, I am b1.” b2.speak(); // alerts: “Hello, I am b2.”

Let’s try to take some comparison looks between this snippet and the previous one. They both accomplish the same thing, but there’s some important differences in how we get there.

First of all, Bar and Foo are now just objects, they’re not functions or constructors anymore. I left them as capital letters just for the symmetry and because some people feel better with them. They make it clear that the objects being linked are what we cared about all along, so instead of the indirectness of linking Bar.prototype toFoo.prototype, we just make Foo and Bar objects themselves and link themAND, we only need one line of code to link them, instead of the extra ugly polymorphic linkage. Bam!

Instead of calling function constructors like new Bar(..), we use Object.create(..), which is an ES5 helper that allows us to create a new object and optionally provide another object to [[Prototype]] link it to. We get the same outcome (object creation and linkage) as a constructor call, but without needing the constructor. BTW, there’s a simple non-ES5 polyfill for Object.create(..), so you can safely use this style of code in all browsers without concern.

Secondly, notice that because we’re not worried about constructors anymore, we have eliminated any concerns about awkward polymorphisms forcing us to do manual implied mixins to call Foo() from Bar(). Instead, we put the code we wanted to run to initialize our objects into a init() method, on Foo, and we can now callb1.init(..) directly via the delegation chain and it “magically” just works like we want.

So, we have a tradeoff here. We don’t get automatic constructor calls, which means we create the object likevar b1 = Object.create(Bar) and then we have to additionally call b1.init(“b1”). That’s “more code”.

But the benefits we get, which I think are much better and well worth it, are no awkwardness with the linkage between Foo and Bar -- instead we leverage [[Prototype]] delegation to get at the code reuse ininit(). Also, no more verbose/repetitive .prototype references, and neither do we need to use.call(this) nearly as often (especially if we avoid polymorphism!).

Looks are everything

And to visualize the simplicity this approach brings us, here’s the diagram when we remove the functions entirely and focus only on the objects:

I don’t know about you, but I just think that mental model is so much cleaner, and the bonus is that its semantics perfectly match the code.

I have shown you simple enough code using only core JS syntax, that I don’t need any helper libraries to wire up my objects. Of course, I could use one, but why? Simpler is better. KISS.

Any fool can make something complicated. It takes a genius to make it simple. --Woody Guthrie

For the record, I’m not even remotely the genius here. Brendan Eich, creator of our language, was the genius for making something so powerful yet so simple.

Object self-reflection

Last thing to address: how does this simplification affect the process of reflecting on an object? In other words, can we inspect an object and find out about its relationships to other objects?

For prototype-style code, reflection looks like this:

b1 instanceof Bar; // true
b2 instanceof Bar; // true
b1 instanceof Foo; // true
b2 instanceof Foo; // true
Bar.prototype instanceof Foo; // true
Object.getPrototypeOf(b1) === Bar.prototype; // true
Object.getPrototypeOf(b2) === Bar.prototype; // true
Object.getPrototypeOf(Bar.prototype) === Foo.prototype; // true

Notice that you’re using instanceof and having to think in terms of the constructor functions that made your objects, and their .prototypes, rather than just reflecting on the objects themselves. Each of those reflections comes with slightly more mental tax as a result.

And when there’s only objects?

Bar.isPrototypeOf(b1); // true
Bar.isPrototypeOf(b2); // true
Foo.isPrototypeOf(b1); // true
Foo.isPrototypeOf(b2); // true
Foo.isPrototypeOf(Bar); // true
Object.getPrototypeOf(b1) === Bar; // true
Object.getPrototypeOf(b2) === Bar; // true
Object.getPrototypeOf(Bar) === Foo; // true

By contrast, reflection on objects is only about the objects. There’s no awkward references to a constructor’s.prototype property for the checks. You can just inspect if one object is related via [[Prototype]] to another object. Same capabilities as above, but with less mental tax.

Moreover, as I mentioned in part 2, this sort of explicit object reflection is preferable and more robust/reliable than implicit detection through duck typing.

Object.wrapItUpAlready()

Take a deep breath! That was a lot to take in. If you’ve followed all 3 parts of the article series, I hope by now you see the bottom line: JS has objects and when we link them, we get powerful behavior delegation.

There’s just no need to pile on class-orientation on top of such a great system, because it ultimately just leads to the confusion and distraction that has kept JS’ object mechanism shrouded and covered up by all these helper libraries and misunderstandings about JS syntax.

If you stop thinking about inheritance, and instead think with the arrows headed the other way: delegation, your JS code will be simpler. Remember: it’s just objects linked to objects!