Hacker Timesnew | past | comments | ask | show | jobs | submitlogin

This seems pretty standard to me. Could you produce an example of a language which doesn't have this behavior?

EDIT:

I think I understand your confusion. You've gotten confused because you're expecting `y = 5; let x = y` be able to hold a reference to `y` the number in the same way that `y = {}; let x = y` holds a reference to y the object. But that's honestly the behavior of practically every language I could name - numbers and other small values that can fit in a register are always copied.

You can see the referential behavior if you change your `i` to an object:

    let result = []

    for (let obj = {}; Object.keys(obj).length < 3; obj[Math.random()] = "hello") {
       result.push(obj);
    }

    console.log(result); // notice all 3 entries in the array are the same

But yeah - JavaScript does nothing special in either of these cases. Similar code will get you similar results across any language I could name. The only way to get around it would be to manually mark your numeric i as a reference in a language that has them, like C/C++/Rust.


The confusion is not about assignment behavior, it's about block scopes, specifically within the for loop.

Before JS had `let` and `const` there was no built in way of obtaining a block scope (for, if, else, while, do and {}), you had to obtain one artificially by passing variables into a function scope, e.g a predefined function or an IIFE.

I'm talking about `var`... paste this into your browsers console, you get 8 logged 8 times:

  for (var i = 0; i < 8; i ++) {
   setTimeout(() => {
    console.log(i)
   }, 100);
  }
I'm pretty sure this is the issue the OP was trying to demonstrate.

With `var` i is _not_ bound to the for loop scope, it's bound the closest parent function scope. The key difference is not only that i can be accessed after the for loop, but that there is only one parent function scope, so i is effectively overwritten - whereas if i was bound to the for block there would be a separate scope for each iteration.

Since the timeout function has no i in it's scope it walks up the parent scopes, it can only find i in the parent function scope and by the time it has executed it will be whatever the last iteration assigned it - or even more confusingly what something after the loop assigned it.

This is why `let` was added:

  for (let i = 0; i < 8; i ++) {
   setTimeout(() => {
    console.log(i)
   }, 100);
  }
Which gives you 0 through 7

let assigns i to the for loop's scope, with a different one for each iteration, this preserves the local context the setTimeout function was defined in which is intuitive and what people expect.

Another nice thing that people forget with this feature (including myself out of habbit) is that you can now replace IIFEs with block scope literals {} (provided you stop using var, which you should have anyway)

In short, var was a nightmare, it's only benefit is obfuscation, it was the source of countless needless bugs, and that's why we now have const/let and block scopes.


Yes, exactly. I think what was confusing to me here is the interaction between child environments in for(;;).

Usually, scopes in JS work like the OP link describes them: If a block is entered, a child environment is created that has a reference to its parent environment. A variable either belongs to the parent and is shared with all children - or belongs to one particular child and is initialised through assignment. However different child environments can never influence each other.

for(;;) does something different: It takes one child environment (of iteration n) and clones it for iteration n+1. The effect is that there is a single let i = ... statement which leads to the creation of several independent variables.

It's easy to see why this was done - it's the only way how you can have both mutating statements like i++ and closures that capture state from a particular iteration - but I'm not aware this is done anywhere else in JS.


You are right, although this doesn't feel all that exotic to me, even though it might be unique for built-ins, it's essentially the same as what happens when you pass variables to functions (you get a copy of the references, i.e they are added to the scope), in this sense a block scoped for loop feels similar to a series of function calls passing and returning a variable.

In fact you could emulate a block scoped for loop with an object with little trouble and without being all that confusing:

  const FOR = (s, c, f) => c(s) &&
    (f({...s}), FOR(s, c, f))

  FOR({i: 0}, s => s.i++ < 8, s => {
    setTimeout(() => {
      console.log(s.i)
    }, 100)
  })
This doesn't prove anything, but the fact that blocked for can almost be implemented with in one small function makes me feel like it's not all that magic, rather it's just nice syntax.

[edit]

getting a bit silly now but was seeing how close I could get to built-in syntax... yes this is horrible, never use `with` or `eval` like this.

  const EVIL = (f, s) => eval(`
    with (s) {
      ${(f + '').replace(/^.+>/, '')}
    }
  `)

  const FOR = (s, ce, fe, sb, ii) => {
    if (EVIL(ce, s)) {
      EVIL(sb, {...s})
      EVIL(fe, s)
      FOR(s, ce, fe, sb, 1)
    }
  }

  FOR ({i: 0}, _=> i < 8, _=> i++, _ => {
    setTimeout(() => {
      console.log(i)
    }, 100)
  })


You have to clone from the previous iteration - because in theory, the block itself could modify the loop variable again and that modification gets lost if you keep cloning from the initial environment.

So your function would produce incorrect results for cases like this:

  for (let i = 0; i < 100; i++) {
    i += 10;
    setTimeout(() => console.log(i), 1000);
  }
I think the following should work though:

  function FOR(env, cond, incr, block) {
    if (cond(env)) {
      block(env);
      const env2 = {...env};
      incr(env2);
      FOR(env2, cond, incr, block);
    }
  }

  FOR({i: 0}, s=>(s.i < 8), s=>s.i++, s=>{
    setTimeout(() => {
      console.log(s.i)
    }, 100)
  });
And yeah, I agree, it's not really complicated to implement (as long as you don't care about the waste of memory). It was just an unexpected bit of logic at that point.


But closures still hold references to variables, even if they are numbers.

    let counter = () => {
      let i = 0
      return () => { ++i; return i }
    }

    let x = counter()
    console.log(x(), x(), x()) // 1 2 3
If you reimplement the OP's for (let i...) loop with a loop where the i variable is declared before the loop, you will get the behavior OP mentioned.




Guidelines | FAQ | Lists | API | Security | Legal | Apply to YC | Contact

Search: