Day 4:動態型別加弱型別不是罪——怎麼 JavaScript 一摔就變成個印度阿三?

前兩篇文章介紹了什麼是動態型別、靜態型別、強型別、弱型別,也知道了幾個常見語言分屬哪種類別。

懶人包複習一下:

  • 靜態語言/動態語言:變數和型別的綁定方法。

  • 弱型別/強型別:語言型別系統(Type System)對型別檢查的嚴格程度,也就是型別安全的程度。

其中動態型別加弱型別是最不安全的組合,也是我認為 JavaScript 撰寫上鬆散不安全的源頭之一。

但怪了,動態型別加弱型別的語言又不是只有 JavaScript,同樣使用者眾多的 PHP 也是屬於動態型別加弱型別,為何 PHP 不像 JavaScript 這麼讓人詬病?

套句港片「賭俠」裡星爺的台詞:我摔你也摔,怎麼你一摔就變成個印度阿三啦?

動態型別加弱型別不是罪,動態型別加超級弱型別就是一場夢魘,更別說攪和了 JavaScript 自身型別系統(Type System)的混亂

很多問題是相對性的,需要經過比較才會凸顯出來,因此前兩篇文章刻意都以其他語言做舉例,本篇文章將把目光拉回 JavaScript,用實例來檢視 JavaScript 在型別處理上的各種特性,透過和其他語言的比較,將更能體會 JavaScript 在型別安全上的脆弱。

究竟~是動態型別加弱型別的原罪使然,還是型別系統的命運糾葛,讓我們繼續看下去~

動態型別

前面介紹過動態型別和靜態型別的差別,現在我們可以很清楚判斷 JavaScript 屬於動態語言:

  • 宣告變數時不需指定型別。

  • 過程可以任意更換變數的型別。

var x = 123;
console.log(x);
x = 'Hello';
console.log(x);

執行結果:

123
Hello

弱型別

Day3 文章介紹到,弱型別常導致 Bug 的一個情境就是允許不同型別間的比對

這部分 JavaScript 和 PHP 一樣,提供了兩種嚴格程度不同的比較運算子:=====

console.log( "111" == 111 ? "Yes" : "No" );     // "Yes"
console.log( "111" === 111 ? "Yes" : "No" );    // "No"

另一個情境是如果在數字運算過程混進一個字串,會發生什麼事?

強型別的 Java 會發生編譯錯誤;弱型別的 PHP 會企圖將字串轉成數字,以完成算術運算的任務。

那 JavaScript 呢?

var x = 123 + "456";
console.log(x);

執行結果:

123456

同樣是弱型別的 JavaScript,執行時一樣不會發生錯誤,會自動做一些轉型動作以讓程式繼續運行下去。

不同的是,JavaScript 不會去將字串轉成數字,反而是將前面的數字結果轉成字串,再和後面的字串作串接,所以得到 "123456" 的結果。

幾乎一模一樣的語法情境,為什麼 JavaScript 的處理邏輯和 PHP 截然不同?

數學運算?字串串接?

用另一個例子來看,差異會更明顯。

如果是 PHP,以下寫法會得到單純的數字運算結果,也就是一個整數型態的 999

<?php
$x = 111 + 222 + "333" + 111 + 222;
echo $x;
?>

執行結果:

999

如果是 JavaScript 呢?

var x = 111 + 222 + "333" + 111 + 222;
console.log(x);

執行結果:

333333111222

是一個讓人黑人問號的答案呢。

程式不是魔術師,其實有跡可循。前面的 111 + 222 被視為算術運算做相加,得到數字 333 ;繼續和中間的字串 "333" 相加,被視為字串的串連,得到 "333333";而最後的 111222 雖然是數字型態,都被以字串相接的方式處理,以至於得到 "333333111222" 這麼滑稽的答案。

為什麼一樣都是整數型態的 111222,JavaScript 連自己前後處理的方式都不一樣?

在 JavaScript 裡,如果我們想對兩個變數的值做數學相加,語法表現方式如下:

var v1 = 111;
var v2 = 222;
var x = v1 + v2;    // 333

如果想對兩個變數的值做字串串接,語法表現方式如下:

var v1 = "111";
var v2 = "222";
var x = v1 + v2;    // "111222"

發現問題了嗎?無論是數學相加,還是字串串接,用的都是同一個符號 + 來表達,事實上他們是不同的運算子:

  • 用在數學相加時:Arithmetic Operators (算數運算子)

  • 用在串連字串時:Concatenation Operators (串接運算子)

