DZone
Thanks for visiting DZone today,
Edit Profile
  • Manage Email Subscriptions
  • How to Post to DZone
  • Article Submission Guidelines
Sign Out View Profile
  • Post an Article
  • Manage My Drafts
Over 2 million developers have joined DZone.
Log In / Join
Refcards Trend Reports Events Over 2 million developers have joined DZone. Join Today! Thanks for visiting DZone today,
Edit Profile Manage Email Subscriptions Moderation Admin Console How to Post to DZone Article Submission Guidelines
View Profile
Sign Out
Refcards
Trend Reports
Events
Zones
Culture and Methodologies Agile Career Development Methodologies Team Management
Data Engineering AI/ML Big Data Data Databases IoT
Software Design and Architecture Cloud Architecture Containers Integration Microservices Performance Security
Coding Frameworks Java JavaScript Languages Tools
Testing, Deployment, and Maintenance Deployment DevOps and CI/CD Maintenance Monitoring and Observability Testing, Tools, and Frameworks
Partner Zones AWS Cloud
by AWS Developer Relations
Culture and Methodologies
Agile Career Development Methodologies Team Management
Data Engineering
AI/ML Big Data Data Databases IoT
Software Design and Architecture
Cloud Architecture Containers Integration Microservices Performance Security
Coding
Frameworks Java JavaScript Languages Tools
Testing, Deployment, and Maintenance
Deployment DevOps and CI/CD Maintenance Monitoring and Observability Testing, Tools, and Frameworks
Partner Zones
AWS Cloud
by AWS Developer Relations
The Latest "Software Integration: The Intersection of APIs, Microservices, and Cloud-Based Systems" Trend Report
Get the report
  1. DZone
  2. Data Engineering
  3. Data
  4. Subtyping JavaScript built-ins

Subtyping JavaScript built-ins

Axel Rauschmayer user avatar by
Axel Rauschmayer
·
Dec. 29, 11 · Interview
Like (0)
Save
Tweet
Share
3.23K Views

Join the DZone community and get the full member experience.

Join For Free

JavaScript’s built-ins are difficult to subtype. This post explains why and presents solutions.

Terminology

We use the phrase subtype a built-in and avoid the term extend, because it is taken in JavaScript:
  • Subtyping a built-in A: Create a sub-constructor B of a given built-in constructor A. B’s instances are also instances of A.
  • Extending a built-in A: Adding new methods to A.prototype.
There are two obstacles to subtyping a built-in: First, instances with internal properties. Second, a constructor that can’t be called as a function.

Obstacle 1: instances with internal properties

Most built-in types have instances with so-called internal properties (whose names are written in double square brackets, for example: [[PrimitiveValue]]). Internal properties are managed by the JavaScript engine and usually not directly accessible in JavaScript. The normal subtyping technique in JavaScript is to call a super-constructor as a function with the this of the sub-constructor [2].
    function Super(x, y) {
        this.x = x;
        this.y = y;
    }
    function Sub(x, y, z) {
        // Add super-properties to sub-instance
        Super.call(this, x, y); // (*)
        // Add sub-property
        this.z = z;
    }
Most built-ins ignore the sub-instance passed in as this (*), which is described below, as “obstacle 2”. Additionally, adding internal properties to an existing instance is in general impossible, because they tend to fundamentally change the instance’s nature. Hence, the call at (*) can’t be used to add internal properties. The following types have instances with internal properties:
  • Wrapper types: Instances of Boolean, Number and String wrap primitives. They all have the internal property [[PrimitiveValue]] whose value is returned by valueOf(). Additionally, String instances support indexed access of characters.
    • Boolean: internal property [[PrimitiveValue]]
    • Number: internal property [[PrimitiveValue]]
    • String: internal property [[PrimitiveValue]], custom method [[GetOwnProperty]], normal property length. [[GetOwnProperty]] accesses the wrapped primitive string when an array index is used.
  • Array: The custom internal method [[DefineOwnProperty]] intercepts properties being set. It ensures that the length property works correctly, by keeping length up to date when array elements are added or removed and by removing excess elements when length is made smaller.
  • Date: internal property [[PrimitiveValue]] stores the time represented by a date instance.
  • Function: internal property [[Call]] (the code to execute when the instance is called) and possibly others.
  • RegExp: internal property [[Match]] in addition to non-internal properties. Quoting the ECMAScript specification:
    The value of the [[Match]] internal property is an implementation dependent representation of the Pattern of the RegExp object.
