How to get object itself in custom Object.prototype.xxx function?
Asked Answered
H

1

3
Object.prototype.getB = function() {

  // how to get the current value a
  return a.b;

};
const a = {b: 'c'};
a.getB();

As you can see, I want to make a function to all Object value. And I need to get the object value in this function then do something.

Haim answered 29/9, 2017 at 14:38 Comment(0)
L
5

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:

  1. Checking own property existence
  2. Using property descriptors
  3. Using the correct function kind
  4. 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 functions, 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.

Lutanist answered 29/9, 2017 at 14:42 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.