Why is extending native objects a bad practice?
Asked Answered
I

9

178

Every JS opinion leader says that extending the native objects is a bad practice. But why? Do we get a perfomance hit? Do they fear that somebody does it "the wrong way", and adds enumerable types to Object, practically destroying all loops on any object?

Take TJ Holowaychuk's should.js for example. He adds a simple getter to Object and everything works fine (source).

Object.defineProperty(Object.prototype, 'should', {
  set: function(){},
  get: function(){
    return new Assertion(Object(this).valueOf());
  },
  configurable: true
});

This really makes sense. For instance one could extend Array.

Array.defineProperty(Array.prototype, "remove", {
  set: function(){},
  get: function(){
    return removeArrayElement.bind(this);
  }
});
var arr = [0, 1, 2, 3, 4];
arr.remove(3);

Are there any arguments against extending native types?

Interfere answered 25/12, 2012 at 21:39 Comment(13)
What do you expect to happen when, later, a native object is changed to include a "remove" function with different semantics to your own? You don't control the standard.Acidfast
It's not your native type. It's everyone's native type.Ivar
"Do they fear that somebody does it "the wrong way", and adds enumerable types to Object, practically destroying all loops on any object?": Yep. Back in the days when this opinion was formed it was impossible to create non-enumerable properties. Now things might be different in this regard, but imagine every library just extending native objects how they want it. There is a reason why we started using namespaces.Joiejoin
That was a thougt I had, too. But as a developer you should know, which frameworks you implement and what functions they register. Nobody would want to recreate _.js so that it bind itself to native types. This only makes sense on rare edge cases.Interfere
For what it's worth some "opinion leaders" for example Brendan Eich think it's a perfectly fine to extend the native prototypes.Satterfield
A good read: perfectionkills.com/extending-native-builtinsMavilia
Okay, so extending native objects is bad...so what happens when you build an API which has a constructor function ("Foo" for example), then the ES-20xx spec decides to introduce Foo? all of a sudden, You're completely overriding the ES-20xx specs Foo with your own Foo. Isn't this all swings and roundabouts, to some extent?Urias
Foo should not be global these days, we have include, requirejs, commonjs etc.Vernonvernor
As a nit-pick, "native" should be "built-in". The term "native" is no longer used in ECMA-262, but it used to be defined as any built-in object or those created by running code, so var x = {} creates a native object. E.g. see ECMA-262 ed 5 §4.3.6.Pulpit
I would say that if it's done correctly it can be almost harmless. Other languages such as Objective-C has used this approach for years. In Objective-C there are some conventions to follow when you do it, such as prefixing your functions with a unique three character prefix (see link below). So I could in essence just prefix my extensions with tsw_, and the chances of collision becomes so low that it's almost insignificant. developer.apple.com/library/archive/documentation/Cocoa/…Pageantry
I'm coding JS for almost 23 years. I wrote the trim() prototype to String, because I was needing it, later vendor shipped trim() function came... What happended? Nothing, websites I delivered before, with the trim() protoype continued to work as before because it was overwriting vendor's. Years later when it became widespread within browsers, I decided to remove it from my functions library... Use prototypes, overwrite vendor when needed, don't worry, be brave!Receivable
@Receivable So you got lucky because your self-invented trim() method did exactly the same thing as the native one does now. What if it didn't (e.g. used different parameter)?Mavilia
@Mavilia I was almost sure that it will come with same name and same method. When I'm not sure, I'm using an unrelated prefix (ie. my name initials or smtg) to solve the problem easily. So far didn't have any problems. Also somewhen, if I collect my methods together, then they will be a library for someone else who likes.Receivable
T
164

When you extend an object, you change its behaviour.

Changing the behaviour of an object that will only be used by your own code is fine. But when you change the behaviour of something that is also used by other code there is a risk that you will break that other code.

When method are added to the object and array classes in javascript, the risk of breaking something is very high - due to how javascript works. Long years of experience have taught me that this kind of stuff causes all kinds of terrible bugs in javascript.

If you need custom behaviour, it is far better to define your own class (perhaps a subclass) instead of changing a native one. That way you will not break anything at all.

The ability to change how a class works without subclassing is an important feature of any good programming language, but it is one that must be used rarely and with caution.

