JavaScript inheritance by example

This blog post illustrates several JavaScript inheritance topics via an example: We start with naive implementations of a constructor Point and its sub-constructor ColorPointand then improve them, step by step.

Objects

JavaScript is one of the few object-oriented languages that lets you directly create objects. In most other languages, you need a class to do so. Let us create an object for a point:
    var point = {
        x: 5,
        y: 2,
        dist: function () {
            return Math.sqrt((this.x*this.x)+(this.y*this.y));
        },
        toString: function () {
            return "("+this.x+", "+this.y+")";
        }
    };
The syntactic construct with the curly braces that creates the point is called an  object initializer  or an  object literal . The object  point  has four  properties  (slots for data):  x y , dist , and  toString . You can read the values of properties by writing the name of an object, followed by a dot, followed by the name of the property:
    > point.x
    5
Properties whose value are functions are called  methods . Methods can be called by putting parentheses with arguments behind the name of a property:
    > point.dist()
    5.385164807134504

    > point.toString()
    '(5, 2)'

Constructors

If you don’t want to create just a single point, but several ones, then you need a factory for objects. In class-based languages such factories are called  classes , in JavaScript they are called  constructors . Every function  foo  can be invoked in two ways:
  • as a function: foo(arg1, arg2)
  • as a constructor: new foo(arg, arg2)
The following function is a constructor for points:
    function Point(x, y) {
        this.x = x;
        this.y = y;
        this.dist = function () {
            return Math.sqrt((this.x*this.x)+(this.y*this.y));
        };
        this.toString = function () {
            return "("+this.x+", "+this.y+")";
        };
    }
When we execute  new Point() , the constructor’s job is to set up the fresh object passed to it via the implicit parameter  this . You can see that where we previously used an object initializer to define properties, we now add them via assignments to  this . The fresh object is (implicitly) returned by the constructor and considered its instance:
    > var p = new Point(3, 1);
    > p instanceof Point
    true
Like before, you can invoke methods and access non-method properties:
    > p.toString()
    '(3, 1)'

    > p.x
    3
Methods shouldn’t be in each instance, they should be shared between instances, to save memory. You can use a  prototype  for that purpose. The object stored in Point.prototype  becomes the prototype of the instances of  Point . The relationship between an object (the “prototypee”) and its prototype works as follows: The prototypees inherit all of the prototype’s properties. In general, there is a single prototype and several prototypees, so all prototypees share the prototype’s properties. Consequently,  Point.prototype  is where you put the methods:
    function Point(x, y) {
        this.x = x;
        this.y = y;
    }
    Point.prototype = {
        dist: function () {
            return Math.sqrt((this.x*this.x)+(this.y*this.y));
        },
        toString: function () {
            return "("+this.x+", "+this.y+")";
        }
    }
We assign an object to  Point.prototype , via an object initializer with two properties  dist and  toString . Now there is a clear separation of responsibility: The constructor is responsible for setting up instance-specific data, the prototype contains shared data (i.e., the methods). Note that prototypes are highly optimized in JavaScript engines, so there is usually no performance penalty for putting methods there. Methods are called just like before, you don’t notice whether they are stored in the instance or in the prototype. One problem remains: For every function  f  the following equation should hold [1] :
    f.prototype.constructor === f
Every function is set up like that by default. But we have replaced the default value of Point.prototype . To satisfy the equation, we can either add a property  constructor  to the object literal above or we can keep the default value, by not replacing it, by adding the methods to it:
    function Point(x, y) {
        this.x = x;
        this.y = y;
    }
    Point.prototype.dist = function () {
        return Math.sqrt((this.x*this.x)+(this.y*this.y));
    };
    Point.prototype.toString = function () {
        return "("+this.x+", "+this.y+")";
    };
The constructor property is not that important; it mainly allows you to detect what constructor created a given instance:
    > var p = new Point(2, 2)
    > p.constructor
    [Function: Point]
    > p.constructor.name
    'Point'

Extending

In JavaScript, the term  extending an object  means destructively adding new properties to it: To extend an object A with an object B, we (shallowly) copy B’s properties to A. JavaScript’s slightly uncommon definition of that term is due to the Prototype framework, which has a method  Object.extend() . The following is a naive implementation:
    function extend(target, source) {
        // Don’t do this:
        for (var propName in source) {
            target[propName] = source[propName];
        }
        return target;
    }