The only built-in constructors that don’t have internal properties are Error and Object.

Work-around. MyArray is a subtype of of Array. It has a getter size that returns the actual elements in an array, ignoring holes (where length counts holes). The trick used to implement MyArray is that it creates an array instance and copies its methods to it. Credit: inspired by a blog post by Ben Nadel [3].

    function MyArray(/*arguments*/) {
        var arr = [];
        // Don’t use the Array constructor which does not work for, e.g. [5]
        // (new Array(5) creates an array of length 5 with no elements in it)
        [].push.apply(arr, arguments);
        return copyOwnFrom(arr, MyArray.methods);
    }
    MyArray.methods = {
        get size() {
            var size = 0;
            for(var i=0; i < this.length; i++) {
                if (i in this) size++;
            }
            return size;
        }
    }
The above code uses the following helper function:
    function copyOwnFrom(target, source) {
        Object.getOwnPropertyNames(source).forEach(function(propName) {
            Object.defineProperty(target, propName,
                Object.getOwnPropertyDescriptor(source, propName));
        });
        return target;
    };
Interaction:
    > var a = new MyArray("a", "b")
    > a.length = 4;
    > a.length
    4
    > a.size
    2
Caveats. Copying methods to an instance leads to redundancies that could be avoided with a prototype (if we had the option to use one). Additionally, MyArray creates objects that are not its instances:
    > a instanceof MyArray
    false
    > a instanceof Array
    true

Obstacle 2: a constructor that can’t be called as a function

Even though Error and subtypes don’t have instances with internal properties, one still can’t subtype them easily, because the standard pattern for subtyping won’t work (repeated from above):
    function Super(x, y) {
        this.x = x;
        this.y = y;
    }
    function Sub(x, y, z) {
        // Add super-properties to sub-instance
        Super.call(this, x, y);  // (*)
        // Add sub-property
        this.z = z;
    }
The problem is that Error always produces a new instance, even if called as a function (*). That is, it ignores the parameter this handed to it via call():
    > var e = {}
    > Object.getOwnPropertyNames(Error.call(e))
    [ 'stack', 'arguments', 'type' ]
    > Object.getOwnPropertyNames(e)
    []
Error does return an instance with own properties, but it’s a new instance, not e. The subtyping pattern only works if Error adds the own properties to this (e, in the above case).

Work-around. Inside the sub-constructor, create a new super-instance and copy its properties to the sub-instance.

    function MyError() {
        // Use Error as a function
        var superInstance = Error.apply(null, arguments);
        copyOwnFrom(this, superInstance);
    }
    MyError.prototype = Object.create(Error.prototype);
    MyError.prototype.constructor = MyError;
Trying out the new error type:
    try {
        throw new MyError("Something happened");
    } catch (e) {
        console.log("Properties: "+Object.getOwnPropertyNames(e));
    }
Output on Node.js:
    Properties: stack,arguments,message,type
The instanceof relationship is as it should be:
    > new MyError() instanceof Error
    true
    > new MyError() instanceof MyError
    true
Caveat. The main reason for subtyping Error is to have the stack property in sub-instances. Alas, Firefox seems to store that value in an internal property, which is why the above approach does not work there (Firefox 8).

Another solution: delegation

Delegation is a very clean alternative to subtyping. For example, to create your own array type, you keep an array in a property:
    function MyArray(/*arguments*/) {
        this.array = [];
        // Don’t use the Array constructor which does not work for, e.g. [5]
        // (new Array(5) creates an array of length 5 with no elements in it)
        [].push.apply(this.array, arguments);
    }
    Object.defineProperties(MyArray.prototype, {
        size: {
            get: function () {
                var size = 0;
                for(var i=0; i < this.array.length; i++) {
                    if (i in this.array) size++;
                }
                return size;
            }
        },
        length: {
            get: function () {
                return this.array.length;
            },
            set: function (value) {
                return this.array.length = value;
            }
        }
    });
