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

JavaScript declare a variable and use the comma operator in one statement?

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

let k = 0,
    j = 5 /*etc....*/

还知道在一行中执行多条语句(这对箭头函数很有用,不需要写return关键字),也使用逗号“,”操作符,比如所以:

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

console.log(r)

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

所以问题:

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

以下无效:

let k = 0, console.log(k), k += 8

Uncaught SyntaxError: Unexpected token '.'

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

let k = 0, k += 8

给予

Uncaught SyntaxError: Identifier 'k' has already been declared

然后像这样将整个内容放在括号中:

(let k = 0, k += 8);

给予

Uncaught SyntaxError: Unexpected identifier

指的是关键字"let"。但是,没有那个关键字,也没有问题:

(k = 0, k += 8);

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

这里有某种解决方法吗?

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

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

function meval(mainStr, argList) {
    let ID = (
        Math.random().toString() + 
        performance.now().toString()
        
    ).split(".").join("").split("")
    .map(x => ("qwertyuio")[x])
    .join(""),
        varName = "$______"+ID+"_____$",
        str = `
        var ${varName} = {};
        (argList => {
            Object.entries(argList).forEach(x => {
                ${varName}[x[0]] = x[1];   
            })
             
        });
    `;
 let myEval = eval;
    
    
    
    
 return (() => {

  myEval(str)(argList)
  myEval(`
   ${
    Object.keys(argList).map(x => 
     "let " + x + " = " + varName + "['" + x +"'];"
    ).join("\n")
   }
   ${mainStr}
   delete window[${varName}];
  `)
  
 })()
}

meval(`
    var g = a.ko + " world!"
    
`, {
    a: {ko: "hi"}
})
console.log(g);

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

(var | let | const) variable1 [= value1], variable2 [= value2], variable3 [= value3], ..., variableN [= valueN]

然而,那不是 comma operator。就像 parseInt("42", 10) 中的逗号也不是逗号运算符一样 - 它只是逗号 字符 在不同的上下文中具有不同的含义。

然而,真正的问题是逗号运算符与 表达式 一起使用,而变量声明是 语句 .

差异的简短说明:

Expressions

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

表达式可以在很多情况下嵌套:例如 2 + fn()( a ? ( 2 + 2 ) : ( fn() ) )(为清楚起见,每个表达式都用方括号括起来)。即使表达式没有产生不会改变事情的可用值 - 没有显式 return 的函数会产生 undefined 所以 2 + noReturnFn() 会产生乱码,但它仍然是一个有效的表达式语法。

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

let foo;
console.log(foo = "bar")

Statements

这些产生值。不是 undefined 什么都没有。示例包括 if(cond){}return resultswitch

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

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

console.log("the console.log call itself is an expression statement")

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

Note 2 of 2: 变量赋值是一个表达式,然而变量声明with赋值 不是。它只是变量声明语句语法的一部分。所以,两者重叠但不相关,只是逗号运算符和声明多个变量是相似的(允许你做多个事情)但不相关。

console.log(let foo = "bar"); //invalid - statement instead of expression

与逗号运算符的关系

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

exp1, exp2, exp3, ..., expN

并接受表达式,而不是语句。它一个一个地执行它们并且 returns 最后一个值。由于语句 具有 return 值,因此它们在这种情况下永远不会有效:(2 + 2, if(7) {}) 从 compiler/interpreter 的角度来看是无意义的代码,因为那里不能在这里return编辑任何内容。

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

可能的解决方法

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

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

所以,它是一个产生值并在少数地方使用它的函数。我们将尝试将其转换为 shorthand 语法。

IIFE

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

fn(42);

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

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

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

fn(42);

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

的值

使用参数作弊

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

fn(42);

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

即使某些body调用fn(42, "foo"),第二个参数也会被覆盖,所以实际上它和fn只接受一个参数是一样的。

使用函数的正常 body 作弊

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

fn(42);

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

函数组合和函数式编程

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

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

fn(42);

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

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

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

在 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

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

什么是功能组合

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

const getPrice = item => item.price * item.basketAmount;
const addShippingTax = price => price + 12.50;

但你并不需要做两次映射操作。我们可以 re-write 他们变成:

const getPriceWithShippingTax = item => (item.price * item.basketAmount) + 12.50;

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

const getPriceWithShippingTax = item => addShippingTax(getPrice(item));

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

现在终于到了函数组合的地方了。调用这些有顺序,我们可以概括它。

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

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

const composed = x => {
  const temp1 = f(x);
  const temp2 = g(temp1);
  const temp3 = h(temp2);
  return temp3;
}

但支持任意数量的函数并且不使用临时变量。因此,我们可以概括很多过程,我们在这些过程中做同样的事情——从一个函数传递一些输入,获取输出并将其提供给下一个函数,然后重复。

我哪里作弊了

