javascript箭头函数:我们能像在c++ lambdas中那样捕获值吗?

bbuxkriu  于 2023-01-07  发布在  Java
关注(0)|答案(4)|浏览(142)

当定义一个c++ lambda https://en.cppreference.com/w/cpp/language/lambda时,有一个捕获块捕获封闭作用域中的变量的 * 值 *(至少如果变量是通过复制而不是通过引用捕获的)。因此,如果lambda使用了一个已经捕获的变量,并且该lambda稍后被执行,则lambda内的相应变量将具有当lambda被定义时它所具有的值。
使用javascript的arrow函数,我可以从封闭的作用域中引用变量,但是,当arrow函数被调用时,它将使用它现在拥有的变量值(而不是定义arrow函数时拥有的值)。
是否有类似的变量捕获机制,允许使用箭头函数对象存储捕获的变量值?
下面是一个c++的例子:

// 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);
  }
}

输出:

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

使用箭头函数的javascript等价物是什么?

w7t8yxp5

w7t8yxp51#

我认为与之最接近的等价方法是使用immediately invoked function expression,并传入要锁定的值。由于代码现在访问的是函数参数而不是原始变量,因此对原始变量的赋值无关紧要。例如:

let a = 'a';
let b = 'b';

((a, b) => {
  setTimeout(() => {
    console.log(a);
    console.log(b);
  }, 1000);
})(a, b);

a = 'new a';
b = 'new b';

因为我对所有的变量都使用了相同的名称,所以可能会有点混淆什么是什么,所以这里有一个唯一变量名的相同问题:

let a = 'a';
let b = 'b';

((innerA, innerB) => {
  setTimeout(() => {
    console.log(innerA);
    console.log(innerB);
  }, 1000);
})(a, b);

a = 'new a';
b = 'new b';
afdcj2ne

afdcj2ne2#

我的javascript不是很好。
但你所寻找的概念是一个结束:

// Create a function that takes two values and returns a result
// Your simplest type of function.
// I am sure the language is a handy syntactic sugar for the same.
x = function(x, y) {
    return x+y;
}
console.log( x(2,3) )

// The trick is to capture a variable
// The easiest way to do this is to create a function
// that returns a function but internal function uses the parameter
// of the parent.
//
// Again most languages have syntactic sugar to create this type of
// object as well.
function closure(rhs)
{
    // Notice this function is the one returned
    // but it captures the parameter `rhs` to be used
    // by the returned function.
    return function(lhs){return x(lhs, rhs);}
}

y = closure(2);
console.log( y(3) )

回到你的例子:

function captureByValue(rhs)
{
    localRhs = rhs.value;
    return function(lhs){return lhs + localRhs;}
}
function captureByRef(rhs)
{
    return function(lhs){return lhs + rhs.value;}
}

data = {value:12};
funcValue  = captureByValue(data);
data.value = 13;
funcRef    = captureByRef(data);
data.value = 14;

console.log( funcValue(8)); // 12 + 8   12 Captured by value
console.log( funcRef(6));   // 14 + 6   data captured by ref and updated to 14 after capture
fzsnzjdm

fzsnzjdm3#

您描述的行为涉及几个相关的JavaScript概念:作用域、执行上下文(提升)和闭包。
我没有看到JavaScript代码示例,所以我对你的代码结构做了一些假设。我的示例是用TypeScript编写的,但在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);
  }
})();

此代码具有以下输出:

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

尽管有这样的行为,但说这是通过引用传递并不完全准确,实际情况是作用域在JavaScript中的工作方式与在C++中的不同。
JavaScript在2015年之前没有任何区块范围,当时JavaScript的规范ECMAScript 6(ES6)发布。
看这个例子:

{
  var notBlockScoped = 42;
}
console.log(notBlockScoped); // 42

这段代码是有效的JavaScript并打印42。其他语言的开发人员可能会认为它有错误或打印未定义。为了向后兼容,即使在现代JavaScript中,这种行为也是相同的。
JavaScript确实有函数作用域,结果如下:

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);
  }
})();

当输入一个函数时,JavaScript解释器会像其他语言一样将执行上下文推送到堆栈上。从堆栈开始,它基本上会执行两次传递:1)它为所有的函数和变量创建绑定,2)它执行代码。因为解释器在执行代码时知道一切,所以函数和变量看起来"提升"到作用域的顶部。
这种提升允许你在函数定义之前调用它,你也可以提前调用变量,但这几乎总是一个坏主意。
回到这个例子,正如你所观察到的,lambda函数中的变量直到运行它才被求值,在此之前,函数创建一个闭包来决定它可以访问哪些函数和变量,闭包包括函数本身和它的词法环境。
在本例中,虽然这10个函数都是唯一的,但它们都共享相同的词法环境,当对它们求值时,它们都指向相同的ik值,解释器之前将这些值提升到函数的顶部。
这里的ik是原语,在JavaScript中总是通过值传递,所以尽管行为类似于通过引用传递,但这里不是这样。
有几种方法可以在lambda创建时捕获值,可以使用立即调用函数表达式(IFFE):

byCopyCaptures[i] = ((i) => (j: number) =>
  console.log(`capture by copy: i is ${i} and j is ${j}`))(i);

IFFE和其他函数一样,只不过是在声明的时候执行的。这样写会更清楚:

byCopyCaptures[i] = ((otherI) => (j: number) =>
  console.log(`capture by copy: i is ${otherI} and j is ${j}`))(i);

i通过值直接传入,然后lambda有一个副本。

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

你也可以 Package 一个更大的代码段来创建一个新的作用域,JavaScript库曾经必须这样做,因为所有的东西都是全局作用域。

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}`);
}

如果不喜欢IFFE语法,可以像调用其他函数一样调用它。

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}`);
  }
}

另一个选项是创建一个helper函数,这是一种更简洁的方法,这个选项的作用和上面的一样,i直接传值。

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}`);
}

最好的方法,也是除了极少数情况之外普遍推荐的方法,是永远不要再使用var,而是使用constlet,这是ES6中规范添加的声明变量的新方法。
关于最佳实践的共识通常建议尽可能多地使用const,并且仅在需要修改变量时使用let
当使用这些函数时,它们实际上是块作用域的,并且会像您期望的那样从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

我希望这能有所帮助。JavaScript中有很多怪癖,尽管它比以前好得多。我建议使用像ESLint这样的静态分析工具。它会指出像这样的常见问题和陷阱。
TypeScript也要好得多,是我现在对任何新项目的推荐。它会在编写代码时指出更多的问题,而不是在运行时弄清楚。

9rygscc1

9rygscc14#

这是我今天学习Go语言时发现的一个“按值捕获”方法。使用const iCopy = i强制立即复制值,然后闭包捕获iCopy的常量值,而不是对i的引用。

function main() {
  var byCopyCaptures = [];
  var byRefCaptures = [];

  for (var i = 0; i < 5; i++) {
    const iCopy = i;
    byCopyCaptures[i] = (j) =>
      console.log(`capture by copy: i is ${iCopy} and j is ${j}`);
    byRefCaptures[i] = (j) =>
      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);
  }
}

产出

"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"

相关问题