JavaScript 声明一个变量并在一个语句中使用逗号运算符?

blu*_*yke 5 javascript string comma let comma-operator

众所周知,要声明多个变量,可以使用如下格式:

let k = 0,
    j = 5 /*etc....*/
Run Code Online (Sandbox Code Playgroud)

众所周知,要在一行中执行多个语句(这对箭头函数很有用,因此不必编写return关键字),还使用逗号“,”运算符,如下所示:

let k = 0,
    j = 5 /*etc....*/
Run Code Online (Sandbox Code Playgroud)

不是最优雅的例子,但重点是你可以在一行中执行多个语句,用逗号“,”分隔,并返回最后一个值。

所以问题是:

你如何结合这两种技术?意思是,我们如何在一行中声明一个变量,然后在一个逗号之后,将该变量用于某事?

以下不起作用:

let k = 0, console.log(k), k += 8
Run Code Online (Sandbox Code Playgroud)

未捕获的语法错误:意外的标记“。”

没有console.log,它认为我在重新声明k:

let k = 0, k += 8
Run Code Online (Sandbox Code Playgroud)

Uncaught SyntaxError: Identifier 'k' has already been declared
Run Code Online (Sandbox Code Playgroud)

并将整个内容放在括号中,如下所示:

(let k = 0, k += 8);
Run Code Online (Sandbox Code Playgroud)

Uncaught SyntaxError: Unexpected identifier
Run Code Online (Sandbox Code Playgroud)

指的是关键字“让”。但是,没有那个关键字,就没有问题:

(k = 0, k += 8);
Run Code Online (Sandbox Code Playgroud)

除了 k 现在成为一个全局变量,这是不想要的。

这里有某种解决方法吗?

如何在 JavaScript 中将逗号运算符与局部变量声明一起使用?

编辑响应 VLAZ 的 eval 部分答案,将参数传递给 eval,可以制作自定义函数:

let r = "hello there world, how are you?"
.split("")
.map(x => (x+=5000, x.split("").map(
  y => y+ + 8
).join("")))
.join("")

console.log(r)
Run Code Online (Sandbox Code Playgroud)

VLA*_*LAZ 6

你不能这样做。变量声明语法允许使用逗号,以便一次声明多个变量。每个变量也可以选择性地作为声明的一部分进行初始化,因此语法是(更抽象地):

(var | let | const) variable1 [= value1], variable2 [= value2], variable3 [= value3], ..., variableN [= valueN]
Run Code Online (Sandbox Code Playgroud)

但是,这不是逗号运算符。就像逗号 inparseInt("42", 10)也不是逗号运算符一样 - 它只是在不同上下文中具有不同含义的逗号字符

然而,真正的问题是逗号运算符适用于表达式,而变量声明是语句

差异的简短说明:

表达式

基本上任何产生值的东西:2 + 2, fn(), a ? b : c, 等等。它会被计算并产生一些东西。

表达式可以在很多情况下嵌套:2 + fn()或者( a ? ( 2 + 2 ) : ( fn() ) )(每个表达式都用括号括起来,以便清晰)例如。即使表达式不会产生不会改变事物的可用值 - 没有显式返回的函数也会产生,undefined因此2 + noReturnFn()会产生乱码,但它仍然是有效的表达式语法。

Note 1 of 2(更多在下一节):变量赋值一个表达式,这样做a = 1会产生被赋值的值:

let foo;
console.log(foo = "bar")
Run Code Online (Sandbox Code Playgroud)

声明

这些不会产生价值。不undefined只是什么。示例包括if(cond){}return resultswitch

语句仅独立有效。您不能像这样嵌套它们,if (return 7)因为这在语法上是无效的。您不能进一步在需要表达式的地方使用语句 -console.log(return 7)同样无效。

请注意,表达式可以用作语句。这些被称为表达式语句

console.log("the console.log call itself is an expression statement")
Run Code Online (Sandbox Code Playgroud)

因此,您可以在语句有效的情况下使用表达式,但不能在表达式有效的情况下使用语句。

注2 2:可变分配是一个表达式,但是变量声明与分配不是。它只是变量声明语句语法的一部分。因此,两者重叠但不相关,只是逗号运算符和声明多个变量的方式相似(允许您执行多项操作)但不相关。

