Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Using 'ForOfStatement' is not allowed (no-restricted-syntax) #1271

Closed
MartialSeron opened this issue Jan 20, 2017 · 130 comments

Comments

Projects
None yet
@MartialSeron
Copy link

commented Jan 20, 2017

Hi,
Since everybody tell us to not use forEach() but use for loop instead because it is much faster (https://jsperf.com/test-if-using-forofstatement-is-not-allowed-makes-sens), I would like to know why this is not allowed by airbnb ?

@ljharb

This comment has been minimized.

Copy link
Member

commented Jan 20, 2017

a) microbenchmarks (ie, jsperfs) are not accurate reflections of real-world performance
b) performance isn't important. It's easy to make clean code fast; it's hard to make fast code clean.

@ljharb ljharb closed this Jan 20, 2017

@ljharb ljharb added the question label Jan 20, 2017

@AKST

This comment has been minimized.

Copy link

commented Feb 21, 2017

While I get this is an opinionated linter, the readability of forEach over for of is pretty debatable. But more importantly for of is actually more polymorphic as you can add the iterator property to any data type.

techinically you can add a forEach method to any data type, but 🤷‍♂️

Plus It's also more consistent, as the jQuery implementation of forEach orders parameters differently (yes some people still have to interact with jQuery), that and nodeLists's implementation of forEach is not present on all browsers, where as using for of works once you start using the babel polyfill.

for of is simply a more powerful feature.


For people landing here via google, here's the original definition if you want to over load it.

@ljharb

This comment has been minimized.

Copy link
Member

commented Feb 21, 2017

Array.prototype.forEach.call(nodeList) works in every ES5+ browser.

Separately, jQuery iteration methods should only be used for element collections, so I'm not sure why its element ordering really applies.

You can't yet add the iterator property to any data type, because Symbol is not polyfillable nor available in every browser we support. This guide does not allow using symbols currently.

@AKST

This comment has been minimized.

Copy link

commented Feb 22, 2017

Array.prototype.forEach.call(nodeList) works in every ES5+ browser.

True, but if the original argument against for of is that it isn't readability, then this isn't a strong argument in favour of forEach.

I'm not sure why its element ordering really applies.

It's less the ordering of the elements and more the ordering ordering of the parameters on invocation, the function you pass to jQuery forEach each passes the index through the first argument and the actual element in the second. 

However if the readability of Array.prototype.forEach.call isn't an issue for you, then this may be a non issue.

There's also the fact you can't yield from a forEach for an outer function the same way you can with a for of, or you use await in an async function (unless this rule accommodates for this).

Symbol is not polyfillable nor available in every browser we support.

Doesn't core-js (used by bable I believe) polyfillSymbol's, as well as well known symbols like the iterator symbol?


edit: Just so I don't waste your time, is there a link to previous discussion on what decisions led to for of being disabled? Because surely stuff like iterating in a generator or async function is something that's been considered.

@petertrotman

This comment has been minimized.

Copy link

commented Feb 22, 2017

I'd just like to chime in and say that I would also like a justification for why ForOfStatement is restricted. I would like to use for (let element of array) { ... } in places where I am producing side effects in the iteration.

for(let i = 0; i < array.length; i ++) { ... } is antiquated syntax, and while I know everyone understands what it means, we should be leaving it behind.

array.map has functional connotations and we shouldn't be producing side effects in the closure.

array.forEach is an option, but I personally don't like it for this sort of imperative work.

So I think the ForOfStatement should be removed from the restricted syntax for the above reasons - anyone with any conflicting viewpoints? Do we know what the original justification is?

@ljharb

This comment has been minimized.

Copy link
Member

commented Feb 22, 2017

The primary argument against for..of is that loops are awful, and should always be avoided. forEach is not a loop, it's an iteration.

The primary reason it's disabled in this guide is that Symbols can not be polyfilled, thus we forbid Symbols, thus Symbol.iterator is unavailable, thus for..of is useless in a generic sense.

@AKST

This comment has been minimized.

Copy link

commented Feb 22, 2017

The primary reason it's disabled in this guide is that Symbols can not be polyfilled, thus we forbid Symbols, thus Symbol.iterator is unavailable, thus for..of is useless in a generic sense.

