It seems like much has been written in the last years advocating the use of Object.create over constructor functions and new. This write up by the almighty Douglas Crockford seemed to lay the foundations for this "movement". Well, I pretty much agree with Crockford in that Object.create is much more in line with the prototypical nature of JavaScript. You create an object and then decide to use it as the prototype for another object, and so on, Object.create seems a much better fit than "new constructorFuntion", a construction imported from class based languages.
However, even when I love the prototypical nature of JavaScript, and all the freedom and plasticity that it provides (modifying the prototype of a function or the [[Prototype]] of another object, creating inheritance relations "on the fly")... I'm afraid I'm too used to the class based paradigm, and in many occasions I need that approach: classes as templates for creating objects, as a way to identify what an instance is (myInstance.constructor...).
Indeed, I think JavaScript gives us the best of both worlds, we can have certain structure by emulating classes, but we have total freedom to escape those restrictions and use the prototype or [[Prototype]] to augment a "class" or instance, use traits and so on... with this in mind, Object.create for me is a useful addition, but not a replacement.
I thought it was just me that was missing something about the Object.create advantages, so finding 2 articles [1 and 2] by 2 rather respectful guys, that very closely align with my thoughts, made me feel some relief :-) The main idea in both articles is that Object.create can help us to improve the class based (constructor based) inheritance in JavaScript. Furthermore, Ben Nadel article does a very interesting exploration on how to create a "classical design" (Person-Child with instances of each one) just using Object.create, and draws the conclusion that it doesn't seem to fit well.
So, the idea from both articles is that we would continue to write constructor functions and add methods to the constructorFunction.prototype object, but would create the inheritance chain with:
Child.prototype = Object.create(Person.prototype);
Child.prototype = new Person();
This way we avoid invoking the Person "constructor function" when setting the prototype chain, I guess in most cases this is not a big problem, but anyway it's better if we can avoid it.
There's something that both articles are missing and that I consider important, setting the constructor property of the prototype objects in the Prototype chain, that is, in old school inheritance we do:
Child.prototype = new Person();
Child.prototype.constructor = Child;
with the new approach we'll do:
Child.prototype = Object.create(Person.prototype, { constructor: {value: Child} });
This way we can use myInstance.constructor to obtain from the prototype chain the function that was used to create this instance (think of C#'s Object.GetType()). We can argue that this is very static approach of little use in such a dynamic language where irrespective of how an object was created we can freely augment it ending up with something that has little to do with the original "template" used for its creation. Yes, I rather agree, for JavaScript we should mainly think in terms of Duck typing, but well, that's a matter of choice (or style), and depending on that having the correct .constructor property can be important.
So, in the end this is how the code would look:
// Shape "class" function Shape() { this.x = 0; this.y = 0; console.log('Shape constructor called'); } Shape.prototype = { constructor: Shape, move: function(x, y) { this.x += x; this.y += y; console.log("moving"); }, }; console.log("Shape.prototype.constructor.name: " + Shape.prototype.constructor.name); var s1 = new Shape(); console.log("s1.constructor: " + s1.constructor.name); // Rectangle "class" function Rectangle() { console.log('Rectangle constructor called'); Shape.call(this); } //very important, remember we have to use the new "properties syntax" Rectangle.prototype = Object.create(Shape.prototype, { constructor: {value: Rectangle}, rectangleOperation: {value: function(){ console.log("rectangleOperation"); }} });
There's one interesting point mentioned in the daily.js article when they talk about Object.create as the only way to obtain an object without a prototype chain (that is, [[Prototype]] points to null). We could think of achieving the same by setting the constructorFunction.prototype to null, but contrary to what we could expect, that does not give us a null [[Prototype]] when creating an instance, but makes it point to Object.prototype. I guess it's one of those corner cases explained somewhere in the ECMAScript reference
function Shape() { } //this is pretty interesting, even if we define the Shape prototype as null, an instance object created with it ends up getting a [[prototype]], the Object.[[prototype]] Shape.prototype = null; var s1 = new Shape(); if (!Object.getPrototypeOf(s1)) console.log("s1.__proto__ is null"); //prints nothing if (Object.getPrototypeOf(s1) === Object.prototype) console.log("s1.__proto__ === Object.prototype"); //this is true
Finally, another interesting use for Object.create that comes to my mind, is for mapping "data objects" to "full objects". I mean, a normal object has data and behaviour, but when we sent it over the wire (let's say an Ajax call asking for JSON data) we just receive the data, so we need to map it back to a full object with also includes behaviour (we mainly need to set the [[Prototype]])
I think this example will make it clear (note the use of a fieldsToProperties function, as Object.create expects an object with properties, not just with data fields)
//interesting use of Object.create to map a "data object" returned from the server to a full object function fieldsToProperties(obj){ var res = {}; Object.keys(obj).forEach(function(key){ res[key] = {value: obj[key]}; }); return res; } //Employee "class" function Employee(nm){ this.name = nm; }; Employee.prototype = { constructor: Employee, generatePayroll: function(){ console.log("generating payroll for " + this.name); } }; //let's say I receive this Employee object via an Ajax call, so I have only data and want to add the Employee functionality to it var e1 = { name: "xuan", age: 30, city: "Uvieu" }; e1 = Object.create(Employee.prototype, fieldsToProperties(e1)); e1.generatePayroll();
No comments:
Post a Comment