4

So, fundamentally, this question is Not opinion-based. I seriously pursuit this issue objectively without feeling mostly arisen from the predominant opinion - Why is extending native objects a bad practice?

and this quesion is related but unanswered questions:

If Object.prototype is extended with Symbol property, is it possible to break code without specifically designed APIs to access Symbol in JavaScript?

Should the extension of built-in Javascript prototypes through symbols also be avoided?

The first question is already closed as they say it's opinion based, and as you might know in this community once a question is Banned, however we modified it, moderators will never bother to re-open. That is the way how the things work here.

For the second question. For some unknown reason, the question has been taken more seriously and not considered as opinion based although the context is identical.

There are two parts to the "don't modify something you don't own" rule:

  1. You can cause name collisions and you can break their code.

    By touching something you don't own, you may accidentally overwrite something used by some other library. This will break their code in unexpected ways.

  2. You can create tight dependencies and they can break your code.

    By binding your code so tightly to some other object, if they make some significant change (like removing or renaming the class, for example), your code might suddenly break.

Using symbols will avoid #1, but you still run into #2. Tight dependencies between classes like that are generally discouraged. If the other class is ever frozen, your code will still break. The answers on this question still apply, just for slightly different reasons.

Also, I've read opinions(how can we discuss such a thing here without "opinion" base?), they claim

a) Library code Using Symbols exists and they may tweak Symbol API (such as Object.getOwnPropertySymbols())

b) extending object property with Symbol is not different from non-Symbol property fundamentally.

Here, for the major rationale of untouching Object.prototype is due to #1, almost all answers I saw claimed that and we don't have to discuss if there is no Symbol usage.

However, Using symbols will avoid #1 as they say. So most of the traditional wisdom won't apply anymore.

Then, as #2 says,

By binding your code so tightly to some other object, if they make some significant change (like removing or renaming the class, for example), your code might suddenly break.

well, in principle, any fundamental API version upgrade will break any code. The well-known fact is nothing to do with this specific question. #2 did not answer the question.

Only considerable part is Object.freeze(Object.prototype) can be the remaining problem. However, this is essentially the same manner to upgrade the basic API by some other unexpectedly.

As the API users not as API providers, the expected API of Object.prototype is not frozen.

If some other guys touch the basic API and modifies it as frozen, it is he/she who broke the code. They upgraded the basic API without notice.

For instance, in Haskell, there are many language extensions. Probably they solve the collision issue well, and most importantly, they won't "freeze" the basic API because freezing the basic API would brake their eco.

Therefore, I observe that Object.freeze(Object.prototype) is the anti-pattern. It cannot be justified as a matter of course to prevent Object.prototype extension with Symbols.

So here is my question. Although I observe this way, is it safe to say:

In case of that Object.freeze(Object.prototype) is not performed, which is the anti-pattern and detectable, it is safe to perform extending Object.prototype with Symbols?

If you don't think so, please provide a concrete example.

