Вопрос

I often see this pattern to define javascript objects

function Person(name) {
    this.name = name;
}
Person.prototype.describe = function () {
    return "Person called "+this.name;
};

And in this article it says that adding properties directly to the prototype objct is considered an anti-pattern.

Coming from "classical class based" languages, having to define the properties apart from methods doesn't sound quite right, moreoever in javascript, where a method should be just a property with a function value (am I right here?)

I wanted to know if anybody can explain this, or even suggest a better way to handle these situations

Это было полезно?

Решение

In usual object-oriented languages, you have a definition of the class describing members, methods and the constructor.

In JS, the definition of the "class" (it is not really class like in other languages... sometimes the term pseudoclass is used) is the constructor itself. If your object is parametrised by name, it makes sense to write

function Person(name) {
    this.name = name;
}

i.e. the property name must be set in the constructor.

Of course, you can write

function Person(name) {
    this.name = name;
    this.describe = function() { ... };
}

and it will work as you expect.

However, in this case you are creating a separate instance of the method with every call of the constructor.

On the other hand, here:

Person.prototype.describe = function () {
    return "Person called "+this.name;
};

you only define the method once. All instances of Person will receive a pointer (called __proto__ and not accessible by programmer in most browsers) to Person.prototype. So if you call

var myPerson = new Person();
myPerson.describe();

it will work, because JS looks object members directly in the object, then in its prototype etc. all the way up to Object.prototype.

The point is that in the second case, only one instance of the function will exist. Which you will probably agree that is a better design. And even if you don't, it simply takes less memory.

Другие советы

There's nothing wrong with that code. This is supposedly what is meant:

function Person(name) {
    this.name = name;
}
Person.prototype.age = 15; //<= adding a hardcoded property to the prototype

Now you will see this:

var pete = new Person('Pete'), mary = new Person('Mary');
pete.age; //=> 15
mary.age  //=> 15

And most of the time, that's not what you want. Properties assigned to the prototype of a constructor are shared between all instances, properties assigned within the constructor (this.name) are specific for the instance.

As arxanas says, the article mentions data properties.

The reason, I assume, is that data is typically specific to an instance, so it does not make sense to add it to the prototype.

Furthermore, if your data is of a mutable type, e.g. an array, and you assign it to the prototype, then this array instance is shared between all instances and you cannot use it as if every instance had its own array.


Example: The following leads to incorrect behaviour:

function Set() {

}

// shared between instances
// each instance adds values to **the same** array
Set.prototype.elements = [];

Set.prototype.add = function(x) {
   this.elements.push(x);
};

It should be:

function Set() {
    // each instance gets its own array
    this.elements = [];
}

Set.prototype.add = function(x) {
   this.elements.push(x);
};

To sum it up:

  • Add properties that should be shared between all instances to the prototype.
  • Assign instance specific data inside the constructor function.

Just like arxanas wrote in his comment. Data properties in prototype are more or less similar to class-level variables in traditional oop. And these are not used on a daily-basis, unless you have a very specific need. That's all.

Declaring properties on a prototype is not an anti pattern at all. When I look at a prototype object, I think "this is what the prototypical object of this type has for data and methods."

Others have warned against giving properties a reference value in the prototype, for instance: Foo.prototype.bar = []; --- because Arrays and Objects are reference types. A reference type is immutable, therefore every instance of a "class" refers to the same Array or Object. Just set them to null in the prototype, then give them a value in the constructor.

I always include all properties in the prototype for one very clear reason: Communicating to other programmers what properties are publically available and what their default values are without requiring them to sift through a constructor to figure it out.

This becomes especially useful if you are creating a shared library that requires documentation.

Consider this example:

/**
 * class Point
 * 
 * A simple X-Y coordinate class
 *
 * new Point(x, y)
 * - x (Number): X coordinate
 * - y (Number): Y coordinate
 *
 * Creates a new Point object
 **/
function Point(x, y) {
    /**
     * Point#x -> Number
     *
     * The X or horizontal coordinate
     **/
    this.x = x;

    /**
     * Point#y -> Number
     *
     * The Y or vertical coordinate
     **/
    this.y = y;
}

Point.prototype = {
    constructor: Point,

    /**
     * Point#isAbove(other) -> bool
     * - other (Point): The point to compare this to
     *
     * Checks to see if this point is above another
     **/
    isAbove: function(other) {
        return this.y > other.y;
    }
};

(Documentation format: PDoc)

Just reading the documentation is a little awkward here because the information about the x and y properties are embedded inside the constructor function. Contrast that with the "anti-pattern" of including these properties in the prototype:

/**
 * class Point
 * 
 * A simple X-Y coordinate class
 *
 * new Point(x, y)
 * - x (Number): X coordinate
 * - y (Number): Y coordinate
 *
 * Creates a new Point object
 **/
function Point(x, y) {
    this.x = x;
    this.y = y;
}

Point.prototype = {

    /**
     * Point#x -> Number
     *
     * The X or horizontal coordinate
     **/
    x: 0,

    /**
     * Point#y -> Number
     *
     * The Y or vertical coordinate
     **/
    y: 0,

    constructor: Point,

    /**
     * Point#isAbove(other) -> bool
     * - other (Point): The point to compare this to
     *
     * Checks to see if this point is above another
     **/
    isAbove: function(other) {
        return this.y > other.y;
    }

};

Now looking at the prototype gives you a snapshot of the actual object, which is much easier to visualize in your head, and easier for the author to write the documentation. The constructor function is also not cluttered up with documentation and sticks to the business of bringing a Point object to life.

The prototype has everything, and is the canonical source of information about what the "prototypical" Point object has for both methods and data.

I would argue that not including data properties in the prototype is the anti pattern.

Perhaps related: In general modifying an object that you don't own is considered an anti pattern.

Meaning, if you didn't create the object then you don't "own" that object. Including:

  • Native objects (Object, Array, etc)
  • DOM objects
  • Browser Object Model (BOM) objects (such as window)
  • Library objects

Source Maintainable Javascript by Nicholas C. Zakas

Лицензировано под: CC-BY-SA с атрибуция
Не связан с StackOverflow
scroll top