1 回答

TA貢獻1877條經驗 獲得超6個贊
你不能這樣做。變量聲明語法允許使用逗號以便一次聲明多個變量。每個變量也可以選擇性地初始化為聲明的一部分,因此語法是(更抽象地):
(var | let | const) variable1 [= value1], variable2 [= value2], variable3 [= value3], ..., variableN [= valueN]
但是,這不是逗號運算符。就像逗號 inparseInt("42", 10)也不是逗號運算符一樣 - 它只是逗號字符在不同的上下文中具有不同的含義。
然而,真正的問題是逗號運算符與表達式一起使用,而變量聲明是一個語句。
差異的簡短說明:
表達式
基本上任何產生值的東西:2 + 2, fn(), a ? b : c, 等等。它將被計算并產生一些東西。
表達式可以在很多情況下嵌套:2 + fn()或者( a ? ( 2 + 2 ) : ( fn() ) )(為清楚起見,每個表達式都用括號括起來)。即使一個表達式沒有產生一個不會改變事物的可用值——一個沒有顯式返回的函數也會產生undefined,所以2 + noReturnFn()會產生亂碼,但它仍然是一個有效的表達式語法。
注意 1 of 2(下一節中更多):變量賦值是一個表達式,doinga = 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;
}
因此,它是一個產生一個值并在少數地方使用它的函數。我們將嘗試將其轉換為速記語法。
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標識符而不影響全局范圍。這是我們的第一個表達式,然后我們繼續其余的。
即使有人調用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 程序本質上是使用命令式結構將效果“粘合”在一起。因此,您會期望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. 只要我們調用的下一個函數使用前一個函數的輸入,這就會起作用。但這并不是很好——如果我們想調用三個函數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工作。所以,如果你想打電話-> ->你實際上會做. 這是有道理的——畢竟是真實的版本,所以這就是模仿。但它讀起來不太好。我展示的從左到右的組合通常被命名為(如在Ramda中)或(如在Lodash中)。我認為如果用于功能組合標題會更好,但你的閱讀方式一開始是違反直覺的,所以我選擇了從左到右的版本。fghcompose(h, g, f)h(g(f(x)))composepipeflowcomposecompose
函數式編程真的非常非常多。有一些結構(類似于數組是 FP 結構)允許您從某個值開始,然后使用該值調用多個函數。但是組合更容易開始。
禁術eval
Dun, dun, dunn!
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了 , 而不是letorconst嗎?這只是你可以讓自己陷入的第一個陷阱。使用的原因var是當使用or調用時eval 總是會創建一個新的詞法環境。您可以查看規范章節18.2.1.1 Runtime Semantics: PerformEval。由于和僅在封閉的詞法環境中可用,因此您只能在內部訪問它們而不能在外部訪問它們。letconstletconsteval
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變量。
根據字符串的內容,你會得到各種各樣的東西——你可能會得到相同的值但轉換為數字,你可能會得到完全不同的東西,或者你可能會得到一個 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.
如果你得到數組和對象,那就更糟了,因為你必須對它們進行序列化才能將它們傳遞給eval. 并且JSON.stringify不會削減它,因為它不能完美地序列化對象 - 例如,它會刪除(或更改)undefined值、函數,并且在保留原型或循環結構方面完全失敗。
此外,eval編譯器無法優化代碼,因此它比簡單地創建綁定要慢得多。如果您不確定是否會出現這種情況,那么您可能沒有點擊該規范的鏈接?,F在就這樣做。
后退?好的,你注意到跑步時涉及了多少東西eval嗎?每個規范有 29 個步驟,其中多個引用了其他抽象操作。是的,有些是有條件的,是的,步驟的數量并不一定意味著它需要更多的時間,但它肯定會做比創建綁定所需的更多的工作。提醒一下,引擎無法動態優化,所以它會比“真實”(非eval編輯)源代碼慢。
那是在提到安全性之前。如果你不得不對你的代碼進行安全分析,你會非常討厭 eval。是的,eval 可以安全eval("2 + 2")不會產生任何副作用或問題。問題是您必須絕對確定您將已知的良好代碼提供給eval. 那么,分析的目的是eval("2 + " + x)什么?在我們追溯所有可能的路徑x以進行設置之前,我們不能說。然后追溯用于設置的任何內容x。然后追溯那些,等等,直到你發現初始值是安全的。如果它來自不受信任的地方,那么你就有問題了。
示例:您只需獲取 URL 的一部分并將其放入x. 說,你有一個example.com?myParam=42,所以你myParam從查詢字符串中獲取值。攻擊者可以輕而易舉地制作一個查詢字符串,該字符串已myParam設置為竊取用戶憑據或專有信息并將其發送給自己的代碼。因此,您需要確保過濾myParam. 但是你也必須經常重新做同樣的分析——如果你引入了一個新的東西,你現在x從一個cookie中獲取價值怎么辦?好吧,現在這很脆弱。
即使的每個可能值x都是安全的,您也不能跳過重新運行分析。而且您必須定期執行此操作,然后在最好的情況下,只需說“好的,沒關系”。但是,您可能還需要證明這一點。您可能需要一個充實的x一天。如果您eval再使用四次,則需要整整一周。
所以,只要遵守古老的格言“eval is evil”。當然,它不一定是,但它應該是最后的工具。
添加回答
舉報