天天看點

javascript基礎知識系列 —— 2(資料類型)

作者:黃金與風

資料類型是計算機語言的基礎知識,資料類型廣泛用于變量、函數參數、表達式、函數傳回值等場合。JavaScript 規定了八種資料類型:未定義(Undefined)、空(Null)、數字(Number)、字元串(String)、布爾值(Boolean)、符号(Symbol)、任意大整數(BigInt)、對象(Object)

1、語言類型

說到JavaScript的資料存儲機制,首先我們需要知道,JavaScript 究竟是什麼類型的語言?

一般情況下可以根據聲明變量的特點,将語言分為靜态語言和動态語言:

靜态語言: 在使用之前就需要确認其變量資料類型的稱為靜态語言;

動态語言: 相反的,在運作過程中需要檢查資料類型的語言稱為動态語言。

很顯然,JavaScript 就是一門動态語言,因為在聲明變量之前并不需要确認其資料類型。

對于一個變量,我們既可以給他設定為一個數字,也可以給他設定為一個字元串,還可以讓字元串類型的變量和數值類型的變量相加。在這個過程中,可以發生隐式的類型轉化。弱類型語言可以發生隐式類型轉換,而強類型語言不能發生隐式類型轉換。而JavaScript就是弱類型語言。 ​

現在我們知道了,JavaScript是一種弱類型、動态的語言。這就意味着,無需告訴JavaScript引擎變量是什麼類型,JavaScript 引擎在運作代碼時會自己計算出來。同時,我們可以使用同一個變量儲存不同類型的資料。

2.記憶體中的堆和棧

對于 JavaScript 中的 8 種資料類型,其中前7種都是基礎類型,最後1種是引用類型。這些資料主要存儲在棧空間和堆空間中,。

function fn() {
	var a = "hello";
  var b = a;
  var c = { name: "CUGGZ"};
	var d = c;
}
fn()           

當這段代碼執行時,需要先進行編譯,并建立執行上下文,最後在按照順序執行代碼。當執行到第三行時,調用棧執行狀态如下:

javascript基礎知識系列 —— 2(資料類型)

此時變量 a 和 b 的值都被儲存在執行上下文中, 而執行上下文又被壓入到了棧中,是以變量 a 和 b 的值都是存放在棧中 的。

接下來繼續執行後面的代碼。當執行到第四行代碼時,JavaScript 引擎判斷變量 c 的值是一個引用類型,這時JavaScript 引擎會将該對象配置設定到堆空間裡,配置設定後該對象會有一個在堆中的位址,然後再将該資料的位址寫進 c 的變量值,最終配置設定好記憶體的執行上下文如下

javascript基礎知識系列 —— 2(資料類型)

可以看到,對象類型存儲在堆空間中,在棧空間中隻保留了對象的引用位址,當 JavaScript 通路該資料時,會通過棧中的引用位址來通路。

是以,基本資料類型的值直接儲存在棧中,引用類型的值會存放在堆中。

那為什麼要區分堆空間和棧空間呢?将資料都存在棧空間中不行嗎?

答案肯定是不可以的。JavaScript 引擎需要使用棧來維護程式執行期間上下文的狀态,如果将所有資料都放在棧空間中,就會影響到上下文切換的效率,進而影響到整個程式的執行效率。 ​

是以,通常情況下,棧空間不會設定的很大,主要用來存放一些基本類型的小資料。由于引用類型的資料占用空間都比較大,是以這類資料會被存放到堆中,堆空間比較大,能存放很多較大的資料。 ​

最後,我們再看看上面執行個體代碼中第五行,也就是将變量 c 指派給變量 d 是怎麼執行的。在 JavaScript 中,原始類型的指派會完整複制變量值,而引用類型的指派是複制引用位址。 是以d = c 的操作就是把 c 的引用位址指派給 d

javascript基礎知識系列 —— 2(資料類型)

可以看到,變量 c 和 d 都指向了同一個堆中的對象,當我們修改c的值時,d也會發生變化

3.資料類型

Undefined:Undefined 類型表示未定義,它的類型隻有一個值,就是 undefined。任何變量在指派前是 Undefined 類型、值為 undefined,可以用全局變量 undefined 來表達這個值。可以通過以下方式來得到 undefined:

