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

只谈情不闲聊 提交于 2020-04-11 05:45:25

问题


it's known that to declare multiple variables, one uses a format like:

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

It's also known that to execute multiple statements in one line (which is useful for arrow functions, making it not necessary to write the return keyword), the comma "," operator is also used, like so:

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

console.log(r)

not the most elegant example, but the point is you can execute multiple statements in one line, separated by a comma ",", and the last value is returned.

So the question:

how do you combine both of these techniques? Meaning, how do we declare a variable in one line, and, one comma later, use that variable for something?

The following is not working:

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

says

Uncaught SyntaxError: Unexpected token '.'

and without the console.log, it thinks I'm re-declaring k:

let k = 0, k += 8

gives

Uncaught SyntaxError: Identifier 'k' has already been declared

And putting the whole thing in parenthesis like so:

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

gives

Uncaught SyntaxError: Unexpected identifier

referring to the keyword "let". However, without that keyword, there is no problem:

(k = 0, k += 8);

except for the fact that k now becomes a global variable, which is not wanted.

Is there some kind of workaround here?

How can I use the comma operator together with a local variable declaration in JavaScript?

EDIT in response to VLAZ's eval part of the answer, to pass parameters into eval, a custom function can be made:

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

回答1:


You cannot do that. The variable declaration syntax allows for a comma in order to declare multiple variables at once. Each variable can also be optionally initialised as part of the declaration, so the syntax is (more abstractly):

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

However, that is NOT the comma operator. Same like how the comma in parseInt("42", 10) is also not the comma operator - it's just the comma character that has a different meaning in a different context.

The real problem, however, is that the comma operator works with expressions, while the variable declaration is a statement.

Short explanation of the difference:

Expressions

Basically anything that produces a value: 2 + 2, fn(), a ? b : c, etc. It's something that will be computed and produces something.

Expressions can be nested in many occasions: 2 + fn() or ( a ? ( 2 + 2 ) : ( fn() ) ) (each expression surrounded by brackets for clarity) for example. Even if an expression doesn't produce a usable value that doesn't change things - a function with no explicit return will produce undefined so 2 + noReturnFn() will produce gibberish but it's still a valid expression syntax.

Note 1 of 2 (more in the next section): variable assignment is an expression, doing a = 1 will produce the value being assigned:

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

Statements

These don't produce a value. Not undefined just nothing. Examples include if(cond){}, return result, switch.

A statement is only valid standalone. You cannot nest them like if (return 7) as that's not syntactically valid. You can further not use statements where an expression is expected - console.log(return 7) is equally invalid.

Just a note, an expression can be used as a statement. These are called expression statements:

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

So, you can use an expression where a statement is valid but you cannot use a statement where an expression is valid.

Note 2 of 2: variable assignment is an expression, however variable declaration with assignment is not. It's just part of the syntax for variable declaration statement. So, the two overlap but aren't related, just how the comma operator and declaring multiple variables are similar (allow you to do multiple things) but not related.

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

Relation with the comma operator

Now we know that the difference and it should become easier to understand. The comma operator has a form of

exp1, exp2, exp3, ..., expN

and accepts expressions, not statements. It executes them one by one and returns the last value. Since statements don't have a return value then they can never be valid in such context: (2 + 2, if(7) {}) is meaningless code from compiler/interpreter perspective as there cannot be anything returned here.

So, with this in mind we cannot really mix the variable declaration and comma operator. let a = 1, a += 1 doesn't work because the comma is treated as variable declaration statement, and if we try to do ( ( let a = 1 ), ( a += 1 ) ) that's still not valid, as the first part is still a statement, not an expression.

Possible workarounds

If you really need to produce a variable inside an expression context and avoid producing implicit globals, then there are few options available to you. Let's use a function for illustration:

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

So, it's a function that produces a value and uses it in few places. We'll try to transform it into shorthand syntax.

IIFE

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

fn(42);

Declare a new function inside your own that takes k as parameter and then immediately invoke that function with the value of computeValueFrom(x). If we separate the function from the invocation for clarity we get:

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

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

fn(42);

So, the function is taking k and using it in sequence few times with the comma operator. We just call the function and supply the value of k.

Cheat using parameters

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

fn(42);

Basically the same as before - we use the comma operator to execute several expressions. However, this time we don't have an extra function, we just add an extra parameter to fn. Parameters are local variables, so they behave similar to let/var in terms of creating a local mutable binding. We then assign to that k identifier without affecting global scope. It's the first of our expressions and then we continue with the rest.

Even if somebody calls fn(42, "foo") the second argument would be overwritten, so in effect it's the same as if fn only took a single parameter.

Cheat using normal body of a function

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

fn(42);

I lied. Or rather, I cheated. This is not in expression context, you have everything the same as before, but it's just removing the newlines. It's important to remember that you can do that and separate different statements with a semicolon. It's still one line and it's barely longer than before.

Function composition and functional programming

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

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

fn(42);

This is a huge topic, so I'm barely going to scratch the surface here. I'm also vastly oversimplifying things only to introduce the concept.

So, what is functional programming (FP)?

It's programming using functions as the basic building blocks. Yes, we do have functions already and we do use them to produce programs. However, non-FP programs essentially "glue" together effects using imperative constructs. So, you'd expect ifs, fors, and calling several functions/methods to produce an effect.

In the FP paradigm, you have functions that you orchestrate together using other functions. Very frequently, that's because you're interested in chains of operations over data.

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

Arrays support methods that come from the functional world, so this is a valid FP example.

What is functional composition

Now, let's say you want to have reusable functions from the above and you extract these two:

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

But you don't really need to do two mapping operations. We could just re-write them into:

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

but let's try doing it without directly modifying the functions. We can just call them one after another and that would work:

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

We've reused the functions now. We'd call getPrice and the result is passed to addShippingTax. This works as long as the next function we call uses the input of the previous one. But it's not really nice - if we want to call three functions f, g, and h together, we need x => h(g(f(x))).

Now finally here is where function composition comes in. There is order in calling these and we can generalise it.

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

And there you go, we've "glued" the functions together with another function. It is equivalent to doing:

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

but supports any amount of functions and it doesn't use temporary variables. So, we can generalise a lot of processes where we do effectively the same - pass some input from one function, take the output and feed it into the next function, then repeat.

Where did I cheat here

Hoo, boy, confession time:

  • As I said - functional composition works with functions that take the input of the previous one. So, in order to do what I had in the very beginning of the FP section, then doSomething1 and doSomething2 need to return the value they get. I've included that log to show what needs to happen - take a value, do something with it, return the value. I'm trying to just present the concept, so I went with the shortest code that did it to enough of a degree.
  • compose might be a misnomer. It varies but with a lot of implementations compose works backwards through the arguments. So, if you want to call f -> g -> h you'd actually do compose(h, g, f). There is rationale for that - the real version is h(g(f(x))) after all, so that's what compose emulates. But it doesn't read very well. The left-to-right composition I showed is usually named pipe (like in Ramda) or flow (like in Lodash). I thought it'd be better if compose was used for the functional composition headline but the way you read compose is counter-intuitive at first, so I went with the left-to-right version.
  • There is really, really a lot more to functional programming. There are constructs (similar to how arrays are FP constructs) that will allow you to start with some value and then call multiple functions with said value. But composition is simpler to start with.

The Forbidden Technique eval

Dun, dun, dunn!

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

fn(42);

So...I lied again. You might be thinking "geez, why would I use anybody this guy wrote hereme if it's all lies". If you are thinking that - good, keep thinking it. Do not use this because it's super bad.

At any rate, I thought it's worth mentioning before somebody else jumps in without properly explaining why it's bad.

First of all, what is happening - using eval to dynamically create local binding. And then using said binding. This does not create a global variable:

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

With that in mind, let's see why this should be avoided.

Hey did you notice I used var, instead of let or const? That's just the first of the gotchas you can get yourself into. The reason to use var is that eval always creates a new lexical environment when called using let or const. You can see the specs chapter 18.2.1.1 Runtime Semantics: PerformEval. Since let and const are only available within the enclosing lexical environment, then you can only access them inside eval and not outside.

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

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

So, as a hack, you can only use var so that the declaration is available outside eval.

But that's not all. You have to be very careful with what you pass into eval because you are producing code. I did cheat (...as always) by using a number. Numeric literals and numeric values are the same. But here is what happens if you don't have a numeric:

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

The problem is that the code produced for numericString is var a = 42; - that's the value of the string. So, it gets converted. Then with nonNumericString you get error since it produces var a = abc and there is no abc variable.

Depending on the content of the string, you'd get all sorts of things - you might get the same value but converted to a number, you might get something different entirely or you might get a SyntaxError or ReferenceError.

If you want to preserve the string variable to still be a string, you need to produce a string 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

This works...but you lose the types you actually put in - var a = "null" is not the same as null.

It's even worse if you get arrays and objects, as you have to serialise them in order to be able to pass them to eval. And JSON.stringify will not cut it, since it does not perfectly serialise objects - for example, it will remove (or change) undefined values, functions, and it flat out fails at preserving prototypes or circular structures.

Furthermore, eval code cannot be optimised by the compiler, so it will be drastically slower than simply creating a binding. If you are unsure that would be the case, then you probably haven't clicked on the link to the spec. Do this now.

Back? OK did you notice how much stuff is involved when running eval? There are 29 steps per the spec and multiple of them reference other abstract operations. Yes, some are conditional and yes, the number of steps doesn't necessarily mean it takes more time but it's definitely going to do a lot more work than you need just to create a binding. Reminder, that cannot be optimised by the engine on the fly, so you it's going to be slower than "real" (non-evaled) source code.

That's before even mentioning security. If you ever had to do security analysis of your code you'd hate eval with passion. Yes, eval can be safe eval("2 + 2") will not produce any side effects or problems. The problem is that you have to be absolutely sure that you are feeding known good code to eval. So, what would be the analysis for eval("2 + " + x)? We cannot say until we trace back all possible paths for x to be set. Then trace back anything that is used to set x. Then trace back those, etc, until you find that the initial value is safe or not. If it comes from untrusted place then you have a problem.

Example: you just take part of the URL and put it in x. Say, you have a example.com?myParam=42 so you take the value of myParam from the query string. An attacker can trivially craft a query string that has myParam set to code that will steal the user's credentials or proprietary information and send them over to himself. Thus, you need to ensure that you are filtering the value of myParam. But you also have to re-do the same analysis every so often - what if you've introduced a new thing where you now take the value for x from a cookie? Well, now that's vulnerable.

Even if every possible value for x is safe, you cannot skip re-running the analysis. And you have to do this regularly then in the best case, just say "OK it's fine". However, you might need to also prove it. You might need a fill day just for x. If you've used eval another four times, there goes a full week.

So, just abide to the old adage "eval is evil". Sure, it doesn't have to be but it should be a last resort tool.



来源:https://stackoverflow.com/questions/61077989/javascript-declare-a-variable-and-use-the-comma-operator-in-one-statement

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!