What's the Best Way to Extend Native JavaScript Objects?

You might have seen a new property being added to a native JavaScript object through assignment like so:

String.prototype.capitalize = function () {
    return this.charAt(0).toUpperCase() + this.slice(1);
}

console.log('foo'.capitalize()); // Foo

Or, some people use Object.assign() too for this, like so:

// ES6+
Object.assign(String.prototype, {
    capitalize() {
        return this.charAt(0).toUpperCase() + this.slice(1);
    }
});

console.log('foo'.capitalize()); // Foo

While they both "work", the implementations are problematic for the following reasons:

  1. The property you add would show up during property enumeration (for example using for...in loop or Object.keys method) which might not be be the desired behavior in some cases;
  2. The property could easily be overwritten by another implementation of the same name (for e.g. by a library, or another piece of code in your project code base);
  3. The property could be deleted.

All of the above could easily lead to unwanted side-effects and problems and break your code. Therefore, you should consider using any of the other solutions suggested in this article.

Using Object.defineProperty() to Extend Native JavaScript Objects

An ideal approach would be to use Object.defineProperty() (which is available since ES5). It provides us with finer control over the property we wish to add to an existing object, by allowing us to add descriptors. For example, we can decide whether we want to allow the property to be overwritten, deleted, or enumerable.

Let's re-write our capitalize function using Object.defineProperty():

// ES5+
Object.defineProperty(String.prototype, 'capitalize', {
    // prevent it from being overwritten
    writable: false,
    // prevent it from being enumerated
    enumerable: false,
    // prevent it from being deleted
    configurable: false,

    value() {
        return this.charAt(0).toUpperCase() + this.slice(1);
    }
});

console.log('foo'.capitalize()); // Foo

Please note that writable: false, enumerable: false and configurable: false are default values and can safely be ommited. They were added to show how the behavior of the new property can be defined using Object.defineProperty.

Creating a New Class That Extends the Native JavaScript Object

Another approach we could take is to create a separate class altogether that extends the native object. This way we do not pollute the native object, and are more conscious about the methods we add/remove. For example:

// ES6+
class MyString extends String {
    capitalize() {
        return this.charAt(0).toUpperCase() + this.slice(1);
    }
}

const str = new MyString('foo');

console.log(str.capitalize()); // Foo

// and you have access to all the native `String` methods
console.log(str.toUpperCase()); // FOO

This could especially be useful if you have many new methods you would like to introduce to the same object type.

You could also re-write the capitalize method as a static method, like so:

class MyString extends String {
    static capitalize(str) {
        return str.charAt(0).toUpperCase() + str.slice(1);
    }
}

console.log(MyString.capitalize('foo')); // Foo

Use of static methods is a highly opinionated topic — you might want to spend some time to understand the arguments for and against it before you use it.

Creating a Function

You could alternatively, create a function, like so:

function capitalize(str) {
    return str.charAt(0).toUpperCase() + str.slice(1);
}

console.log(capitalize('foo')); // Foo

The advantage of this approach is that we could export and import/require individual functions and use them wherever we need them.

Please note that import and export are a feature of ES6.


This post was published by Daniyal Hamid. Daniyal currently works as the Head of Engineering in Germany and has 20+ years of experience in software engineering, design and marketing. Please show your love and support by sharing this post.