吼小子,告白时间:

  • 正如我所说 - 函数组合与接受前一个输入的函数一起工作。因此,为了完成我在 FP 部分开头所做的事情,doSomething1doSomething2 需要 return 他们获得的值。我已经包含了 log 来显示需要发生的事情 - 取一个值,用它做一些事情,return 这个值。我只是想展示这个概念,所以我使用了最短的代码,在一定程度上做到了这一点。
  • compose 可能用词不当。它有所不同,但有很多实现 compose 通过参数 向后 工作。所以,如果你想调用 f -> g -> h 你实际上会调用 compose(h, g, f)。这是有原因的——毕竟 真正的 版本是 h(g(f(x))),所以这就是 compose 所模拟的。但是读起来不是很好。我展示的 left-to-right 组合通常命名为 pipe(如 Ramda) or flow (like in Lodash)。我认为如果 compose 用于 功能组合 标题会更好,但你阅读 compose 的方式首先是 counter-intuitive,所以我选择了 left-to-right 版本。
  • 函数式编程真的,真的很多。有一些构造(类似于数组是 FP 构造)允许您从某个值开始,然后使用该值调用多个函数。但是组合更容易入手。

禁术eval

咚咚咚!

const fn2 = x => (eval(`var k = ${computeValueFrom(x)}`), doSomething1(k), doSomething2(k), console.log(k), k)

fn(42);

所以...我又撒谎了。你可能会想“天哪,如果这都是谎言,我为什么要使用任何 body 这个家伙写在这里的东西”。如果您认为 - good,请继续思考。 不要使用它,因为它超级糟糕

无论如何,我认为在一些body else 没有正确解释为什么它不好之前跳进来之前值得一提。

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

const f = x => (eval(`var y =  ${x} + 1`), y);

console.log(f(42));         // 42
console.log(window.y);      // undefined
console.log("y" in window); // false
console.log(y);             // error

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

嘿,你有没有注意到我使用了 var,而不是 letconst?这只是你能遇到的第一个问题让自己进入。使用 var 的原因是 eval 在使用 letconst 调用时总是 创建一个新的词法环境。您可以查看规格 chapter 18.2.1.1 Runtime Semantics: PerformEval。由于 letconst 仅在封闭的词法环境中可用,因此您只能在 eval 内部访问它们,而不能在外部访问它们。

eval("const a = 1; console.log('inside eval'); console.log('a:', a)");

console.log("outside eval");
console.log("a: ", a); //error

因此,作为 hack,您只能使用 var 以便声明在 eval 之外可用。

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

const f = (x) => (eval("var a = " + x), a);

const number = f(42);
console.log(number, typeof number); //still a number

const numericString = f("42");
console.log(numericString, typeof numericString); //converted to number

const nonNumericString = f("abc"); //error
console.log(nonNumericString, typeof nonNumericString);

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

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

如果你想保留字符串变量仍然是一个字符串,你需要产生一个字符串literal:

const f = (x) => (eval(`var a = "${x}"`), a);

const numericString = f("42");
console.log(numericString, typeof numericString); //still a string

const nonNumericString = f("abc"); //no error
console.log(nonNumericString, typeof nonNumericString); //a string

const number = f(42);
console.log(number, typeof number); //converted to string

const undef = f(undefined);
console.log(undef, typeof undef); //converted to string

const nul = f(null);
console.log(nul, typeof nul); //converted to string

这行得通...但是您丢失了实际输入的类型 - var a = "null"null 不同。

如果你得到数组和 objects,情况会更糟,因为你必须序列化它们才能将它们传递给 evalJSON.stringify 不会削减它,因为它没有完美地序列化 objects - 例如,它将删除(或更改)undefined 值、函数,并且它在保留原型方面完全失败或圆形结构。

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

回来了吗?好的,您是否注意到 运行 eval 涉及了多少东西 ?每个规范有 29 个步骤,其中多个步骤引用其他抽象操作。是的,有些是有条件的,是的,步骤的数量并不一定意味着需要更多的时间,但它肯定会做 很多 比创建绑定所需的更多工作。提醒一下,引擎无法即时优化它,因此它会比“真实”(非evaled)源代码慢。

这还没有提到安全性。如果您不得不对您的代码进行安全分析,您会痛恨 eval。是的,eval可以是安全的eval("2 + 2")不会产生任何副作用或问题。问题是你必须绝对确定你正在向eval提供已知的好代码。那么,eval("2 + " + x) 的分析是什么?在我们追溯要设置 x 的所有可能路径之前,我们不能说。然后追溯任何用于设置 x 的东西。然后追溯那些等等,直到你发现 initial 值是否安全。如果它来自不受信任的地方,那么你就有问题了。

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

即使 if x 的每个可能值都是安全的,您 不能 跳过 re-running 分析。你必须定期这样做,然后在最好的情况下,只是“好的,没关系”。但是,您可能还需要证明这一点。您可能需要 x 的填充日。如果您已经 eval 再使用四次,那么整整一个星期就过去了。

所以,只要遵守古老的格言“eval is evil”即可。当然,它没有,但它应该是最后的手段。