console.log(let foo = "bar"); //invalid - statement instead of expression
Run Code Online (Sandbox Code Playgroud)

与逗号运算符的关系

现在我们知道了区别,它应该变得更容易理解。逗号运算符的形式为

exp1, exp2, exp3, ..., expN
Run Code Online (Sandbox Code Playgroud)

并接受表达式,而不是语句。它一一执行它们并返回最后一个值。由于语句没有返回值,因此它们在这种上下文中永远不会有效:(2 + 2, if(7) {})从编译器/解释器的角度来看是无意义的代码,因为这里不能返回任何内容。

因此,考虑到这一点,我们不能真正混合变量声明和逗号运算符。let a = 1, a += 1不起作用,因为逗号被视为变量声明语句,如果我们尝试这样做( ( let a = 1 ), ( a += 1 ) )仍然无效,因为第一部分仍然是语句,而不是表达式。

可能的解决方法

如果您确实需要在表达式上下文中生成变量避免生成隐式全局变量,那么您可以使用的选项很少。让我们用一个函数来说明:

exp1, exp2, exp3, ..., expN
Run Code Online (Sandbox Code Playgroud)

因此,它是一个生成值并在少数地方使用它的函数。我们将尝试将其转换为速记语法。

国际金融研究所

const fn = x => {
  let k = computeValueFrom(x);
  doSomething1(k);
  doSomething2(k);
  console.log(k);
  return k;
}
Run Code Online (Sandbox Code Playgroud)

在您自己的内部声明一个新函数,该函数将其k作为参数,然后立即使用 的值调用该函数computeValueFrom(x)。如果我们为了清楚起见将函数与调用分开,我们会得到:

const fn = x => (k => (doSomething1(k), doSomething2(k), console.log(k), k))
                   (computeValueFrom(x));

fn(42);
Run Code Online (Sandbox Code Playgroud)

因此,该函数k使用逗号运算符按顺序使用并使用它几次。我们只需调用该函数并提供 的值k

使用参数作弊

const extractedFunction = k => (
  doSomething1(k), 
  doSomething2(k), 
  console.log(k), 
  k
);

const fn = x => extractedFunction(computeValueFrom(x));

fn(42);
Run Code Online (Sandbox Code Playgroud)

基本上和以前一样——我们使用逗号运算符来执行几个表达式。但是,这次我们没有额外的函数,我们只是在fn. 参数是局部变量,因此它们在创建局部可变绑定方面的行为类似于let/ var。然后我们在k不影响全局范围的情况下分配给该标识符。这是我们的第一个表达式,然后我们继续其余的。

即使有人调用fn(42, "foo")第二个参数也会被覆盖,所以实际上它就像fn只接受一个参数一样。

使用函数的正常体作弊

const fn = (fn, k) => (
  k = computeValueFrom(x), 
  doSomething1(k), 
  doSomething2(k), 
  console.log(k), 
  k
);

fn(42);
Run Code Online (Sandbox Code Playgroud)

我撒了谎。或者更确切地说,我作弊了。这不在表达式上下文中,您拥有与以前相同的所有内容,但只是删除了换行符。重要的是要记住,您可以这样做并用分号分隔不同的语句。它仍然是一行,几乎没有比以前长。

函数组合和函数式编程

const fn = x => { let k = computeValueFrom(x); doSomething1(k); doSomething2(k); console.log(k); return k; }

fn(42);
Run Code Online (Sandbox Code Playgroud)

这是一个很大的话题,所以我在这里几乎不打算触及表面。我也大大简化了事情只是为了介绍这个概念。

那么,什么函数式编程(FP)?

它使用函数作为基本构建块进行编程。是的,我们已经有了函数,我们确实用它们来制作程序。然而,非 FP 程序本质上使用命令式结构将效果“粘合”在一起。因此,您希望ifs、fors 和调用多个函数/方法来产生效果。

在 FP 范式中,您拥有使用其他函数编排在一起的函数。很多时候,那是因为您对数据操作链感兴趣。

itemsToBuy
  .filter(item => item.stockAmount !== 0)      // remove sold out
  .map(item => item.price * item.basketAmount) // get prices
  .map(price => price + 12.50)                 // add shipping tax
  .reduce((a, b) => a + b, 0)                  // get the total
