你不能那样做。变量声明语法允许使用逗号以便一次声明多个变量。每个变量也可以选择初始化为声明的一部分,所以语法是(更抽象的):
(var | let | const) variable1 [= value1], variable2 [= value2], variable3 [= value3], ..., variableN [= valueN]
但是,这不是comma operator。就像parseInt("42", 10) 中的逗号也不是逗号运算符一样 - 它只是逗号 character 在不同的上下文中具有不同的含义。
然而,真正的问题是逗号运算符与表达式一起使用,而变量声明是一个语句。
区别的简要说明:
基本上任何产生值的东西:2 + 2、fn()、a ? b : c 等。它会被计算并产生一些东西。
表达式可以在很多场合嵌套:例如2 + fn() 或( a ? ( 2 + 2 ) : ( fn() ) )(为清楚起见,每个表达式都用括号括起来)。即使表达式不会产生不会改变事物的可用值 - 没有显式返回的函数也会产生 undefined 所以 2 + noReturnFn() 会产生乱码,但它仍然是有效的表达式语法。
注意 1 of 2(下一节会详细介绍):变量赋值是表达式,执行a = 1 将产生被赋值的值:
let foo;
console.log(foo = "bar")
这些不产生价值。不是undefined 什么都没有。示例包括if(cond){}、return result、switch。
语句仅在独立时有效。您不能像 if (return 7) 那样嵌套它们,因为这在语法上无效。您不能进一步在需要表达式的地方使用语句 - console.log(return 7) 同样无效。
只是一个注释,一个表达式可以用作语句。这些被称为表达式语句:
console.log("the console.log call itself is an expression statement")
因此,您可以在语句有效的情况下使用表达式,但不能在表达式有效的情况下使用语句。
注意 2 of 2:变量 assignment 是一个表达式,但变量声明 with assignment 不是。它只是变量声明语句语法的一部分。所以,两者重叠但不相关,只是逗号运算符和声明多个变量是相似的(允许你做多件事)但不相关。
console.log(let foo = "bar"); //invalid - statement instead of expression
与逗号运算符的关系
现在我们知道了区别,它应该变得更容易理解了。逗号运算符的形式为
exp1, exp2, exp3, ..., expN
并接受表达式,而不是语句。它一个一个地执行它们并返回最后一个值。由于语句没有有返回值,因此它们在这种情况下永远不会有效:(2 + 2, if(7) {}) 从编译器/解释器的角度来看是无意义的代码,因为 不能返回任何东西在这里。
因此,考虑到这一点,我们不能真正混合变量声明和逗号运算符。 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;
}
所以,它是一个产生一个值并在少数地方使用它的函数。我们将尝试将其转换为简写语法。
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 标识符而不影响全局范围。这是我们的第一个表达式,然后我们继续其余的。
即使有人调用fn(42, "foo"),第二个参数也会被覆盖,所以实际上它就像fn 只接受一个参数一样。
使用正常的函数体作弊
const fn = x => { let k = computeValueFrom(x); doSomething1(k); doSomething2(k); console.log(k); return k; }
fn(42);
我撒谎了。或者更确切地说,我作弊了。这 not 在表达式上下文中,您拥有与以前相同的所有内容,但它只是删除了换行符。重要的是要记住您可以这样做并用分号分隔不同的语句。它仍然是一行,并且比以前几乎没有长。
函数组合和函数式编程
const log = x => {
console.log(x);
return x;
}
const fn = compose(computeValueFrom, doSomething1, doSomething2, log)
fn(42);
这是一个巨大的话题,所以我几乎不会在这里触及表面。为了介绍这个概念,我也过于简单化了。
那么,什么是函数式编程(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
数组支持来自函数世界的方法,所以这个是一个有效的 FP 示例。
什么是功能组合
现在,假设您想从上面获得可重用的函数,然后提取这两个:
const getPrice = item => item.price * item.basketAmount;
const addShippingTax = price => price + 12.50;
但您实际上并不需要进行两次映射操作。我们可以将它们重写为:
const getPriceWithShippingTax = item => (item.price * item.basketAmount) + 12.50;
但是让我们尝试在不直接修改函数的情况下这样做。我们可以一个接一个地调用它们,这样就可以了:
const getPriceWithShippingTax = item => addShippingTax(getPrice(item));
我们现在已经重用了这些函数。我们会调用getPrice 并将结果传递给addShippingTax。只要我们调用的 next 函数使用前一个函数的输入,这就会起作用。但这并不是很好——如果我们想同时调用三个函数f、g和h,我们需要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 部分一开始所做的事情,
doSomething1 和 doSomething2 需要返回他们得到的值。我已经包含 log 以显示需要发生的事情 - 获取一个值,用它做某事,返回值。我只是想展示这个概念,所以我使用了最短的代码来做到这一点。
-
compose 可能用词不当。它有所不同,但有很多实现compose 通过参数向后工作。所以,如果你想打电话给f -> g -> h,你实际上会打电话给compose(h, g, f)。这是有原因的 - real 版本毕竟是 h(g(f(x))),所以这就是 compose 所模拟的。但它读起来不太好。我展示的从左到右的组合通常命名为pipe(如Ramda)或flow(如Lodash)。我认为如果将compose 用于功能组合 标题会更好,但您阅读compose 的方式一开始是违反直觉的,所以我从左到右版本。
- 真的,真的函数式编程还有很多。有一些结构(类似于数组是 FP 结构)允许您从某个值开始,然后使用该值调用多个函数。但是组合更容易开始。
禁术eval
邓恩,邓恩,邓恩!
const fn2 = x => (eval(`var k = ${computeValueFrom(x)}`), doSomething1(k), doSomething2(k), console.log(k), k)
fn(42);
所以……我又撒谎了。你可能会想“天哪,如果这都是谎言,我为什么要使用这个人在这里写的任何人”。如果您认为 - 好,请继续思考。 不要使用它,因为它超级糟糕。
无论如何,我认为在没有正确解释为什么不好的情况下,我认为值得一提。
首先,发生了什么——使用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,而不是let 或const?这只是你可以让自己陷入的第一个陷阱。使用var 的原因是eval 总是 在使用let 或const 调用时会创建一个新的词法环境。您可以查看规格chapter 18.2.1.1 Runtime Semantics: PerformEval。由于let 和const 仅在封闭的词法环境中可用,因此您只能在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; - 这是字符串的值。所以,它被转换了。然后使用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 不同。
如果你得到数组和对象,那就更糟了,因为你必须对它们进行序列化才能将它们传递给eval。而JSON.stringify 不会删除它,因为它不能完美地序列化对象 - 例如,它会删除(或更改)undefined 值、函数,并且它完全无法保留原型或循环结构。
此外,eval 代码无法由编译器优化,因此它比简单地创建绑定要慢得多。如果您不确定是否会出现这种情况,那么您可能没有点击该规范的链接。立即执行此操作。
回来了?好的,您是否注意到运行eval 时涉及多少内容?每个规范有 29 个步骤,其中多个引用了其他抽象操作。是的,有些是有条件的,是的,步骤的数量并不一定意味着它需要更多的时间,但它肯定会做很多比创建绑定所需的工作更多。提醒一下,引擎无法动态优化,所以它会比“真实”(非evaled)源代码慢。
那是在提到安全性之前。如果您必须对代码进行安全分析,您会讨厌 eval 充满热情。是的,eval可以安全eval("2 + 2")不会产生任何副作用或问题。问题是您必须绝对确保将已知的良好代码提供给eval。那么,eval("2 + " + x) 的分析结果是什么?在我们追溯所有可能的路径以设置 x 之前,我们不能说。然后追溯用于设置x 的任何内容。然后追溯这些,等等,直到您发现 initial 值是否安全。如果它来自不受信任的地方,那么你就有问题了。
示例:您只需将 URL 的一部分放入 x。假设您有一个example.com?myParam=42,因此您从查询字符串中获取myParam 的值。攻击者可以轻而易举地制作一个将myParam 设置为代码的查询字符串,该代码将窃取用户的凭据或专有信息并将其发送给自己。因此,您需要确保过滤myParam 的值。但是您还必须不时地重新进行相同的分析——如果您引入了一个新事物,现在您从 cookie 中获取 x 的值怎么办?好吧,现在这很容易受到攻击。
即使如果x 的每个可能值都是安全的,您不能跳过重新运行分析。而且您必须定期这样做,然后在最好的情况下,只需说“好的,这很好”。但是,您可能还需要证明这一点。您可能需要为x 填满一天只是。如果您又使用了四次eval,那么就会有整整一周的时间。
所以,请遵守古老的格言“eval is evil”。当然,它不是必须,但它应该是最后的手段。