The problem with this code is that for-in iterates over all properties of an object, including those inherited from a prototype. This can be seen here:
    > extend({}, new Point())
    { x: undefined,
      y: undefined,
      dist: [Function],
      toString: [Function] }
We do want the “own” (direct) properties  x  and  y  of the  Point  instance. But we don’t want its inherited properties  dist  and  toString . Why are the inherited properties copied to the first argument? Because for-in sees all properties of an object, including inherited ones.  Point  inherits several properties from  Object , for example  valueOf :
    > var p = new Point(7, 1);
    > p.valueOf
    [Function: valueOf]
These properties are not copied over, because for-in can only see  enumerable properties  [2]  and they are not enumerable:
    > p.propertyIsEnumerable("valueOf")
    false
    > p.propertyIsEnumerable("dist")
    true
To fix  extend() , we must ensure that only own properties of  source  are considered.
    function extend(target, source) {
        for (var propName in source) {
            // Is propName an own property of source?
            if (source.hasOwnProperty(propName)) { // (1)
                target[propName] = source[propName];
            }
        }
        return target;
    }
There is one more problem: The above code fails if  source  has an own property whose name is “hasOwnProperty”  [3] :
    > extend({}, { hasOwnProperty: 123 })
    TypeError: Property 'hasOwnProperty' is not a function
The failure is due to  source.hasOwnProperty  (line 1) accessing the own property (a number) instead of the inherited method. We can solve this problem by referring to that method directly and not via  source :
    function extend(target, source) {
        var hasOwnProperty = Object.prototype.hasOwnProperty;
        for (var propName in source) {
            // Invoke hasOwnProperty() with this = source
            if (hasOwnProperty.call(source, propName)) {
                target[propName] = source[propName];
            }
        }
        return target;
    }
On ECMAScript 5 engines (or older engines where a shim  [4]  has been loaded), the following version of  extend()  is better, because it preserves property  attributes  such as enumerability:
    function extend(target, source) {
        Object.getOwnPropertyNames(source)
        .forEach(function(propName) {
            Object.defineProperty(target, propName,
                Object.getOwnPropertyDescriptor(source, propName));
        });
        return target;
    }

Setting the prototype of an object

So far, we have seen how to add properties to an object in a destructive manner. We have also seen that prototypes do the same thing, but non-destructively: Its properties “show up” in an instance, but are not among its own properties. It would be nice if we could influence this kind of inheritance more directly and set the prototype of an object without a constructor. Given that an object’s prototype is such a fundamental, heavily optimized feature, the only standard way of doing so is by creating a new object. That is, you can only set an object’s prototype once, when you create it. The following code uses ECMAScript 5’s  Object.create()  to create a new object whose prototype is the object proto .
    var proto = { bla: true };
    
    var obj = Object.create(proto);
    obj.foo = 123;
    obj.bar = "abc";
obj  has both inherited and own properties:
    > obj.bla
    true
    > obj.foo
    123
The ECMAScript 5 shim uses code similar to the following to make  Object.create available on older browsers.
    if (Object.create === undefined) {
        Object.create = function (proto) {
            function Tmp() {}
            Tmp.prototype = proto;
            // New empty object whose prototype is proto
            return new Tmp();
        };
    }
The above code uses a temporary constructor to create a single instance that has the given prototype. So far, we have ignored the optional second parameter of Object.create()  that allows you to define properties on the newly created object:
    var obj = Object.create(proto, {
        foo: { value: 123 },
        bar: { value: "abc" }
    });
The properties are defined via  property descriptors . With a descriptor, you can specify property attributes such as enumerability, not just values. As an exercise, let us implement  protoChain() , a simplified version of  Object.create() . It avoids the complexities of property descriptors and simply extends the new object with the second parameter. For example:
    var obj = protoChain(proto, {
        foo: 123,
        bar: "abc"
    });
We can generalize the above idea to an arbitrary amount of parameters:
    protoChain(obj_0, obj_1, ..., obj_n-1, obj_n)
Remember that we have to create fresh objects in order to assign prototypes. Hence, protoChain()  returns a shallow copy of  obj_n  whose prototype is a shallow copy of obj_n-1 , etc.  obj_0  is the only object in the returned chain that has not been duplicated. protoChain()  can be implemented like this:
    function protoChain() {
        if (arguments.length === 0) return null;
        var prev = arguments[0];
        for(var i=1; i < arguments.length; i++) {
            // Create duplicate of arguments[i] with prototype prev
            prev = Object.create(prev);
            extend(prev, arguments[i]);
        }
        return prev;
    }