Tocopherol answered 25/12, 2012 at 21:51 Comment(25)
So adding something like .stgringify() would be considered safe?Interfere
@silvinci no, I don't think it is safe to add anything.Tocopherol
@AbhiBeckert According to LSP, extending an object should extend the behavior (not completely change it) in such a way that the object can be used in the same context as the base object. Also, can you elaborate more on the last paragraph?Mayenne
@Mayenne Note though that "extend" in the context of the LSP means "create a subtype", not "modify the existing type in-place". Nevertheless, a fair point.Ivar
@delnan that's true, but we only talked about adding something to the prototype, not changing anything. In that sense, I see no issues with that, except when you need to integrate libraries. And the code may be harder to understand.Mayenne
There are plenty of issues, for example what happens if some other code also tries to add it's own stringify() method with different behaviour? It's really not something you should do in everyday programming... not if all you want to do is save a few characters of code here and there. Better to define your own class or function that accepts any input and stringifies it. Most browsers define JSON.stringify(), best would be to check if that exists, and if not define it yourself.Tocopherol
@Mayenne It "adds" by using assignment, which will also override anything that's already stored under that attribute name. One can avoid this with relative ease, but as the question does not anticipate this, it should at least be mentioned.Ivar
I prefer someError.stringify() over errors.stringify(someError). It's straightforward and perfectly suits the concept of js. I'm doing something that's specifically bound to a certain ErrorObject.Interfere
The only (but good) argument that remains is, that some humongous macro-libs could start to take over of your types and may interfere with each other. Or is there anything else?Interfere
You're right, it does create nice code when you extend native objects. But it's not worth the cost. Ugly code is better than buggy code. I'm not sure what you consider a "humongous macro-lib", but everything I work on is big enough to contain code written by many people, not all of whom I have even met, let alone know what assumptions they have made or will make in future about the behaviour of native objects.Tocopherol
@LanceCaraccioli yeah, but mootools/etc are in the unique position of extending something that is an international standard moving at glacial speeds. It takes 5 or 6 years for a proposed change in JavaScript to actually be implemented. XMLHttpRequest was first proposed by Microsoft in 1998 or 1999, and added to the standard as a draft in April of 2006. Microsoft added it to IE in October 2006. It's still only a draft standarda today, last modified in january 2014. Who knows when it will be an actual standard.Tocopherol
@LanceCaraccioli Apple's proprietary frameworks are different, they make backwards incompatible changes to the SDK at the world wide developer conference each and every year. There is always a long list of things that break each year, and if your code is affected you'll have a few short months of beta period to update your code to handle the changes. Experienced Obj-C programmers avoid doing things that are likely to run into this problem, and Apple's official recommendation is that you can extend native objects but it should be avoided as much as possible.Tocopherol
If I look confused it's because I'm giving you the benefit of the doubt. Otherwise @Abhi Beckert JS is the context of the OP and Apple's proprietary frameworks is not the context of the OP. I'll clarify. Changing the behavior of a global resource in unexpected and backward incompatible ways is obviously incorrect. Augmenting such objects to add additional and intuitive behaviors is done by many leading JS libraries. To take it one step further. If a lib changes Array.push to Array.pop, then don't use it. Otherwise extensions like MooTools' Object.toQueryString are rather useful.Tiein
@LanceCaraccioli the problem is anything you add might be added by Apple in future, especially if it obviously belongs as part of that class. Your addition may have the same name as something Apple has, and then yours will replace Apple's, which will cause bugs. Also unlike JavaScript, UIKit makes extensive use of it's own classes, so if you change how one UIKit class behaves you will break many other classes in unknowable ways. I've been doing obj-c for more than 10 years, trust me - adding methods to a UIKit class is dangerous.Tocopherol
Who is talking about Apple? If standards change you are correct you may have to update your source code. This is not the topic at hand.Tiein
I find myself agreeing with @LanceCaraccioli. If you're not overriding functionality of preexisting methods, then adding your own methods is fine.Banal
@Banal adding your own methods is fine as long as your method is not the same name as any method Apple adds in future or is already using secretly. The last part is the problem, Cocoa and CocoaTouch are both full of instance methods that exist, but are not publicly documented anywhere. If you implement a method with the same name on the same class, your method will override the "secret" method without any error message or warning, and your app may or may not continue to function. You will also be rejected from the app store.Tocopherol
Here's a quick real example on what kind of bug this can cause (try this in console): function printObj(a){ for(var i in a) { console.log("i->a: ",[i,a[i]]) } }; arr=[1,2,3]; printObj(arr); console.log("------------"); Array.prototype.example = function(){console.log('hi')}; printObj(arr); ^I shot myself in the foot with this one. You might expect the prototype method not to appear as a property in the iterations.Adele
@Adele that's a bad example since it relies on a terrible language design flaw in JavaScript and would not happen in any other language.Tocopherol
@AbhiBeckert Hmmm, I thought this question's scope was limited to JS Could you expand on what you mean? What is the design/feature flaw called? Thanks for the feedback!Adele
@Adele the question was about JS but my answer applied to all languages. JavaScript has two design flaws applicable here 1. it doesn't properly segregate code and data, so your executable code is treated as if it's data (and the other way around too, data is treated as code). 2. the Array implementation doesn't define a "proper" data structure, it just uses the built in data structure used for all objects, meaning it's impossible for an array to contain properties that aren't part of the array. These two mistakes do not exist in almost any other language.Tocopherol
If the issue is that you might have a collision, could you use a pattern where everytime you did this, you always confirm that the function is undefined before you define it? And if you always put third party libraries before your own code, you should be able to always detect such collisions.Senter
@Sahuagin that only helps you detect a problem, it doesn't actually solve it. Renaming a function can be a lot of work depending how much it's being used, especially if third party libraries are involved.Tocopherol
@Adele You made an enumerable property in the Array prototype. That's your error. Use defineProperty as mentioned in many other places and your bug goes away. Maybe it still helps you today.Aric
You didn't provide any examples but you mentioned your lengthy experience. How does this answer the question and if you can justify it through your so called experience? The code should not get so lengthy in the first place for you not to be aware what can be using it. If you don't know how your code is affected by the builtins then its not your code and you should know at least all the side effects to mitigate risk. Good job on mentioning that it needs to be used with caution, being cautious of everything that is going on is a programming requirement, good job on pointing the obvious.Mammalian
I
41

