探索 Python 來反補 JavaScript,帶你 Cross Fire —— JS 資料型別的奧祕

原始碼終結者發表於2018-11-30

寫在最前

資料型別可以說是程式語言的基石,重要性不言而喻。那麼現在就從資料型別開始,打破你的思維認知,做一個充滿想象力的FEE。針對上篇的一些偏激評論,我想強調的一點是:我寫的文章,並不是給那些偏激到說髒話的人看的,請尊重每一位為前端貢獻微薄力量的Blogger

好像,我這標題起的也太秀了,會不會被打?。

多說一句

這篇可以算是 前端獵奇系列中的 探索 Python 來反補 JavaScript 的中篇。 如果沒有看過上篇文章的,可以去我的專欄裡讀讀上篇,在知識上沒有啥關聯的地方,相對獨立。基本是我在學習PY的時候,學到某一個地方,突然會想到JS在這一方面是如何表現的,然後隨著思考,真的會有不少收穫吧。

關於資料型別

有句話說的好,掌握資料型別是學習一門語言的基礎。我們從這句話中可以看出,掌握好資料型別是有多麼重要。你曾經是不是有想過JS的資料型別為什麼會是這樣,為什麼要有nullundefined。也許你有過疑問,但是疑問觸發後,簡簡單單的探尋後,就把疑問扔到回撥函式裡了,這一扔就是到如今。現在我將從PY的角度來反補JS,通過PY去看清JS的資料型別,看清程式語言的一些規律。now go!

JS的資料型別分為值型別和引用型別:

  1. 值型別有:數字、字串、布林、Null、Undefined、Symbol、
  2. 引用型別有:Array、Function、Set、Map

PY的資料型別分為數值型別、序列型別、Set型別、字典型別:

  1. 數值型別有:Integer、Long integer、Boolean、Double-precision floating
  2. 序列型別有:String、Tuple、List(和JS的Array相同)
  3. Set型別(和JS的Set相同)
  4. Dictionary型別(和JS的Map相同)

現在我們看一下PYJS的資料型別,這裡我不闡述具體是什麼,我只是總結一下,當我學習到這的時候,我對JS的資料型別有了什麼樣新的理解。現在,你會發現幾個很有趣的地方,請看如下:

關於 Set 和 Map

這和PYSetDictionary不謀而合,但是ES6規範的制定者,沒有選擇使用Dictionary作為鍵值對的類名稱,而選擇了使用Map作為鍵值對的類名稱。而Map正是Java的鍵值對的類名稱。所以給我的感覺就是,JS在吸收很多語言的優秀的特性,我個人認為,命名成Map要比Dictionary好,畢竟少寫7個字元呢?。

關於 Array 和 List

就這樣就結束了嗎?No,我們再看上面兩種型別,首先注意PYListJSArray是相同的,都是可以動態進行修改的。但是很多FEE,因為掌握的知識不夠寬泛,導致了對很多事情不能理解的很透徹。比如,我們的思維中就是這樣一種固定的模式:陣列是可以動態修改的,陣列就是陣列型別。。我個人建議,FEE一定不能將自己的思維束縛在某個維度裡。這真的會阻礙你 開啟那種瞬間頓悟的能力。

如果你瞭解了PY或者其他語言,你會發現其實JS的陣列,其在程式語言裡面,只能算是List型別,屬於序列型別的一種。而且很重要的是,JSArray是動態的,長度不固定,瞭解過Java的同學應該知道,在Java中,陣列是分為ArrayArrayListAarry是長度固定的,ArrayList是長度可以動態擴充套件的。所以JSArray其實只是程式語言 的Array中的一種。如果你知道這些,我覺得這對去深刻理解JS的資料型別將有很大的幫助。雖然JS對一些知識點進行了簡化,但是作為一個合格的計算機工程師,我們不能習慣的接受簡化的知識點,一定要去多維度理解和 掌握簡化的知識點。瞭解其背後的世界,也是非常異彩紛呈的。

關於 JS 的 String 和 PY 的 String

你會發現JSString是被歸類為數值型別,而PYString是被歸類為序列型別。其實我個人更傾向於把JSString歸為序列型別,為什麼這麼說呢,因為JS的字串本身就帶有很多屬性和方法,既然有方法和屬性,也就意味著至少是個物件吧,也就是隱式執行了new String。字串物件可以通過方法和屬性來操作自己的字元序列。所以這被歸類為數值型別的話,我個人認為是不科學的,而PY就分的很清楚。

