6 ms·
Environments: under the hood of variables – Deep JavaScript chapter
- xg15 6y agoThis is also cause for the following entertaining bug: for(let i=0; i<buttons.length; i++) { buttons[i].addEventListener('click', e => alert('button ' + i + ' clicked!'); } The above code is supposed to show a unique number for each button when the button is clicked. What it will actually do is to show the same number for all buttons, the number being equal to buttons.length. The reason is that an event handler does not inherit the value of i when it is created, but a pointer to the environment in which i is defined. Because all event handlers point to the same parent environment, they will also pick up changes to i's value that happen after they were created. If you add seemingly redundant function call to separate environments, everything will work. So the following code will work correctly: function makeHandler(index) { return e => alert('button ' + index + ' clicked!'); } for(let i=0; i<buttons.length; i++) { buttons[i].addEventListener('click', makeHandler(i)); }
- pcr910303 6y agoThat's not the case with a let-binding, which creates it's own scope for blocks. I think you're mixing it up with var, which yes it does have the problem that you're mentioning. (And hence the universal advice of avoid using var.)
- dgb23 6y agoRust has a related concept of capturing references by default: https://doc.rust-lang.org/rust-by-example/fn/closures/capture.html https://doc.rust-lang.org/rust-by-example/fn/closures/captur...
- dgb23 6y agoIs the comparison too far off to be useful or straight up incorrect?
- millstone 6y agoIt reads like an attempt to hijack the thread to make it about Rust.
- dgb23 6y agoThank you for clarifying. I can see how it may come off as this. I guess this is a symptom of learning language X in our free time in combination with a "I know this" dopamine hit while being in the early learning stages. By the way I always thought of JS closing over references (instead of values) was a feature. A closure over a mutable reference or pointer essentially becomes an object interface. Otherwise defaulting to 'const' makes the intent clear.
- aikah 6y ago> The above code is supposed to show a unique number for each button when the button is clicked. What it will actually do is to show the same number for all buttons, the number being equal to buttons.length. no "let" will work as expected (lexical scoping). It's var which worked at the function scope, that's why "var" usage is not encouraged. It's very confusing to JS newcomers, that's why it is important to learn the history of the language.
- ylyn 6y agoActually it's not just lexical scoping. If you had pure lexical scoping, given the way the for statement works, you would expect it to show the same value because it is the same x being captured. JS actually specifically handles a for (let ...) specially by creating a _copy_ of the variable. See the spec[1] (the third clause starting with for (LexicalDeclaration ...)). [1]: https://www.ecma-international.org/ecma-262/11.0/index.html#sec-for-statement-runtime-semantics-labelledevaluation https://www.ecma-international.org/ecma-262/11.0/index.html#...
- xg15 6y agoThanks for the replies. I really wasn't aware of the behavior of let here and did indeed learn something. The "buggy" code really does work. It's important to note that this is not just due to scoping but that a regular for loop apparently really makes copies of its environment for each iteration, as ylyn described[1]. You can demonstrate this as follows: const objs = []; for (let i = 0; i < 3; i++) { // i++ seems to be executed after copying but before execution of the loop body. objs.push({ getI() {return i}, add10() {i += 10} }); } // each object gets a copy of i from its respective iteration: console.info(objs[0].getI(), objs[1].getI(), objs[2].getI()); // prints 0 1 2 // the copies can be modified independently: objs[0].add10(); objs[2].add10(); console.info(objs[0].getI(), objs[1].getI(), objs[2].getI()); // prints 10 1 12 I'm not sure I like this design, to be honest. This is an awfully complex special case hidden in something as mundane-looking as a regular for loop. Seems to me, this can make understanding the general principles behind scopes and environments harder at the expense of making a particular special case more convenient. [1] https://news.ycombinator.com/item?id=23955809 https://news.ycombinator.com/item?id=23955809
- johnfn 6y agoThis 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.
- tomxor 6y ago
- jedimastert 6y agoI've been working on a minimax tic tac toe solver[0] to stretch my legs in JS and I ran into this exact same bug. I ended up making objects for the positions instead[1] [0]:https://github.com/amtunlimited/ttt-js https://github.com/amtunlimited/ttt-js [1]:https://github.com/amtunlimited/ttt-js/blob/0994d6ff03b338a6c0ca938aaaf52e3c6d5d71c9/frontend.js#L68 https://github.com/amtunlimited/ttt-js/blob/0994d6ff03b338a6...
- deleted 6y ago[deleted]
- wwright 6y agoDo modern JS engines still map an entire lexical scope to an environment at all times? It seems like it would be fairly simple to optimize it down and only box up and share the variables that the code actually closes over.
- hajile 6y agoModern JITs make a C++ class to represent objects. Closures are just objects. We'll ignore non-strict code which allows external functions to access variables. If you want to actually eliminate these variables, you'd have to make TWO classes per function. Next you'd have to COPY all the relevant stuff from one class to the other. Maybe you instead keep the same class and add some code to make the various pointers null. That saves all the copying at the expense of some literals taking up the same space, but doesn't solve this next issue. JS has weak reference maps/sets along with upcoming weak values. If I reference an object in a closure (one that is used in a weak map), but don't return that variable, what happens? If it's set to null, then something is going to potentially be GC'd when it shouldn't be. Perhaps this kind of lifetime issue could be detected with a sufficiently smart JIT, but in such a dynamic language without types, I doubt it's possible.
- ridiculous_fish 6y agoThere's a lot of misunderstanding here. A closure does not store the variables it creates. Instead, the closure's code block will allocate space for those variables when it is called. The GC must trace all reachable values. This includes the stack, and also variables captured by closures which are themselves reachable. The same principles apply whether or not a JIT is in use.
- hajile 6y agoI didn't misunderstand what I was writing (though I may not have succeeded in conveying what I meant). At compile time, a hidden class will be created to represent all the variables (both user-visible and user-inaccessible). Every function is actually called by an internal [[Call]]. This will create an instance of that class which is placed on the heap and may or may not copy some values into the stack (it's essentially a necessary performance optimization, but not actually hard requirement in all ISAs). A closure is an object that has only one public method or property (a function application sometimes called apply). In theory, if you could guarantee that the apply method and all nested lexical scopes never accessed a variable and the variable was not reference counted, you could safely null the entry in the class and allow whatever it pointed to to be GC'd. My only contention is that this is easier to theorize about than to actually do.