122

I need to write a few extension methods in JS. I know just how to do this in C#. Example:

public static string SayHi(this Object name)
{
    return "Hi " + name + "!";
}

and then called by:

string firstName = "Bob";
string hi = firstName.SayHi();

How would I do something like this in JavaScript?

James McMahon
  • 48,506
  • 64
  • 207
  • 283
Matt Cashatt
  • 23,490
  • 28
  • 78
  • 111

2 Answers2

198

JavaScript doesn't have an exact analogue for C#'s extension methods. JavaScript and C# are quite different languages.

The nearest similar thing is to modify the prototype object of all string objects: String.prototype. In general, best practice is not to modify the prototypes of built-in objects in library code meant to be combined with other code you don't control. (Doing it in an application where you control what other code is included in the application is okay.)

If you do modify the prototype of a built-in, it's best (by far) to make that a non-enumerable property by using Object.defineProperty (ES5+, so basically any modern JavaScript environment, and not IE8¹ or earlier). To match the enumerability, writability, and configurability of other string methods, it would look like this:

Object.defineProperty(String.prototype, "SayHi", {
    value: function SayHi() {
        return "Hi " + this + "!";
    },
    writable: true,
    configurable: true,
});

(The default for enumerable is false.)

If you needed to support obsolete environments, then for String.prototype, specifically, you could probably get away with creating an enumerable property:

// Don't do this if you can use `Object.defineProperty`
String.prototype.SayHi = function SayHi() {
    return "Hi " + this + "!";
};

That's not a good idea, but you might get away with it. Never do that with Array.prototype or Object.prototype; creating enumerable properties on those is a Bad Thing™.

Details:

JavaScript is a prototypical language. That means that every object is backed by a prototype object. In JavaScript, that prototype is assigned in one of four ways:

  • By the constructor function for the object (e.g., new Foo creates an object with Foo.prototype as its prototype)
  • By the Object.create function added in ES5 (2009)
  • By the Object.setPrototypeOf function (ES2015+) [or the deprecated __proto__ setter (ES2015+, optional, and only exists on objects that inherit [directly or indirectly] from Object.prototype), or
  • By the JavaScript engine when creating an object for a primitive because you're calling a method on it (this is sometimes called "promotion")

So in your example, since firstName is a string primitive, it gets promoted to a String instance whenever you call a method on it, and that String instance's prototype is String.prototype. So adding a property to String.prototype that references your SayHi function makes that function available on all String instances (and effectively on string primitives, because they get promoted).

Example:

Object.defineProperty(String.prototype, "SayHi", {
    value: function SayHi() {
        return "Hi " + this + "!";
    },
    writable: true,
    configurable: true
});

console.log("Charlie".SayHi());

There are some key differences between this and C# extension methods:

  • (As DougR pointed out in a comment) C#'s extension methods can be called on null references. If you have a string extension method, this code:

      string s = null;
      s.YourExtensionMethod();
    

    works (unless YourExtensionMethod throws when it receives null as the instance parameter). That isn't true with JavaScript; null is its own type, and any property access on null throws an error. (And even if it didn't, there's no prototype to extend for the Null type.)

  • (As ChrisW pointed out in a comment) C#'s extension methods aren't global. They're only accessible if the namespace they're defined in is used by the code using the extension method. (They're really syntactic sugar for static calls, which is why they work on null.) That isn't true in JavaScript: If you change the prototype of a built-in, that change is seen by all code in the entire realm you do that in (a realm is the global environment and its associated intrinsic objects, etc.). So if you do this in a web page, all code you load on that page sees the change. If you do this in a Node.js module, all code loaded in the same realm as that module will see the change. In both cases, that's why you don't do this in library code. (Web workers and Node.js worker threads are loaded in their own realm, so they have a different global environment and different intrinsics than the main thread. But that realm is still shared with any modules they load.)


¹ IE8 does have Object.defineProperty, but it only works on DOM objects, not JavaScript objects. String.prototype is a JavaScript object.

T.J. Crowder
  • 1,031,962
  • 187
  • 1,923
  • 1,875
  • @DougR: Sorry, standard comment cleanup. When a comment becomes obsolete, mods or power users can remove it (mods can do it directly, power users have to team up in the review queue). I've updated the answer to call out that you flagged this up. :-) – T.J. Crowder Jan 11 '15 at 10:24
  • 1
    @Grundy: Thanks, yeah, the whole point of the `String s = null;` part was to use `s.YourExtensionMethod()`! Appreciate the catch. :-) – T.J. Crowder Jan 11 '15 at 11:07
  • Thanks for highlighting that extension method does work for null entities. – Ram Apr 19 '16 at 07:40
  • 1
    If you do this in Node.js for example I guess that will affect (add the new property to) every string in the program -- including other modules? Would two modules conflict if they both define a "SayHi" property? – ChrisW Nov 30 '18 at 22:56
  • @ChrisW - Yes, and yes. Node.js modules loaded by the main thread are all in the same *realm*: They share a global environment and all of the intrinsic objects. So if you modify the prototype of any of the built-ins, they all see that modification. It's one of the reasons not to modify built-ins. (If you use a worker thread, it's loaded in its own realm, so only it and the modules it loads will see any modifications to the intrinsics.) – T.J. Crowder Dec 01 '18 at 09:11
  • Thanks. That's in contrast to a "C# extension method", which I think is only visible to classes which use the namespace in which the extension class is defined. – ChrisW Dec 01 '18 at 09:15
  • @ChrisW - Yes indeed, because they're syntactic sugar for a static method call. :-) – T.J. Crowder Dec 01 '18 at 09:21
  • The JavaScript array has a `find()` method (which returns the *first*) but not a `singleOrDefault()` (which, [like .NET's](https://stackoverflow.com/q/33065696/49942), could return the *only*). I was wondering whether I might define that as an extension, but changing the behaviour of the whole "realm" sounds dangerous (e.g. because, "what if everybody did that?"). – ChrisW Dec 01 '18 at 09:29
  • 1
    @ChrisW - Quite. :-) What you might do instead is create an Array subclass (`class MyArray extends Array { singleOrDefault() { ... }}`) but it means that you have to do `MyArray.from(originalArray)` before using it if `originalArray` is a boring old array. There are also LINQ-like libraries for JavaScript ([here's a roundup](https://www.ginktage.com/2012/09/5-linq-for-javascript-library/)), which similarly involve creating a Linq object from the array before doing things (well, LINQ to JavaScript did, I haven't used others [and haven't used that one in years]). (BTW, updated the answer, thx.) – T.J. Crowder Dec 01 '18 at 09:40
  • 1
    I was today years old when I learned that you can safely call a C# extension method on a null instance. Thanks! – Daniel Aug 27 '21 at 00:56
  • 1
    @Daniel - yeah its really nice for null checking parameters – StingyJack Jan 09 '22 at 14:05
3

Every object has a parent (prototype), you can prove this by logging any object to the console and you'll see a prototype object, you can expand the prototype object to see all the methods and properties (use dev tools in your browser). The example below will add a new method to the Array prototype that will get inherited.

Array.prototype.hello = function() {
        console.log('hello')
}
Jimmy
  • 575
  • 5
  • 10
  • See "[Explaining entirely code-based answers](https://meta.stackoverflow.com/q/392712/128421)". While this might be technically correct, it doesn't explain why it solves the problem or should be the selected answer. We should educate along with helping solve the problem. – the Tin Man Mar 22 '22 at 04:46