關於 null 和 undefined

null 和 undefined 的爭論就在此結束吧。

可能一開始會對nullundefined比較陌生,可能會有那麼一刻,你懷疑過JSnullundfined為什麼會被單獨作為資料型別,但是過了那一刻,你就默許其是一個語言設計規則了。但是我想說的是,語言設計規則也是人設計的,是人設計的就應該多一份懷疑,不必把設計語言的人看成神一樣。程式語言那麼多,哪有那麼多神。網上有很多好文章介紹JSundefinednull的,也都說了有多坑。想深入理解有多坑的可以自行百度谷歌。我也不重複造解釋了,比如,undefined居然不是保留字,也是夠神奇的,看了下部落格,有篇解釋的很不錯,可以瞅瞅為什麼undefined可以被賦值,而null不可以?。寫部落格的時候,並不是一味的寫自己的東西,有時候別人總結好的東西,在我寫部落格過程中,也能帶給我很多靈感和收穫。這也是算是和站在巨人的肩膀上是一個道理吧。

不過我還是有點個人獨特的看法的。而且我認為我的看法要比網上絕大多數的見解要更加深刻(不要臉)。我不想說undefined有多坑,我只想去探究和理解undefined的本質。掌握了本質後,坑不坑也就不重要了。我個人認為,JSundefined是一種為了處理其他問題而強行做出的一種折中方案,且聽我娓娓道來。

既然PYJS都是解釋性語言,那麼為什麼PY可以不依賴undefined,只需要使用None就可以了呢? 我寫一個簡單的例子,可以從我下面的分析中,找到一些更深層的真相,找到設計undefined真正的原因。程式碼如下:

let x
console.log(x)
複製程式碼
# coding=utf-8
print(x)
複製程式碼

我們來看執行結果:

image

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

image

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

image

我們發現,我們宣告瞭,也沒有去呼叫它,程式還是報錯了。是為什麼呢?

為什麼在JavaC++C語言中,可以宣告變數,而不用賦值,並且不會報錯。而在PY中會報錯呢,而在JS中是undefined呢?其實仔細一想,會恍然大悟,一個非常關鍵的一點就是:

JavaC++C是強型別語言,在宣告的時候,就已經確定了資料型別。所以就算不去賦值,JavaC++等也會根據宣告的資料型別,設定一個預設的資料型別的值。但是這裡注意一點,如果整個程式執行完,在只宣告,卻沒有賦值的情況下,去輸出或者呼叫該變數,程式會報錯的。為什麼會報錯呢,是因為此變數的地址是系統隨機生成的,並不在此程式內的地址範圍內,也就是說此變數的地址可能是指向其他程式的地址,在這種情況下,如果去呼叫該地址,那麼可能會出現很大的危險性,比如你呼叫了其它很重要的東西。這裡我覺得可以把它理解為遊離的指標,雖然這樣形容不好,但是很形象,遊離的指標是很危險的東西。有多危險,哈哈哈,自己體會✧(≖ ◡ ≖✿)。

中斷結束,繼續PS,從上面的敘述知道了Java等語言是強型別語言。但是我們知道而PYJS是指令碼語言,屬於弱型別語言,而弱型別語言的特點就是:在宣告變數的時候,不需要指定資料型別,這樣的好處就是一個變數可以指向萬物,缺點是效能差一些,需要讓編譯器在賦值的時候自己去做判斷。請緊跟著我的腳步,我們來看下面這段程式碼:

let x
console.log(x)
複製程式碼

可以看到,xJS宣告的變數,由於指令碼語言是動態的,所以這個變數x可以指向萬物,那麼如果直接使用x,而不讓其報錯的話,該怎麼做呢。

一個原則一定不能忘,就是不賦值的話,呼叫一定會報錯,OK,那就賦值,給一個預設值,那麼這個預設值用什麼來表示呢,指向萬物的話,那這型別的可能性有好幾種,如果使用null來表示的話,由於null代表空物件,這裡說一個很關鍵的點,就是,為什麼其他語言中比如JavaC++,他們對於空,都是使用null來代表一個空物件的?

其實最本質的原因還是因為他們是強型別語言,必須在變數前面宣告資料型別,對於值型別,他們系統可以自動給一個預設值。所以在強型別語言中的null,其作用只是給引用型別用的。而到了弱型別語言中,比如PYJS,我們看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中的nullundefined是完全不同的兩碼事,很容易去區分。