This thing is though, they are polyfilled... Just look at this bable output of typeof Symbol.iterator, it's pretty clear their not using typeof right out of the box...

@ljharb

This comment has been minimized.

Copy link
Member

commented Feb 22, 2017

@AKST Symbol semantics can not be fully polyfilled, and typeof foo in dependencies will never print out "symbol". It's not a shim, it's a sham.

@AKST

This comment has been minimized.

Copy link

commented Feb 22, 2017

@kouhin

This comment has been minimized.

Copy link

commented Feb 23, 2017

@ljharb I wonder without ForOfStatement, how to loop Map.prototype.keys() and Map.prototype.values(),
e.g.

const map1 = new Map();
// map1.set(someKey, someValue);

for (let key of map1.keys()) {
  console.info(key);
}

map1.keys() and map1.values() are MapIterator. They are non-array and we can't use forEach or map1.keys().length.

Should we convert MapIterator to Array for loop?

@ljharb

This comment has been minimized.

Copy link
Member

commented Feb 23, 2017

@kouhin Array.from(mapOrSet, mapperFunction),Array.from(mapOrSet.keys(), mapperFunction), same for values/entries

in other words, yes - convert it to an array.

@johannpinson

This comment has been minimized.

Copy link

commented Mar 1, 2017

@ljharb for...of allow us to use break and continue statement (which can be usefull) but not a Array.map or Array.forEach

What's the solution to simulate this two behavior? (only encapsulate part of code in an if...else and so force to complete the iteration?)

Thanks for your advice

@ljharb

This comment has been minimized.

Copy link
Member

commented Mar 1, 2017

@johannpinson yes, you shouldn't need break or continue ever, that's GOTO. Can you provide a code example with a loop, and I'll try to provide a non-loop example?

@ljharb

This comment has been minimized.

Copy link
Member

commented Mar 1, 2017

Also for anyone struggling to understand the array iteration methods they should be using instead, https://gist.github.com/ljharb/58faf1cfcb4e6808f74aae4ef7944cff may be helpful.

@johannpinson

This comment has been minimized.

Copy link

commented Mar 2, 2017

hi @ljharb
I haven't special example which needed it, but i read that for simple loop/iteration of an array, for...of can be use instead of forEach (so only for ES6 / Babel), and also for larger ones, the possibility to break the loop avoid the need to finish the iteration of the array.

This is two simple examples with for..of that i have :

let artistName = ''
for (const artist of data.artists) {
  if (artistName === '') artistName = `<span class="Infos-artist-name">${artist.name}</span>`
  else artistName += `, <span class="Infos-artist-name">${artist.name}</span>`
}

and

database.getIndexes()
  .then((data) => {
    const dates = data.sort((a, b) => b - a)
    for (const date of dates) {
      const li = document.createElement('li')
      li.innerHTML = moment(date).format('ddd<br>DD/MM')
      li.classList.add('Nav-panel-date')
      li.setAttribute('data-date', date)
      li.addEventListener('click', (e) => { this.navTo(e) })

      document.querySelector('.Nav-panel-archive').appendChild(li)
    }
  })

I already use some methods as .map, .filter or .find, but always for...of or for...in for simple loop, often when it doesn't need a return value (like forEach).

An example of the use of break can be with the second code sample.
I retrieve an unkown number of dates, and in the case that i will stop to process it when I exceed the two last month. How can I do it with iteration ?
(Of course, I know that the best solution will be to pass a specific query to the api call that send back directly the good data 😉)

Thanks!

@ljharb

This comment has been minimized.

Copy link
Member

commented Mar 2, 2017

@johannpinson

const artistName = data.artists.map(({ name }) => `<span class="Infos-artist-name">${name}</span>`).join(', ');

and

  database.getIndexes().then((data) => {
    const dates = data.sort((a, b) => b - a);
    const listItems = dates.map((date) => {
      const li = document.createElement('li');
      li.innerHTML = moment(date).format('ddd<br>DD/MM');
      li.classList.add('Nav-panel-date');
      li.setAttribute('data-date', date);
      li.addEventListener('click', (e) => { this.navTo(e) });
      return li;
    });
    const panel = document.querySelector('.Nav-panel-archive');
    listItems.forEach((li) => { li.appendChild(li); });
  });

