The behavior you described involves several related JavaScript concepts: scope, execution contexts (hoisting), and closure.
I didn't see a JavaScript code example, so I made some assumptions about how you structured your code. My examples are in TypeScript, but it wouldn't look that much different in JavaScript.
void (function main() {
var byCopyCaptures: ((n: number) => void)[] = [];
var byRefCaptures: ((n: number) => void)[] = [];
for (var i = 0; i < 5; i++) {
byCopyCaptures[i] = (j: number) =>
console.log(`capture by copy: i is ${i} and j is ${j}`);
byRefCaptures[i] = (j: number) =>
console.log(`capture by ref: i is ${i} and j is ${j}`);
}
for (var k = 0; k < 5; k++) {
byCopyCaptures[k](k);
byRefCaptures[k](k);
}
})();
This code has the following output:
capture by copy: i is 5 and j is 0
capture by ref: i is 5 and j is 0
capture by copy: i is 5 and j is 1
capture by ref: i is 5 and j is 1
capture by copy: i is 5 and j is 2
capture by ref: i is 5 and j is 2
capture by copy: i is 5 and j is 3
capture by ref: i is 5 and j is 3
capture by copy: i is 5 and j is 4
capture by ref: i is 5 and j is 4
Despite the behavior, it's not entirely accurate to say this is pass by reference. What's happening is scope works differently in JavaScript than it does in C++.
JavaScript did not have any block scope until 2015, when ECMAScript 6 (ES6), the specification for JavaScript, was released.
Look at this example:
{
var notBlockScoped = 42;
}
console.log(notBlockScoped); // 42
This code is valid JavaScript and prints 42. Developers from other languages would expect it to have an error or print undefined. This behavior is the same even in modern JavaScript for backward compatibility.
JavaScript does have function scope and what ends up happening looks something like this:
void (function main() {
var byCopyCaptures: ((n: number) => void)[] = [];
var byRefCaptures: ((n: number) => void)[] = [];
var i: number; // i hoisted to top of scope
var k: number; // k hoisted to top of scope
for (i = 0; i < 5; i++) {
byCopyCaptures[i] = (j: number) =>
console.log(`capture by copy: i is ${i} and j is ${j}`);
byRefCaptures[i] = (j: number) =>
console.log(`capture by ref: i is ${i} and j is ${j}`);
}
for (k = 0; k < 5; k++) {
byCopyCaptures[k](k);
byRefCaptures[k](k);
}
})();
When entering a function, the JavaScript interpreter pushes an execution context onto the stack as other languages do. From there, it essentially does two passes: 1) it creates bindings for all of the functions and variables, and 2) it executes the code. Because the interpreter knows about everything when executing the code, functions and variables appear "hoisted" to the top of the scope.
This hoisting allows you to call a function before it is defined. You can also call variables early, but that is almost always a bad idea.
Going back to the example, as you have observed, the variables in a lambda function do not get evaluated until running it. Before that, the function creates a closure that determines which functions and variables it can access. The closure includes the function itself and its lexical environment.
In this case, while each of the ten functions is unique, they all share the same lexical environment. When they get evaluated, they will all point to the same i and k values the interpreter hoisted to the top of the function earlier.
i and k here are primitives and are always passed by value in JavaScript. So despite the behavior resembling pass by reference that is not the case here.
There are a few ways to capture the value at the lambda creation time. You can use an immediately invoked function expression (IFFE):
byCopyCaptures[i] = ((i) => (j: number) =>
console.log(`capture by copy: i is ${i} and j is ${j}`))(i);
An IFFE is like any other function, except you execute it at the time of declaration. It would be more clear to write it like this:
byCopyCaptures[i] = ((otherI) => (j: number) =>
console.log(`capture by copy: i is ${otherI} and j is ${j}`))(i);
The i gets passed in immediately by value, and then the lambda has a copy.
capture by copy: i is 0 and j is 0
capture by ref: i is 5 and j is 0
capture by copy: i is 1 and j is 1
capture by ref: i is 5 and j is 1
capture by copy: i is 2 and j is 2
capture by ref: i is 5 and j is 2
capture by copy: i is 3 and j is 3
capture by ref: i is 5 and j is 3
capture by copy: i is 4 and j is 4
capture by ref: i is 5 and j is 4
You can also wrap a larger section of code to create a new scope. JavaScript libraries used to have to do this because everything was global scope.
for (var i = 0; i < 5; i++) {
(function () {
var localI = i;
byCopyCaptures[i] = (j: number) =>
console.log(`capture by copy: i is ${localI} and j is ${j}`);
})();
byRefCaptures[i] = (j: number) =>
console.log(`capture by ref: i is ${i} and j is ${j}`);
}
If you don't like the IFFE syntax, you can call it like any other function.
for (var i = 0; i < 5; i++) {
function createCopyCaptureLambda() {
var localI = i;
byCopyCaptures[i] = (j: number) =>
console.log(`capture by copy: i is ${localI} and j is ${j}`);
}
createCopyCaptureLambda();
byRefCaptures[i] = (j: number) =>
console.log(`capture by ref: i is ${i} and j is ${j}`);
}
}
Another option is to create a helper function, one of the cleaner ways of doing it. This option does the same as above, where i is passed immediately by value.
function createCopyCaptureLambda(num: number) {
return (j: number) =>
console.log(`capture by copy: i is ${num} and j is ${j}`);
}
for (var i = 0; i < 5; i++) {
byCopyCaptures[i] = createCopyCaptureLambda(i);
byRefCaptures[i] = (j: number) =>
console.log(`capture by ref: i is ${i} and j is ${j}`);
}
The best way, and what is pretty much universally recommended outside of rare cases, is never to use var again and use const and let, which the specification added in ES6 as new ways to declare variables.
The consensus on best practices usually suggests using const as much as possible and only using let when needing to modify a variable.
When using these, they are effectively block-scoped and will behave as you would expect coming from C++.
void (function main() {
const byCopyCaptures: ((n: number) => void)[] = [];
const byRefCaptures: ((n: number) => void)[] = [];
for (let i = 0; i < 5; i++) {
byCopyCaptures[i] = (j: number) =>
console.log(`capture by copy: i is ${i} and j is ${j}`);
byRefCaptures[i] = (j: number) =>
console.log(`capture by ref: i is ${i} and j is ${j}`);
}
for (let k = 0; k < 5; k++) {
byCopyCaptures[k](k);
byRefCaptures[k](k);
}
})();
capture by copy: i is 0 and j is 0
capture by ref: i is 0 and j is 0
capture by copy: i is 1 and j is 1
capture by ref: i is 1 and j is 1
capture by copy: i is 2 and j is 2
capture by ref: i is 2 and j is 2
capture by copy: i is 3 and j is 3
capture by ref: i is 3 and j is 3
capture by copy: i is 4 and j is 4
capture by ref: i is 4 and j is 4
I hope that helps. There are a lot of quirks in JavaScript, although it's much better than it used to be. I would suggest using a static analysis tool like ESLint. It will point out common issues and pitfalls like this.
TypeScript is also much better and is my recommendation for any new project these days. It will point out even more issues when writing the code instead of figuring it out at runtime.