Future JavaScript: what is still missing?
2ality.com
2ality.com
Warts and all, one of the best things about ES5 and earlier is that the language was so simple that you could pick it up in a day.
The later additions have added developer convenience for sure, but at the expense of taking something simple, consistent, and very easy to read, and turning it into a complex and subtle language with a lot of redundant syntax. For example all the different syntaxes for defining a function with the subtle differences between them. And in this articles proposal - since switch doesn’t do what I want, we’ll have both switch AND case with different semantics and syntax. Ugh.
At this point I’d rather see an effort to deprecate cruft or standardize features over throwing in new things.
Promises made it better but that was still verbose. Then async/await was added that made code read more linearly.
There was also the unintuitiveness of how var declarations were always function scoped compared to how most developers would think that variables should be blocked scoped using let and const.
Also I understand how prototypical inheritance works and I know that the class syntax is just syntactic sugar, but it is a lot more readable.
And it feels like an odd move in that class based OOP isn't the zeitgeist it once was. Shoehorning it in feels like a throw back to 90's and 00's.
One way to look at it is that you could consider not teaching the underlying prototypal inheritance model at all. If students will rarely run into code that relies on it, it may be vestigial semantics.
Think of it like how people used to teach patterns in C for doing object-oriented programming. For example, you could have a struct whose first field was a pointer to a table of functions for the operations you could perform on the object. In other words, how to implement your own vtables from scratch.
When C++ came along, it was still useful to understand vtables sometimes, but it wasn't something every programmer needed to fully internalize. C++ let them program at a higher level of abstraction.
> And it feels like an odd move in that class based OOP isn't the zeitgeist it once was.
Citation needed. :) Two of the newest, fastest growing languages are class-based: Kotlin and Swift. I'm not aware of any new prototype-based language that has noticeable growth.
Prototypes are a neat idea, but I think the market has pretty clearly shown that classes are a better practical approach to OOP.
In particular, I still run into usage of `call` and `apply` on a regular basis.
class Foo {
constructor () { this.foo = 0; }
log () { return this.foo; }
}
(new Foo()).log.call({ foo: 2 });
I guess I can explain the above without prototype, but it's certainly weird behavior for a classical OOP language, and at the very least it's dancing around prototype-like behavior.Or, much worse:
class Bar { constructor () { return new Foo(); } }
And you can say, "how often is a student ever going to run into that case", but part of being a student is experimenting with code. If an abstraction only holds as long as you never do something weird, then it's probably not a very good abstraction. In that case it's worth asking, "is it good language design to have a feature give surprising results as soon as you do something that's even slightly off of the beaten path?"However, I would say there's some difference in severity between a surprising behavior that results from the intersection between two features, and a surprising behavior that results because the way a feature is described is literally inaccurate.
You can break Javascript classes without using any other feature of the language. You don't have to resort to something like Proxies, or overriding native prototypes. This is because it's not that other parts of Javascript provide escape hatches to get around behavior, it's that the class syntax itself does not cover the entirety of the prototype system it is trying to obscure.
It's not ideal to have any breakage in an abstraction, but some breaks are worse than others. To me, edge cases like this:
let foo = function () { this.x = 5; }
class Bar extends foo { //No error? But why?
constructor () { super(); }
}
foo = function () { this.x = 7; }
console.log((new Bar()).x); // 5, not 7 or undefined.
are on a whole nother level of leaky abstractions over even some of the worst parts of C#. To me, this is an edge case that nearly any reasonably imaginative student would likely find at some point using only very basic components of the Javascript language. And I have no idea how I would even begin to tackle explaining this without saying, "Okay, throw out everything I taught you about classes, here's how the language really works."To understand what that means, you need to understand what `this` is in Javascript -- that classes don't actually bind methods to an instance. So we have this idea of methods from other languages that turn out to be kind of the wrong way of thinking about things.
From `this`, the obvious next question is, "so what's the difference between adding a method to a class and just attaching it to an object?" And it turns out that practically, the difference is very small -- and in fact it's very easy for us to remove or add methods to a class that look and act very similarly to the methods in a class descriptor. So what we're calling a "class" is no longer a descriptor or a set of laws about what an instance is and what it can do, it's just a blueprint of how to build an instance at runtime.
We haven't gotten rid of classes yet, but we're already working with something that feels foreign to a number of programmers coming out of traditional OOP languages. `this` means that what a traditional OOP programmer from Java thinks of as a "method" doesn't exist. It means that the context of a function is determined at runtime, not at compile time.
You don't need to use the word "prototype" to talk about that, but it's what I would call very prototype-ish behavior. Arguably, by the time someone understands `this`, they've already learned about half of what they need to know to grok prototypes anyway -- all that's left is to tell them that if a property isn't found on an object, there's a special property JS will traverse to look for it elsewhere. But sure, we can delay talking about that for right now.
> A constructor can override its return value and return something else besides the new instance of its class.
Constructors complicate things more. We already know from above that a class isn't really a descriptor of what an object can do, instead we're thinking about it like a blueprint that tells us how to create an object at runtime (this is also incorrect, but we're sticking with that definition because we don't want to use the word "prototype"). Now we find out we can override the return value of a constructor. The obvious question a good student will ask is, "why? What use-case is there for treating a constructor function like a normal function?"
Again, we don't have to jump to prototype from here, but we do kind of need to explain that the `new` keyword is actually doing most of the work when we build a class, and the reason we can do weird things with a constructor is because a constructor is just another function. But we'll avoid prototype by saying that `new` just makes an instance of a class, and that behavior can be overridden by returning another object from the constructor that `new` invokes.
And when the student figures out that they can call `new` on an anonymous function without returning anything and it still creates an object, we can delay talking about prototype even farther, because we'll just lie and say that pure functions are a shorthand for making classes that are only a constructor.
But increasingly the question becomes, why wouldn't we talk about prototype now? Ostensibly, the point of classes is that it hides all the weirdness of the prototype system. In my experience, people struggle with prototypes because they're not Java. But JS classes also aren't Java, or at least they stop being Java as soon as you do anything even mildly interesting with them. So if people who learn classes in depth still have to grapple with the fact that class definitions are constructed at runtime, and that instance methods can be modified after they're created, and that methods aren't inherently bound to instances at all, what have we gained?
I just think people really over-emphasize the difference between "class-based" and "prototype-based" languages as if they're completely different in how users code with them. In my experience, the primary interesting consequences of JS's prototype-basedness vs class-based languages is that you can call methods with arbitrary `this` values, monkey-patch classes at runtime, or even change the class of an object at runtime. In other languages, these kinds of features would be called reflection, they would be in a late chapter labeled "advanced features", and people wouldn't worry about teaching them to students before they had a good handle on the rest of the language.
I think prototype inheritance is often more useful than class inheritance; I do not agree that classes are necessarily better. Prototype inheritance can be used for more kind of things (especially with Object.create and Object.getPrototypeOf; I sometimes use these).
In the mobile space, Swift, Java, and now Kotlin are all class based.
“The zeitgeist” is different than “what companies are paying real money for”.
Besides, even with OOP based inheritance, the language is still “lying” just as much. Most developers who don’t understand how the lowest level of computers work don’t understand that when you call a function it’s always basically passing in a this pointer to one static instance of the function.
Object orientation as in "There must be classes, there must be inheritance, each method and instance value must be able to be private, public, or protected, and have official constructors and destructors" and probably a couple of other things I'm missing, is also alive and well and not going anywhere any time soon, but is no longer the only definition of Object Orientation.
If you were not around at the time, you'll have to take my word for this, although the echos still reverberate in Javascript tutorials to this day (because of the way they talk about prototype-based inheritance as if you've got to be placated long enough about the fact that it isn't "true" OO long enough to learn what it actually is), but when it was released, Javascript (prior to any sort of "class" keyword) was not considered an "object oriented" language, because it didn't have "real" inheritance, or private values, etc. etc.
The point is that in a world where the definition of "OO" has expanded enough to encompass the original JS, it's a bit odd to bodge on a particular definition of OO so late in the process.
(People also argued back then about whether Python was object oriented (!), because it didn't have "true" support for private instances or methods. By contrast, I find it amusing when sometimes the question comes up "Is Go really Object Oriented if it has no inheritance?" and the answer generally amounts to "Honestly, who cares?")
But personally I prefer prototypal inheritance and the everything is an object mentality.
What does class mean?
It’s all just syntactic sugar in any language. Before the class keyword, it was just more explicit in JS.
A class is a function in JS, a class is a separate type in most other languages. I don’t see a meaningful difference.
In class oriented languages like C# and Java, sure. JS with ES modules has the alternative of using functions exported directly from a module.
The latter approach has the benefit of better tree shaking than the class based equivalent.
class a {}
typeof a == 'function'
Objects in JavaScript are not class based. You can read here about the diferences:
As that link says, objects are not fundamentally class-based because objects don't have to have a class. But you could add classless objects to almost any language without making their classes fake. And when it talks about inheritance, well, you can inherit state in C++ too, it's called 'static'.
Or in shorter form: It's not class-based but it does have classes.
You can implement anything in any general purpose language. But that doesn't mean the language has that concept.
It's pretty normal for runtimes to not know about certain language concepts. The C runtime has never heard of switches or while loops, no big deal. If it's in the compiler it's part of the language.
And it doesn't matter what typeof says. I could take a C++ program, replace every instance of the word 'class' with 'struct', and it would work fine. It would still be using classes, even though typeof says 'struct'.
Typeof in javascript calls the constructor a function and it calls objects objects. That's correct and fine. You can't put a class itself into a variable in C++ anyway.
And if you really want to get into the gritty details, it's possible to make a very similar argument that C++ of all languages doesn't have classes. Sure they exist in the source code, but all that pretty syntax gets thrown away and you end up with a naked object or one that just has a pointer to a struct it inherits from. And there's not even a 1:1 relationship between source-code 'classes' and those structs!
Yes, but prototype is not fixed. Prototype chain is not fixed (you can extend it at any point). All of that is changeable at runtime. You can also change the object's prototype at runtime.
It's all dynamic, and prototype ("class" as you say) doesn't provide any structure/constraints to the object. Thus it's pretty pointless to call it a class. It's just property sharing via a prototype chain. It's nothing like classes in languages like C++, Java, PHP, etc. And it doesn't help anyone understand the language, if you try to see it through that paradigm.
I’m just now putting the finishing touches on my first Node/Express microservice for production. I’m putting off front end development for another year or so.
I am very much against constant change in a fundamental platform. I should be able to write JS that works for decades without worrying about deprecation or any other sort of useless "churn". The less the platform changes, the more you can focus on understanding it and doing more. But of course it seems the majority of developers would rather have "new and shiny" over "old and stable", which I think is quite unfortunate.
The old saying is "do more with less". What's happening now in development, especially Web, seems to be more like "do less with more".
And what does `case` not do that you want it to? Fall-through? Re: forced destructuring, if you want to switch on a value, why not just wrap it in an object, eg, `case({ someVal })`? It's not perfect, but I'd prefer consistently using `case` to the confusion you call out re: `case` vs `switch`.
You can't remove features from JS runtimes (browsers or node), but you can certainly remove them from a new language version. "use strict" already did it once in JS. It can be replaced/upgraded with "use ecmascriptX.Y" for each language version.
And transpilers targeting JS and tools like packers and minifiers can enforce language versions without declaring them in each file.
These days I work mainly with C#, and to a lesser extent Typescript and JavaScript - the anemic nature of the ECMAScript standard library is really jarring.
It also has a rich collection of concurrent data structures, but those aren't useful in Javascript (yet).
LinkedList is almost indistinguishable from an growable array. Since the JS array grows by default, you're covered for the kinds of things you'd want a linked list for (in fact, most older JS implementations actually implemented arrays as linked lists).
ArrayDeque's big claim to fame is add/remove from both ends. That's also covered by the standard array (shift/unshift, pop/push).
Linked hashmap's big feature is maintaining insertion order. The JS Map type does this automatically. JS Objects don't do this officially, but since so much legacy code depends on this "feature", it is implemented for objects across all major browsers.
Priority Queue isn't in JS, but I'd argue it's rather niche and rather few async frontend JS projects need it. On the backend, one extra import is hardly a big deal.
There is also no treemap. They are simple to implement (esp with proxies). JS objects store all keys as strings and `Object.keys(foo).sort()` is probably about as fast. As an additional issue, JS maps can have keys of different types, so deciding how to perform that sort would be very difficult. This also seems like a niche issue that belongs outside the standard library.
Not only they have different performance for various operations (append, prepend, indexing, removal in the middle), but perhaps even more importantly, linked lists can share data if it's immutable. So an array is not a replacement.
JS implementations are free to choose whichever array representation works best be that hashtable, struct, class, linked list, array, or whatever. Modern JITs use a couple different approaches that have been found to be fastest and can even switch representations on the fly based on several factors (is the array homogeneous, is it sparse, is it short, med, or long, and so on).
Even moving away from JS, there should be a healthy skepticism for basic O notation about data structures and algorithms. When your processor is thousands or millions of times faster than your ability to send over data from memory, the notion of what is faster changes a lot. If my linked list misses in cache because it's memory isn't sequential, then the time it takes to add an element and read may be much longer than copying an array and expanding it a few entries (especially if a fill pointer is used).
That said, JS has Object.freeze() which can make an object immutable. You could do this recursively to make a guaranteed immutable object. Immutable libraries in JS tend to use other methods for performance reasons though IIRC.
This is the world we live in and it is unique to javascript.
Shipping a decent set of core libraries would do wonders for package sizes. You could cover a lot of use cases with a relatively small API surface area (the classic 80/20 rule) and you'd find that a lot of duplication in implementing the fundamentals in third party packages would just go away.
Browsers update pretty quickly now, anyway. It's likely that in the future we will see most sites using a lightweight package for modern browsers and a heavy polyfill for IE, which is fine by me.
I also missed the awesome bind-operator. EG:
const getFoo = () => this.foo;
const bar = { foo: 1 };
console.log(bar::getFoo()); // 1
https://github.com/tc39/proposal-bind-operator +value|0
There's also TypedArray [0]. I think you'll be interested in the BigInt proposal [1] [2], which is stage 3. It's already available in Chrome, and behind a flag in Firefox. Now that it's possible to accurately represent 64-bit values, we're getting BigInt64Array and BigUint64Array.[0] https://developer.mozilla.org/en-US/docs/Web/JavaScript/Refe...
You would need to use `function` for this:
function getFoo() {
return this.foo;
}If you're putting getFoo at whatever level of scope that you'd run it over multiple bar objects that don't have it bound, why is it conceptually different than writing it as
getFooOfBar(this_bar:hasfoo) => this_bar.foo
(If you get my drift from the terrible notation; and you could drop the type-safety and just take an any obj if we're being more literal to your pseudocode)I had honestly not seen the bind operator used in at least the last decade, so perhaps the dissonance is just whatever corner of tech I work in having a distinct style/idioms.
Why exactly do you want that? Is it arbitrary precision you want? Is the 56 bits or whatever not enough for your use-case? Or do you find the range checks are too expensive in practice?
Keeping the extensible web manifesto in mind, I wonder if maybe what we really want is to have some kind of standardization around transpiling code.
With new features like promises, we had extensive testing via user libraries before they made their way into the language. But with these kinds of syntactic features, that doesn't seem to be happening.
If a feature is almost purely syntactic sugar, maybe it would make more sense for it to be supplied via a transpiler rather than being baked into the core language? Are there pain points or disadvantages to libraries like Babel that we could address via the core language instead?
I think the main disadvantage is that compilers need to target something, and then JS engines need to be able to optimize that code even after it's transpiled. That's something that bringing these features into the language itself can help.
Take a look at async/await for an example of this. For a fairly long time it was extremely slow to run async/await when it was compiled to es5, because the subtleties required some fancy work on the part of the compilers, even going so far as to include a small runtime to achieve it.
Once it was supported by the JS engines themselves, it pretty quickly matched the speed of code full of `.then` with promises.
It's obviously not impossible, but I'm guessing it's much harder. There's a reason why the x86 architecture is still getting new features and additions, and it's not just for the comfort of people writing raw assembly.
Then you need to think about other tooling. Linters, compilers, static analyzers, minifiers, they all need a way to analyze the code you are writing. If that code is using features that aren't "standard" (or are on track to become standard maybe), then it would quickly get much messier than it already is.
You aren't just talking about compiling now, but also a unified way of adding "plugins" to linters, compilers, static analysis tools, syntax highlighters, minifiers, and more. Because adding a "syntax plugin" is a non-starter if it means we can't minify our code or run our linter, and creating a new "syntax plugin" would be much harder if you also had to make plugins and changes to all of those things to even let it begin to become useful.
Adding this stuff to the language is where it belongs, things like types on the other hand I firmly believe belong in "compile to JS" languages or dialects like TypeScript or Flow. That's a great abstraction level for that, simply because it doesn't change the output of the code (for the most part), it just enforces restrictions on it.
Are there ways we can help with this? Improvements we could be making to sourcemaps? Maybe we should be looking into adding new descriptors that are designed specifically to help generated code get read by debuggers? Maybe some kind of support for marking regions of code so you can run multiple transpilers on the same codebase?
I dislike transpilation in its current form, but I can think of things off the top of my head that would make it easier.
> On top of that having as a standard means you can go anywhere and just use it without worrying about which transpiler they are using.
Fair enough, there are definitely advantages to having a large set of core features. But if you look at languages like modern C++, it seems that there are also disadvantages that come along with that.
Given that the web is far more permanent than most other languages (we can never remove a JS feature), are the advantages of having one code style worth the disadvantages of not being able to test new syntax in the wild before we standardize?
There's a balance here -- I like lodash, a lot. But I don't want lodash to be part of the core language. Maybe some syntax fits into that category as well?
Note that Node is actually in a much better position than Javascript as a whole, because Node apps can be shipped with a specific version of Node. This means that the Node team actually has more power than web browsers to deprecate old features and remove parts of the library, as long as they do so carefully. It is much harder to make breaking changes in a browser than it is to make them in Node.
One middle ground I've seen people suggest is to have officially "blessed" external libraries that still need to be imported, but that are cached locally or have special privileges. For example, JQuery was at one point so common that the Firefox dev tools actually ship with a subset of the JQuery API enabled -- just so devs that are used to working with it can use it in the debugger on any site.
I look at JQuery as a massive success story for how 3rd-party extensions should work. It was extremely ubiquitous, it did influence the core language, but now we've grown and because it was kept separate, we're still free to abandon it and move on.
On the Node side maybe that means that Node ships with dependencies that still need to be added to your package.json, but that are always bundled in your installation so that you have them locally and don't need to hit a network request when you type `npm install X`.
Also on the Node side we have modules that are built by the core team, but just not distributed in the core library. This helps out a bit with security, because you can still have a large organization like Google maintaining a feature and validating it, they just ship it separately from their browser.
JavaScript is already jam packed with subtleties and gotchas that make it hard to learn, including situations where two things have the same name or the name doesn't quite match the underlying concept: C-style "for" vs. "for ... in" vs. "for ... of," object vs. Object, Object.prototype vs. "the prototype of the object," etc. We don't need another one of those.
I'd actually like this if it used another word than "case." "choose ... when", maybe? Kinda awkward, but I bet someone could think of something better. "case" has the nice feature that it's already a reserved word, but it's not valid syntax to have a variable name right next to an open brace, so the parsers could figure it out.
const func = do {
let result;
() => {
if (result === undefined) {
result = someComputation();
}
return result;
};
};
Shouldn't that be "if (result === undefined) then someComputation else result"? Why import functional style and proceed to make it inherently imperative with a return in the middle of the block? obj?.foo?.bar?.baz
The point of optional chaining is that you can place functions within the chain. That seems to be a very surprising and useless way to do them. const func = (() => {
let result; // cache
return () => {
if (result === undefined) {
result = someComputation();
}
return result;
}
})();
You could write it without the immediately invoked arrow funciton or a do expression if `result` is in the same scope as `func` let result;
const func = () => {
if (result === undefined) {
result = someComputation();
}
return result;
}
Result is in the same scope as func, inviting anyone to read from or write to it.In all three examples `func` is assigned a zero argument function. That function returns the result of calling `someComputation`, but it stores the value in `result`. If `result` is not undefined `func` returns `result`. `func` is a cached, memoized, or lazy-loaded version of someComputation.
A do expression will return the last evaluated expression in it's scope, in this example that expression is `() => {...}`, not `return` because the inner anonymous function wasn't called.
---
I think a clearer demonstration of do expressions is an assigning switch or ifelse
const value = do {
switch (someString) {
case 'Monday':
0;
break;
case 'Tuesday':
1;
break;
...
}
}
const other = do {
if (isPrime(n)) {
'prime'
} else if (isOdd(n)) {
'odd'
} else {
'even'
}
}Is the example on the article wrong? A functional "if" shouldn't accept just one side of the branch, a functional switch shouldn't accept a "break", and although it's not absurd that a functional code block accepts a "return", it is not usual.
const value = (() => {
switch (someString) {
case 'Monday':
return 0;
case 'Tuesday':
return 1;
...
}
})(); function getStatusString(n) {
if(isPrime(n)) {
return 'prime';
}
if(isOdd(n)) {
return 'odd';
}
return 'even';
}
Much easier to read, understand and maintain than an inline monster. No need to add complicated new language constructs for simple situations.....Javascript doesn't have private modules so moving logic to helper functions in util modules doesn't reduce scope bloat.
function memoize = function (fn) {
var value
return function() {
return value !== undefined ? value : (value = fn())
}
}
var func = memoize(someComputation)
Why would anyone want to write that do/iife abomination inline?People who come from other languages often make “dumb mistakes” because they try to apply their existing mental models of programming to a language that works differently.
Honestly I'm kind of surprised that Racket didn't stand out to you, it's a super cool language and very different from everything else on your list. How much time have you spent using it?
I really liked Racket a lot actually! It turned me on to the whole functional programming style that I still try to maintain in JavaScript. Unfortunately, I only got to write it for a semester in school and when I moved on to JavaScript enjoyed not having the insanity of all those parentheses and not having to do everything myself (via js array functions, lodash, and the npm ecosystem)
> enjoyed not having the insanity of all those commas and not having to do everything myself
Racket has a pretty robust std library that includes things like folds, maps, zips and whatever else you'd be using from lodash. It's also pretty void of commas, not really sure what you mean.
Always? I hope you're only speaking for yourself? Lots of people come to JavaScript from another language, never put in the time to learn JavaScript, and then just complain about it. That used to be me. And then I committed myself to learning the language really well. And now I enjoy using it. I know lots of programmers who also love it. I've used a lot of languages but I don't think I'm qualified to say which is the "best". But I do know that I really like JS.
But then again, even the author mentions at the beginning the "risk of adding too much" and, well, I wonder if this has not already happened to JavaScript.
The examples the article gives don't really look all that syntactically different to me.
const func = (() => {
let result; // cache
return () => {
if (result === undefined) {
result = someComputation();
}
return result;
}
})();
const func = do {
let result;
() => {
if (result === undefined) {
result = someComputation();
}
return result;
};
};
And I guess I wouldn't be able to recurse in a do expression like I can with a named function? So it's not like I can actually universally stop using immediately invoked expressions everywhere.In this specific case, the IIF is harder to read. You have to go all the way to the end of the expression in order to realize intent. With 'do', you see it right away.
Like anything in engineering, it's a question of trade-offs. Do the benefits of better readability outweigh the downside of making the language more complex?
I'm a fan of 2.2: destructuring switch, although I occasionally employ something similar in JavaScript and other languages with a `switch` statement:
const resource = await fetch(jsonService);
switch (true) {
case resource.status === 200:
console.log(`size is ${resource.headers['Content-Length']}`);
break;
case resource.status === 404:
console.log('JSON not found');
break;
case resource.status >= 400:
throw new RequestError(res);
break;
}
It's missing the destructuring aspect, so the first case is a bit more verbose, and I imagine there's better potential to optimize something like the OP's example. But it has a similar effect to the example and works today.Slightly related: I really wish `case` worked with brackets instead of break, similar to OP's example. It looks cleaner and is easier to visually parse. I suppose that's only better if you prefer brackets (which I do).
return data?.error?.message ?? "no error";
Also pattern matching would be a big win.
https://github.com/tc39/proposal-optional-chaining
https://github.com/tc39/proposal-nullish-coalescing
Recently started using optional chaining in our codebase (via Babel 7) and it's great so far. Also gives us proper static typing safety unlike Lodash `get(...)`
let val = 0
val || 30 // 30
val ?? 30 // 0
The same goes for other falsy values.1. The ability to run my code without transpiling and reinstall dependencies for 10 minutes.
2. The ability to know which of the last 5 version of javascript to use.
3. Stability in my dependencies.
4. Consolidation and standardizing on established frameworks.
Basically undoing all the mess the ecosystem has done over the last 8 years.
In most traditional languages if you learn a framework you'll get a decade of use out if it. ROR was the first hacker community to really screw this up.
Vue got a lot of attention on Hacker News because it was new, but it has plateaued with at most 1/8th of React's market share. It is not going to be replacing it. React is more dominant than any JavaScript technology has ever been except maybe jQuery (whose responsibility was a lot narrower). It's not my personal favourite, but I appreciate having a de-facto standard across almost everything I have to work on.
Google is mostly irrelevant to this discussion: they have extreme Not Invented Here syndrome and cling to terrible in-house technologies for front-end development. (They have been gradually increasing their React adoption over the last few years, but it will probably remain small.) They were once the world leader in JavaScript development, but they haven't treated it seriously and are now significantly worse than all of their competitors. If you go to Google, you're committing to learning a lot of non-transferable knowledge (in addition to a lot of valuable practices).
For instance in C# you can take the same LINQ expressions and they can be translated to sql by EF or MongoQuery by the Mongo driver at runtime.
var retiredMales = custRepo.Find(c => c.Age > 65 and c.Sex == “M”)
and then have an interface with a member:
List<Customer> Find(Expression<Func<Customer,bool>> expression):
You can implement three classes for that interface - one that uses EF, one that uses Mongo, and one that just uses in an memory List for unit testing and they all translate the expression to the underlying data store.
Theoretically, you could have an “expression” type in JS that can be parsed by the provider.
For those who gave never used linq: this allows the above function to translate the function to a sql, sparql or mongo query depending on the currently selected backend
With enough of this, at what point will all languages start to look identical (Youngme Moon calls this “homogeneous heterogeneity”)?
And how often does a language come along that introduces something truly new, rather than porting an idea from another popular language?
Is there any reason to "leave features to others" if they make sense in your own language? The only issue I can come up with is that some language might die off because it doesn't have any unique features. But if that happens and the momentum is not enough to keep it alive, that doesn't seem like loss to me anyway.
I think it's wonderful. Developers can move between languages interchangeably, teaching burden is reduced, languages aren't dismissed because they lack feature X.
I especially like the pipe operator, even though I think the `|>` is kinda clunky asthetically.
That way you avoid bloating the core language with everyone’s pet features (I’d also love pipe syntax, but am not sure it belongs in JS core).
(OTOH, note that nobody is proposing to add this sort of high-level stuff to wasm. That's because it's designed to be a compilation target, and it's actually good at it.)
'like' operator that is used as a match
if( obj like /[A-Z]/g ) … // "string".match
if( obj like {name:String} ) … // obj has name string field
if( obj like [String,Object] ) … // obj is an array of at least two elements, string and object.
extended switch() : switch(obj) {
case 1: … // standard JS
like /[A-Z]/g: … // RegExp, Object, Array match
instanceof cls: … // obj instanceof cls
in [1,2,3]: … // obj
} var style = {
font-size: 10px,
border-width: 1em
}
New built-in value types: Length, Duration, Angle, Color. So these are valid constructs: this.timer(200ms, function() {...});
element.style.width = 1em;
var c = rgb(128,128,0);
assert c.r == 128;
var (h,s,v) = c.toHSV();
Float and Integer are distinct types as these are different entities indeed. 1 in [1,2,3] -> true
"foo" in {foo:1,bar:2} -> true
Real namespaces: namespace Foo
{
function a() { return "a"; }
function b() { return a(); } // Foo.a() call
}
namespace Key { // enums
const ENTER = 13;
const SPACE = 32;
}
if( evt.keyCode == Key.ENTER )
... [1,2,3] == [1,2,3] -> true in Sciter, false in JS
[1,2,3] === [1,2,3] -> false in Sciter and in JS
{a:1} == {a:1} -> true in Sciter, false in JS
{a:1} === {a:1} -> false in Sciter and in JS[1, 2, 3] - [1, 3] #Yields [2]
The code is readable, intuitive and short yet many languages never have this simplicity.
JS example. https://stackoverflow.com/questions/1187518/how-to-get-the-d...
[1, 2, 3] + [1] == [1, 2, 3, 1]
but then if I subtract [1] again, does it take away the first 1, or the last 1?It turns out it removes both of them. So subtracting a list is not the inverse of adding a list. In Python, there is the distinction between lists and sets, and while both support the + operator, only sets support the - operator for this reason.
Please. I am not a Ruby coder but I fully expect any language that a + b - b is just a and also a + b - a is just b.
[1b, 1c, 1d, 3] (remove the first occurrence only, no matter the RHS has many occurrences)
[1a, 1b, 1c, 3] (remove the last occurrence only)
any of [1a, 1b, 1c, 3], [1a, 1b, 1d, 3] etc. (remove any single unspecified occurrence)
[1c, 1d, 3] (remove the first K occurrences where K is the multiplicity)
[1a, 1b, 3] (remove the last K occurrences)
any of [1a, 1b, 3], [1b, 1c, 3], [1c, 1d, 3] etc. (remove any K unspecified occurrences)
[1a, 1b, 1c, 1d, 3] (remove a whole group of equal occurrences only when multiplicites match)
[1a, 3] (flatten both the LHS and the RHS and calculate the set difference)
[1d, 3] (same as above, but the set flattening prefers later occurrences)
any of [1a, 3], [1b, 3], [1c, 3] or [1d, 3] (same as above, but the set flattening is unspecified)
any of [1a, 3], ..., [1d, 3], [1e, 3] or [1f, 3] (same as above, but without any guarantee of the identical element)
runtime error, as there are multiple same elements in the LHS and/or the RHS
[0, 0, -1, NaN, NaN, NaN] (okay, I'll stop being clever and calculate actual numeric differences)
See? The "array difference" is not enough. Something like the "array difference with multiple occurrences removed in the order of appearance" would work, but this refutes the OP's claim of the "array difference" being "readable" and "intuitive".I'd argue backwards compatibility is actually a lot less important in the JS world than it currently appears to be. Why not aggressively deprecate all the uglyness, broken semantics, etc. If you write ES 2020, or whatever they will call it, why would you still want broken floating point math, wacky equals behavior that nobody can explain with a straight face, etc. Just deprecate it, start disallowing it, and eventually remove it from the language in a couple of versions.
Browser javascript is effectively a compilation target these days. Yes you can write in it directly but many people don't do that. Transpilers seem to be the norm these days. Mostly transpilers already do work around javascript legacy but they still have to deal with hiding some of the ugliness.
The good news is that people update their browsers. So there is very little need for transpilers to be able to target old versions of browsers. Mostly they just need to deal with current and recent versions.
With the emergence of WASM, that is becoming a more obvious compilation target. You can already run C, Kotlin, Rust, Code, etc. using it. Doing so, you bypass all of the legacy of javascript. It's just another compilation target for compilers.
It's not such a big leap to simply run javascript interpreters/vms on top of WASM as well. Why not? It's not any different from doing the same with ruby or python. It would be a great way to support legacy applications and it completely removes the need for javascript to continue to support legacy features. IMHO, it would greatly simplify browser implementations if all they had to do is support running WASM + whatever standardized bindings they ship for things like the DOM, browser storage, webgl, etc.
You could freeze a current version of v8, call it legacy js and use that to run legacy javascript if/when you actually need it and rely on a modern JS interpreter without any of the baggage. My guess is that running legacy js would very rapidly become a niche thing.
Anyone have a sensible pure JavaScript example for:
range(5).forEach(fn)
I haven't found one that felt small enough to use inline. It end up using traditional for loops. for (var n in 5) // n is in [0..5)
should just work as it does in Sciter. _.range(5).forEach(i => {/*...*/}) Array(5).fill().forEach(() => console.log('hello')) Array.from({ length: 5 }).forEach(...)
But I'm not sure what you gain from that compared to just wrapping a for loop in a function. You still don't get ranges the way you do in, say Ruby: ('a'..'z').each {|letter| puts letter }I'm guessing it constructs the whole array upfront. Maybe there's a generator way to do it too.
One language that gets it mostly right is Python, where "==" always compares values, and "is" always compares references. Thus, you can use either for e.g. lists and sets, depending on what you actually need at any given time. However, Python fails in that it doesn't give the same flexibility in many contexts where the comparisons are implicit - e.g. in dicts and sets (it always uses ==, instead of letting the user specify an equality function).
Ironically, a language that gets it fully right - including the standard library - is VB.NET.
I'm not even sure that value types themselves are such a useful concept. A value type can be thought of as an immutable reference type, where any mutations done via the reference simply cause a new object to be created and the reference changed (semantically; under the hood, it can be easily optimized down to in-place update).
case (resource) {
when {status: 200, headers: {'Content-Length': s}} -> {
console.log(`size is ${s}`);
}
when {status: 404} -> {
console.log('JSON not found');
}[1]: https://blogs.msdn.microsoft.com/dotnet/2019/01/24/do-more-w...
“Perfection is achieved when there is nothing left to take away.” --Antoine de Saint-Exupéry
This principal should be applied more to programming languages.
Or even just complete support for asmjs with interop and program in completely other languages, especially since the person who wrote this seems to want to program in languages with different semantics...
https://developer.mozilla.org/en-US/docs/Web/JavaScript/Refe...
I wouldn't expect him to discuss "dropping compatibility" and "completely other languages"
At this point programming in other languages makes more sense and is arguably a better vision for “the future of JavaScript.”
Well, uh, okay
I wish I knew about python's [itertools](https://docs.python.org/3/library/itertools.html) before I started making [streaming-iterables](https://www.npmjs.com/package/streaming-iterables#api) for example.
Many of the modules and libraries currently support Promises, and `async/await`, but try to find a way to cancel a long-running Promise. Some libraries have resorted to using their own mechanisms, and some (e.g. `axios`) resorted to using a withdrawn TC39 proposal.
I'd like a standard, easy way to cancel a Promise. I know it's not simple, and Promises can wrap around other Promises, etc. but if I'm e.g. uploading a large file to a server, there should be an easy way to provide my users with a "cancel" button that works.
[1] https://developer.mozilla.org/en-US/docs/Web/API/AbortContro...
[2] https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API
They're not panaceas, they're just alternative approaches to programming no worse nor better and they don't need to be brought up in every. Single. Post.
That "without qualification" is an odd thing to say regarding a couple of (flamewars) discussions that have been going on for sixty years. There are thousands, millions of lines of literature discussing advantages and disadvantages of every paradigm and every type system.
> It is entirely possible (if not highly likely) that one approach is better
And when such approach is discovered and/or conclusively proven as better, I'm sure the entire industry will converge upon it. Computer science is, after all, a science.
What I just wrote is obvious and common knowledge that I'm sure you're aware of, but I couldn't think of another way to explain my point. Hopefully I was clear.
One addition that could greatly improve simplicity is assignable blocks. A block provides scope, much like a function, but accepts no input and returns nothing. Most functions I write take no input and return undefined because I only need them for code reuse by reference.
Imagine code like this:
if (expression) myBlock else otherBlock; if (expression) myBlock() else otherBlock()
is really if (expression) {myBlock();} else {otherBlock();}
In that case a block is still there and not reused. Both the semantics and structure are different if the block itself were reused. You could extend the above like so: if (expression) {myBlock();x+=1;} else {otherBlock();}
If instead the block were reused that would not work. A finer degree of separation is imposed by reference that reenforces separated structures by name. Separation is the foundation of simplicity. It also makes for code that is easier to read like a natural language sentence.Basically if I want to keep a cache of expensive objects that will grow monotonically, I need to do a lot of work to make sure I'm not leaking memory right now, but if I could instead store in a Map<string, weakref>, it'd be much easier, things could go out of ref and I could deal with that.
Other than that, JS could benefit tremendously from a more complete standard library, and a build system, including a file watcher, that doesn't rely on 200 nonsensical dependencies.
Swift's is also tied to their `Optional<T>` wrapper type, any potential JS implementation would be more like Groovy's.
[1] https://web.archive.org/web/20050815012153/http://groovy.cod... , bottom of the page
https://github.com/apple/swift/blob/master/LICENSE.txt
TC39 (who defines Javascript) is a collaboration of several companies, so I'm not sure who Apple could/would even sue if this was proposed and adopted for JS. I doubt we would see legal activity over it.
Which has been around since 1998 at least (When Haskell 1.3 added monads)
[0]: https://github.com/jashkenas/coffeescript/blob/1.0.0/lib/par...
Will it lead to many JS bugs simply disappearing and applications getting more stable or will it just obfuscate bugs in the long run?
In TypeScript, these properties would explicitly have a `MyType|null` or `MyType|undefined` type.
"or will it just obfuscate bugs in the long run?"
Probably. :shrug emojii:
But more stable in the sense of turning hard crashes into UI quirks.
if (let myVar = exp) { if(let [x,y] = thing) ...
Would only be true when thing is an array with two elements (not super sure bow pattern matching is defined in ha. I would guess that actually if the match fails then things get to be undefined which means the match can’t really fail so this would likely be confusing)It is one of the most convenient and readable pieces of syntax, and one of the primary reasons I pause before using javascript instead of Elixir in a given moment.
Bigint is a good thing they added. Sometimes you need to deal with 64-bit integers, and other times you might want unlimited range, so that helps. However, some operations that are not currently built-in perhaps should be in order to be more efficient, such as log2 of bigint numbers or counting trailing bits.
A built-in RegExp.quote() function is another thing (although it is easy to make, it seem to me it is something that ought to be built-in).
If macros are implemented, then many features can be done by macros and do not need to be built-in features.
One feature of JavaScript that I dislike is automatic semicolons.
And yet, many proposals (including these) may be rejected if better ways are found.
+1 for pipeline operator +1 for a destructuring switch
https://blogs.msdn.microsoft.com/devops/2013/07/01/debugging...
So you don't need to deal with "inscrutable callback state machines" anymore so than you need to deal with the assembly labels and jumps that your for-loop compiles into.
A handy helper I use all the time for this:
export const jsonEqual = (a, b) =>
JSON.stringify(inspect(a)) === JSON.stringify(inspect(b))We can open a window, write arbitrary text to it, use the browswer to 'Save as...'. Close but no cigar.
I'd like to use JS for a lot more than the web. MS Basic wrote user-entered text to a file 40 years ago.
Otherwise, Node.js can do exactly what you just said Python and MS Basic can do.
I can already write to file with 'window.(something).createObjectURL' ... which queries user. I simply suggest that 'future javascript' open() (or whatever) supports that with no-more-code needed.
Either all variables shall have types and so static, compile time analysis can be performed.
Or no types at all.
Types are useful only if they can be checked/verified at compile time.
Types in only arguments cannot be verified by compiler. So type mismatch can be checked only at runtime. So would be this:
function foo(a:Number) { … }
Is essentially this: function foo(a) {
if( !(a instanceof Number)) throw "Not a number";
…
}
And that rises the question: who and at what moment will handle such errors?Nobody will handle such errors - contract violations are not supposed to be handled, they're a coding bug. You "handle" them by fixing the caller such that it never violates the contract. The benefit of having a runtime check is that it can detect the error instead of silent wrong behavior potentially followed by state corruption and further bugs. Most of the time, these will fire in automated tests, anyway.
4 6 8
Extend the operators to work on lists!
Obligatory lodash reference: _.map(_.unzip(arrays), _.sum)
I also find personally that recognising something as bad is the first step to making something better.
(I write javascript almost every day)
At the moment however (I believe that it's still the case), WASM still needs to interact with the DOM via JS, so there's a minimum amount of JS boilerplate required for bootstrapping.
But seriously, be careful not to ruin it with too much “I need this!”
It's like saying promises shouldn't have been added because you can still write bad code with them and create nested promise hells ( I sure did when I first learned them). Isn't it better to say, no, best practice is to add ? only where you actually need them. The solutions he cites are not really solutions, they're just abstracting the problem away which most of as already do, but then the abstraction becomes hard to read because those checks have to be somewhere.
Worse I think is the comparing objects by value idea. I think it would be way too easy for beginners to reach for that solution. There are often better work arounds, it's quite rare imo to need to do a deep nested check of two objects that could be very different (i.e. contain different properties). Plus it's not like using a helper function is that verbose and a custom one can be really fast (if both objects contain the same properties, creating a function that "manually" checks for them all is blazing fast), same thing with deep clones.
Things I would really like, from most to least: optional chaining obviously, realms, pipeline operator, optional type checking, and enums (you can sort of fake them though, hence why they're last)
You can't redesign your code not to need asynchronous callbacks. They're fundamental, given the constraints of the language implementation. Given that, it makes sense to make them as painless as possible (and I'd argue async/await is... well, it's not good, but it's better than Promise alone).
I would argue it makes a bad thing easier to read so you can actually tell what's being checked. It's already an easy thing to do, leaving things as they are is not deterring anyone, except it reads horribly. I'm not arguing too many aren't code smell, but what about when it's needed?
The object check syntax proposed on the other hand, imo, does not make things clearer and is of little benefit.
Also regarding callbacks, I meant you can avoid callback hell without using promises. Promises were not technically needed to solve that problem even though they're talked about as a solution to it, you can have promise hells too (and they are exactly what most beginners do with promises). But if used right, the solution is much easier to read and understand. That's how I see optional chaining.