sailsky
  • 325
  • 1
  • 9
  • 1
    Your question is pretty confusing. Could you state it in a single sentence? – Nick Bailey Mar 17 '22 at 01:42
  • @NickBailey You can see the title as the single sentence. thanks. Or the last sentence. I designed this question as so. – sailsky Mar 17 '22 at 01:44
  • What do you mean by 'is it safe' safe in what sense? – Nick Bailey Mar 17 '22 at 01:53
  • In the meaning of anyone won't present a significant counter claim that will be considered as valid by majority of the community. – sailsky Mar 17 '22 at 01:56
  • 2
    I mean, that's ultimately a matter of opinion, which was what got your question in trouble in the first place. The biggest arguments against it have nothing to do with feasibility and everything to do with readability and ease of understanding, which are subjective criteria. – Nick Bailey Mar 17 '22 at 01:58
  • It's not. You can arise examples. It's the matter of concrete cases. So far, what I see is `Object.freeze` for the basic API, that is the anti-pattern in general. – sailsky Mar 17 '22 at 01:59
  • Let us [continue this discussion in chat](https://chat.stackoverflow.com/rooms/243007/discussion-between-nick-bailey-and-sailsky). – Nick Bailey Mar 17 '22 at 02:01
  • Can you share the code of how you would extend `Object.prototype`, and how you would use that symbol property? Would you expect this property to be available on all objects, and why? It's hard to tell what could "break" without a concrete example. – Bergi Mar 17 '22 at 03:05
  • @Bergi Thanks, my main concern is to provide the pipe-operator on `Object.prototype` and the function-composition operator on `Function.prototype`. They are binary operation, but technically works as method on the both Objects. So especially for these extension by Symbols, compared to what I/we obtain by those , I really doubt how "the conventional wisdom" is valid. I've been thinking of this nearly a year now. So I decided to ask very seriously. – sailsky Mar 17 '22 at 05:13
  • @sailsky You mean you want `something[pipe](f)` to evaluate like `f(something)`, for arbitrary values `something`? – Bergi Mar 17 '22 at 13:39
  • @begi, yes. It is like `something |> f` in F# or the previous proposal of pipe-operator of TC39. – sailsky Mar 18 '22 at 05:40

2 Answers2

1

Is it safe? Yes, if you are aware of all the hazards that come with it, and either choose to ignore them, shrug them off, or invest effort to ensure that they don't occur (testing, clear documentation of compatibility requirements), then it is safe. Feel free to do it in your own code where you can guarantee these things.

Is it a good idea? Still no. Don't introduce this in code that other people will (have to) work with.

If some other guys touch the basic API and modifies it as frozen, it is he/she who broke the code. Therefore, I observe that Object.freeze(Object.prototype) is the anti-pattern.

Not quite. If you both did something you shouldn't have done, you're both to blame - even if doing only one of these things gets away with working code. This is exactly what point #2 is about: don't couple your code tightly to global objects that are shared with others.

However, the difference between those things is that freezing the prototype is an established practice to harden an application against prototype pollution attacks and generally works well (except for one bit), whereas extending the prototype with your own methods is widely discouraged as a bad practice (as you already found out).

In Haskell, there are many language extensions. Probably they solve the collision issue well, and most importantly, they won't "freeze" the basic API because freezing the basic API would brake their eco.

Haskell doesn't have any global, shared, mutable object, so the whole problem is a bit different. The only collision issue is between identifiers from "star-imported" modules, including the prelude from the base API. However, this is per module, not global, so it doesn't break composability as you can resolve the same identifier to different functions in separate modules.

Also yes, their base API is frozen and versioned, so they can evolve it without breaking old applications (who can continue using old dependencies and old compilers). This is a luxury that JavaScript doesn't have.

Is it safe to extend Object.prototype with a pipe symbol so that something[pipe](f) does f(something), like something |> f in F# or the previous proposal of pipe-operator?

No, it's not safe, not for arbitrary values of something. Some obvious values where this doesn't work are null and undefined.

However, it doesn't even work for all objects: there are objects that don't have Object.prototype on their prototype chain. One example is Object.create(null) (also done for security purposes), another example are objects from other realms (e.g. iframes). This is also the reason why you shouldn't expect .toString() to work on all objects.

So for your pipe operator, better use a static standalone method, or just use a transpiler to get the syntax you actually want. An Object.prototype method is only a bad approximation.

Bergi
  • 630,263
  • 148
  • 957
  • 1,375
-1

Extending the Object prototype is a dangerous practice.

You have obviously done some research and found that the community of javascript developers overwhelmingly considers it to be a very bad practice.

If you're working on a personal project all by yourself, and you think the rest of us are all cowards for being unwilling to take the risk, then by all means: go ahead and modify your Object prototype! Nobody can stop you. It's up to you whether you will be guided by our advice. Sometimes the conventional wisdom is wrong. (Spoiler: in this case, the conventional wisdom is right.)

But if you are working in a shared repository, especially in any kind of professional setting, do not modify the Object prototype. Whatever you want to accomplish by this technique, there will be alternative approaches that avoid the dangers of modifying the base prototypes.

The number one job of code is to be understood by other developers (including yourself in the future), not just to work. Even if you manage to make this work, it is counterintuitive, and nobody who comes after you will expect to find this. That makes it unacceptable by definition, because what matters here is reasonable expectations, NOT what the language supports. Any person who fails to recognize that professional software development is a team effort has no place writing software professionally.

You are not going to find a technical limitation to extending the Object prototype. Javascript is a very flexible language -- it will give you plenty of rope with which to hang yourself. That does not mean it's a good idea to place your head in the noose.

Tom
  • 8,509
  • 7
  • 49
  • 78
  • Thanks, but again, it seems to me that you simply repeat the phrase of conventional wisdom is right, without any concrete example of code. and... – sailsky Mar 17 '22 at 05:04
  • Do I understand correctly the concrete proof is "The number one job of code is to be understood by other developers ". Well, then, what if comments are provided? So far, I'm afraid to say, I'm not sold at all in terns of my serious consideration of this issue. Unfortunately, I evaluate your reasoning for " in this case, the conventional wisdom is right." is too subjective, and weak. – sailsky Mar 17 '22 at 05:08
  • Designing software is more art than science. As I said, there will be no "concrete proof." You can do what you want in your own software, but it would be professional malpractice to do this in an employer's or client's code. Comments will not be adequate because so much of the JS language is impacted: you would need to add comments to practically every single file in the codebase. And if you're looking for ammunition you can use to defend this pattern from colleagues who have objected to it in a code review, you will not find it here. – Tom Mar 17 '22 at 05:39
  • Finally, I've heard countless times "there will be alternative approaches", and that is not true as they eagerly advertised. For instance, pipe-operator on Objects, or composition-operator on Functions cannot be implemented without modifying those prototypes. Surely many FP libraries provides helper-functions such as `pipe(a,f1,f2)` or `compose(f,g)` and those are merely functions not binary operators or method on native objects. – sailsky Mar 17 '22 at 05:40
  • 1
    You say that pipe and composition operators cannot be implemented without modifying the Object prototype, but then you cite two examples that are apparently built with vanilla functions. This makes me think you don't understand your own evidence, since those FP libraries _prove_ that these things can be done without modifying the Object prototype. – Tom Mar 17 '22 at 05:43
  • I'm afraid to say. "Professional" aspect does not justify how OOP had been advertised as the solid approach but it turned out the design failure and we shift to FP now. We need proof . Unfortunately, art/science claim does not tell us anything. Too subjective as "professional" thing. – sailsky Mar 17 '22 at 05:45
  • [Pipe](https://github.com/ramda/ramda/blob/master/source/pipe.js) and [compose](https://github.com/ramda/ramda/blob/master/source/compose.js), implemented without modifying any base prototypes. – Tom Mar 17 '22 at 06:08
  • Why did you google out the Ramda helper functions? Again your reply does not correspond to this question at all. – sailsky Mar 17 '22 at 06:11
  • Ramba author mentions another FP library called Sanctuary. https://github.com/ramda/ramda-fantasy and the Sanctuary author mentions in TC39 pipe operator proposal. https://github.com/tc39/proposal-pipeline-operator/issues/233 Why do you think the FP library author have to mention to TC39 functional binary operator issue if they already have better alternative? Because currently there is the limitation. – sailsky Mar 17 '22 at 06:20