5
\$\begingroup\$

The more I consider object oriented JavaScript the more I am confused. There are so many different ways and concepts and I simply do not know any longer what fits best for my purposes.

I like the constructor pattern but I have the feeling that I've mixed different concepts:

Subclass.prototype = Object.create(Superclass.prototype);

function Subclass(name, value) {
    Superclass.call(this);

    this._name = name;
    this._value = value;

    this._init();
}

Object.defineProperties(Subclass.prototype, {
    'name': {
        get: function() { return this._name; }
    },
    'value': {
        get: function() { return this._value; },
        set: function(value) { this._value = value; }
    }
})

Subclass.prototype._init = function() {
    // some initialization
};

Subclass.prototype.compute = function() {
    var name = this.getName();
    var value = this.getName();
    // do something with name and value...
};
  • Is there a cleaner way?
  • Should one use getters and setters for "internal" access (like I would do it in other languages)?

Edit #1

Define properties of prototype (Object.defineProperties(Subclass, {} to Object.defineProperties(Subclass.prototype, {})

\$\endgroup\$

1 Answer 1

4
\$\begingroup\$

These are the general rules I follow:

  1. If using actual private variables in my class, create get/set properties for those I want accessible to the outside world:

    function Point(x, y) {
        var _x = x || 0,
            _y = y || 0,
            _foo = "bar";
    
        Object.defineProperty(this, "x", {
            enumerable: true,
            get: function() {
                return _x;
            },
            set: function(value) {
                _x = value;
            }
        });
    
        Object.defineProperty(this, "y", {
            enumerable: true,
            get: function() {
                return _y;
            },
            set: function(value) {
                _y = value;
            }
        });
    }
    
    var p = new Point(2, 10);
    p.x; // is 2
    p.y; // is 10;
    p.foo; // is undefined
    p._foo; // is undefined
    
  2. If private variables aren't all that beneficial, or the property is meant to be public, I'll just declare that as part of the prototype:

    function Point(x, y) {
        this.x = x || 0;
        this.y = y || 0;
    }
    
    Point.prototype = {
        x: 0,
        y: 0,
    
        constructor: Point
    };
    
  3. Or if I want something to be read only, I'll use a combo of private variables and public getters (Example: Viewport on GitHub):

    function Foo(bar) {
        var _bar = bar || "baz";
    
        Object.defineProperty("bar", {
            enumerable: true,
            get: function() { return _bar; }
        });
    }
    
    var foo = new Foo("foo");
    foo.bar = "abc";
    alert(foo.bar); // alerts "foo"
    

I tend to gravitate towards making everything public, and part of the prototype. As a personal convention properties and methods prefixed with an underscore are not meant to be called publically. Some people force encapsulation by using private variables, but this always feels like I'm trying to fit a square peg into a round hole. Unless you jump through hoops, everything in JavaScript is public. Embrace it. It makes unit testing your JavaScript code easier as well.

As an added benefit, most browsers have optimized the Constructor Function + Prototype code by generated classes behind the scenes for your JavaScript "classes", making property access in JavaScript almost as efficient as native code. Browsers haven't done much to optimize Constructor Functions that use private variables --- which is not to say that kind of code won't ever be optimized. It just isn't optimized very much right now.

Edit #1:

From OP's comment below:

The point I've got at is that adding a property to each object might result in "larger" objects. If the property is defined in the object's prototype (same for all) it is only defined once.

This is not necessarily the case. Consider this code:

Using Object.defineProperty

function Point(x, y) {
    this._x = x;
    this._y = y;
}

Object.defineProperty(Point.prototype, "x", {
    get: function() {
        return this._x;
    },
    set: function(value) {
        this._x = value;
    }
});

Object.defineProperty(Point.prototype, "y", {
    get: function() {
        return this._y;
    },
    set: function(value) {
        this._y = value;
    }
});

var p = new Point(3, 0);

There is absolutely no difference between p.x = 5; and p._x = 5;. The x and y properties created using Object.defineProperty are superfluous, and only serve to add weight to your JavaScript code. There is no logic behind the getting and setting of those values.

On the contrary, consider a case where you do want some special logic around setting a value. In the code below, the element property is a reference to a DOM node. When setting the DOM node we also want to set properties for the document and window to which the DOM node belongs:

function Foo(element) {
    this.element = element;
}

Foo.prototype._element = null;
Foo.prototype.document = null;
Foo.prototype.window = null;

Object.defineProperty(Foo.prototype, "element", {
    get: function() {
        return this._element;
    },
    set: function(value) {
        this._element = value;
        this.document = value.ownerDocument;
        this.window = this.document.defaultView;
    }
};

Now we perform special logic in the set function for element. Defining the element property via Object.defineProperty is now beneficial.

You might find Inheritance and the prototype chain a worthwhile read, as well as Introduction to Object-Oriented JavaScript.

Edit #2:

Defining a property using Object.defineProperty is useful for creating delegate properties. I'm going to build on the Foo class from my previous edit.

The element belongs to a document and a window. In the previous example, there is nothing preventing you from running this code:

var foo = new Foo(document.body);
foo.document = someOtherDocumentObjectFromAnIFRAME;

This breaks the relationship between element and document because we assume foo.document is the document object that contains foo.element. Instead, we want to create delegate properties for document and window so we can maintain that relationship:

function Foo(element) {
    this.element = element;
}

Foo.prototype.element = null;

Object.defineProperty(Foo.prototype, "document", {
    get: function() {
        return this.element.ownerDocument;
    }
});

Object.defineProperty(Foo.prototype, "window", {
    get: function() {
        return this.element.ownerDocument.defaultView;
    }
});

Notice in this variation that the element property is now defined as part of the prototype since it has no special logic around getting or setting its value.

Now the document and window properties are read-only, maintaining the relationship that document is the Document object that contains element, and window is the Window object that contains both document and element.

\$\endgroup\$
6
  • \$\begingroup\$ Thanks! One more thing I've just came upon: defining methods as well as getters and setters using Object.defineProperty, does this add the properties to each single object or to the prototype (like I did on Subclass.prototype.compute)? \$\endgroup\$
    – user43005
    Commented May 23, 2014 at 13:13
  • 1
    \$\begingroup\$ Your calls to Object.defineProperty(Subclass, ...) are creating properties on the Function object, essentially making them class properties. There is no benefit to doing Object.defineProperty(Subclass or Object.defineProperty(Subclass.prototype because the resulting values are always going to be public, unless you want to prevent a property or method from being enumerable. \$\endgroup\$ Commented May 23, 2014 at 13:23
  • \$\begingroup\$ The point I've got at is that adding a property to each object might result in "larger" objects. If the property is defined in the object's prototype (same for all) it is only defined once. I'm not sure whether this thoughts make sense. \$\endgroup\$
    – user43005
    Commented May 23, 2014 at 14:14
  • \$\begingroup\$ See "Edit #1" in response to your last comment, @user43005. \$\endgroup\$ Commented May 23, 2014 at 15:27
  • \$\begingroup\$ Added an example of creating Delegate Properties in "Edit #2" \$\endgroup\$ Commented May 23, 2014 at 15:36

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.