众所周知,要声明多个变量,可以使用如下格式:
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
说
未捕获的语法错误:意外的标记".">
如果没有控制台.log,它认为我在重新声明 k:
let k = 0, k += 8
给
Uncaught SyntaxError: Identifier 'k' has already been declared
并将整个事情放在括号中,如下所示:
(let k = 0, k += 8);
给
Uncaught SyntaxError: Unexpected identifier
指关键词"让"。但是,如果没有该关键字,则没有问题:
(k = 0, k += 8);
除了 k 现在成为全局变量这一事实,这是不需要的。
这里有某种解决方法吗?
如何在 JavaScript 中将逗号运算符与局部变量声明一起使用?
编辑 响应 VLAZ 的答案评估部分,要将参数传递到 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]
但是,这不是逗号运算符。就像parseInt("42", 10)
中的逗号也不是逗号运算符一样 - 它只是逗号字符在不同的上下文中具有不同的含义。
然而,真正的问题是逗号运算符使用表达式,而变量声明是一个语句。
区别的简短解释:
表达 式
基本上任何产生值的东西:2 + 2
、fn()
、a ? b : c
等。它是将被计算并产生一些东西的东西。
表达式可以在许多场合嵌套:例如2 + fn()
或( a ? ( 2 + 2 ) : ( fn() ) )
(为了清楚起见,每个表达式都用括号括起来)。即使表达式没有产生不会改变事物的可用值 - 没有显式返回的函数也会产生undefined
因此2 + noReturnFn()
会产生乱码,但它仍然是一个有效的表达式语法。
注 1(共 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:变量赋值是一个表达式,但带赋值的变量声明不是。它只是变量声明语句语法的一部分。因此,两者重叠但不相关,只是逗号运算符和声明多个变量的方式相似(允许您做多件事)但不相关。
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);
我撒谎了。或者更确切地说,我作弊了。这不是在表达式上下文中,您的所有内容都与以前相同,但它只是删除了换行符。重要的是要记住,您可以这样做并用分号分隔不同的语句。它仍然是一条线,几乎不比以前长。
函数组成和函数编程
const log = x => {
console.log(x);
return x;
}
const fn = compose(computeValueFrom, doSomething1, doSomething2, log)
fn(42);
这是一个巨大的话题,所以我在这里几乎不打算触及表面。我也过度简化了事情,只是为了引入这个概念。
那么,什么是函数式编程(FP)?
它使用函数作为基本构建块进行编程。是的,我们已经有了函数,我们确实使用它们来生成程序。然而,非FP程序本质上是使用命令式构造将效果"粘合"在一起。因此,您希望if
s,for
s并调用多个函数/方法来产生效果。
在 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
。只要我们调用的下一个函数使用前一个函数的输入,这就可以工作。但这并不好 - 如果我们想同时调用三个函数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)
.这是有道理的——毕竟真实版本是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
的原因是,当使用let
或const
调用时,eval
总是创建一个新的词汇环境。您可以看到规范第18.2.1.1 章运行时语义:执行Eval。由于let
和const
仅在封闭的词汇环境中可用,因此您只能在eval
内部访问它们,而不能在外部访问它们。
eval("const a = 1; console.log('inside eval'); console.log('a:', a)");
console.log("outside eval");
console.log("a: ", a); //error
因此,作为黑客,您只能使用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
变量。
根据字符串的内容,你会得到各种各样的东西 - 你可能会得到相同的值,但转换为一个数字,你可能会得到完全不同的东西,或者你可能会得到一个语法错误或引用错误。
如果要将字符串变量保留为字符串,则需要生成字符串文本:
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 个步骤,其中多个步骤引用其他抽象操作。是的,有些是有条件的,是的,步骤数并不一定意味着它需要更多的时间,但它肯定会做比创建绑定所需的更多工作。提醒一下,引擎无法即时优化,因此它将比"真实"(非eval
编辑)源代码慢。
这甚至在提到安全性之前。如果你曾经不得不对你的代码进行安全分析,你会热情地讨厌eval
。是的,eval
可以是安全的eval("2 + 2")
不会产生任何副作用或问题。问题是您必须绝对确定您正在向eval
提供已知良好的代码。那么,eval("2 + " + x)
的分析是什么?在我们追溯所有可能的路径来设置x
之前,我们不能说。然后追溯用于设置x
的任何内容。然后追溯这些等,直到您发现初始值是否安全。如果它来自不受信任的地方,那么您就有问题了。
示例:您只需获取 URL 的一部分并将其放入x
.假设您有一个example.com?myParam=42
,因此您可以从查询字符串中获取myParam
的值。攻击者可以轻而易举地手工创建一个查询字符串,该字符串myParam
设置为代码,这些代码将窃取用户的凭据或专有信息并将其发送给自己。因此,您需要确保过滤myParam
的值。但是你也必须经常重新做同样的分析 - 如果你引入了一个新东西,你现在从cookie中获取x
的值怎么办?好吧,现在这很脆弱。
即使x
的每个可能值都是安全的,也不能跳过重新运行分析。你必须定期这样做,那么在最好的情况下,只需说"好吧,没关系"。但是,您可能还需要证明这一点。您可能需要一个填充日才能x
.如果你已经用了四次eval
,那么整整一周的时间。
所以,只要遵守古老的格言"评估是邪恶的"。当然,它不一定是,但它应该是最后的手段。