Subtyping

The idea of subtyping is to create a new constructor that is based on an existing one. The new constructor is called the sub-constructor, the existing one the super-constructor. The following is a sub-constructor of  Point :
    function ColorPoint(x, y, color) {
        Point.call(this, x, y);
        this.color = color;
    }
The above code sets up the instance properties  x y  and  color . It does so by passing this  (an instance of  ColorPoint ) to  Point Point  is called as a function, but the  call() method allows us to keep the  this  of  ColorPoint . Therefore,  Point()  adds  x  and  y  for us and we add  color  ourselves. We still need to take care of methods: On one hand, we want to inherit  Point ’s methods, on the other hand, we want to define our own methods. This is a simple way of doing so via  extend() :
    // function ColorPoint: see above
    extend(ColorPoint.prototype, Point.prototype);
    ColorPoint.prototype.toString = function () {
        return this.color+" "+Point.prototype.toString.call(this);
    };
We first copy the methods in  Point.prototype  to  ColorPoint.prototype  and then add our own method: We replace  Point ’s  toString()  with a version whose result combines the color with the output of  Point.prototype.toString() . We directly refer to the latter method and call it with  ColorPoint ’s  this . For more information on invoking methods of a super-prototype consult  [5] ColorPoint  works as expected:
    > var cp = new ColorPoint(5, 3, "red");
    > cp.toString()
    'red (5, 3)'
As an improvement, we can avoid adding redundant properties to  ColorPoint.prototype , by making  Point.prototype  its prototype.
    // function ColorPoint: see above
    ColorPoint.prototype = Object.create(Point.prototype);
    ColorPoint.prototype.constructor = ColorPoint;
    ColorPoint.prototype.toString = function () {
        return this.color+" "+Point.prototype.toString.call(this);
    };
In line 1, we have replaced the default value of  ColorPoint.prototype  and thus need to set the  constructor  property in line 2. While writing a single constructor is fairly straightforward, the above code is too complicated to be performed by hand. A helper function  inherits()  would make it simpler:
    // function ColorPoint: see above
    ColorPoint.prototype.toString = function () {
        return this.color+" "+Point.prototype.toString.call(this);
    };
    inherits(ColorPoint, Point);
The function  inherits()  is modeled after Node.js’s  util.inherits() . It gives you subtyping while keeping the simplicity of normal constructors. Requirements:
  • It shouldn’t matter whether we call inherits() before or after we are adding methods to the prototype.
  • inherits() should ensure that the constructor property is set correctly.
This is an implementation:
    function inherits(SubC, SuperC) {
        var subProto = Object.create(SuperC.prototype);
        // At the very least, we keep the "constructor" property
        // At most, we keep additions that have already been made
        extend(subProto, SubC.prototype);
        SubC.prototype = subProto;
    };
Referring to super-properties
There is one more thing that we can improve:  ColorPoint.prototype.toString()  makes the following call.
    Point.prototype.toString.call(this)
That is not ideal, because we have hard-coded the super-constructor. Instead, we’d rather use:
    ColorPoint._super.toString.call(this)
To make the above code possible,  inherits()  only has to make the following assignment:
    SubC._super = SuperC.prototype;
Here we diverge from Node.js, where  SubC.super_  refers to  SuperC . The  ColorPoint constructor still contains a hard-coded reference to  Point . It can be eliminated by replacing  Point.call(...)  with
    ColorPoint._super.constructor.call(this, x, y);
Not exactly pretty, but it gets the job done. Our final version of  ColorPoint  looks like this:
    function ColorPoint(x, y, color) {
        ColorPoint._super.constructor.call(this, x, y);
        this.color = color;
    }
    ColorPoint.prototype.toString = function () {
        return this.color+" "+ColorPoint._super.toString.call(this);
    };
    inherits(ColorPoint, Point);

Conclusion, what to read next

You can download the source code of this post as project  inheritance-by-example  on GitHub.

Via our running example, we have seen how to go from objects to constructors, how to extend objects, how to set an object’s prototype and how to create sub-constructors. You can read the following blog posts to deepen your understanding of JavaScript inheritance:

References

  1. What’s up with the “constructor” property in JavaScript?
  2. JavaScript properties: inheritance and enumerability
  3. The pitfalls of using objects as maps in JavaScript
  4. es5-shim: use ECMAScript 5 in older browsers
  5. A closer look at super-references in JavaScript and ECMAScript.next
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值