聲明 本系列文章內容全部梳理自以下幾個來源: 《JavaScript權威指南》 "MDN web docs" "Github:smyhvae/web" "Github:goddyZhao/Translation/JavaScript" 作為一個前端小白,入門跟著這幾個來源學習,感謝作者的分享,在其基 ...
聲明
本系列文章內容全部梳理自以下幾個來源:
- 《JavaScript權威指南》
- MDN web docs
- Github:smyhvae/web
- Github:goddyZhao/Translation/JavaScript
作為一個前端小白,入門跟著這幾個來源學習,感謝作者的分享,在其基礎上,通過自己的理解,梳理出的知識點,或許有遺漏,或許有些理解是錯誤的,如有發現,歡迎指點下。
PS:梳理的內容以《JavaScript權威指南》這本書中的內容為主,因此接下去跟 JavaScript 語法相關的系列文章基本只介紹 ES5 標準規範的內容、ES6 等這系列梳理完再單獨來講講。
正文-數據類型、變數
JavaScript 里有兩種數據類型:原始類型和對象類型
原始類型
原始類型里包括:
- 數字(Number)
- 布爾(Boolean)
- 字元串(String)
- null
- undefined
布爾類型和字元串類型跟 Java 沒多大區別,主要就講一下數字類型、null 和 undefined。
數字
JavaScript 里不像 Java 一樣會區分 int,float,long 等之類的數字類型,全部都歸屬於一個 Number 數字類型中。之所以不加區分,是因為,在 JavaScript 里,所有的數字,不管整數還是小數,都用浮點數來表示,採用的是 IEEE 754標准定義的 64 位浮點格式表示數字。
那麼,它所能表示的數值範圍就是有限的,除了正常數值外,還有一些關鍵字表示特殊場景:
- Infinity(正無窮)
- -Infinity(負無窮)
- NaN(非數值)
對於小數,支持的浮動小數表示法如下:
3.14
-.2345789 // -0.23456789
-3.12e+12 // -3.12*1012
.1e-23 // 0.1*10-23=10-24=1e-24
另外,因為浮點表示法只能精確的表示如:1/2, 1/8, 1/1024 這類分數,對於 1/10 這種小數只能取近視值表示,因此在 JavaScript 里有個經典的有趣現象:
0.1 + 0.2 在 JavaScript 里是不等於 0.3 的,因為用浮點表示法,無法精確表示 0.1 和 0.2,所以會捨棄一些精度,兩個近似值相加,計算結果跟實際算術運算結果自然有些偏差。
上圖裡也顯示了,在 JavaScript 里,0.1 + 0.2 的運算結果是 0.30000000000000004。
那麼,是否所有非 1/2, 1/4, 1/8 這類 1/2^n 小數的相加結果最後都不會等於實際運算結果呢?
0.1, 0.2, 0.3 都是浮點數無法精確表示的數值,所以在 JavaScript 里都是以近似值存儲在記憶體中,那麼,為何 0.1 + 0.2 != 0.3
,但 0.1 + 0.3 == 0.4
?
這是因為,JavaScript 里在處理這類小數時,允許一定程度的誤差,比如 0.10000000000000001 在允許的誤差中,所以在 JavaScript 里就將這個值當做 0.1 來看待處理。
所以如果兩個是以近似值存儲的小數運算之後的結果,在誤差允許範圍內,那麼計算結果會按實際算術運算結果來呈現。
總之,不要用 JavaScript 來計算一些小數計算且有精度要求,如果非要不可,那麼建議先將小數都按比例擴展到整數運算後,再按比例縮小,如:
還有另外一點,由於 JavaScript 的變數是不區分類型的,那麼當有需要區分某個變數是不是數字時,可用內置的全局函數來處理:
isNaN()
-- 如果參數是 NaN 或者非數字值(如字元串或對象),返回 trueisFinite()
-- 如果參數不是 NaN,或 Infinity 或 -Infinity 時返回 true,通俗理解,參數是正常的數字
null
跟 Java 一樣,JavaScript 里也有 null 關鍵字,但它的含義和用法卻跟 Java 里的 null 不太一樣。
在 Java 里,聲明一個對象類型的變數後,如果沒有對該變數進行賦值操作,預設值為 null,所以在程式中經常需要對變數進行判空處理,這是 Java 里 null 的場景。
但在 JavaScript 中,聲明一個變數卻沒有進行賦值操作的話,預設值不是 null,而是 undefined。
那麼,什麼場景下,變數的值會是 null 呢?我可以告訴你,沒有,沒有任何場景下某個變數或某個屬性的值預設會是 null,除非你在程式中手動將某個變數賦值為 null,那麼此時這個變數的值才會是 null。
所以,才有些書本中會說,null 是表示程式級、正常的或在意料之中的值的空缺。意思就是說,null 是 JavaScript 設計出來的一個表示空值含義的數據類型,用來給你在程式中當有需要給某個變數手動設置為空值的場景時使用。
舉個通俗的例子,對於數字類型變數,你可以用 0 表示它的初始值;對於字元串類型變數,你可以用 "" 表示它的初始值;那麼對於對象類型,當你也需要給它一個表示空值無具體含義的初始值時,你就可以給它賦值為 null。
這也是為什麼用 typeof 運算符獲取 null 的數據類型時,會發現輸出的是 Object。因為 null 實際上是個實際存在的數據值,只是它的含義是空值的意思,用於賦值給對象類型的變數。
那麼,也就是說,不能沿用 Java 里使用 null 的思維應用到 JavaScript 中了,null 可以作為初始值賦值給變數,但變數如果沒有進行初始化,預設值不再是 null 了,這點是 JavaScript 有區別於 Java 的地方,需要註意一下。
不然再繼續挪用 Java 的使用 null 思維,可能在編程中,會遇到一些意料外,沒想通的問題。
undefined
如果聲明瞭一個變數,缺沒有對這個變數進行賦值操作,那麼這個值預設就是 undefined。
那麼在 Java 中的判空操作來判斷變數是否有進行初始化的行為在這裡就是對應判斷變數的值是否為 undefined 的,但實際上,在 JavaScript 里,由於 if 判斷語句接收的為真值,而不像 Java 只支持布爾類型,所以基本沒有類似 Java 的判空的編程場景。
undefined 還有另外一種場景:
當訪問對象中不存在的屬性時,此時會輸出 undefined,表示這個屬性並未在對象中定義。
針對這種場景,undefined 可用於判斷對象中是否含有某些指定的屬性。
總結一下 null 和 undefined:
- null 是用於在程式中,如果有場景需要,如某個變數在某種條件下需要有一個表示為空值含義的取值,此時,可手動為該變數賦值為 null;
- 當聲明某個變數,卻沒有對其進行賦值初始化操作時,這個變數預設為 undefined
- 當訪問對象某個不存在的屬性時,會輸出 undefined,可用於判斷對象中是否含有指定屬性
對象類型
除了原始類型外,其餘都是對象類型,但有一些內置的對象類型,所以大概可以這麼表示
- 對象類型(Object)
- 函數(Function)
- 數組(Array)
- 日期(Date)
- 正則(RegExp)
- ...
也就是,在 JavaScript 里,函數和數組,本質上也是對象。
變數相關
由於我本身有 Java 的基礎了,所以 JavaScript 一些很基礎的語法我可能會漏掉了,但影響不大。
弱類型
雖然 JavaScript 中有原始類型和對象類型,而且每個分類下又有很多細分的數據類型,但它實際上是一門弱類型語言,也叫動態語言。也就是說,使用變數時,無需指明變數是何種類型,運行期間會自動確定。
變數聲明
既然使用變數時不必指明變數的數據類型,那麼自然沒有類似於 Java 中那麼多種的變數聲明方式,在 JavaScript 中聲明變數很簡單,都是通過 var 來:
var name = dasu;
ES5 中,聲明變數的方式就是通過 var
關鍵字,而且同一變數重覆聲明不會出問題,會以後面聲明的為主。
變數的提前聲明
先看段代碼:
<script type="text/javascript">
console.log(a); //輸出 undefined
var a = 1;
console.log(a); //輸出 1
b();
function b() {
console.log(a); //輸出 undefined
var a = 2;
console.log(a); //輸出 2
}
</script>
JavaScript 中有變數的提前聲明特性,也就是在代碼開始執行前,所有通過 var 或 function 聲明的變數和函數都已經提前聲明瞭(下麵統稱變數),所以在聲明語句之前訪問聲明的這個變數並不會拋異常。
但提前的只有變數的聲明,變數的賦值初始化操作並沒有提前,所以第一行代碼輸出變數 a 的值時,因為變數已經被提前聲明瞭,但沒賦值,按照上面介紹的,此時變數 a 值為 undefined,當賦值語句執行完,輸出自然就是賦值的 1 了。
同樣,由於 b 函數已經被提前聲明瞭,所以可以在聲明它的位置之前就調用函數了,而函數調用後,開始執行函數內的代碼時,也同樣會有變數提前聲明的特性。
因此,在執行函數內第一行代碼時,輸出的變數 a 是函數內聲明的局部變數,而不是函數外部的變數,這點行為跟 Java 不一樣,需要註意一下。
有些腳本語言並沒有變數聲明提前的特性,使用的變數或函數只能在聲明瞭它的位置之後才能使用,這是 JavaScript 區別它們的一點。
全局屬性
上面說過,聲明變數時是通過 var
關鍵字聲明,那如果漏掉 var 呢,看個例子:
<script type="text/javascript">
//console.log(a); //拋異常,因為沒有找到a變數
a = 1;
b();
function b() {
console.log(a); //輸出 1
a = 2;
console.log(a); //輸出 2
}
console.log(a); //輸出 2
</script>
第一行代碼如果不註釋掉,那麼它執行的結果會是拋出一個異常,因為沒有找到 a 變數。
接著執行了 a = 1
,a 是一個不存在的變數,直接對不存在的變數進行賦值語句,其實是會自動對全局對象 window 動態添加了一個 a 屬性並賦值,所以後續調用了 b 函數,函數里操作的 a 其實都是來自全局對象 window 的屬性 a,所以在函數內對 a 進行的操作結果,當函數執行結束後,最後再次輸出 a 才會是 2。
這其實是因為對象的特性導致的,在對象一節會來講講,但這裡要清楚一點,切記聲明使用變數時,不要忘記在前面要使用 var
。
另外,順便提一下,第一行被註釋掉的代碼,如果換成輸出 this.a,那麼此時程式是不會拋異常的,而是輸出 undefined,這是因為前面也有稍微提過,訪問對象不存在的屬性時,會輸出 undefined,都是在講對象時會來說說。
變數作用域
ES5 中,變數有兩種作用域,全局作用域和函數內作用域。
在函數外聲明的變數都具有全局作用域,即使跨 js 文件都能夠訪問;而在函數內聲明的變數,不管聲明變數的語句在哪個位置,整個函數內都可以訪問該變數,因為有變數的提前聲明特性,所以是函數內作用域。
由於在 JavaScript 中,同一變數的重覆聲明不會出問題,所以對於全局變數而言,在多人協作,多模塊編程中,很容易造成全局變數衝突,即我在我寫的 js 文件中聲明的 a 全局變數,其他人在其他 js 文件中,又聲明瞭 a 全局變數,對於瀏覽器而言,它就只是簡單的以後聲明的為主。
但對於程式而已,就會發生不可控的問題,而且極難排查,所以要慎用全局變數。當然針對這種情況也有很多解決方案,後續講到函數一節中會來講講。
包裝對象
JavaScript 里的對象具有很多特性,比如可以動態為其添加屬性等等。但原始類型都不具有對象的這些特性,那麼當需要對原始類型也使用類似對象的特性行為時,這時候包裝對象就出現了。
包裝對象跟 Java 中的包裝類基本是類似的概念,原始數據類似對應的對象類型的值稱為包裝對象:
- 數字類型 -> Number 包裝對象
- 布爾類型 -> Boolean 包裝對象
- 字元串類型 -> String 包裝對象
- null 和 undefined 沒有包裝對象,所以不允許對 null 和 undefined 的變數進行屬性操作
接下來就講講原始類型和包裝對象之間的轉換,存在兩種場景,程式運行期間自動轉換,或者手動顯示的進行轉換。
隱式轉換
因為屬性是對象才有的特性,所以當對某個原始類型的變數進行屬性操作時,此時會臨時創建一個包裝對象,屬性操作結束後銷毀包裝對象。
看個例子:
var s = "test"; //創建一個字元串,s是原始類型的變數
s.len = 4; //對s動態添加一個屬性len並賦值,執行這行代碼時,會臨時創建一個包裝對象,所以這裡的s已經不是上面的原生類型變數,進行了一次自動轉換
console.log(s.len); //輸出 undefined,上一行雖然進行了一次包裝對象的自動轉換,但是是臨時的,那一行代碼執行結束,包裝對象就銷毀了。所以這一行又對s原始類型變數進行屬性操作,又再一次創建一個臨時的包裝對象
需要註意一點,當對原始類型的操作進行屬性操作時,會創建一個臨時的包裝對象,註意是臨時的,屬性操作完畢,包裝對象就銷毀了。下一次再繼續對原始類型進行屬性操作時,創建的又是新的一個臨時包裝對象。
顯示轉換
除了隱式的自動轉換外,也可以顯示的手動轉換。
如果是原始類型 -> 包裝類型的轉換,可使用相對應的包裝對象的構造函數方式:
var a = new Number(123);
var b = new Boolean(true);
var s = new String("dasu");
此時,a, b, s 都是對象類型的變數了,可以對它們進行一些屬性操作。
如果是包裝類型 -> 原始類型的轉換,使用不加 new 的調用全局函數的方式:
var aa = Number(a);
var bb = Boolean(b);
var ss = String(s);
在後續講函數時會講到,一個函數被調用的方式有多種:其中,有跟 new 關鍵字一起使用,此時叫這個函數為構造函數;如果只是簡單的調用,此時叫函數調用;如果是作為對象的屬性被調用,此時稱方法調用;不同的調用方式會有一些區別。
所以,這裡當包裝對象使用構造函數方式使用時,可以顯示的將原始類型數據轉換為包裝對象;但如果不作為構造函數,只是簡單的函數調用,其實就是將傳入的參數轉換為原始類型,參數不單可以是包裝對象類型,也可以是其他類型。
數據類型間相互轉換
上面講了原始類型與包裝對象間的相互轉換,其實本質上也就是不同數據類型間的相互轉換。
按數據類型細分來講的話,一共包括:數字、布爾、字元串、null、undefined、對象(函數、數組等),由於 JavaScript 是弱類型語言,運行期間自動確定變數類型,所以,其實這些不同數據類型之間都存在相互轉換的規則。
先看個例子:
10 + " objects"; // => "10 objects",這裡的 10 自動轉換成 "10"
"7" * "4"; // => 28, 這裡的兩個字元串都自動轉換為數字
var n = 1 - "x"; // => NaN,字元串 "x" 無法轉換為數字
n + " objects"; // => "NaN objects", NaN 轉換為字元串 "NaN"
數字可以轉換成字元串,字元串也可以轉換為數字,原始類型也可以轉換為對象類型等等,反正不同類似之間都可以相互轉換。
基本轉換規則
具體的規則,可以參見下表:
待轉換值 | 轉換為字元串 | 轉換為數字 | 轉換為布爾值 | 轉換為對象 |
---|---|---|---|---|
undefined | "undefined" | NaN | false | throws TypeError |
null | "null" | 0 | false | throws TypeError |
true(布爾->其他) | "true" | 1 | -- | new Boolean(true) |
false(布爾->其他) | "false" | 0 | -- | new Boolean(false) |
""(空字元串->其他) | -- | 0 | false | new String("") |
"1.2"(字元串內容為數字->其他) | -- | 1.2 | true | new String("1.2") |
"dasu"(字元串內容非數字->其他) | -- | NaN | true | new String("dasu") |
0(數字->其他) | "0" | -- | false | new Number(0) |
-0(數字->其他) | "0" | -- | false | new Number(-0) |
1(數字->其他) | "1" | -- | true | new Number(1) |
NaN | "NaN" | -- | false | new Number(NaN) |
Infinity | "Infinity" | -- | true | new Number(Infinity) |
-Infinity | "-Infinity" | -- | true | new Number(-Infinity) |
{}(對象 -> 其他) | 單獨講 | 單獨講 | true | -- |
[] (數組 -> 其他) | "" | 0 | true | -- |
[1] (一個數字元素的數值 -> 其他) | "1" | 1 | true | -- |
['a'] (普通數組 -> 其他) | 使用join()方法 | NaN | true | -- |
function(){} (函數 -> 其他) | 單獨講 | NaN | true | -- |
總之不同類型之間都可以相互轉換,除了 null 和 undefined 不能轉換為對象之外,其餘都可以。
那麼什麼時候會進行這些轉換呢?
其實在程式運行期間,就不斷的在隱式的進行著各種類型轉換,比如 if 語句中不是布爾類型時,比如算術表達式兩邊是不同類型時等等。
那麼,如何進行手動的顯示轉換呢?
在上一小節中,其實有稍微提過了,就是使用:
- Number()
- String()
- Boolean()
- Object()
註意是以函數調用方式使用,即不加 new 關鍵字的使用方式。參數傳入的值就是表示上表中第一列待轉換的值,而四種不同的函數,就對應著上表中右邊四列的轉換規則。如
Number("dasu") // => NaN,表示待轉換值為字元串 "dasu",需要轉換為數字類型,按照上表規則,轉換結果NaN
String(true) // => "true",同理,將布爾類型true轉為字元串類型
Boolean([]) // => true,將空數組轉為布爾類型
Object(3) // => new Number(3),將數字類型轉為包裝對象
換句話說,這四個函數,其實就是用於將任意類型轉換為函數對應的類型,比如 Number() 函數就是用於將任意類型轉為數字類型,至於具體轉換規則,就是按照表中的規則來進行轉換。
一般來說,應該可以不用將表中所有的轉換規則都詳記,需要自己手動轉換的場景應該也不多,記住一些常用基本的就行了,至於哪些是常見的,寫多了就清楚了,比如數字類型 -> 布爾類型,對象類型 -> 布爾類型等。
對象轉換為原始值規則
所有的數據類型之間的轉換,就對象轉換到原始值的規則會複雜點,其餘的需要的時候,看一下表就行了。
- 對象 -> 布爾
首先,所有的對象,不管的函數、數組還是普通對象,只要這個對象是定義後存在的,那麼它轉換為布爾值都是 true,所以對象轉布爾也很簡單。反正就記住,對象存在,那麼轉布爾就為 true。
所以,即使一個布爾值 false,先轉成包裝對象 new Boolean(false),再從包裝對象轉為布爾值,那麼此時,包裝對象轉布爾後是 true,因為包裝對象存在,就這麼簡單,不關心這個包裝對象原本是從布爾 false 轉來的。
- 對象 -> 字元串
對象轉字元串,主要是需要藉助兩個方法:
- 如果對象具有
toString()
,則調用這個方法,如果調用後返回了一個原始值,那麼就將這個原始值轉為字元串,轉換結束。 - 如果對象沒有
toString()
方法,或者調用該方法返回的並不是一個原始值,那麼調用對象的valueOf()
方法,同樣,如果調用後返回一個原始值,那麼將原始值轉為字元串後,轉換結束。 - 否則,拋類型錯誤異常。
這就是對象轉字元串的規則,有些內置的對象,比如函數對象,或數組對象就可能會對這兩個方法進行重寫,對於自定義的對象,也可以重寫這兩個方法,來手動控制它轉成字元串的規則。
- 對象 -> 數字
對象轉數字的規則,也是需要用到這兩個方法,只是它將步驟替換了下:
- 如果對象具有
valueOf()
方法,且調用後返回一個原始值,那麼將這個原始值轉為數字,轉換結束。 - 如果對象沒有
valueOf()
方法,或者調用後返回的不是原始值,那麼看對象是否具有toSring()
方法,且調用它後返回一個原始值,那麼將原始值轉為數字,轉換結束。 - 否則,拋類型錯誤異常。
大家好,我是 dasu,歡迎關註我的公眾號(dasuAndroidTv),公眾號中有我的聯繫方式,歡迎有事沒事來嘮嗑一下,如果你覺得本篇內容有幫助到你,可以轉載但記得要關註,要標明原文哦,謝謝支持~