3

In ES6 we can use a rest parameter, effectively creating an Array of arguments. TypeScript transpiles this to ES5 using a for loop. I was wondering is there any scenarios where using the for loop approach is a better option than using Array.prototype.slice? Maybe there are edge cases that the slice option does not cover?

// Written in TypeScript
/*
const namesJoinTS = function (firstName, ...args) {
    return [firstName, ...args].join(' ');
}

const res = namesJoinTS('Dave', 'B', 'Smith');
console.log(res)
*/

// TypeScript above transpiles to this:
var namesJoinTS = function (firstName) {
  var args = [];
  for (var _i = 1; _i < arguments.length; _i++) {
    args[_i - 1] = arguments[_i];
  }
  return [firstName].concat(args).join(' ');
};
var res = namesJoinTS('Dave', 'B', 'Smith');
console.log(res); //Dave B Smith

// Vanilla JS
var namesJoinJS = function (firstName) {
  var args = [].slice.call(arguments, 1);
  return [firstName].concat(args).join(' ');
};
var res = namesJoinJS('Dave', 'B', 'Smith');
console.log(res); // //Dave B Smith
Drenai
  • 11,315
  • 9
  • 48
  • 82
  • 2
    it's for performance reasons: https://stackoverflow.com/a/24011235/6567275 – Thomas Feb 16 '19 at 12:58
  • your outcommented code seems to be usual js code due to it is missing any types – messerbill Feb 16 '19 at 13:00
  • ts would look like this: `const namesJoinTS = (firstName: string, ...args: any): Array { return [firstName, ...args].join(' '); }` – messerbill Feb 16 '19 at 13:05
  • 1
    @messerbill every valid piece of JS is valid TS. Whether it contains Types or not. Although when some parts of your code rely on JS' Type coercion TS may get confused and assume that it is a mistake; as TS prefers explicit Type casts/conversions – Thomas Feb 16 '19 at 13:06
  • @Thomas sure, but if you do not make use of the typings you should just use javascript – messerbill Feb 16 '19 at 15:29
  • @Drenai just like Thomas mentioned, every valid JS is also valid TS code – messerbill Feb 16 '19 at 15:33

1 Answers1

2

This weird transpilation is a side effect of the biased optimization older versions of V8 had (and might still have). They optimize(d) some certain patterns greatly but did not care about the overall performance, therefore some strange patterns (like a for loop to copy arguments into an array *) did run way faster. Therefore the maintainers of libraries & transpilers started searching for ways to optimize their code acording to that, as their code runs on millions of devices and every millisecond counts. Now as the optimizations in V8 got more mature and are focused on the average performance, most of these tricks don't work anymore. It is a matter of time till they get refactored out of the codebase.

Additionally JavaScript is moving towards a language that can be optimized more easily, older features like arguments are replaced with newer ones (rest properties) that are more strict, and therefore more performant. Use them to achieve good performance with good looking code, arguments is a mistake of the past.

I was wondering is there any scenarios where using the for loop approach is a better option than using Array.prototype.slice?

Well it is faster on older V8 versions, wether that is still the case has to be tested. If you write the code for your project I would always choose the more elegant solution, the millisecond you might theoretically loose doesn't matter in 99% of the cases.

Maybe there are edge cases that the slice option does not cover?

No (AFAIK).


*you might ask "why is it faster though?", well that's because:

arguments itself is hard to optimize as

1) it can be reassigned (arguments = 3)

2) it has to be "live", changing arguments will get reflected to arguments

Therefore it can only be optimized if you directly access it, as the compiler then might replace the arraylike accessor with a variable reference:

 function slow(a) {
   console.log(arguments[0]);
 }

 // can be turned into this by the engine:
 function fast(a) {
  console.log(a);
 }

This also works for loops if you inline them and fall back to another (maybe slower) version if the number of arguments changes:

 function slow() {
   for(let i = 0; i < arguments.length; i++) {
     console.log(arguments[i]);
   }
}

slow(1, 2, 3); 
slow(4, 5, 6);
slow("what?");

// can be optimized to:
function fast(a, b, c) {
  console.log(a);
  console.log(b);
  console.log(c);
}

function fast2(a) {
  console.log(a);
}

fast(1,2,3);
fast(4, 5, 6);
fast2("what?");

Now if you however call another function and pass in arguments things get really complicated:

 var leaking;

 function cantBeOptimized(a) {
   leak(arguments); // uurgh
   a = 1; // this has to be reflected to "leaking" ....
 }

function leak(stuff) { leaking  = stuff; }

cantBeOptimized(0);
 console.log(leaking[0]); // has to be 1

This can't be really optimized, it is a performance nighmare. Therefore calling a function and passing arguments is a bad idea performance wise.

Jonas Wilms
  • 132,000
  • 20
  • 149
  • 151
  • 1
    Thanks. That's a great answer. I prefer the more succinct `slice` approach. Good to know about the optimization - that would be a priority for transpilers all right – Drenai Feb 16 '19 at 17:06