Run Code Online (Sandbox Code Playgroud)

数组支持来自函数世界的方法,所以这一个有效的 FP 示例。

什么是功能组合

现在,假设您想要从上面获得可重用的函数,并提取这两个函数:

const getPrice = item => item.price * item.basketAmount;
const addShippingTax = price => price + 12.50;
Run Code Online (Sandbox Code Playgroud)

但是你真的不需要做两个映射操作。我们可以将它们重写为:

const getPriceWithShippingTax = item => (item.price * item.basketAmount) + 12.50;
Run Code Online (Sandbox Code Playgroud)

但是让我们尝试在不直接修改函数的情况下进行。我们可以一个接一个地调用它们,这样就行了:

const getPriceWithShippingTax = item => addShippingTax(getPrice(item));
Run Code Online (Sandbox Code Playgroud)

我们现在已经重用了这些功能。我们会调用getPrice并将结果传递给addShippingTax。只要我们调用的下一个函数使用前一个函数的输入,这就会起作用。但这并不是很好 - 如果我们想调用三个函数f, g, 并且h一起调用,我们需要x => h(g(f(x))).

现在终于是函数组合的用武之地了。调用这些是有顺序的,我们可以对其进行概括。

const log = x => {
  console.log(x);
  return x;
}

const fn = compose(computeValueFrom, doSomething1, doSomething2, log) 

fn(42);
Run Code Online (Sandbox Code Playgroud)

好了,我们已经将这些功能与另一个功能“粘合”在一起了。这相当于做:

const composed = x => {
  const temp1 = f(x);
  const temp2 = g(temp1);
  const temp3 = h(temp2);
  return temp3;
}
Run Code Online (Sandbox Code Playgroud)

但支持任意数量的函数,并且不使用临时变量。因此,我们可以概括许多我们有效执行相同操作的过程 - 从一个函数传递一些输入,获取输出并将其提供给下一个函数,然后重复。

我在哪里作弊

呼,男孩,表白时间:

  • 正如我所说 - 函数组合适用于接受前一个输入的函数。因此,为了做什么,我不得不在一开始就FP部分,然后doSomething1doSomething2需要返回他们得到的价值。我已经包含了它log以显示需要发生的事情 - 获取一个值,用它做一些事情,返回该值。我只是想展示这个概念,所以我使用了最短的代码,在一定程度上做到了这一点。
  • compose可能用词不当。它有所不同,但有很多实现compose可以通过参数向后工作。所以,如果你想打电话f-> g->h你实际上会做compose(h, g, f). 这样做是有理由的 -毕竟是真实版本h(g(f(x))),所以这就是compose效仿。但是读起来不是很好。左到右的组合物I显示通常被命名为pipe(如在Ramda)或flow(如在Lodash)。我认为如果compose用于功能性组合标题会更好,但您的阅读方式一开始compose是违反直觉的,所以我使用了从左到右的版本。
  • 有真的,真的多了很多函数式编程。有一些构造(类似于数组是 FP 构造的方式)允许您从某个值开始,然后使用该值调用多个函数。但是组合更容易开始。

禁忌之术 eval

顿,顿,顿!

itemsToBuy
  .filter(item => item.stockAmount !== 0)      // remove sold out
  .map(item => item.price * item.basketAmount) // get prices
  .map(price => price + 12.50)                 // add shipping tax
  .reduce((a, b) => a + b, 0)                  // get the total
Run Code Online (Sandbox Code Playgroud)

所以……我又撒谎了。您可能会想“天哪,如果这一切都是谎言,我为什么要使用这个人在这里写的任何人”。如果你这么想——很好,继续这么想。难道不是因为它的使用这个超级坏

无论如何,我认为值得一提的是,在其他人没有正确解释为什么不好之前就跳进去了。

首先,发生了什么 - 使用eval动态创建本地绑定。然后使用上述绑定。这不会创建全局变量:

const getPrice = item => item.price * item.basketAmount;
const addShippingTax = price => price + 12.50;
Run Code Online (Sandbox Code Playgroud)

考虑到这一点,让我们看看为什么应该避免这种情况。