There's no measurable drawback, like a performance hit. At least nobody mentioned any. So this is a question of personal preference and experiences.

The main pro argument: It looks better and is more intuitive: syntax sugar. It is a type/instance specific function, so it should be specifically bound to that type/instance.

The main contra argument: Code can interfere. If lib A adds a function, it could overwrite lib B's function. This can break code very easily.

Both have a point. When you rely on two libraries that directly change your types, you will most likely end up with broken code as the expected functionality is probably not the same. I totally agree on that. Macro-libraries must not manipulate the native types. Otherwise you as a developer won't ever know what is really going on behind the scenes.

And that is the reason I dislike libs like jQuery, underscore, etc. Don't get me wrong; they are absolutely well-programmed and they work like a charm, but they are big. You use only 10% of them, and understand about 1%.

That's why I prefer an atomistic approach, where you only require what you really need. This way, you always know what happens. The micro-libraries only do what you want them to do, so they won't interfere. In the context of having the end user knowing which features are added, extending native types can be considered safe.

TL;DR When in doubt, don't extend native types. Only extend a native type if you're 100% sure, that the end user will know about and want that behavior. In no case manipulate a native type's existing functions, as it would break the existing interface.

If you decide to extend the type, use Object.defineProperty(obj, prop, desc); if you can't, use the type's prototype.


I originally came up with this question because I wanted Errors to be sendable via JSON. So, I needed a way to stringify them. error.stringify() felt way better than errorlib.stringify(error); as the second construct suggests, I'm operating on errorlib and not on error itself.