那 JavaScript 怎麼決定它要把眼前的 + 視為算數運算子還是串接運算子

JavaScript 的做法是:如果 + 左右兩邊的運算元有任何一個型別是字串,就會被當作串接運算子,否則就是算數運算子

不幸的,由於 JavaScript 是動態型別,單從 var x = v1 + v2; 這行程式本身,我們無法判斷 JavaScript 會將 + 當成算數運算子還是串接運算子,因為變數 v1v2 有可能是任何型別。

這就是為什麼 JavaScript 常常容易出現一些意想不到的相加結果。

可能會有疑問:為什麼 PHP 沒這個問題?

很簡單,因為 PHP 的算數運算子和串接運算子是不同符號,PHP 的串接運算子是 .

<?php
$v1 = 111;
$v2 = 222;
$x = $v1 + $v2; // 333
$x = $v1 . $v2; // "111222"
?>

另一個動態型別加弱型別的語言 Perl 也有同樣的設計,讓數學運算和字串串接可以被清楚區隔。

超級弱型別

Day3 的文章提到一點:強型別和弱型別不是 1 或 0 二元論,而有「程度」的差別

例如 PHP 是弱型別,但還是有個容忍的限度,例如將數字和陣列做算術運算就會發生 Error:

$x = 123 + array("Apple", "Banana");
echo $x;
PHP Fatal error:  Unsupported operand types in /home/cg/root/6938116/main.php on line 3

JavaScript 要命在於它是超級弱型別

幾乎什麼都能相加、什麼都能比較、什麼都不奇怪,一堆毫無邏輯的寫法在 JavaScript 通通都可以執行!

console.log( [] + [] );                     // ""
console.log( [] + {} );                     // "[object Object]"
console.log( [] == {} );                    // false
console.log( [] == false );                 // true
console.log( [] == 0 );                     // true
console.log( ['Apple', 'Banana'] == 2 );    // false
console.log( ['Apple', 'Banana'] == true ); // false
console.log( 123 + ['Apple', 'Banana'] );   // "123Apple,Banana"
console.log( true + true + true );          // 3
console.log( false + false + false );       // 0

動態型別加弱型別不是問題,動態型別加超級弱型別就變成一場夢魘。

令人混淆的型別系統

JavaScript 自身型別系統的設計也讓人感覺不夠嚴謹,或是令人困惑。

null 都不 null 了

最具代表性的型別問題應該就是「null 是個 object」的弔詭現象。

null 的概念在各種程式語言很普遍,代表「nothing」,也就是這個變數裡面沒有值。

但在 JavaScript 裡如果用 typeof 去取得其型別,卻會回傳 "object" 的結果。

W3Schools 也稱呼這是一個 JavaScript 的 Bug:

You can consider it a bug in JavaScript that typeof null is an object. It should be null.

獨特的 undefined 型別

JavaScript 除了常見的 null 型別,還有另一個特別的型別:undefined

undefined 在概念上和使用上都非常類似 null,但在 JavaScript 的型別系統中他們又確確實實屬於不同的型別。

事實上,在 JavaScript 程式,比起 null 型別,undefined 更接近其他語言對 NULL 的習慣。

(例如去印一個沒有初始化的變數,會得到 undefined 而不是 null)

var x;
console.log(x); // "undefined"

相形之下,null 就顯得很雞肋(而且是有 Bug 的雞肋...),徒增混淆。

JavaScript 的型別夢魘真的沒救了嗎?

當然不。

JavaScript 終究是動態網頁不可缺少的角色,為了降低開發 JavaScript 程式的風險、提升程式穩定性,許多轉譯語言被發明出來,例如 TypeScript 是近年竄起的代表。

這些轉譯語言試圖在語法上引入一些特性,例如強型別、物件導向,讓 JavaScript 的程式撰寫更容易、更安全、更易於管控。

短期來說,學習這些轉譯語言需要增加額外的時間成本;但長期而言,當專案規模越來越大,轉譯語言有助於提升 JavaScript 程式開發和維護上的穩定性。

各家轉譯語言各有不同特性,是否應該導入轉譯語言、導入哪一種,沒有絕對答案,視乎各團隊的技能狀況和專案性質,權衡其中的成本效益和風險。

References:

Last updated