(i'd also recommend using event delegation and adding the click listener onto panel rather than adding it onto every li, but that's out of scope for this question)

@petertrotman

This comment has been minimized.

Copy link

commented Mar 2, 2017

This discussion has gone on for a while, and I am convinced that all for of statements could be transformed into Array.forEach statements.

However, it is my opinion that side effects in array iteration is a bad practice and you should be explicit when you are looping over some iterator to produce side effects (e.g. appendChild, Array.push, whatever). In this case, for of has much less of a 'code smell' than Array.forEach. All array methods (for me) have functional connotations that are completely broken by enforcing the use of Array.forEach.

Besides, there is currently no way to distinguish between Array.map and Array.forEach for side effects - you are quite able to do Array.map(el => root.appendChild(el)) without ESLint complaining - even where I'm sure we'd all agree that Array.forEach(el => root.appendChild(el)) is the better alternative. For this reason, for of should be the canonical way of side effect iteration, not Array.forEach, and this should be encouraged by the AirBnB preset.

@ljharb

This comment has been minimized.

Copy link
Member

commented Mar 2, 2017

@petertrotman that's exactly the point. side effecty iterations SHOULD be a code smell. (Array.map and Array.forEach are not functions, but I assume you're using Array as a placeholder here).

Specifically array.forEach((el) => { root.appendChild(el); }) is the better alternative (explicit return, not implicit).

for..of will not be allowed by the airbnb preset because it requires Symbols to exist, and Symbols can not truly be polyfilled - and regenerator-runtime is too heavyweight. This concern outweighs the minor concern that side-effecty iterations (which are rare) are slightly more statically detectable as for..of than as .forEach.

@petertrotman

This comment has been minimized.

Copy link

commented Mar 2, 2017

@ljharb Ok, I sympathise with your reasoning. Ideally, I would like two things to result from this discussion:

  1. A clear statement of 'This ESLint preset prefers Array.forEach over for of because the polyfill for for of is too heavyweight for the purpose.' somewhere in the README.
  2. Some way of determining whether Array.map is producing side-effects, and raising an error in this case which demands the use of Array.forEach instead.

I think the first should be ok, but I'm not savvy enough of the internals of ESLint to know how feasible the second part is.

@ljharb

This comment has been minimized.

Copy link
Member

commented Mar 2, 2017

The first has been made, by me, in this thread, multiple times.

The latter is unrelated to the use of forEach vs for..of, because it's a problem with .map. It'd be great to support, but it's impossible to statically know whether .map is called on an array or not.

@petertrotman

This comment has been minimized.

Copy link

commented Mar 2, 2017

@ljharb I understand. Still, I think it would be nice for this conclusion to be in the official README - when I searched for the reasoning I found this (closed) issue. I think that, given that this is a 'compromise' solution, it deserves some lines justifying it in the README. I agree with the justification now that you've pointed it out, I just think it is not obvious, and so should be officially made clear.

EDIT: I'll make a pull request for this myself - no reason to expect anyone else to do it :-)

@ljharb

This comment has been minimized.

Copy link
Member

commented Mar 2, 2017

A PR is always welcome, thanks!

@josser

This comment has been minimized.

Copy link

commented Mar 7, 2017

@petertrotman there is at least one exception when for..of can't be directly transformed to .forEach / .map:
yield can't be used in map / forEach callback, only in root scope of generator.

@ljharb

This comment has been minimized.

Copy link
Member

commented Mar 7, 2017

@josser yes but this guide forbids using generators, so that isn't relevant to this discussion :-)

@josser

This comment has been minimized.

Copy link

commented Mar 8, 2017

@ljharb I know, generators are forbidden because there is no lightweight polyfill for them in browsers.
But in current versions of nodejs we already use them wide.
Does it mean that airbnb is just wrong preset for backend developers?

@ljharb

This comment has been minimized.

Copy link
Member

commented Mar 12, 2017

@josser Although we haven't had the conversation internally yet, I don't think generators are ever a good idea to use, frontend or backend, even if they were trivially polyfillable. Where in node do you find yourself wanting to use them?