(1)聲明了一個變量,但沒有指派

var foo; //undefined           

2)引用未定義的對象屬性

var obj = {}
obj.a// undefined           

(3)函數定義了形參,但沒有傳遞實參

function fn(a) {
    console.log(a); //undefined
}
fn();           

(4)執行 void 表達式

void 0 // undefined           

推薦通過 void 表達式來得到 undefined 值,因為這種方式既簡便又不需要引用額外的變量和屬性;同時它作為表達式還可以配合三目運算符使用,代表不執行任何操作。

如下面的代碼就表示滿足條件 x 大于 0 且小于 5 的時候執行函數 fn,否則不進行任何操作:

x > 0 && x < 5 ? fn() : void 0;           

那如何判斷一個變量的值是否為 undefined 呢?可以通過 typeof 關鍵字擷取變量 x 的類型,然後與 'undefined' 字元串做真值比較:

if(typeof x === 'undefined') {
  ...
}           

Null:Null 資料類型和 Undefined 類似,隻有一個值 null,表示變量被置為空對象,而非一個變量最原始的狀态。null 是 JavaScript 保留關鍵字,而 undefined 隻是一個常量。也就是說可以聲明名稱為 undefined 的變量,但将 null 作為變量使用時則會報錯。 ​

對于null,還有一個比較關鍵的問題

typeof null == 'object' // true           

實際上,null 有自己的類型 Null,而不屬于Object類型,typeof 之是以會判定為 Object 類型,如下:

在 JavaScript 第一個版本中,所有值都存儲在 32 位的單元中,每個單元包含一個小的 類型标簽(1-3 bits) 以及目前要存儲值的真實資料。類型标簽存儲在每個單元的低位中,共有五種資料類型

000: object   - 目前存儲的資料指向一個對象。
  1: int      - 目前存儲的資料是一個 31 位的有符号整數。
010: double   - 目前存儲的資料指向一個雙精度的浮點數。
100: string   - 目前存儲的資料指向一個字元串。
110: boolean  - 目前存儲的資料是布爾值。           

如果最低位是 1,則類型标簽标志位的長度隻有一位;如果最低位是 0,則類型标簽标志位的長度占三位,為存儲其他四種資料類型提供了額外兩個 bit 的長度。

有兩種特殊資料類型:

  • undefined的值是 (-2)30(一個超出整數範圍的數字);
  • null 的值是機器碼 NULL 指針(null 指針的值全是 0)。

那也就是說null的類型标簽也是000,和Object的類型标簽一樣,是以會被判定為Object。

可以通過另一種方法擷取 null 的真實類型:

Object.prototype.toString.call(null) ; // [object Null]           

當通過 “==” 來比較null 和 undefined 是否相等時,得到的結果是 true:

undefined == null; //true           

在 Javascript 規範中提到,要比較相等性之前,不能将 null 和 undefined 轉換成其他任何值,并且規定null 和 undefined 是相等的。null 和 undefined都代表着無效的值。

Boolean:Boolean 資料類型隻有兩個值:true 和 false,分别代表真和假。很多時候我們需要将各種表達式和變量轉換成 Boolean 資料類型來當作判斷條件。

下面是将星期數轉換成中文的函數,比如輸入數字 1,函數就會傳回“星期一”,輸入數字 2 會傳回“星期二”,以此類推,如果未輸入數字則傳回 undefined:

function getWeek(week) {
  const dict = ['日', '一', '二', '三', '四', '五', '六'];
  if(week) return `星期${dict[week]}`;
}           

這裡在 if 語句中會進行類型轉換,将 week 變量轉換成 Boolean 資料類型,而 0、空字元串、null、undefined 在轉換時都會傳回 false。是以在輸入 0 并不會傳回“星期日”,而會傳回 undefined。這是我們需要注意的問題

String:String 用于表示字元串,String 有最大長度是 253 - 1,這個所謂的最大長度并不是指字元數,而是字元串的 UTF16 編碼長度。 字元串的 charAt、charCodeAt、length 等方法針對的都是 UTF16 編碼。是以,字元串的最大長度,實際上是受字元串的編碼長度影響的。JavaScript 中的字元串是永遠無法變更的,一旦構造出來,就無法用任何方式改變其内容,是以字元串具有值類型的特征。

