Object.create

原文链接:http://dailyjs.com/post/js101-object-create

The inheritance examples we looked at last week used the form Rectangle.prototype = new Shape(). The reason I like this example is it shows how powerful prototypes are, despite their simplicity. The downside is the constructor for the parent object is executed, which isn't always what's desired.

ECMAScript 5 introduced Object.create, which creates new objects based on a prototype object and an additional set of properties.

The main differences between B.prototype = new A(); and B.prototype = Object.create(A.prototype) are as follows:

The constructor, A isn't called, so B remains uninitialised until instantiated Object.create accepts a second argument that causes Object.create to behave as if Object.defineProperties was called

Using Object.create

Last week's Shape example could be rewritten to use Object.create:

function Shape() {
  this.x = 0;
  this.y = 0;
  console.log('Shape constructor called');
}

Shape.prototype = {
  move: function(x, y) {
    this.x += x;
    this.y += y;
  }
};

// Rectangle
function Rectangle() {
  console.log('Rectangle constructor called');
  this.x = 0;
  this.y = 0;
}

Rectangle.prototype = Object.create(Shape);

Now rectangles can be created with var rect = new Rectangle() and the original Shape constructor won't be called. This leaves us with a cleaner prototype chain, but what if we still want to call the previous constructor? In this particular example, calling the Shape constructor is desirable because we'll avoid duplicating some initialisation code.

Calling Constructors

By using the Function.prototype.call or apply methods, it's entirely possible to call another constructor even when using Object.create. For example:

function Shape() {
  this.x = 0;
  this.y = 0;
  console.log('Shape constructor called');
}

Shape.prototype = {
  move: function(x, y) {
    this.x += x;
    this.y += y;
  }
};

// Rectangle
function Rectangle() {
  console.log('Rectangle constructor called');
  Shape.call(this);
}

Rectangle.prototype = Object.create(Shape.prototype);

The fact call and apply take a this parameter (known as ThisBinding in the ECMAScript specification) allows us to reuse constructors where required.

No Inheritance: Object.create(null)

By passing null to Object.create, objects can be created that don't inherit from anything. By default Object.prototype is used, which has several built-in methods. What if we don't want to inherit from Object.prototype?

function Shape() {
}

Shape.prototype = Object.create(null);

var shape = new Shape();
console.log(shape.toString);

In this example, undefined will be printed -- objects created using the Shape constructor inherit from null.

Notice that this is not equivalent:

function Shape() {
}

Shape.prototype = null;

var shape = new Shape();
console.log(shape.toString);

This should print something like [Function: toString] rather than undefined.

It's interesting to think about exactly why this is useful. In An Object is not a Hash, Guillermo Rauch discusses how the properties of Object.prototype can be used to potentially cause security issues, and Object.create(null) was suggested as a suitable means for creating a "clean" object to avoid the problem.

Another point is performance. These Object.create(null) benchmarks demonstrate iterating over various objects, and the Object.create(null) tests run faster than object literals.

However, be very careful when using this approach because so many libraries expect objects to have the standard methods.

The Second Argument to Object.create

According to the Annotated ECMAScript 5 Object.create documentation, passing a second argument behaves as if Object.defineProperties had been called. This method requires a bit of knowledge before it can be used -- the properties have to be passed in the expected format.

In this example, Rectangle inherits from Shape and gets a property called animate at the same time:

Rectangle.prototype = Object.create(Rectangle.prototype, {
  animate: {
    value: function() {
      this.animating = true;
    }
  }
});

var rect = new Rectangle();

Now rect.animate() can be called, just like any other method. Notice that the second argument is in the form { propertyName: { value: function() {} } } -- the value property is important and I haven't arbitrarily picked it. These properties are known as property attributes.

Property attributes can be "named data" and "named attribute" properties. These additional flags can be applied to named data properties:

  • writable: Determines if the property is writable
  • enumerable: Should this property be included in for-in enumeration?
  • configurable: If false, attempts to delete or change the property's attributes will fail

Although this is new to ECMAScript 5, it adds a much desired level of control to properties and their definition.

Getters and Setters

Property attributes allow JavaScript to support getters and setters with a lightweight syntax:

function Rectangle() {
  this._animating = false;
}

Rectangle.prototype = Object.create(Shape.prototype, {
  animating: {
    get: function() {
      console.log('Rectangle.prototype.animating get');
      return this._animating;
    },

    set: function(value) {
      console.log('Rectangle.prototype.animating set');
      this._animating = value;
    }
  }
});

var rect = new Rectangle();

rect.animating = true;
console.log(rect.animating);

In this example I've renamed animating to _animating, but it can still be accessed using rect.animating because I've defined an animating property with a get and set method.

This makes it possible to track whenever this value is changed, as illustrated by the console.log calls.

In JavaScript implementations that don't include Object.create, this second argument may not be supported. The ECMAScript 5 compatibility table by Kangax has a wide range of compatibility tests that can help you decide if it's safe to use it.

Fork me on GitHub