嘿,你注意到我用var, 而不是letconst吗?这只是你可以让自己陷入的第一个陷阱。使用的原因var是在调用 using或时eval 总是会创建一个新的词法环境。您可以查看规范章节18.2.1.1 Runtime Semantics: PerformEval。由于和仅在封闭的词法环境中可用,因此您只能在内部而不是外部访问它们。letconstletconsteval

const getPriceWithShippingTax = item => (item.price * item.basketAmount) + 12.50;
Run Code Online (Sandbox Code Playgroud)

所以,作为一个黑客,你只能使用var这样的声明在eval.

但这还不是全部。您必须非常小心传递的内容,eval因为您正在生成代码。我确实使用数字作弊(......一如既往)。数字文字和数值是相同的。但是如果您没有数字,则会发生以下情况:

const getPriceWithShippingTax = item => addShippingTax(getPrice(item));
Run Code Online (Sandbox Code Playgroud)

问题是生成的代码numericStringvar a = 42;- 这就是字符串的。所以,它被转换了。然后nonNumericString你会得到错误,因为它产生var a = abc并且没有abc变量。

根据字符串的内容,你会得到各种各样的东西——你可能会得到相同的值但转换为一个数字,你可能会得到完全不同的东西,或者你可能会得到 SyntaxError 或 ReferenceError。

如果要保留字符串变量仍然是字符串,则需要生成字符串文字

const compose = (...functions) => input => functions.reduce(
    (acc, fn) => fn(acc),
    input
)

const f = x => x + 1;
const g = x => x * 2;
const h = x => x + 3;

//create a new function that calls f -> g -> h
const composed = compose(f, g, h);

const x = 42

console.log(composed(x));

//call f -> g -> h directly
console.log(h(g(f(x))));
Run Code Online (Sandbox Code Playgroud)

这有效......但是你丢失了你实际输入的类型 -var a = "null"null.

如果您获得数组和对象,情况会更糟,因为您必须序列化它们才能将它们传递给eval. 并且JSON.stringify不会削减它,因为它不能完美地序列化对象——例如,它会删除(或更改)undefined值、函数,并且在保留原型或循环结构方面完全失败。

此外,eval编译器无法优化代码,因此它比简单地创建绑定要慢得多。如果您不确定是否会出现这种情况,那么您可能还没有单击该规范的链接。现在就这样做。

后退?好的,您是否注意到运行时涉及多少内容eval?每个规范有 29 个步骤,其中多个步骤引用了其他抽象操作。是的,有些是有条件的,是的,步数并不一定意味着要花费更多的时间,但它肯定会做很多更多的工作比你只需要创建一个绑定。提醒一下,这不能由引擎即时优化,所以它会比“真实”(非eval编辑)源代码慢。

那是在提到安全之前。如果您不得不对代码进行安全分析,您会非常讨厌 eval。是的,eval 可以是安全的,eval("2 + 2")不会产生任何副作用或问题。问题是您必须绝对确定您正在向eval. 那么,分析是为了eval("2 + " + x)什么?我们不能说,直到我们追溯所有可能的路径x以进行设置。然后追溯用于设置的任何内容x。然后追溯那些,等等,直到你发现初始值是否安全。如果它来自不受信任的地方,那么你就有问题了。

示例:您只需获取 URL 的一部分并将其放入x. 说,你有一个,example.com?myParam=42所以你myParam从查询字符串中获取值。攻击者可以轻而易举地制作一个查询字符串,该字符串已myParam设置为窃取用户凭据或专有信息并将其发送给自己的代码。因此,您需要确保您正在过滤 的值myParam。但是,您还必须时不时地重新进行相同的分析——如果您引入了一个新事物,现在您x从 cookie 中获取其价值,该怎么办?好吧,现在这很脆弱。

即使如果每一个可能的值x是安全的,你不能跳过重新运行分析。而且你必须定期这样做然后在最好的情况下,只需“好的,没关系”。但是,您可能还需要证明这一点。您可能需要填充一天刚刚进行x。如果您再使用eval四次,则需要整整一周。

所以,只要遵守古老的格言“eval is evil”。当然,这并不要,但它应该是一个不得已的手段。