Number: Number 類型表示數字。JavaScript 中的 Number 類型有 18437736874454810627(即 264-253+3) 個值。JavaScript 中的 Number 類型基本符合 IEEE 754-2008 規定的雙精度浮點數規則,但是 JavaScript 為了表達幾個額外的語言場景(比如為了不讓除以 0 出錯,而引入了無窮大的概念),規定了幾個例外情況:

NaN,占用了 9007199254740990,這原本是符合 IEEE 規則的數字,通常在計算失敗時會得到該值。要判斷一個變量是否為 NaN,則可以通過 Number.isNaN 函數進行判斷。

Infinity,無窮大,在某些場景下比較有用,比如通過數值來表示權重或者優先級,Infinity 可以表示最高優先級或最大權重。

-Infinity,無窮小。

注意,JavaScript 中有 +0 和 -0 的概念,在加法類運算中它們沒有差別,但是除法時需要特别注意。可以使用 1/x 是 Infinity 還是 -Infinity來區分 +0 和 -0。

根據雙精度浮點數的定義,Number 類型中有效的整數範圍是 -0x1fffffffffffff 至 0x1fffffffffffff,是以 Number 無法精确表示此範圍外的整數。根據浮點數的定義,非整數的 Number 類型無法用 == 或者 === 來比較,這也就是在 JavaScript 中為什麼 0.1+0.2 !== 0.3。 ​

出現這種情況的原因在于計算的時候,JavaScript 引擎會先将十進制數轉換為二進制,然後進行加法運算,再将所得結果轉換為十進制。在進制轉換過程中如果小數位是無限的,就會出現誤差。 ​

實際上,這裡錯誤的不是結果,而是比較的方法,正确的比較方法是使用 JavaScript 提供的最小精度值,檢查等式左右兩邊差的絕對值是否小于最小精度:

console.log( Math.abs(0.1 + 0.2 - 0.3) <= Number.EPSILON);  // true           

Symbol:Symbol 是 ES6 中引入的新資料類型,它表示一個唯一的常量,通過 Symbol 函數來建立對應的資料類型,建立時可以添加變量描述,該變量描述在傳入時會被強行轉換成字元串進行存儲

var a = Symbol('1')
var b = Symbol(1)
a.description === b.description // true
var c = Symbol({id: 1})
c.description // [object Object]
var d = Symbol('1')
d == a // false           

基于以上特性,Symbol 屬性類型比較适合用于兩類場景中:常量值和對象屬性。

BigInt:BigInt 可以表示任意大的整數

BigInt(value);           

其中 value 是建立對象的數值。可以是字元串或者整數。在 JavaScript 中,Number 基本類型可以精确表示的最大整數是253。是以早期會有這樣的問題

let max = Number.MAX_SAFE_INTEGER;    // 最大安全整數
let max1 = max + 1
let max2 = max + 2
max1 === max2   // true           

有了BigInt之後,這個問題就不複存在了:

let max = BigInt(Number.MAX_SAFE_INTEGER);
let max1 = max + 1n
let max2 = max + 2n
max1 === max2   // false           

Object:Object 是 JavaScript 中最複雜的類型,它表示對象。在 JavaScript 中,對象的定義是屬性的集合。簡單地說,Object 類型資料就是鍵值對的集合,鍵是一個字元串(或者 Symbol) ,值可以是任意類型的值; 複雜地說,Object 又包括很多子類型,比如 Date、Array、Set、RegExp。

其實,JavaScript的幾個基本資料類型在對象類型中都有一個對應的類:

  • Number;
  • String;
  • Boolean;
  • Symbol。

對于 Number 類,1 與 new Number(1) 是完全不同的值,一個是 Number 類型, 一個是對象類型。Number、String 和 Boolean 構造器是兩用的:當跟 new 搭配時,它們産生對象;當直接調用時,它們表示強制類型轉換。Symbol 函數比較特殊,直接用 new 調用它會抛出錯誤,但它仍然是 Symbol 對象的構造器

對于Object類型,有一種很常見的操作,那就是深拷貝。

4.類型轉換