@josser

This comment has been minimized.

Copy link

commented Mar 13, 2017

@ljharb We are usually not using plain generators but async/await.
In #851 I posted example:

for (const model of Object.values(db)) {
    const trigger_version = await db.query('sql get trigger version for ${model}');
    if ( trigger_version =< current_trigger_version)  {
       await db.query('install trigger for sql ${model}');
    }
}

As I understand from discussion in those thread, the reasons of deny generators is same as for async/await. So I'm just called this 'generators' meaning both, generators and async/await.

However, I have interesting idea of using generators in web-scrapping. https://scrapy.org
I know it's python, but I have similar js-code to test this approach and it looks cool.

@ljharb

This comment has been minimized.

Copy link
Member

commented Mar 14, 2017

@josser async function is awesome, and it should be used any time that such use doesn't necessitate regenerator-runtime. await has many pitfalls, but this guide does not yet explain them because on the web, async/await usage still requires regenerator-runtime. In async functions, you do not ever need a loop to do useful things, although certainly using await in a for..of loop seems appealing.

@1pete

This comment has been minimized.

Copy link
Contributor

commented Jul 25, 2018

@nue-melexis i purpose

sortByObjectProperties(properties = []) {
  return (a, b) => {
    const prop = properties.find(p => a[p] !== b[p])

    if (prop == null) return 0;

    if (a[prop] < b[prop]) return -1;
    
    return 1;
  };
},
@nue-melexis

This comment has been minimized.

Copy link

commented Jul 25, 2018

You gave me a lesson 🤔 ,
will need to dig deeper into it. But nice different solutions.

Thx for you time and brain

@ericblade

This comment has been minimized.

Copy link

commented Jul 25, 2018

@simonbuchan is a human JSCompress. :-D

@vkarpov15

This comment has been minimized.

Copy link

commented Aug 2, 2018

I might be missing something here, so please excuse me if I'm ignorant, but iterators !== generators and the fact that this warning conflates the two looks bad. I don't see why for/of on an array would require regenerator. You'd just need to polyfill Symbol and convert for (let x of y) to something like let iter = y[Symbol.iterator]; for (let { value, done } = iter.next(); !done; { value, done } = iter.next()) {}. Admittedly I'm not an expert on how transpiling for/of loops works, so I'd appreciate some clarification on that.

I understand that Symbol polyfills are a "sham", but then perhaps the warning should be about Symbols and not about regenerator?

@ljharb

This comment has been minimized.

Copy link
Member

commented Aug 2, 2018

@vkarpov15 that's a fair question. You can't necessarily unroll a for..of loop into a regular loop in a generic fashion; the iterable protocol requires a try/catch, and a few other things.

Certainly polyfilling Symbols requires the warning, but that's a much more minimal issue than needing regenerator - however, even if for..of loops were possible without that transform, they're still loops, and this guide forbids loops.

@vkarpov15

This comment has been minimized.

Copy link

commented Aug 2, 2018

Then why does the warning mention regenerator at all, rather than indicating that disallowing loops is a purely stylistic choice? I'm not that concerned with the stylistic choice, I'm more concerned that the warning message makes some very wide logical leaps.

@ljharb

This comment has been minimized.

Copy link
Member

commented Aug 2, 2018

The warning includes "Separately, loops should be avoided in favor of array iterations.".

The reason both are mentioned is because indeed, some people avoid the clarity concern (note, this is not merely "stylistic", not that that would make it any less critical), but in general people are (sadly) more responsive to performance concerns.

@vkarpov15

This comment has been minimized.

Copy link

commented Aug 2, 2018

So what you're saying is that you're stretching the truth in order to make your opinion that this paradigm has better "clarity" seem more legitimate?

@ljharb

This comment has been minimized.

Copy link
Member

commented Aug 2, 2018

@vkarpov15 no, in NO WAY am i stretching the truth; both parts of the warning are 100% accurate. I'm saying that the reason we mention regenerator at all is because even though the clarity argument should be more than sufficient, some people needed the extra rationale.

@simonbuchan

This comment has been minimized.

Copy link