綜上,我猜一下JS作者的腦洞應該是這樣的,既然我想讓呼叫宣告未賦值的變數不報錯,那ojbk。不是弱語言麼,不是指向萬物嗎?那要來就來刺激點,我就單獨設定一個資料型別,名為undefined。專門用來counter指向萬物的宣告卻未賦值的變數。哈哈哈哈,是不是很刺激?。

解決最後一公里的疑惑

看下面程式碼

let x
let y = [1,2,3]
console.log(x, y[3])
複製程式碼

你會發現xy[3]都是undefined。我們來透過現象看本質,本質上就是宣告瞭,但是未賦值。為什麼可以這麼說,難道y[3],也是宣告瞭,但未賦值嗎?我可以明確告訴你,是的,沒毛病。你可能不相信我說的話,下面我在白板上畫一個圖就頓悟了。。請看圖:

image

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

image

所以是不是發現其實undefined也就那麼回事吧。一般來說,如果某一個知識點越繞人,那我們就應該從更底層的角度去看清這個知識點。只要你真的是從一個更加深刻和底層的角度去看待undefined,其實 just so so 啦。對了,null我也順帶解釋了,只不過沒有重點關注,但是整篇下來,其實null是什麼,也差不多一清二楚了。總之nullundefined就是完全不同的兩碼事。

總結

JSPY的資料型別,我們可以看出,PY在設計資料型別的時候,明顯考慮的很多,或者說,PY語言在被創造的時候,其資料型別的設計是比較規範的。而我們去看JS,會發現,有很多坑,感覺是當初為了簡化知識點難度,而留下了很多坑。雖然我沒有經歷過IE時代的前端,但現在也能深刻體會到前端工程師的不容易。以前還有同行說前端很簡單啊,現在也有,我都遇到過好幾次這種人了:

我:我是前端開發。

人家:噢,我知道了,就是寫網頁的對吧。。。

我心裡os:對你個錘子。。

FEE們都是從坑裡一步步爬上來的,真的不容易。總之,現在的前端正在一步步走上規範,走上體面。。。

文末彩蛋一,動態引數

PY中如何處理動態引數的呢,其實PY是通過元組或者字典來處理動態引數的,程式碼如下,這裡只寫使用 元組 實現動態引數的程式碼

# coding=utf-8
def add(x, *tupleName):
    print(x, tupleName)

add('hello', 'godkun', '大王叫我來巡山')
複製程式碼

執行結果圖如下:

image

我們再看JS是如何實現的

function fun(a, ...tupleName) {
  console.log(a, tupleName)
}
fun('hello', 'godkun', '大王叫我來巡山')
複製程式碼

執行結果圖如下:

image

看上面兩種方式,看完你應該就明白了,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環境執行:結果如圖所示:

image

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

image

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

ie arguments 1

上面第二種情況,你會發現在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。嚇的我趕緊試了下面這段程式碼,程式碼如圖所示:

image

靠,還果真返回了長度。。。但是為什麼__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

為什麼要這樣做呢,因為一些內建型別自帶迭代器行為,比如StringArraySetMap,但是Object是不帶迭代器的,也就意味著我們可以推斷出,如果從chrome瀏覽器的那種寫法的表面上分析,假定argumentsArray,那麼就完全沒必要增加Symbol.iterator,所以矛盾,所以可以得出,arguments還是物件,而物件是不帶迭代器的。所以要給形式為 []arguments 增加 Symbol.iterator。使其具有迭代器功能。從而可以使用for of。從而完成了 [1,2,3]{'0':1, '1':2, '2':3}的轉變

所以:上述答案被證明為正確。

當然,也可能是:

有理有據的胡謅。。。

備註:

  1. 我是根據學習PY來去思考JS的資料型別的,對於比如JS的Symbol,Set,Map,沒有去說官方用法,我覺得沒有必要吧。
  2. 我說的一些都是我個人出於一個心流狀態下的一些思考。可能有點問題,但是都是吾之所感。

文末的可愛宣告: 如果轉發或者引用,請貼上原文連結,尊重一下勞動成果?。文章可能 (肯定) 有一些錯誤,歡迎評論指出,也歡迎一起討論。文章可能寫的不夠好,還請多多包涵。人生苦短,我學前端,多一點貢獻,多一分開心,歡迎關注,後續更加精彩哦~

小夥伴覺得我寫得還不錯的話,就點個贊 以茲鼓勵 一下吧?。

相關文章