Interfere answered 26/12, 2012 at 4:24 Comment(6)
I'm open on further opinions on this.Interfere
Are you implying that jQuery and underscore extend native objects? They don't. So if you're avoiding them for that reason, you're mistaken.Kepi
Here's a question that I believe is missed in the argument that extending native objects with lib A may conflict with lib B: Why would you have two libraries that either are so similar in nature or are so broad in nature that this conflict is possible? I.e. I either choose lodash or underscore not both. Our current libraray landscape in javascript is so overly saturated and developers (in general) become so careless in adding them that we end up of avoiding best practices to appease our Lib LordsCatherin
@Catherin - a library could decide to modify a standard object just for the convenience of its own internal programming (that's often why people want to do this). So, this conflict isn't avoided just because you wouldn't be using two libraries that have the same function.Waltner
-1 for the (possibly not intended) implication that jQuery and underscore extend native types, which isn't true, and also because I reject the idea that merely having third-party library functions available that you don't use is somehow problematic. For one thing, I probably understand a greater proportion of Lodash or jQuery than I do of the native APIs that my browser ships with; for another, why should I be scared just because some functions exist that I don't use? They aren't going to eat me.Anta
You can also (as of es2015) create a new class which extends an existing class, and use that in your own code. so if MyError extends Error it can have a stringify method without colliding with other subclasses. You still have to handle errors not generated by your own code, so it may be less useful with Error than with others.Archbishop
M
22

In my opinion, it's a bad practice. The major reason is integration. Quoting should.js docs:

OMG IT EXTENDS OBJECT???!?!@ Yes, yes it does, with a single getter should, and no it won't break your code

Well, how can the author know? What if my mocking framework does the same? What if my promises lib does the same?

If you're doing it in your own project then it's fine. But for a library, then it's a bad design. Underscore.js is an example of the thing done the right way:

var arr = [];
_(arr).flatten()
// or: _.flatten(arr)
// NOT: arr.flatten()
Mayenne answered 25/12, 2012 at 22:26 Comment(3)
I'm sure TJ's response would be to not use those promises or mocking frameworks :xSlowworm
The _(arr).flatten() example actually convinced me not to extend native objects. My personal reason to do so was purely syntactical. But this satisfies my aesthetic feeling :) Even using some more regular function name like foo(native).coolStuff() to convert it into some "extended" object looks great syntactically. So thanks for that!Pleasurable
Underscore.js is an example of the thing done the right way How can you know? What if my mocking framework uses _ as function? What if my promises lib uses _ as function?Israelite
A
19

If you look at it on a case by case basis, perhaps some implementations are acceptable.

String.prototype.slice = function slice( me ){
  return me;
}; // Definite risk.

Overwriting already created methods creates more issues than it solves, which is why it is commonly stated, in many programming languages, to avoid this practice. How are Devs to know the function has been changed?

String.prototype.capitalize = function capitalize(){
  return this.charAt(0).toUpperCase() + this.slice(1);
}; // A little less risk.

In this case we are not overwriting any known core JS method, but we are extending String. One argument in this post mentioned how is the new dev to know whether this method is part of the core JS, or where to find the docs? What would happen if the core JS String object were to get a method named capitalize?

What if instead of adding names that may collide with other libraries, you used a company/app specific modifier that all the devs could understand?

String.prototype.weCapitalize = function weCapitalize(){
  return this.charAt(0).toUpperCase() + this.slice(1);
}; // marginal risk.

var myString = "hello to you.";
myString.weCapitalize();
// => Hello to you.

If you continued to extend other objects, all devs would encounter them in the wild with (in this case) we, which would notify them that it was a company/app specific extension.

This does not eliminate name collisions, but does reduce the possibility. If you determine that extending core JS objects is for you and/or your team, perhaps this is for you.

Ambassadoratlarge answered 16/7, 2015 at 17:57 Comment(0)
A
11

Extending prototypes of built-ins is indeed a bad idea. However, ES2015 introduced a new technique that can be utilized to obtain the desired behavior:

Utilizing WeakMaps to associate types with built-in prototypes

The following implementation extends the Number and Array prototypes without touching them at all:

// new types

const AddMonoid = {
  empty: () => 0,
  concat: (x, y) => x + y,
};

const ArrayMonoid = {
  empty: () => [],
  concat: (acc, x) => acc.concat(x),
};

const ArrayFold = {
  reduce: xs => xs.reduce(
   type(xs[0]).monoid.concat,
   type(xs[0]).monoid.empty()
)};


// the WeakMap that associates types to prototpyes

types = new WeakMap();

types.set(Number.prototype, {
  monoid: AddMonoid
});

types.set(Array.prototype, {
  monoid: ArrayMonoid,
  fold: ArrayFold
});


// auxiliary helpers to apply functions of the extended prototypes

const genericType = map => o => map.get(o.constructor.prototype);
const type = genericType(types);


// mock data

xs = [1,2,3,4,5];
ys = [[1],[2],[3],[4],[5]];


// and run