The obvious limitation is that you can’t access elements of MyArray via square brackets, you must use methods to do so:
    MyArray.prototype.get = function (index) {
        return this.array[index];
    }
    MyArray.prototype.set = function (index, value) {
        return this.array[index] = value;
    }
Normal methods of Array.prototype can be transferred via the following bit of meta-programming.
    [ "toString", "push", "pop" ].forEach(function (name) {
        MyArray.prototype[name] = function () {
            return Array.prototype[name].apply(this.array, arguments);
        }
    });
Using MyArray:
    > var a = new MyArray("a", "b");
    > a.length = 4;
    > a.push("c")
    5
    > a.length
    5
    > a.size
    3
    > a.set(0, "x");
    > a.toString()
    'x,b,,,c'

The future: ECMAScript.next

ECMAScript.next will help in two ways. First, it will allow you to assign arbitrary prototypes to instances with internal properties. Second, it will probably allow you to override the [] operator. Then you can simulate arrays without needing to subtype Array.

1. Creating special instances with an arbitrary prototype. There will be an operator that allows you to assign an arbitrary prototype to a function or array instance. For example, an array instance arr with all necessary internal properties that has the prototype MyArrayProto:

    var arr = MyArrayProto <| [ "a", "b", "c" ];
A function instance with a prototype MyFunctionProto is created as follows:
    let func = MyFunctionProto <| function () {}
One might expect that it would be simpler to automatically make an instance special if a constructor subtypes a built-in such as Array or Function. But given the nature of internal properties, the above solution is probably the most straightforward one.

The <| operator can be used to create a subtype SubArray of Array that works with new and instanceof [source: Allen Wirfs-Brock]:

    // Connect constructors and prototypes
    var SubArray = Array <| function(...values) {
        // Connect instance and prototype
        return SubArray.prototype <| [...values];
    }
   
    // Subtype methods
    SubArray.prototype.method1 = function() {...};
    SubArray.prototype.method2 = function() {...};
Interaction:
    var s = new SubArray(1,2,3);
    console.log(s.length);  // 3
    s[3] = 4;
    console.log(s.length);  // 4
    console.log(Object.isArray(s));  // true
    console.log(s instanceof SubArray);  // true
    console.log(s instanceof Array);  // true
2. Custom square bracket getters and setters. “Object Model Reformation: Decoupling [ ] and Property Access” by Allen Wirfs-Brock proposes to make the operator [] overridable.
  • The best practice is now to use new methods Object.getProperty() and Object.setProperty() whenever you want to use an arbitrary string as a property name.
  • Object.prototype uses those methods to implement the operator [] (so that legacy code works as expected).
  • New collection classes are free to provide their own implementations for []. One possibility is to use keys that are not strings.

References

  • ECMAScript 5.1 specification, Chap. 15 [details on built-ins: instance properties etc.]
  • Prototypes as classes – an introduction to JavaScript inheritance
  • “Extending JavaScript Arrays While Keeping Native Bracket-Notation Functionality” by Ben Nadel

 

From http://www.2ality.com/2011/12/subtyping-builtins.html

Property (programming) JavaScript engine Subtyping Data structure

Opinions expressed by DZone contributors are their own.

Popular on DZone

  • Benefits and Challenges of Multi-Cloud Integration
  • Effective Jira Test Management
  • Low-Code and No-Code Are the Future of Work — For IT and Beyond
  • Isolating Noisy Neighbors in Distributed Systems: The Power of Shuffle-Sharding

Comments

Partner Resources

X

ABOUT US

  • About DZone
  • Send feedback
  • Careers
  • Sitemap

ADVERTISE

  • Advertise with DZone

CONTRIBUTE ON DZONE

  • Article Submission Guidelines
  • Become a Contributor
  • Visit the Writers' Zone

LEGAL

  • Terms of Service
  • Privacy Policy

CONTACT US

  • 600 Park Offices Drive
  • Suite 300
  • Durham, NC 27709
  • support@dzone.com
  • +1 (919) 678-0300

Let's be friends: