1 回答

TA貢獻1877條經(jīng)驗 獲得超6個贊
你不能這樣做。變量聲明語法允許使用逗號以便一次聲明多個變量。每個變量也可以選擇性地初始化為聲明的一部分,因此語法是(更抽象地):
(var | let | const) variable1 [= value1], variable2 [= value2], variable3 [= value3], ..., variableN [= valueN]
但是,這不是逗號運算符。就像逗號 inparseInt("42", 10)也不是逗號運算符一樣 - 它只是逗號字符在不同的上下文中具有不同的含義。
然而,真正的問題是逗號運算符與表達式一起使用,而變量聲明是一個語句。
差異的簡短說明:
表達式
基本上任何產(chǎn)生值的東西:2 + 2, fn(), a ? b : c, 等等。它將被計算并產(chǎn)生一些東西。
表達式可以在很多情況下嵌套:2 + fn()或者( a ? ( 2 + 2 ) : ( fn() ) )(為清楚起見,每個表達式都用括號括起來)。即使一個表達式?jīng)]有產(chǎn)生一個不會改變事物的可用值——一個沒有顯式返回的函數(shù)也會產(chǎn)生undefined,所以2 + noReturnFn()會產(chǎn)生亂碼,但它仍然是一個有效的表達式語法。
注意 1 of 2(下一節(jié)中更多):變量賦值是一個表達式,doinga = 1將產(chǎn)生被賦值的值:
let foo;
console.log(foo = "bar")
展開片段
聲明
這些不會產(chǎn)生價值。不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:變量賦值是一個表達式,但是帶賦值的變量聲明不是。它只是變量聲明語句語法的一部分。所以,這兩者重疊但不相關(guān),只是逗號運算符和聲明多個變量是相似的(允許你做多件事)但不相關(guān)。
console.log(let foo = "bar"); //invalid - statement instead of expression
展開片段
與逗號運算符的關(guān)系
現(xiàn)在我們知道了區(qū)別,它應該變得更容易理解了。逗號運算符的形式為
exp1, exp2, exp3, ..., expN
并接受表達式,而不是語句。它一個一個地執(zhí)行它們并返回最后一個值。由于語句沒有返回值,因此它們在這種情況下永遠不會有效:(2 + 2, if(7) {})從編譯器/解釋器的角度來看是無意義的代碼,因為這里不能返回任何東西。
因此,考慮到這一點,我們不能真正混合變量聲明和逗號運算符。let a = 1, a += 1不起作用,因為逗號被視為變量聲明語句,如果我們嘗試這樣做( ( let a = 1 ), ( a += 1 ) )仍然無效,因為第一部分仍然是語句,而不是表達式。
可能的解決方法
如果您確實需要在表達式上下文中生成變量并避免生成隱式全局變量,那么您可以使用的選項很少。讓我們用一個函數(shù)來說明:
const fn = x => {
let k = computeValueFrom(x);
doSomething1(k);
doSomething2(k);
console.log(k);
return k;
}
因此,它是一個產(chǎn)生一個值并在少數(shù)地方使用它的函數(shù)。我們將嘗試將其轉(zhuǎn)換為速記語法。
IIFE
const fn = x => (k => (doSomething1(k), doSomething2(k), console.log(k), k))
(computeValueFrom(x));
fn(42);
在您自己的內(nèi)部聲明一個以k為參數(shù)的新函數(shù),然后立即使用 的值調(diào)用該函數(shù)computeValueFrom(x)。如果為了清楚起見,我們將函數(shù)與調(diào)用分開,我們會得到:
const extractedFunction = k => (
doSomething1(k),
doSomething2(k),
console.log(k),
k
);
const fn = x => extractedFunction(computeValueFrom(x));
fn(42);
因此,該函數(shù)k使用逗號運算符按順序獲取和使用它幾次。我們只是調(diào)用函數(shù)并提供 的值k。
使用參數(shù)作弊
const fn = (fn, k) => (
k = computeValueFrom(x),
doSomething1(k),
doSomething2(k),
console.log(k),
k
);
fn(42);
基本上和以前一樣——我們使用逗號操作符來執(zhí)行幾個表達式。但是,這次我們沒有額外的功能,我們只是在fn. 參數(shù)是局部變量,因此它們的行為類似于let/var在創(chuàng)建局部可變綁定方面。然后我們分配給該k標識符而不影響全局范圍。這是我們的第一個表達式,然后我們繼續(xù)其余的。
即使有人調(diào)用fn(42, "foo")第二個參數(shù)也會被覆蓋,所以實際上它就像fn只接受一個參數(shù)一樣。
使用正常的函數(shù)體作弊
const fn = x => { let k = computeValueFrom(x); doSomething1(k); doSomething2(k); console.log(k); return k; }
fn(42);
我撒了謊?;蛘吒_切地說,我作弊了。這不在表達式上下文中,您擁有與以前相同的所有內(nèi)容,但它只是刪除了換行符。重要的是要記住,您可以這樣做并用分號分隔不同的語句。它仍然是一條線,幾乎沒有比以前長。
函數(shù)組合和函數(shù)式編程
const log = x => {
console.log(x);
return x;
}
const fn = compose(computeValueFrom, doSomething1, doSomething2, log)
fn(42);
這是一個巨大的話題,所以我?guī)缀醪粫谶@里觸及表面。我也只是為了介紹這個概念而過度簡化了事情。
那么,什么是函數(shù)式編程(FP)?
它使用函數(shù)作為基本構(gòu)建塊進行編程。是的,我們確實已經(jīng)有了函數(shù),并且我們確實使用它們來生成程序。然而,非 FP 程序本質(zhì)上是使用命令式結(jié)構(gòu)將效果“粘合”在一起。因此,您會期望ifs、fors 和調(diào)用多個函數(shù)/方法來產(chǎn)生效果。
在 FP 范式中,您擁有使用其他功能一起編排的功能。很多時候,這是因為您對數(shù)據(jù)上的操作鏈感興趣。
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
數(shù)組支持來自函數(shù)世界的方法,因此這是一個有效的 FP 示例。
什么是功能組合
現(xiàn)在,假設(shè)您想從上面獲得可重用的功能,然后提取這兩個:
const getPrice = item => item.price * item.basketAmount;
const addShippingTax = price => price + 12.50;
但是你真的不需要做兩次映射操作。我們可以將它們重寫為:
const getPriceWithShippingTax = item => (item.price * item.basketAmount) + 12.50;
但是讓我們嘗試在不直接修改函數(shù)的情況下這樣做。我們可以一個接一個地調(diào)用它們,這樣就可以了:
const getPriceWithShippingTax = item => addShippingTax(getPrice(item));
我們現(xiàn)在已經(jīng)重用了這些功能。我們會調(diào)用getPrice并將結(jié)果傳遞給addShippingTax. 只要我們調(diào)用的下一個函數(shù)使用前一個函數(shù)的輸入,這就會起作用。但這并不是很好——如果我們想調(diào)用三個函數(shù)f, g, 和h一起,我們需要x => h(g(f(x))).
現(xiàn)在終于到了函數(shù)組合的用武之地。調(diào)用這些是有順序的,我們可以概括它。
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))));
展開片段
你去了,我們已經(jīng)將這些功能與另一個功能“粘合”在一起。相當于做:
const composed = x => {
const temp1 = f(x);
const temp2 = g(temp1);
const temp3 = h(temp2);
return temp3;
}
但支持任意數(shù)量的函數(shù),并且不使用臨時變量。因此,我們可以概括很多我們有效地執(zhí)行相同操作的過程 - 從一個函數(shù)傳遞一些輸入,獲取輸出并將其饋送到下一個函數(shù),然后重復。
我在哪里作弊
呵呵,小子,告白時間:
正如我所說 - 功能組合與接受前一個輸入的功能一起使用。所以,為了做我在 FP 部分一開始所做的事情,然后doSomething1需要doSomething2返回他們得到的值。我已經(jīng)將其包括在內(nèi)log以顯示需要發(fā)生的事情 - 獲取一個值,用它做某事,返回該值。我只是試圖展示這個概念,所以我使用了最短的代碼來達到足夠的程度。
compose可能是用詞不當。它有所不同,但有很多實現(xiàn)通過參數(shù)向后compose工作。所以,如果你想打電話-> ->你實際上會做. 這是有道理的——畢竟是真實的版本,所以這就是模仿。但它讀起來不太好。我展示的從左到右的組合通常被命名為(如在Ramda中)或(如在Lodash中)。我認為如果用于功能組合標題會更好,但你的閱讀方式一開始是違反直覺的,所以我選擇了從左到右的版本。fghcompose(h, g, f)h(g(f(x)))composepipeflowcomposecompose
函數(shù)式編程真的非常非常多。有一些結(jié)構(gòu)(類似于數(shù)組是 FP 結(jié)構(gòu))允許您從某個值開始,然后使用該值調(diào)用多個函數(shù)。但是組合更容易開始。
禁術(shù)eval
Dun, dun, dunn!
const fn2 = x => (eval(`var k = ${computeValueFrom(x)}`), doSomething1(k), doSomething2(k), console.log(k), k)
fn(42);
所以……我又撒謊了。你可能會想“天哪,如果這都是謊言,我為什么要使用這個人在這里寫的任何人”。如果您認為-很好,請繼續(xù)思考。不要使用它,因為它非常糟糕。
無論如何,我認為在其他人沒有正確解釋它為什么不好的情況下跳進去之前值得一提。
首先,發(fā)生了什么 - 使用eval動態(tài)創(chuàng)建本地綁定。然后使用所述綁定。這不會創(chuàng)建全局變量:
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了 , 而不是letorconst嗎?這只是你可以讓自己陷入的第一個陷阱。使用的原因var是當使用or調(diào)用時eval 總是會創(chuàng)建一個新的詞法環(huán)境。您可以查看規(guī)范章節(jié)18.2.1.1 Runtime Semantics: PerformEval。由于和僅在封閉的詞法環(huán)境中可用,因此您只能在內(nèi)部訪問它們而不能在外部訪問它們。letconstletconsteval
eval("const a = 1; console.log('inside eval'); console.log('a:', a)");
console.log("outside eval");
console.log("a: ", a); //error
展開片段
所以,作為一個黑客,你只能使用var這樣聲明在外部可用eval。
但這還不是全部。您必須非常小心傳入的內(nèi)容,eval因為您正在生成代碼。我確實通過使用數(shù)字作弊(......一如既往)。數(shù)字文字和數(shù)值是相同的。但是,如果您沒有數(shù)字,會發(fā)生以下情況:
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;- 那是字符串的值。所以,它被轉(zhuǎn)換了。然后nonNumericString你會得到錯誤,因為它會產(chǎn)生var a = abc并且沒有abc變量。
根據(jù)字符串的內(nèi)容,你會得到各種各樣的東西——你可能會得到相同的值但轉(zhuǎn)換為數(shù)字,你可能會得到完全不同的東西,或者你可能會得到一個 SyntaxError 或 ReferenceError。
如果要將字符串變量保留為字符串,則需要生成字符串文字:
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.
如果你得到數(shù)組和對象,那就更糟了,因為你必須對它們進行序列化才能將它們傳遞給eval. 并且JSON.stringify不會削減它,因為它不能完美地序列化對象 - 例如,它會刪除(或更改)undefined值、函數(shù),并且在保留原型或循環(huán)結(jié)構(gòu)方面完全失敗。
此外,eval編譯器無法優(yōu)化代碼,因此它比簡單地創(chuàng)建綁定要慢得多。如果您不確定是否會出現(xiàn)這種情況,那么您可能沒有點擊該規(guī)范的鏈接。現(xiàn)在就這樣做。
后退?好的,你注意到跑步時涉及了多少東西eval嗎?每個規(guī)范有 29 個步驟,其中多個引用了其他抽象操作。是的,有些是有條件的,是的,步驟的數(shù)量并不一定意味著它需要更多的時間,但它肯定會做比創(chuàng)建綁定所需的更多的工作。提醒一下,引擎無法動態(tài)優(yōu)化,所以它會比“真實”(非eval編輯)源代碼慢。
那是在提到安全性之前。如果你不得不對你的代碼進行安全分析,你會非常討厭 eval。是的,eval 可以安全eval("2 + 2")不會產(chǎn)生任何副作用或問題。問題是您必須絕對確定您將已知的良好代碼提供給eval. 那么,分析的目的是eval("2 + " + x)什么?在我們追溯所有可能的路徑x以進行設(shè)置之前,我們不能說。然后追溯用于設(shè)置的任何內(nèi)容x。然后追溯那些,等等,直到你發(fā)現(xiàn)初始值是安全的。如果它來自不受信任的地方,那么你就有問題了。
示例:您只需獲取 URL 的一部分并將其放入x. 說,你有一個example.com?myParam=42,所以你myParam從查詢字符串中獲取值。攻擊者可以輕而易舉地制作一個查詢字符串,該字符串已myParam設(shè)置為竊取用戶憑據(jù)或?qū)S行畔⒉⑵浒l(fā)送給自己的代碼。因此,您需要確保過濾myParam. 但是你也必須經(jīng)常重新做同樣的分析——如果你引入了一個新的東西,你現(xiàn)在x從一個cookie中獲取價值怎么辦?好吧,現(xiàn)在這很脆弱。
即使的每個可能值x都是安全的,您也不能跳過重新運行分析。而且您必須定期執(zhí)行此操作,然后在最好的情況下,只需說“好的,沒關(guān)系”。但是,您可能還需要證明這一點。您可能需要一個充實的x一天。如果您eval再使用四次,則需要整整一周。
所以,只要遵守古老的格言“eval is evil”。當然,它不一定是,但它應該是最后的工具。
添加回答
舉報