console.log("reducing an Array of Numbers:", ArrayFold.reduce(xs) );
console.log("reducing an Array of Arrays:", ArrayFold.reduce(ys) );
console.log("built-ins are unmodified:", Array.prototype.empty);

As you can see even primitive prototypes can be extended by this technique. It uses a map structure and Object identity to associate types with built-in prototypes.

My example enables a reduce function that only expects an Array as its single argument, because it can extract the information how to create an empty accumulator and how to concatenate elements with this accumulator from the elements of the Array itself.

Please note that I could have used the normal Map type, since weak references doesn't makes sense when they merely represent built-in prototypes, which are never garbage collected. However, a WeakMap isn't iterable and can't be inspected unless you have the right key. This is a desired feature, since I want to avoid any form of type reflection.

Adipose answered 1/10, 2016 at 16:3 Comment(5)
This is a cool use of WeakMap. Be careful with that xs[0] on empty arrays tho. type(undefined).monoid ...Punchboard
@naomik I know - see my latest question the 2nd code snippet.Adipose
How standard is support for this @ftor?Fannie
-1; what's the point of any of this? You're just creating a separate global dictionary mapping types to methods, and then explicitly looking up methods in that dictionary by type. As a consequence, you lose support for inheritance (a method "added" to Object.prototype in this way can't be called on an Array), and the syntax is significantly longer/uglier than if you genuinely extended a prototype. It'd almost always be simpler to just create utility classes with static methods; the only advantage this approach has is some limited support for polymorphism.Anta
@MarkAmery Hey pal, you don't get it. I don't lose Inheritance but get rid of it. Inheritance is so 80's, you should forget about it. The whole point of this hack is to imitate type classes, which, simply put, are overloaded functions. There is a legitimate criticism though: Is it useful to imitate type classes in Javascript? No, it isn't. Rather use a typed language that supports them natively. I am pretty sure this is what you've meant.Adipose
S
9

One more reason why you should not extend native Objects:

We use Magento which uses prototype.js and extends a lot of stuff on native Objects. This works fine until you decide to get new features in and that's where big troubles start.

We have introduced Webcomponents on one of our pages, so the webcomponents-lite.js decides to replace the whole (native) Event Object in IE (why?). This of course breaks prototype.js which in turn breaks Magento. (until you find the problem, you may invest a lot of hours tracing it back)

If you like trouble, keep doing it!

Styliform answered 2/5, 2018 at 11:51 Comment(0)
A
5

I can see three reasons not to do this (from within an application, at least), only two of which are addressed in existing answers here:

  1. If you do it wrong, you'll accidentally add an enumerable property to all objects of the extended type. Easily worked around using Object.defineProperty, which creates non-enumerable properties by default.
  2. You might cause a conflict with a library that you're using. Can be avoided with diligence; just check what methods the libraries you use define before adding something to a prototype, check release notes when upgrading, and test your application.
  3. You might cause a conflict with a future version of the native JavaScript environment.

Point 3 is arguably the most important one. You can make sure, through testing, that your prototype extensions don't cause any conflicts with the libraries you use, because you decide what libraries you use. The same is not true of native objects, assuming that your code runs in a browser. If you define Array.prototype.swizzle(foo, bar) today, and tomorrow Google adds Array.prototype.swizzle(bar, foo) to Chrome, you're liable to end up with some confused colleagues who wonder why .swizzle's behaviour doesn't seem to match what's documented on MDN.

(See also the story of how mootools' fiddling with prototypes they didn't own forced an ES6 method to be renamed to avoid breaking the web.)

This is avoidable by using an application-specific prefix for methods added to native objects (e.g. define Array.prototype.myappSwizzle instead of Array.prototype.swizzle), but that's kind of ugly; it's just as well solvable by using standalone utility functions instead of augmenting prototypes.

Anta answered 29/12, 2017 at 18:59 Comment(0)
C
3

Perf is also a reason. Sometimes you might need to loop over keys. There are several ways to do this

for (let key in object) { ... }
for (let key in object) { if (object.hasOwnProperty(key) { ... } }
for (let key of Object.keys(object)) { ... }

I usually use for of Object.keys() as it does the right thing and is relatively terse, no need to add the check.

But, it's much slower.

for-of vs for-in perf results

Just guessing the reason Object.keys is slow is obvious, Object.keys() has to make an allocation. In fact AFAIK it has to allocate a copy of all the keys since.

  const before = Object.keys(object);
  object.newProp = true;
  const after = Object.keys(object);

  before.join('') !== after.join('')

It's possible the JS engine could use some kind of immutable key structure so that Object.keys(object) returns a reference something that iterates over immutable keys and that object.newProp creates an entirely new immutable keys object but whatever, it's clearly slower by up to 15x

Even checking hasOwnProperty is up to 2x slower.

The point of all of that is that if you have perf sensitive code and need to loop over keys then you want to be able to use for in without having to call hasOwnProperty. You can only do this if you haven't modified Object.prototype

note that if you use Object.defineProperty to modify the prototype if the things you add are not enumerable then they won't affect the behavior of JavaScript in the above cases. Unfortunately, at least in Chrome 83, they do affect performance.

enter image description here

I added 3000 non-enumerable properties just to try to force the any perf issues to appear. With only 30 properties the tests were too close to tell if there was any perf impact.

https://jsperf.com/does-adding-non-enumerable-properties-affect-perf

Firefox 77 and Safari 13.1 showed no difference in perf between the Augmented and Unaugmented classes maybe v8 will get fixed in this area and you can ignore the perf issues.

But, Let me also add there is the story of Array.prototype.smoosh. The short version is Mootools, a popular library, made their own Array.prototype.flatten. When the standards committee tried to add a native Array.prototype.flatten they found the couldn't without breaking lots of sites. The devs who found out about the break suggested naming the es5 method smoosh as a joke but people freaked out not understanding it was a joke. They settled on flat instead of flatten

The moral of the story is you shouldn't extend native objects. If you do you could run into the same issue of your stuff breaking and unless your particular library happens to be as popular as MooTools the browser vendors are unlikely to work around the issue you caused. If your library does get that popular it would be kind of mean to force everyone else work around the issue you caused. So, please Don't Extend Native Objects

Crashland answered 3/11, 2019 at 3:38 Comment(4)
Wait, hasOwnProperty tells you whether the property is in the object or the prototype. But the for in loop only gets you enumerable properties. I've just tried this out: Add a non-enumerable property to the Array prototype and it won't show up in the loop. No need not to modify the prototype for the simplest loop to work.Aric
But it's still a bad practice for other reasons ;)Crashland
hahah, I read the SmooshGate and what they've done about renaming the feature from "flatten" to "flat" is totally wrong, why? Renaming it to "flat" is itself the scandal! Because custom defined prototypes overwrite to vendor specific ones, so websites having MooTools library wouldn't get affected! Because when mootools is loaded to memory, it would overwrite vendor version and continue to work flawlessly as beforeReceivable
@Receivable The problem was specifically that MooTools didn't overwrite it if it already existed natively. I'm not sure why MooTools was written that way, but in actual fact it did affect websites using MooTools.Honghonied
M
0

Edited:

After a time I've changed my mind - prototype pollution is bad (however I left example at the end of the post).

It may cause a lot more trouble than mentioned in above and below posts.

It's important to have single standard across whole JS/TS universe (it would be nice to have consistent npmjs).

Previously I wrote bull**it and encouraged people to do it in their libraries - sorry for this:

Jeff Clayton proposal seems to be also good - method name prefix can be your package name followed by underscore eg: Array.prototype.<custom>_Flatten (not existing package prefix may became existing package in future)


Part of original answer:

I was personally extending native methods, I am just using x prefix in my libraries (using it also when extending 3rd party libraries).

TS only:

declare global {
  interface Array<T> {
    xFlatten(): ExtractArrayItemsRecursive<T>;
  }
}

JS+TS:

Array.prototype.xFlatten = function() { /*...*/ }
Mars answered 2/7, 2021 at 12:17 Comment(3)
prefixing with a specific unique-ish code name that does not match any packages might be wiser than a single letter for more effective future proofing: Array.prototype.fiderCMD_flattenTanberg
"as the only problem is colliding with future standard" - that is not the only possible problem. The other problem is that some other library could try to add something with the same name but different behaviour, and then either that library or your own code breaks, and which one depends on evaluation order.Honghonied
Add a prefix (I personally prefer a suffix), separated by an underscore (like, Array.prototype.x_Flatten). That way, one will with relative certainty not interfere with future additions to the standard, because they do not use snake_case, just camelCase.Israelite

© 2022 - 2024 — McMap. All rights reserved.