寫在最前
資料型別可以說是程式語言的基石,重要性不言而喻。那麼現在就從資料型別開始,打破你的思維認知,做一個充滿想象力的FEE
。針對上篇的一些偏激評論,我想強調的一點是:我寫的文章,並不是給那些偏激到說髒話的人看的,請尊重每一位為前端貢獻微薄力量的Blogger
。
好像,我這標題起的也太秀了,會不會被打?。
多說一句
這篇可以算是 前端獵奇系列中的 探索 Python 來反補 JavaScript 的中篇。 如果沒有看過上篇文章的,可以去我的專欄裡讀讀上篇,在知識上沒有啥關聯的地方,相對獨立。基本是我在學習PY
的時候,學到某一個地方,突然會想到JS
在這一方面是如何表現的,然後隨著思考,真的會有不少收穫吧。
關於資料型別
有句話說的好,掌握資料型別是學習一門語言的基礎。我們從這句話中可以看出,掌握好資料型別是有多麼重要。你曾經是不是有想過JS
的資料型別為什麼會是這樣,為什麼要有null
、undefined
。也許你有過疑問,但是疑問觸發後,簡簡單單的探尋後,就把疑問扔到回撥函式裡了,這一扔就是到如今。現在我將從PY
的角度來反補JS
,通過PY
去看清JS
的資料型別,看清程式語言的一些規律。now go!
JS
的資料型別分為值型別和引用型別:
- 值型別有:數字、字串、布林、Null、Undefined、Symbol、
- 引用型別有:Array、Function、Set、Map
PY
的資料型別分為數值型別、序列型別、Set型別、字典型別:
- 數值型別有:Integer、Long integer、Boolean、Double-precision floating
- 序列型別有:String、Tuple、List(和JS的Array相同)
- Set型別(和
JS
的Set相同) - Dictionary型別(和
JS
的Map相同)
現在我們看一下PY
和JS
的資料型別,這裡我不闡述具體是什麼,我只是總結一下,當我學習到這的時候,我對JS的資料型別有了什麼樣新的理解。現在,你會發現幾個很有趣的地方,請看如下:
關於 Set 和 Map
這和PY
的Set
、Dictionary
不謀而合,但是ES6
規範的制定者,沒有選擇使用Dictionary
作為鍵值對的類名稱,而選擇了使用Map
作為鍵值對的類名稱。而Map
正是Java
的鍵值對的類名稱。所以給我的感覺就是,JS在吸收很多語言的優秀的特性,我個人認為,命名成Map
要比Dictionary
好,畢竟少寫7
個字元呢?。
關於 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
居然不是保留字,也是夠神奇的,看了下部落格,有篇解釋的很不錯,可以瞅瞅為什麼undefined可以被賦值,而null不可以?。寫部落格的時候,並不是一味的寫自己的東西,有時候別人總結好的東西,在我寫部落格過程中,也能帶給我很多靈感和收穫。這也是算是和站在巨人的肩膀上是一個道理吧。
不過我還是有點個人獨特的看法的。而且我認為我的看法要比網上絕大多數的見解要更加深刻(不要臉)。我不想說undefined
有多坑,我只想去探究和理解undefined
的本質。掌握了本質後,坑不坑也就不重要了。我個人認為,JS
的undefined
是一種為了處理其他問題而強行做出的一種折中方案,且聽我娓娓道來。
既然PY
和JS
都是解釋性語言,那麼為什麼PY
可以不依賴undefined
,只需要使用None
就可以了呢? 我寫一個簡單的例子,可以從我下面的分析中,找到一些更深層的真相,找到設計undefined
真正的原因。程式碼如下:
let xconsole.log(x)複製程式碼
# coding=utf-8print(x)複製程式碼
我們來看執行結果:
從圖中會發現,JS
沒有報錯,但是PY
報錯了,究竟是什麼原因? 這裡中斷一下,我們來看下面這個截圖,是java的一段程式碼的執行結果:
圖中可以看出,在Java中,可以宣告變數,但不賦值,然後不去呼叫此變數,程式是不報錯的,但是在PY中,請看下面截圖:
我們發現,我們宣告瞭,也沒有去呼叫它,程式還是報錯了。是為什麼呢?
為什麼在Java
,C++
,C
語言中,可以宣告變數,而不用賦值,並且不會報錯。而在PY
中會報錯呢,而在JS
中是undefined
呢?其實仔細一想,會恍然大悟,一個非常關鍵的一點就是:
Java
、C++
,C
是強型別語言,在宣告的時候,就已經確定了資料型別。所以就算不去賦值,Java
、C++
等也會根據宣告的資料型別,設定一個預設的資料型別的值。但是這裡注意一點,如果整個程式執行完,在只宣告,卻沒有賦值的情況下,去輸出或者呼叫該變數,程式會報錯的。為什麼會報錯呢,是因為此變數的地址是系統隨機生成的,並不在此程式內的地址範圍內,也就是說此變數的地址可能是指向其他程式的地址,在這種情況下,如果去呼叫該地址,那麼可能會出現很大的危險性,比如你呼叫了其它很重要的東西。這裡我覺得可以把它理解為遊離的指標,雖然這樣形容不好,但是很形象,遊離的指標是很危險的東西。有多危險,哈哈哈,自己體會✧(≖ ◡ ≖✿)。
中斷結束,繼續PS
,從上面的敘述知道了Java
等語言是強型別語言。但是我們知道而PY
和JS
是指令碼語言,屬於弱型別語言,而弱型別語言的特點就是:在宣告變數的時候,不需要指定資料型別,這樣的好處就是一個變數可以指向萬物,缺點是效能差一些,需要讓編譯器在賦值的時候自己去做判斷。請緊跟著我的腳步,我們來看下面這段程式碼:
let xconsole.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
指向萬物的宣告卻未賦值的變數。哈哈哈哈,是不是很刺激?。
解決最後一公里的疑惑
看下面程式碼
let xlet 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-8def 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,沒有去說官方用法,我覺得沒有必要吧。
- 我說的一些都是我個人出於一個心流狀態下的一些思考。可能有點問題,但是都是吾之所感。
文末的可愛宣告: 如果轉發或者引用,請貼上原文連結,尊重一下勞動成果?。文章可能 (肯定) 有一些錯誤,歡迎評論指出,也歡迎一起討論。文章可能寫的不夠好,還請多多包涵。人生苦短,我學前端,多一點貢獻,多一分開心,歡迎關注,後續更加精彩哦~
小夥伴覺得我寫得還不錯的話,就點個贊 以茲鼓勵 一下吧?。