探索Python來反補JavaScript,帶你 Cross Fire —— JS 資料型別的奧祕
資料型別可以說是程式語言的基石,重要性不言而喻。那麼現在就從資料型別開始,打破你的思維認知,做一個充滿想象力的 FEE
。針對上篇的一些偏激評論,我想強調的一點是: 我寫的文章,並不是給那些偏激到說髒話的人看的,請尊重每一位為前端貢獻微薄力量的 Blogger
。
好像,我這標題起的也太秀了,會不會被打:joy:。
多說一句
這篇可以算是 前端獵奇系列中的 探索 Python 來反補 JavaScript 的中篇。 如果沒有看過上篇文章的,可以去我的專欄裡讀讀上篇,在知識上沒有啥關聯的地方,相對獨立。基本是我在學習 PY
的時候,學到某一個地方,突然會想到 JS
在這一方面是如何表現的,然後隨著思考,真的會有不少收穫吧。
關於資料型別
有句話說的好,掌握資料型別是學習一門語言的基礎。我們從這句話中可以看出,掌握好資料型別是有多麼重要。你曾經是不是有想過 JS
的資料型別為什麼會是這樣,為什麼要有 null
、 undefined
。也許你有過疑問,但是疑問觸發後,簡簡單單的探尋後,就把疑問扔到回撥函式裡了,這一扔就是到如今。現在我將從 PY
的角度來反補 JS
,通過 PY
去看清 JS
的資料型別,看清程式語言的一些規律。now go!
JS
的資料型別分為值型別和引用型別:
- 值型別有:數字、字串、布林、Null、Undefined、Symbol、
- 引用型別有:Array、Function、Set、Map
PY
的資料型別分為數值型別、序列型別、Set型別、字典型別:
JS JS
現在我們看一下 PY
和 JS
的資料型別,這裡我不闡述具體是什麼,我只是總結一下,當我學習到這的時候,我對JS的資料型別有了什麼樣新的理解。現在,你會發現幾個很有趣的地方,請看如下:
關於 Set 和 Map
這和 PY
的 Set
、 Dictionary
不謀而合,但是 ES6
規範的制定者,沒有選擇使用 Dictionary
作為鍵值對的類名稱,而選擇了使用 Map
作為鍵值對的類名稱。而 Map
正是 Java
的鍵值對的類名稱。所以給我的感覺就是,JS在吸收很多語言的優秀的特性,我個人認為,命名成 Map
要比 Dictionary
好,畢竟少寫 7
個字元呢:joy:。
關於 Array 和 List
就這樣就結束了嗎?No,我們再看上面兩種型別,首先注意 PY
的 List
和 JS
的 Array
是相同的,都是可以動態進行修改的。但是很多 FEE
,因為掌握的知識不夠寬泛,導致了對很多事情不能理解的很透徹。比如,我們的思維中就是這樣一種固定的模式:陣列是可以動態修改的,陣列就是陣列型別。。我個人建議, FEE
一定不能將自己的思維束縛在某個維度裡。這真的會阻礙你 開啟那種瞬間頓悟的能力。
如果你瞭解了 PY
或者其他語言,你會發現其實 JS
的陣列,其在程式語言裡面,只能算是 List
型別,屬於序列型別的一種。而且很重要的是, JS
的 Array
是動態的,長度不固定,瞭解過 Java
的同學應該知道,在 Java
中,陣列是分為 Array
和 ArrayList
, Aarry
是長度固定的, ArrayList
是長度可以動態擴充套件的。所以 JS
的 Array
其實只是程式語言 的 Array
中的一種。如果你知道這些,我覺得這對去深刻理解 JS
的資料型別將有很大的幫助。雖然 JS
對一些知識點進行了簡化,但是作為一個合格的計算機工程師,我們不能習慣的接受簡化的知識點,一定要去多維度理解和 掌握簡化的知識點。瞭解其背後的世界,也是非常異彩紛呈的。
關於 JS 的 String 和 PY 的 String
你會發現 JS
的 String
是被歸類為數值型別,而 PY
的 String
是被歸類為序列型別。其實我個人更傾向於把 JS
的 String
歸為序列型別,為什麼這麼說呢,因為 JS
的字串本身就帶有很多屬性和方法,既然有方法和屬性,也就意味著至少是個物件吧,也就是隱式執行了 new String
。字串物件可以通過方法和屬性來操作自己的字元序列。所以這被歸類為數值型別的話,我個人認為是不科學的,而 PY
就分的很清楚。
關於 null 和 undefined
null 和 undefined 的爭論就在此結束吧。
可能一開始會對 null
和 undefined
比較陌生,可能會有那麼一刻,你懷疑過 JS
的 null
和 undfined
為什麼會被單獨作為資料型別,但是過了那一刻,你就默許其是一個語言設計規則了。但是我想說的是,語言設計規則也是人設計的,是人設計的就應該多一份懷疑,不必把設計語言的人看成神一樣。程式語言那麼多,哪有那麼多神。網上有很多好文章介紹 JS
的 undefined
和 null
的,也都說了有多坑。想深入理解有多坑的可以自行百度谷歌。我也不重複造解釋了,比如, undefined
居然不是保留字,也是夠神奇的,看了下部落格,有篇解釋的很不錯,可以瞅瞅 ofollow,noindex">為什麼undefined可以被賦值,而null不可以? 。寫部落格的時候,並不是一味的寫自己的東西,有時候別人總結好的東西,在我寫部落格過程中,也能帶給我很多靈感和收穫。這也是算是和站在巨人的肩膀上是一個道理吧。
不過我還是有點個人獨特的看法的。而且我認為我的看法要比網上絕大多數的見解要更加深刻(不要臉)。我不想說 undefined
有多坑,我只想去探究和理解 undefined
的本質。掌握了本質後,坑不坑也就不重要了。我個人認為, JS
的 undefined
是一種為了處理其他問題而強行做出的一種折中方案,且聽我娓娓道來。
既然 PY
和 JS
都是解釋性語言,那麼為什麼 PY
可以不依賴 undefined
,只需要使用 None
就可以了呢? 我寫一個簡單的例子,可以從我下面的分析中,找到一些更深層的真相,找到設計 undefined
真正的原因。程式碼如下:
let x console.log(x) 複製程式碼
# coding=utf-8 print(x) 複製程式碼
我們來看執行結果:

從圖中會發現, JS
沒有報錯,但是 PY
報錯了,究竟是什麼原因? 這裡中斷一下,我們來看下面這個截圖,是java的一段程式碼的執行結果:

圖中可以看出,在Java中,可以宣告變數,但不賦值,然後不去呼叫此變數,程式是不報錯的,但是在PY中,請看下面截圖:

我們發現,我們聲明瞭,也沒有去呼叫它,程式還是報錯了。是為什麼呢?
為什麼在 Java
, C++
, C
語言中,可以宣告變數,而不用賦值,並且不會報錯。而在 PY
中會報錯呢,而在 JS
中是 undefined
呢?其實仔細一想,會恍然大悟,一個非常關鍵的一點就是:
Java
、 C++
, C
是強型別語言,在宣告的時候,就已經確定了資料型別。所以就算不去賦值, Java
、 C++
等也會根據宣告的資料型別,設定一個預設的資料型別的值。但是這裡注意一點,如果整個程式執行完,在只宣告,卻沒有賦值的情況下,去輸出或者呼叫該變數,程式會報錯的。為什麼會報錯呢,是因為此變數的地址是系統隨機生成的,並不在此程式內的地址範圍內,也就是說此變數的地址可能是指向其他程式的地址,在這種情況下,如果去呼叫該地址,那麼可能會出現很大的危險性,比如你呼叫了其它很重要的東西。這裡我覺得可以把它理解為遊離的指標,雖然這樣形容不好,但是很形象,遊離的指標是很危險的東西。有多危險,哈哈哈,自己體會✧(≖ ◡ ≖✿)。
中斷結束,繼續 PS
,從上面的敘述知道了 Java
等語言是強型別語言。但是我們知道而 PY
和 JS
是指令碼語言,屬於弱型別語言,而弱型別語言的特點就是:在宣告變數的時候,不需要指定資料型別,這樣的好處就是一個變數可以指向萬物,缺點是效能差一些,需要讓編譯器在賦值的時候自己去做判斷。請緊跟著我的腳步,我們來看下面這段程式碼:
let x console.log(x) 複製程式碼
可以看到, x
是 JS
宣告的變數,由於指令碼語言是動態的,所以這個變數 x
可以指向萬物,那麼如果直接使用 x
,而不讓其報錯的話,該怎麼做呢。
一個原則一定不能忘,就是不賦值的話,呼叫一定會報錯,OK,那就賦值,給一個預設值,那麼這個預設值用什麼來表示呢,指向萬物的話,那這型別的可能性有好幾種,如果使用 null
來表示的話,由於 null
代表空物件,這裡說一個很關鍵的點,就是,為什麼其他語言中比如 Java
, C++
,他們對於空,都是使用 null
來代表一個空物件的?
其實最本質的原因還是因為他們是強型別語言,必須在變數前面宣告資料型別,對於值型別,他們系統可以自動給一個預設值。所以在強型別語言中的 null
,其作用只是給引用型別用的。而到了弱型別語言中,比如 PY
, JS
,我們看 PY
,由於 PY
老哥不想使用 undefied
,也只想用一個 null
。那麼自然而然的結果就是:直接不允許在未賦值之前,直接呼叫 宣告的變數
,只要調直接提示報錯,那麼你可能會有疑問了,為什麼PY語言中,連只宣告變數,不去呼叫它,程式都會報錯呢。其實我個人覺得原因是因為弱型別語言的資料型別不確定導致的,編譯器無法去給一個預設值,也就意味著不確定因素增加,既然不確定,那 PY
的做法就是直接使其報錯。通過編譯器報錯來顯式讓開發者去遵循編碼規則。
而小可愛 JS
就不一樣了,由於設計者就是不想使其報錯,想允許宣告,並且可以在未賦值的時候還可以直接呼叫而不報錯。所以也就意味著他要給宣告的變數賦一個預設值,怎麼賦值呢?這估計也是困擾了設計者良久,下面我舉一個很簡單易懂的例子,請看下面程式碼:
let x; let y = [1,2,3] console.log(x, y[3]) 複製程式碼
從程式碼可以看出,如果想不報錯,有幾種可能:
第一種:按照其他語言的規範,只保留一個空值 null
,ok,繼續往下推導,由於 JS
是弱型別,變數指向萬物,所以肯定只能給所有宣告但未賦值的變數設定 null
為預設值了。但是這樣的話,問題來了。
看第三行程式碼,其實 y[3]
也是宣告未賦值的 變數
,是不是有點不相信,覺得超出認知了。沒事,先繼續往下看,既然 y[3]
也是未賦值的 變數
,那把 y[3]
的預設值也設定為 null
嗎?很明顯,不合理。
因為 y[3]
可能是各種型別,如果直接都設定為 null
。那使用者直接列印 y[3]
,然後蹦出來一個 null
,還是 object
型別,豈不要炸?所以到這裡,我會慢慢發現,其實 JS
中的 null
和 undefined
是完全不同的兩碼事,很容易去區分。
綜上,我猜一下 JS
作者的腦洞應該是這樣的,既然我想讓呼叫宣告未賦值的變數不報錯,那 ojbk
。不是弱語言麼,不是指向萬物嗎?那要來就來刺激點,我就單獨設定一個數據型別,名為 undefined
。專門用來 counter
指向萬物的宣告卻未賦值的變數。哈哈哈哈,是不是很刺激:joy:。
解決最後一公里的疑惑
看下面程式碼
let x let y = [1,2,3] console.log(x, y[3]) 複製程式碼
你會發現 x
和 y[3]
都是 undefined
。我們來透過現象看本質,本質上就是聲明瞭,但是未賦值。為什麼可以這麼說,難道 y[3]
,也是聲明瞭,但未賦值嗎?我可以明確告訴你,是的,沒毛病。你可能不相信我說的話,下面我在白板上畫一個圖就頓悟了。。請看圖:

圖中可以看到,其實陣列的每一個下標也是在棧裡進行宣告的。和用 let x
進行宣告的操作是一樣的。 let x
的宣告如下圖:

所以是不是發現其實 undefined
也就那麼回事吧。一般來說,如果某一個知識點越繞人,那我們就應該從更底層的角度去看清這個知識點。只要你真的是從一個更加深刻和底層的角度去看待 undefined
,其實 just so so 啦。對了, null
我也順帶解釋了,只不過沒有重點關注,但是整篇下來,其實 null
是什麼,也差不多一清二楚了。總之 null
和 undefined
就是完全不同的兩碼事。
總結
從 JS
和 PY
的資料型別,我們可以看出, PY
在設計資料型別的時候,明顯考慮的很多,或者說,PY語言在被創造的時候,其資料型別的設計是比較規範的。而我們去看 JS
,會發現,有很多坑,感覺是當初為了簡化知識點難度,而留下了很多坑。雖然我沒有經歷過 IE
時代的前端,但現在也能深刻體會到前端工程師的不容易。以前還有同行說前端很簡單啊,現在也有,我都遇到過好幾次這種人了:
我:我是前端開發。
人家:噢,我知道了,就是寫網頁的對吧。。。
我心裡os:對你個錘子。。
FEE
們都是從坑裡一步步爬上來的,真的不容易。總之,現在的前端正在一步步走上規範,走上體面。。。
文末彩蛋一,動態引數
PY
中如何處理動態引數的呢,其實 PY
是通過元組或者字典來處理動態引數的,程式碼如下,這裡只寫使用 元組 實現動態引數的程式碼
# coding=utf-8 def add(x, *tupleName): print(x, tupleName) add('hello', 'godkun', '大王叫我來巡山') 複製程式碼
執行結果圖如下:

我們再看 JS
是如何實現的
function fun(a, ...tupleName) { console.log(a, tupleName) } fun('hello', 'godkun', '大王叫我來巡山') 複製程式碼
執行結果圖如下:

看上面兩種方式,看完你應該就明白了, ES6
增加展開符的原因是什麼,以及為什麼要設計成這個樣子。使用 ...
作為標記。同時為什麼要將所有可變引數放在一個數組裡面。
其實語言都是有相同性的,尤其對於 JS
語言來說,採納了很多語言的優點,這對於我們前端來說,是一個很大的優勢,如果平時善於去這樣比較和反補,我個人覺得, FEE
去承擔其他開發崗位,也是完全能 Hold
住的。
番外二,深夜寫部落格時的意外驚喜(意不意外,刺不刺激)
當我寫下這段程式碼:
function a(a, b, c) { console.log(arguments); console.log({ 0: "1", 1: "2" }); console.log([1, 2, 3]); } a(1, 2, 3); 複製程式碼
第一種情況:我在 node.js
環境執行:結果如圖所示:

第二種情況:我在 chrome
瀏覽器下執行這段程式碼,結果如圖所示:

第三種情況:我在 IE
瀏覽器下執行這段程式碼,結果如圖所示:

上面這個,你會發現在 chrome
瀏覽器下,輸出的結果形式為:
Arguments(3) [1, 2, 3, callee: ƒ, Symbol(Symbol.iterator): ƒ] 0: 1 1: 2 2: 3 callee: ƒ a(a,b,c) length: 3 Symbol(Symbol.iterator): ƒ values() __proto__: Object 複製程式碼
我靠,什麼鬼。居然把 arguments
寫成了陣列的形式:
[1, 2, 3, callee: ƒ, Symbol(Symbol.iterator): ƒ]
但是 __proto__
還是 Object
。嚇的我趕緊試了下面這段程式碼,程式碼如圖所示:

靠,還果真返回了長度。。。但是為什麼 __proto__
是 Object
。。。。
不行,我又看了IE瀏覽器和 node.js
環境下的結果,都是相同的結果,使用 {}
表示類物件陣列
{0: 1, 1: 2, 2: 3, callee: function a(a,b,c){}, length: 3} 複製程式碼
我陷入了沉思。。。。
不知道是 chrome
開發者故意這樣設計的,還是寫錯了。。小老弟,你怎麼回事? chrome
會弄錯? 本著上帝也不是萬能的理念,我打開了我的腦洞。
chrome
瀏覽器既然不按照 {}
這種寫法,直接將 arguments
寫成 []
,使其直接支援陣列操作,同時,其原型又繼續是物件原型。仔細看會發現又加了一行
Symbol(Symbol.iterator): ƒ values()
。
這樣做的目的是什麼,為什麼要這樣設計?搜了 blog
,然而沒搜到。。。這一連串的疑問,讓我再次陷入了沉思。。。
思考了一會,動筆畫了畫,發現好像可以找到理由解釋了。我覺得可以這麼解釋:
chrome
想讓類陣列物件這種不三不四的東西從谷歌瀏覽器中消失。所以下面這種輸出結果
{0: 1, 1: 2, 2: 3, callee: function a(a,b,c){}, length: 3} 複製程式碼
就一去不復返了,那麼如果不這樣寫,用什麼方法去替代它呢。答案就是寫一個原型鏈繼承為物件型別的陣列,同時給繼承物件型別的陣列(其還是物件,不是陣列) 增加 Symbol.iterator
屬性,使其可以 for of
。
為什麼要這樣做呢,因為一些內建型別自帶迭代器行為,比如 String
、 Array
、 Set
、 Map
,但是 Object
是不帶迭代器的,也就意味著我們可以推斷出,如果從 chrome
瀏覽器的那種寫法的表面上分析,假定 arguments
是 Array
,那麼就完全沒必要增加 Symbol.iterator
,所以矛盾,所以可以得出, arguments
還是物件,而物件是不帶迭代器的。所以要給形式為 []
的 arguments
增加 Symbol.iterator
。使其具有迭代器功能。從而可以使用 for of
。從而完成了 [1,2,3]
到 {'0':1, '1':2, '2':3}
的轉變
所以:上述答案被證明為正確。
當然,也可能是:
有理有據的胡謅。。。
備註:
- 我是根據學習PY來去思考JS的資料型別的,對於比如JS的Symbol,Set,Map,沒有去說官方用法,我覺得沒有必要吧。
- 我說的一些都是我個人出於一個心流狀態下的一些思考。可能有點問題,但是都是吾之所感。
文末的可愛宣告:如果轉發或者引用,請貼上原文連結,尊重一下勞動成果:joy:。文章可能 (肯定) 有一些錯誤,歡迎評論指出,也歡迎一起討論。文章可能寫的不夠好,還請多多包涵。人生苦短,我學前端,多一點貢獻,多一分開心,歡迎關注,後續更加精彩哦~
小夥伴覺得我寫得還不錯的話,就點個贊 以茲鼓勵 一下吧:blush:。