string轉換為number:Number 和 String 之間的互相轉換應該是比較複雜的,将字元串轉化為數字的方法很多,比如Number()、parseInt()、parseFloat()。

字元串到數字的類型轉換,存在一個文法結構,類型轉換支援十進制、二進制、八進制和十六進制,比如:

  • 30;
  • 0b111;
  • 0o13;
  • 0xFF。

此外,JavaScript 支援的字元串文法還包括正負号科學計數法,可以使用大寫或者小寫的 e 來表示:

  • 1e3;
  • -1e-2。

需要注意,parseInt 和 parseFloat 并不使用這個轉換:

  • 在不傳入第二個參數的情況下,parseInt 隻支援 16 進制字首“0x”,而且會忽略非數字字元,也不支援科學計數法。是以在任何環境下,都建議傳入 parseInt 的第二個參數,
  • parseFloat 則直接把原字元串作為十進制來解析,它不會引入任何的其他進制。

是以,在多數情況下,Number 是比 parseInt 和 parseFloat 更好的選擇

裝箱轉換:Number、String、Boolean、Symbol 基本類型在對象中都有對應的類,所謂裝箱轉換,就是把基本類型轉換為對應的對象。

我們知道,全局的 Symbol 函數無法使用 new 來調用,但仍可以利用裝箱機制來得到一個 Symbol 對象,可以利用一個函數的 call 方法來強迫産生裝箱。定義一個函數,函數裡面隻有 return this,然後調用函數的 call 方法到一個 Symbol 類型的值上,這樣就會産生一個 symbolObject

let symbolObject = (function(){ return this }).call(Symbol("a"));
    console.log(typeof symbolObject); //object
    console.log(symbolObject instanceof Symbol); //true
    console.log(symbolObject.constructor == Symbol); //true           

可以看到,它的 type of 值是 object;使用 symbolObject instanceof 可以看到,它是 Symbol 這個類的執行個體;它的 constructor 也是等于 Symbol 的。是以,它就是 Symbol 裝箱過的對象。裝箱機制會頻繁産生臨時對象,在一些對性能要求較高的場景下,應該盡量避免對基本類型做裝箱轉換。

可以使用JavaScript中内置的 Object 函數顯式調用裝箱能力。每一類裝箱對象皆有私有的 Class 屬性,這些屬性可以用 Object.prototype.toString 擷取:

var symbolObject = Object(Symbol("a"));
console.log(typeof symbolObject); //object
console.log(symbolObject instanceof Symbol); //true
console.log(symbolObject.constructor == Symbol); //true
console.log(Object.prototype.toString.call(symbolObject)); //[object Symbol]           

在 JavaScript 中,沒有任何方法可以更改私有的 Class 屬性,是以 Object.prototype.toString 是可以準确識别對象對應的基本類型的方法,它比 instanceof 更加準确。但需要注意的是,call 本身會産生裝箱操作,是以需要配合 typeof 來區分基本類型還是對象類型

最後附上一個能判斷所有類型的方法:

function checkDataType(data) {
  const dataType = typeof data;
  let result = "";
  const typeMap = new Map();
  typeMap.set("undefined", "");
  typeMap.set("boolean", "");
  typeMap.set("number", "");
  typeMap.set("string", "");
  typeMap.set("symbol", "");
  typeMap.set("object", "");
  typeMap.set("function", "");

  if (typeMap.has(dataType)) {
    result = dataType.charAt(0).toUpperCase() + dataType.slice(1);
  } else {
    result = "無法識别的資料類型";
  }
  return result;
}
// 示例用法
console.log(checkDataType(undefined)); // 輸出:Undefined
console.log(checkDataType(true)); // 輸出:Boolean
console.log(checkDataType(42)); // 輸出:Number
console.log(checkDataType("Hello, World!")); // 輸出:String
console.log(checkDataType(Symbol("example"))); // 輸出:Symbol
console.log(checkDataType([1, 2, 3])); // 輸出:Object
console.log(checkDataType({ name: "John", age: 30 })); // 輸出:Object
console.log(checkDataType(null)); // 輸出:無法識别的資料類型
console.log(checkDataType(function() {})); // 輸出:Function
console.log(checkDataType(new Date())); // 輸出:無法識别的資料類型           

繼續閱讀