前端入門8-JavaScript語法之資料型別和變數

請叫我大蘇發表於2018-12-02

宣告

本系列文章內容全部梳理自以下幾個來源:

作為一個前端小白,入門跟著這幾個來源學習,感謝作者的分享,在其基礎上,通過自己的理解,梳理出的知識點,或許有遺漏,或許有些理解是錯誤的,如有發現,歡迎指點下。

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 來計算一些小數計算且有精度要求,如果非要不可,那麼建議先將小數都按比例擴充套件到整數運算後,再按比例縮小,如:

浮點精度缺失3

還有另外一點,由於 JavaScript 的變數是不區分型別的,那麼當有需要區分某個變數是不是數字時,可用內建的全域性函式來處理:

  • isNaN() — 如果引數是 NaN 或者非數字值(如字串或物件),返回 true
  • isFinite() — 如果引數不是 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 轉來的。

  • 物件 -> 字串

物件轉字串,主要是需要藉助兩個方法:

  1. 如果物件具有 toString(),則呼叫這個方法,如果呼叫後返回了一個原始值,那麼就將這個原始值轉為字串,轉換結束。
  2. 如果物件沒有 toString() 方法,或者呼叫該方法返回的並不是一個原始值,那麼呼叫物件的 valueOf() 方法,同樣,如果呼叫後返回一個原始值,那麼將原始值轉為字串後,轉換結束。
  3. 否則,拋型別錯誤異常。

這就是物件轉字串的規則,有些內建的物件,比如函式物件,或陣列物件就可能會對這兩個方法進行重寫,對於自定義的物件,也可以重寫這兩個方法,來手動控制它轉成字串的規則。

  • 物件 -> 數字

物件轉數字的規則,也是需要用到這兩個方法,只是它將步驟替換了下:

  1. 如果物件具有 valueOf() 方法,且呼叫後返回一個原始值,那麼將這個原始值轉為數字,轉換結束。
  2. 如果物件沒有 valueOf() 方法,或者呼叫後返回的不是原始值,那麼看物件是否具有 toSring() 方法,且呼叫它後返回一個原始值,那麼將原始值轉為數字,轉換結束。
  3. 否則,拋型別錯誤異常。

大家好,我是 dasu,歡迎關注我的公眾號(dasuAndroidTv),公眾號中有我的聯絡方式,歡迎有事沒事來嘮嗑一下,如果你覺得本篇內容有幫助到你,可以轉載但記得要關注,要標明原文哦,謝謝支援~
dasuAndroidTv2.png

相關文章