Extending Native JavaScript Objects

Disclaimer: This article is for educational purposes only. In these fun examples, I extend native objects. I don't advocate doing that unless you have consensus amongst your team and nobody else's code will depend upon yours.


Let's have some fun with something that common wisdom says you should never do: extend native JavaScript objects. Of course, never is a dubious qualifier, but nonetheless, you need to be aware of the possible side-effects.

random

I have this browser based game that creates instances of enemies for the player to fight. Each time I generate a monster, I want to give it a random weapon. I also want to generate a random monster each time. Then I want to generate a random name for the monster.

Much random.

All that data is stored in collections type Array, Map, and Set. Rather than writing logic to pull a random item from a collection every time I want to do it, I'm going to extend the native objects with a random method.

First, I check to see if the random property exists. If it doesn't I use Object.defineProperty to add it, and give it the appropriate logic. By default, this property is non-enumerable, non-writable, and non-configurable.

Copy pasta this code snippet into the developer console and run it.

// Add a `random()` method to Map
if (!("random" in Map.prototype)) {
  Object.defineProperty(Map.prototype, "random", {
    value: function () {
      return [...this.values()].random();
    }
  });
}

// Add a `random()` method to Set
if (!("random" in Set.prototype)) {
  Object.defineProperty(Set.prototype, "random", {
    value: function () {
      return [...this.values()].random();
    }
  });
}

// Add a `random()` method to Array
if (!("random" in Array.prototype)) {
  Object.defineProperty(Array.prototype, "random", {
    value: function () {
      return this[Math.floor(Math.random() * this.length)];
    }
  });
}

// Example usage
Game = {
   enemies: ["Zombie", "Skeleton", "Dragon", "Orc", "Goblin", "Harpy"],
   armory: new Set([
      {type:"Mace", dmg: 4},
      {type:"Sword", dmg: 6},
      {type:"Dagger", dmg: 2},
      {type:"Arrow", dmg: 3}
   ])
};
let enemy = { breed: Game.enemies.random() };
enemy.weapon = Game.armory.random();

console.log(enemy);
// { breed: "Orc", weapon: { type: "Sword", dmg: 6 } }

listen

I get tired of writing addEventListener, so I created a new method on EventTarget.prototype named listen. In this code snippet, I'm going to extend a native object and then proxy right back to another method on that native object!! You can ready more about this in my 'Fun with Proxies' article.

Copy pasta this code into the developer console, and then click anywhere in the main document.

if (!("listen" in EventTarget.prototype)) {
  Object.defineProperty(EventTarget.prototype, "listen", {
    value: new Proxy(EventTarget.prototype.addEventListener, {
      apply: function (_target, _this, _args) {
        return _target.apply(_this, _args);
      }
    })
  });
}

document.getElementsByTagName("body")[0].listen("click", console.warn);

each

For the truly lazy, typing in forEach is just too much. I'd rather just use each.

Copy pasta this entire code snippet into the developer console and run it.

// Add an `each()` method to Array that is a Proxy to `forEach()`
if (!("each" in Array.prototype)) {
  Object.defineProperty(Array.prototype, "each", {
    value: new Proxy(Array.prototype.forEach, {
      apply: function (_target, _this, _args) {
        return _target.apply(_this, _args);
      }
    })
  });
}


// Example usage
const rocky_planets = ["Mercury", "Venus", "Earth", "Mars"];

rocky_planets.each(planet => {
   console.log(`See ya, Pluto! Yours truly, ${planet}.`);
});