JavaScript 修練 (3) | 運算子執行先後
運算子
前篇文章有提到表達式 ( Expression ) 會回傳值,而除了純值、呼叫函式之外,運算子 ( Operator ) 執行時也會回傳值,所以 MDN 文件把表達式和運算子放在同一章節內是有道理的
等號運算子也會回傳值
我們知道等號運算子 =
( Assignment Operator ) 是將右側的值賦予到左側變數上,但看開發者工具的 Console 執行結果,會發現它也會回傳值
🔮如果是 var a = 1;
則不會回傳值,雖然後方 a = 1
是表達式,但前篇有提過用 var 宣告會讓這行程式碼被判斷為陳述式,所以最終不會回傳值
補充:Assignment Operator 中文翻譯有指派運算子、指定運算子
運算子與運算元
運算子是由一個符號或單字組成,且至少要搭配一個運算元才能執行,所以我們常常會聽到的「一元」、「二元」、「三元」運算子,就是在說這個運算子的運算元數量
以下方的 +
運算子為例,而左右兩個數字是運算元,所以它是「二元運算子」
大部分運算子都是「二元運算子」,且都是以符號形式為主,如算數運算子 ( Arithmetic Operator ): +
、-
、*
、/
,此外也有「一元運算子」和「三元運算子」
一元運算子
前面提到的 delete 就是一元運算子,它是以 delete 這個單字為運算子,搭配後方的運算元組成,執行過程中會刪除物件下的一個屬性,同時也會回傳刪除的結果 ( 下圖紅框處 )
另外 typeof 也是一元運算子,它可以用來取得運算元的型別
三元運算子
三元運算子只有一個,也稱為「條件運算子」,它由兩個運算子 ?
、:
和三個運算元組成,會由 ?
左邊判斷式的真假值決定要執行 :
左邊或右邊的表達式,然後回傳一個結果,寫法示範如下
1 | // 判斷式 ? 真值時執行的表達式 : 假值時執行的表達式 |
因為 1 === 1
為 true
,所以執行 :
左邊的表達式,回傳值為 '相等'
和前篇介紹的 if 判斷式很類似,但寫法與執行還是有差異,它不僅可以作為判斷用,也可以用來回傳最終執行結果
1 | const isSleeping = true; |
無論是哪一種類型的運算子,都要記得「運算子屬於表達式的一種」( 會回傳值 )
運算子位置影響回傳和賦值先後
學習運算子 ++
的過程中,一定有看過類似 ++a
、a++
這兩種寫法,放前面跟放後面有差嗎?答案是有的,但是不一定每次都能看出它們的差異,所以可能會被忽略,導致當這樣的差異影響到程式碼的結果時,找不出原因在哪
部分運算子在執行過程中不會影響原始值,如:前面提過的 +
運算子,雖然會進行計算,但除非將結果另外用等號賦值,否則不會變更到變數的原始值
1 | var a = 1; |
但也有運算子在執行過程中會影響原始值,如:開頭說的 ++
運算子,它屬於一元運算子,在它前方或後方加入一個值為數值的變數作為運算元,就可以讓該運算元變數的值增加 1
以下兩組程式碼,差異只有第二行 ++
運算子放在 a
變數前或後,但無論前後都會影響變數原始值讓它增加 1,所以變數 a
的值最後都被賦予為 2
1 | var a = 1; |
1 | var a = 1; |
仔細看會發現雖然最後 a
的值都是 2,但是 ++a
跟 a++
兩者用 console.log
印出的結果卻不同,這是因為 ++
運算子放在運算元前方或後方,會影響它回傳值的時機點
++
在前方:會先計算並賦值,然後才會回傳運算元的變數值++
在後方:會先回傳運算元的變數值,然後才會計算並賦值
因此,隨然最後都會改變變數 a
的值,但是執行順序的不同,就會導致程式碼運作的不同,接下來看看幾個範例,實際比較兩者運作上的差異吧 ~
以下兩組程式碼就是因為回傳時機點不同,而導致 *10
的計算結果不同,第一組程式碼是將 a
的值 +1
後,才執行計算 *10
,第二組則是先用 +1
前的 a
值和 *10
計算 ( 1 * 10
),再將 a
的值 +1
1 | var a = 1; |
1 | var a = 1; |
小試身手
那麼經過上面的說明,試試看你能不能回答以下三個 console.log
印出的結果吧 ~
1 | var a = 1; |
以上程式碼中 a++
會先回傳變數 a
的值,所以 console.log(a++);
印出的結果不是計算賦值後的結果,是 a
計算前的值 1,而 console.log(a);
印出的結果,才是前面計算賦值後的結果 2,最後 ++a
會先計算並賦值,再回傳變數 a
的值,所以 console.log(++a);
印出的結果是計算後的結果 3
回傳值不一定是賦值
前面提過等號運算子,可以讓 =
右側的值賦予到左側,而且會回傳一個新值,而很多時候我們會誤以為賦值一定會成功,「回傳的值」在我們心裡跟「賦予的值」默默畫上了等號,但是這兩個值並不一定會相等 ( 直接心碎 QAQ,就像以下範例
1 | // 定義 obj 物件,在裡面加入屬性 value |
下圖是執行以上程式碼的結果,可以看到當執行 obj.value = 2;
回傳的值為 2,因為 obj
物件內的屬性 value
無法被更動,所以就算重新賦值,obj.value
的值還是 1,但是紅框處的回傳值卻不是 1 而是 2,因此等號回傳的結果不一定與賦予值相等
連續賦值產生的問題
既然前面提到了賦值,那就順便說說連續賦值會產生的問題吧 ~ 先看看以下範例,newValue 的最終值是 1 還是 2 呢?
1 | const obj = { |
依照前面講的等號執行順序,應該是先執行 obj.value = 2
,把右邊的值 2 賦予給 obj.value
,再執行 newValue = obj.value
,把 obj.value
的值賦予給 newValue
,然後因為 value
無法被更動,所以答案是 1 對吧?
很遺憾答錯了… 答案是 2,這時候一定覺得滿頭問號,為什麼?
現在就來重新拆解上方程式碼的執行流程:
- 執行
obj.value = 2
,這時候會回傳一個新值 2 - 執行
newValue = 2
,而這個 2 就來自於流程 1 回傳的值,所以newValue
的值跟obj.value
的值根本無關
中間的部分就算無法被賦值,賦予值的行為也還是會繼續進行,這就是要避免在實戰中連續賦值的原因,它可能會給你帶來意想不到的結果 (除非你喜歡驚嚇不然請避免這樣寫
其中在 ESLint 中 no-multi-assign 的內容就有提到不要連續賦值的範例如下,當連續賦值時,其實只有左側的變數 a
有被宣告,中間的變數 b
則會因為沒宣告變成全域屬性,所以在外層作用域也可以取得 b
的值
1 | (function() { |
優先序與相依性
我們在學數學四則運算時,老師總會說要「先乘除後加減」,所以當看到 4 + 2 * 3
,我們會習慣先處理後面的 2 * 3
,再將結果 6 與 4 相加,得出結果為 10
這個算式在 JavasScript 中得出的結果也是 10,這邊我們會發現其實 JavasScript 在執行時也有優先順序的概念,這樣的概念不僅限於數學符號,而是所有運算子都會依據此優先順序執行
而 JavasScript 中的「運算子優先序」( Operator precedence ) 就是用來決定執行先後順序的,優先序較高的運算子就會優先執行,上述的的例子中 *
運算子優先序就高於 +
運算子
另外還有一個「相依性」( Associativity ) 則用於決定執行的方向性,通常會認為程式碼是由左向右執行,但是其實也有許多運算子是由右向左執行的,如上面提到的等號運算子就是由右向左執行
而「優先序」與「相依性」會影響整段程式碼的執行結果,如果沒有它們決定執行順序,那麼 JavasScript 執行的概念會大亂,另外在 MDN 文件上也有表格,整理各個運算子的優先序與相依性可以參考
接下來來看幾個範例,觀察優先序與相依性如何影響程式碼執行順序
以下範例中,=
運算子優先序低於右側的 *
和 +
運算子,所以會先執行右側結果,而 *
運算子優先序高於 +
運算子,所以計算結果為 7,接下來再執行 =
把右側結果 7 賦值給左側的 a,所以 a 的值為 7
1 | const a = 3 + 2 * 5; |
接下來看一個比較的範例,兩行程式碼如果以我們固有的思考方式,會覺得兩個都應該回傳 true,但是對 JS 來說這兩行程式碼執行之後的結果卻不同
1 | 1 < 2 < 3; // 回傳 true |
兩行程式碼看似相同結果卻不同,所以跟連續賦值一樣,連續比較也是不建議在實戰中出現的寫法,那麼就來看看這兩行程式碼實際時怎麼執行的吧 ~
第一行 1 < 2 < 3;
<
運算子的相依性是由左向右執行,1 < 2
會得到 true 的結果- 再來執行
true < 3
,true 會轉型為數字 1,而 3 大於 1,所以結果為 true
第二行 3 > 2 > 1;
>
運算子的相依性是由左向右執行,3 > 2
會得到 true 的結果- 再來執行
true > 1
,1 不大於 1,所以結果為 false
看完上面兩行程式碼的實際執行,就會發現 1 < 2 < 3;
會回傳 true,只是個美麗的誤會,實際上並不會照我們期望的那樣比較,而是一步一步按照相依性規則執行比較的結果
結語
前面有提到運算子屬於表達式的一種,結合前篇文章表達式會回傳值的概念和以上程式碼示範,可以觀察到表達式的回傳值會影響前後文的運作,鏈式寫法雖然比較精簡,但還是要了解其中執行先後的概念,才能避開不如預期的錯誤或結果
那我們就下篇文章見囉 ~