Monkey Patching
What you want to do is called monkey patching — you mutate a built-in prototype.
There are many wrong ways to do it, and it should be avoided entirely, because it has negative Web compatibility impacts.
I will, however, demonstrate how this can be done in a way that matches existing prototype features most closely.
In your case, the function body should return this.b
.
In functions called as methods, you can get the object itself with the this
keyword.
See How does the "this" keyword work? (section 4: “Entering function code”, subsection “Function properties”) for more details.
You correctly added the method to Object.prototype
.
See Inheritance and the prototype chain for more details.
The tools
There is a number of tools involved when reasoning about monkey-patching:
- Checking own property existence
- Using property descriptors
- Using the correct function kind
- Getters and setters
Let’s assume you’re trying to implement theMethod
on TheClass
.
1. Checking own property existence
Depending on your use case you may want to check if the method you want to introduce already exists.
You can do that with Object.hasOwn
; this is quite a new method, but in older environments it can simply be replaced by Object.prototype.hasOwnProperty.call
.
Alternatively, use hasOwnProperty
normally, but be aware that if you or someone else monkey-patched the hasOwnProperty
method itself, this may lead to incorrect results.
Note that in
does not check for own properties, exclusively, but for inherited properties as well, which isn’t (necessarily) what you want when you’re about to create an own property on an object.
Also, note that if(TheClass.prototype.theMethod)
is not a property existence check; it’s a truthiness check.
Code samples
if(Object.hasOwn(TheClass.prototype, "theMethod")){
// Define the method.
}
if(Object.prototype.hasOwnProperty.call(TheClass.prototype, "theMethod")){
// Define the method.
}
if(TheClass.prototype.hasOwnProperty("theMethod")){
// Define the method.
}
2. Using property descriptors
You can choose the property descriptor however you like, but existing methods are writable, configurable, and non-enumerable (the last of which is the default when using defineProperty
).
defineProperties
can be used to define multiple properties in one go.
When simply assigning a property using =
, the property becomes writable, configurable, and enumerable.
Code samples
// Define the method:
Object.defineProperty(TheClass.prototype, "theMethod", {
writable: true,
configurable: true,
value: function(){}
});
// Define the method:
Object.defineProperties(TheClass.prototype, {
theMethod: {
writable: true,
configurable: true,
value: function(){}
}
});
3. Using the correct function kind
JavaScript has four major kinds of functions which have different use cases.
“Is invokable” means that it can be called without new
and “Is constructable” means that it can be called with new
.
Function kind |
Example |
Is invokable |
Is constructable |
Has this binding |
Arrow function |
() => {} |
Yes |
No |
No |
Method |
({ method(){} }).method |
Yes |
No |
Yes |
Class |
(class{}) |
No |
Yes |
Yes |
function function |
(function(){}) |
Yes |
Yes |
Yes |
What we’re looking for is a method that can be called (without new
) and has its own this
binding.
We could use function
s, but, looking at existing methods, not only is new "HELLO".charAt();
strange, it also doesn’t work!
So the method should also not be constructable.
Therefore proper Methods are what we’re looking for.
Note that this obviously depends on your use case.
For example, if you want a constructable function, by all means, use a class
instead.
Code sample
We go back to the previous code sample and instead of function(){}
use a method definition.
// Define the method:
Object.defineProperty(TheClass.prototype, "theMethod", {
writable: true,
configurable: true,
value: {
theMethod(){
// Do the thing.
}
}.theMethod
});
Why bother with 2. and 3.?
The goal of the enumerability and constructability considerations is to create something that has the same “look and feel” as existing, built-in methods.
The difference between those and a naive implementation can be demonstrated using this snippet:
class TheClass{}
TheClass.prototype.theMethod = function(){};
Let’s compare this to a different, built-in method, like String.prototype.charAt
:
Code snippet |
Naive example |
Built-in example |
thePrototype |
Is TheClass.prototype |
Is String.prototype |
theMethod |
Is TheClass.prototype.theMethod |
Is String.prototype.charAt |
for(const p in thePrototype){ console.log(p); } |
"theMethod" will be logged at some point. |
"charAt" will never be logged. |
new theMethod |
Creates an instance of theMethod . |
TypeError : theMethod is not a constructor. |
Using the tools from subsections 2 and 3 make it possible to create methods that behave more like built-in methods.
4. Getters and setters
An alternative is to use a getter. Consider this:
const arr = [
"a",
"b",
"c",
];
console.log(arr.indexOfB); // 1
How would an indexOfB
getter on the Array
prototype look like?
We can’t use the above approach and replace value
by get
, or else we’ll get:
TypeError
: property descriptors must not specify a value or be writable when a getter or setter has been specified
The property writable
needs to be removed entirely from the descriptor.
Now value
can be replaced by get
:
Object.defineProperty(Array.prototype, "indexOfB", {
configurable: true,
get: {
indexOfB(){
return this.indexOf("b");
}
}.indexOfB
});
A setter can also be specified by adding a set
property to the descriptor:
Object.defineProperty(Array.prototype, "indexOfB", {
configurable: true,
get: {
indexOfB(){
return this.indexOf("b");
}
}.indexOfB,
set: {
indexOfB(newValue){
// `newValue` is the assigned value.
// Use `this` for the current Array instance.
// No `return` necessary.
}
}.indexOfB
});
Web compatibility impact
There are a few reasons why anyone would want to extend built-in prototypes:
- You got a brilliant idea yourself for a new feature to be added to all instances of whatever class you’re extending, or
- You want to backport an existing, specified feature to older browsers.
If you extend the target object, there are two options to consider:
- If the property doesn’t exist, supply it, otherwise, leave the existing property in place, or
- Always replace the property with your own implementation, no matter if it exists or not.
All approaches mostly have disadvantages:
If you or someone else invented their own feature, but a standard method comes along which has the same name, then these features are almost guaranteed to be incompatible.
If you or someone else try to implement a standard feature in order to backport it to browsers that don’t support it, but don’t read the specification and “guess” how it works, then the polyfilled feature is almost guaranteed to be incompatible with the standard implementation.
Even if the spec is followed closely, who will make sure to keep up with spec changes?
Who will account for possible errors in the implementation?
If you or someone else choose to check if the feature exists before overriding it, then there’s a chance that as soon as someone with a browser which supports the feature visits your page, suddenly everything breaks because the implementations turn out to be incompatible.
If you or someone else choose to override the feature regardless, then at least the implementations are consistent, but then migration to the standard feature may be difficult.
If you write a library that is used a lot in other software, then the migration cost becomes so large that the standard itself has to change; this is why Array.prototype.contains
had to be renamed to Array.prototype.includes
[Reddit] [ESDiscuss] [Bugzilla] [MooTools], and Array.prototype.flatten
could not be used and had to be named Array.prototype.flat
instead[Pull request 1] [Pull request 2] [Bugzilla].
In other words, this is the reason we can’t have nice things.
Also, your library may not be interoperable with other libraries.
Alternatives
The simplest alternative is to define your own plain function:
const theMethod = (object) => object.theProperty; // Or whatever.
const theProperty = theMethod(theObject);
You could also consider a Proxy.
This way you can dynamically query the property and respond to it.
Let’s say you have an object with properties a
through z
and you want to implement methods getA
through getZ
:
const theProxiedObject = new Proxy(theObject, {
get(target, property, receiver){
const letter = property.match(/^get(?<letter>[A-Z])$/)?.groups?.letter.toLowerCase();
if(letter){
return () => target[letter];
}
return Reflect.get(target, property, receiver);
}
});
console.assert(theProxiedObject.getB() === theProxiedObject.b);
You could also extend your object’s prototype using another class, and use this instead:
class GetterOfB extends Object{
b;
constructor(init){
super();
Object.assign(this, init);
}
getB(){
return this.b;
}
}
const theObject = new GetterOfB({
b: "c"
});
const theB = theObject.getB();
All you have to keep in mind is to not modify things you didn’t define yourself.