when defining a c++ lambda https://en.cppreference.com/w/cpp/language/lambda there is the capture block that captures the values of variables in the enclosing scope (at least if the variable is captured by copy rather than by reference). So if the lambda uses a variable that has been captured and the lambda is later executed, the respective variable inside the lambda will have the value it had when then the lambda got defined.
With javascript arrow functions, I can reference variables from the enclosing scope. However, when the arrow function is called, it will use the value of the variable that it has now (and not the value it had when the arrow function got defined).
Is there a similar mechanic of variable captures that allows to store captured variable values with the arrow function object?
Here a c++ example:
// Example program
#include <iostream>
#include <string>
#include <functional>
int main()
{
std::function<void(int)> byCopyCaptures[5];
std::function<void(int)> byRefCaptures[5];
for(int i=0; i<5; i++) {
// The variable i is captured by copy:
byCopyCaptures[i] = [i](int j) {std::cout << "capture by copy: i is " << i << " and j is "<< j <<"\n";};
// The variable i is captured by reference:
byRefCaptures[i] = [&i](int j) {std::cout << "capture by ref: i is " << i << " and j is "<< j <<"\n";};
}
for(int k=0; k<5; k++) {
byCopyCaptures[k](k);
byRefCaptures[k](k);
}
}
output:
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
What would be the javascript equivalent using arrow functions?
I'd say the closest equivalent to that is to use an immediately invoked function expression, and pass in the values you want to be locked in. Since the code is now accessing the function parameters instead of the original variables, assignments to the original variables will not matter. For example:
let a = 'a';
let b = 'b';
((a, b) => {
setTimeout(() => {
console.log(a);
console.log(b);
}, 1000);
})(a, b);
a = 'new a';
b = 'new b';
Since i've used the same name for everything it may be a bit confusing what refers to what, so here's the same thing with unique variable names:
let a = 'a';
let b = 'b';
((innerA, innerB) => {
setTimeout(() => {
console.log(innerA);
console.log(innerB);
}, 1000);
})(a, b);
a = 'new a';
b = 'new b';
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.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With