commented Aug 2, 2018

@vkarpov15 You're being weirdly confrontational?

@ljharb Perhaps swapping the reasons could make your priorities clearer? Currently the message is: iterators/generators require regenerator-runtime, which is too heavyweight for this guide to allow them. Separately, loops should be avoided in favor of array iterations. - it's pretty relevant that for-of transpilation doesn't have anything to do with regenerator-runtime (it's "just" unpacking the iterator and while until done inside a try), so it's weird that this message shows up here (if you could put this on async or function* that would make sense), and double weird it's the first thing you see, when it's not the main reason you're banning this syntax. Separately, (see what I did there 😉) I've no idea what "array iterations" is supposed to mean - obviously from context you're referring to array methods like .map(), but the phrase is confusing.

I'd prefer a message along the lines of: Loops are not permitted, prefer array methods such as `.map()`. Avoid using iterators as they have much worse performance in most cases.

Out of curiosity I checked jsperf again, using this microbench instead. I got on my Surface Book 2 15":

test Chrome 69 Firefox 61
for 2 ~200M ~380M
for ~960k ~1.3M
forEach (both) ~56k ~1.2M
for-of ~430k ~48k

Weird - that perf difference between for-of and forEach is so huge (in different directions!) it probably swamps whatever transpilation overhead there is?

@zeckdude

This comment has been minimized.

Copy link

commented Aug 25, 2018

@ljharb Any idea how I can use an iterator for my usecase? I am running through a series of validation methods for each field config object if a particular verification has been specified and returning the first one that comes up as true. The for...of loop seems great for my situation, so I'd be curious how I could accomplish this with an iteration without running through all the checks each time.

const fieldsConfig = [
  {
    value: email,
    verify: [
      {
        type: 'isPopulated',
        message: 'Please enter your email address',
      },
      {
        type: 'isEmail',
        message: 'Please format your email address correctly',
      },
    ],
  },
  {
    value: password,
    verify: [
      {
        type: 'isPopulated',
        message: 'Please enter your password',
      },
    ],
  },
];

const getValidationErrors = fieldsConfig => fieldsConfig
  .map((field) => {
    for (const verification of field.verify) {
      if (verification.type === 'isPopulated' && !isPopulated(field.value)) {
        return verification.message;
      }

      if (verification.type === 'isGreaterThanLength' && !isGreaterThanLength(field.value, verification.length)) {
        return verification.message;
      }

      if (verification.type === 'isEmail' && !isEmail(field.value)) {
        return verification.message;
      }

      if (
        verification.type === 'isMatched'
              && isPopulated(field.value)
              && isPopulated(verification.matchValue)
              && !isMatched(field.value, verification.matchValue)
      ) {
        return verification.message;
      }

      if (verification.type === 'isCustom' && !verification.condition) {
        return verification.message;
      }
    }
    return false;
  })

// Should return an array like so:
// ["Please enter your email address", "Please enter your password"]
@ljharb

This comment has been minimized.

Copy link
Member

commented Aug 25, 2018

@zeckdude

const getValidationErrors = fieldsConfig => fieldsConfig
  .map((field) => {
    const matched = field.verify.find(verification => (
      if (verification.type === 'isPopulated' && !isPopulated(field.value)) {
        return true;
      }
      if (verification.type === 'isGreaterThanLength' && !isGreaterThanLength(field.value, verification.length)) {
        return true;
      }

      if (verification.type === 'isEmail' && !isEmail(field.value)) {
        return true;
      }

      if (
        verification.type === 'isMatched'
              && isPopulated(field.value)
              && isPopulated(verification.matchValue)
              && !isMatched(field.value, verification.matchValue)
      ) {
        return true;
      }

      if (verification.type === 'isCustom' && !verification.condition) {
        return true;
      }
    ));

    return !!matched && matched.message;
  });

https://gist.github.com/ljharb/58faf1cfcb4e6808f74aae4ef7944cff is a helpful read.

@zeckdude

This comment has been minimized.

Copy link

commented Aug 25, 2018

@ljharb This is awesome. So educational! Thanks for taking the time to explain it and for your patience.

Btw, what does GOTO stand for?

@ljharb

This comment has been minimized.

Copy link
Member

commented Aug 25, 2018

@zeckdude “Goto” is a colloquial reference to programming styles/languages where one statement can arbitrarily jump execution to any other statement - see https://www.quora.com/Is-GOTO-still-considered-harmful for some more info.

@DominicTobias

This comment has been minimized.

Copy link

commented Sep 18, 2018

It's also bizarre when developing for Node which supports it natively that you get this error, even with browser: false

@ljharb

This comment has been minimized.

Copy link
Member

commented Sep 18, 2018

@DominicTobias loops are still something this guide prohibits, entirely unrelated to the bundle size argument, in node too.

@opyh opyh referenced this issue Oct 19, 2018

Merged

WIP: TTL caches #160

2 of 4 tasks complete
@erikeckhardt

This comment has been minimized.

Copy link

commented Apr 6, 2019

@zeckdude

Here's my take on that:

const validators = {
  isPopulated,
  isGreaterThanLength: (value, verification) => isGreaterThanLength(value, verification.length),
  isEmail,
  isMatched: (value, verification) => !isPopulated(value)
    || !isPopulated(verification.matchValue)
    || isMatched(value, verification.matchValue),
  isCustom: (value, verification) => verification.condition
};

const getValidationErrors = fieldsConfig => fieldsConfig
  .map(field =>
    field.verify.filter(verification =>
      !validators[verification.type](field.value, verification)
      && validator.message
    )
  );

This could be shortened by making your validator functions have parameters (field, verification) and using them appropriately. isMatched could also check isPopulated on its arguments instead of doing that here (here seems like the wrong place).

GaurangTandon added a commit to GaurangTandon/ProKeys that referenced this issue Jun 3, 2019

@AndreasClausen

This comment has been minimized.

Copy link

commented Jun 3, 2019

I still don't see a clear answer here, or anywhere else, what we should use as an alternative to:

for (const a of arr) {
    await someAsyncFunc(a);
}

where arr should be handled in series.

@ljharb

This comment has been minimized.

Copy link
Member

commented Jun 3, 2019

@AndreasClausen

arr.reduce((prev, item) => prev.then(() => someAsyncFunc(item)), Promise.resolve())
@joemillervi

This comment has been minimized.

Copy link

commented Jun 4, 2019

@ljharb That is considerably less readable.

@ljharb

This comment has been minimized.

Copy link
Member

commented Jun 4, 2019

@joemillervi that's a valid but subjective opinion, but of course i'd suggest putting that behind a nicely named abstraction that takes arr and someAsyncFunc.

@joemillervi

This comment has been minimized.

Copy link

commented Jun 4, 2019

“Separately, loops should be avoided in favor of array iterations (no-restricted-syntax)”
I agree for 90% of cases, but disagree for:

  1. Breaking out of an iteration early, which forEach, map, filter, reduce can’t do without an exception
  2. Calling await in series on an array of promises
    Currently having issue 1 - I see this was discussed above. My use case is wanting to return early (not waste time doing subsequent iterations) once a match is found.
@ljharb

This comment has been minimized.

Copy link
Member

commented Jun 5, 2019

@joemillervi for 1, some/every/find/findIndex all break early without an exception. For 2, you can do that inside an abstraction with the reduce and a custom rejection.

@vdh

This comment has been minimized.

Copy link
Contributor

commented Jun 5, 2019

@joemillervi Maybe this variation is more your style?

await arr.reduce(async (prev, item) => {
  await prev;
  return someAsyncFunc(item);
}, Promise.resolve());

@ljharb It just occurred to me that your example needs an await prefixed to it to be an exact conversion of that pesky for loop.

@ljharb

This comment has been minimized.

Copy link
Member

commented Jun 5, 2019

@vdh it does not (unless it's inside an async function with other code), because it's the return value in a .then, and await is just sugar for a nested promise .then.

@vdh

This comment has been minimized.

Copy link
Contributor

commented Jun 5, 2019

The await inside the loop implies it must be inside an async function because without that it's a syntax error. I meant that the for loop will block any code that follows it (and the return promise) in this anonymous async function, but the example substitution will not block, unless it has an await.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
You can’t perform that action at this time.