細說 JavaScript 七種數據類型

在 JavaScript 規範中,共定義了七種數據類型,分爲 「基本類型」 和 「引用類型」 兩大類,以下所示:javascript

  • 基本類型:String、Number、Boolean、Symbol、Undefined、Null 
  • 引用類型:Object

下面將詳細介紹這七種數據類型的一些特性。html

一、String 類型

String 類型用於表示由零或多個 16 Unicode 字符組成的字符序列,即字符串 。java

1.1 存儲結構

因爲計算機只能處理數字,若是要處理文本,就必須先把文本轉換爲數字才能處理。在計算機中,1個字節(byte)由 8個比特(bit)組成,因此 1 個字節能表示的最大整數就是 255,若是想表示更大整數,就必須用更多的字節,好比 2 個字節能夠表示的最大整數爲 65535 。最先,只有 127 個字符被編碼到計算機裏,也就是大小寫英文字母、數字和一些符號,這個編碼表被稱爲 ASCII 編碼,好比大寫字母 A 的編碼是 65,小寫字母 z 的編碼是122。es6

但若是要表示中文字符,顯然一個字節是不夠的,至少須要兩個字節。因此,中國製定了 GB2312 編碼,用來表示中文字符。基於一樣的緣由,各個國家都制定了本身的編碼規則 。這樣就會出現一個問題,即在多語言混合的文本中,不一樣的編碼會出現衝突,致使亂碼出現 。segmentfault

爲了解決這個問題,Unicode 編碼應運而生,它把全部的語言都統一到一套編碼中,採用 2 個字節表示一個字符,即最多能夠表示 65535 個字符,這樣基本上能夠覆蓋世界上經常使用的文字,若是要表示更多的文字,也能夠採用 4 個字節進行編碼,這是一種通用的編碼規範 。數組

所以,JavaScript 中的字符也採用 Unicode 來編碼 ,也就是說,JavaScript 中的英文字符和中文字符都會佔用 2 個字節的空間大小 ,這種多字節字符,一般被稱爲寬字符。安全

1.2 基本包裝類型

在 JavaScript 中,字符串是基本數據類型,自己不存任何操做方法 。爲了方便的對字符串進行操做,ECMAScript 提供了一個基本包裝類型:String 對象 。它是一種特殊的引用類型,JS引擎每當讀取一個字符串的時候,就會在內部建立一個對應的 String 對象,該對象提供了不少操做字符的方法,這就是爲何能對字符串調用方法的緣由 。模塊化

var name = 'JavaScript';
var value = name.substr(2,1); 

當第二行代碼訪問變量 str 時,訪問過程處於一種讀取模式,也就是要從內存中讀取這個字符串的值。而在讀取模式中訪問字符串時,引擎內部會自動完成下列處理:函數

  • 建立 String 類型的一個實例
  • 在實例上調用指定的方法
  • 銷燬這個實例

用僞代碼形象的模擬以上三個步驟:post

var obj = new String('JavaScript'); 
var value = obj.substr(2,1); 
name = null; 

能夠看出,基本包裝類型是一種特殊的引用類型。它和普通引用類型有一個很重要的區別,就是對象的生存期不一樣 。使用 new 操做符建立的引用類型的實例,在執行流離開當前做用域以前都一直保存在內存中。而自動建立的基本包裝類型的對象,則只存在於一行代碼的執行瞬間,而後當即被銷燬。在 JavaScript 中,相似的基本包裝類型還有 Number、Boolean 對象 。

1.3 經常使用操做方法

做爲字符串的基本包裝類型,String 對象提供瞭如下幾類方法,用以操做字符串:

  • 字符操做:charAt,charCodeAt,fromCharCode
  • 字符串提取:substr,substring ,slice
  • 位置索引:indexOf ,lastIndexOf
  • 大小寫轉換:toLowerCase,toUpperCase
  • 模式匹配:match,search,replace,split
  • 其餘操做:concat,trim,localeCompare

charCodeAt 的做用是獲取字符的 Unicode 編碼,俗稱 「Unicode 碼點」。fromCharCode 是 String 對象上的靜態方法,做用是根據 Unicode 編碼返回對應的字符。

var a = 'a';
// 獲取Unicode編碼
var code = a.charCodeAt(0); // 97
// 根據Unicode編碼獲取字符
String.fromCharCode(code);  // a

經過 charCodeAt 獲取字符的 Unicode 編碼,而後再把這個編碼轉化成二進制,就能夠獲得該字符的二進制表示。

var a = 'a';
var code = a.charCodeAt(0); // 97
code.toString(2); // 1100001

對於字符串的提取操做,有三個相相似的方法,分別以下:

substr(start [, length])
substring(start [, end])
slice(start [, end])

從定義上看,substring 和 slice 是同類的,參數都是字符串的某個 start 位置到某個 end 位置(但 end 位置的字符不包括在結果中);而 substr 則是字符串的某個 start 位置起,數 length 個長度的字符才結束。兩者的共性是:從 start 開始,若是沒有第 2 個參數,都是直到字符串末尾。

substring 和 slice 的區別則是:slice 能夠接受 「負數」,表示從字符串尾部開始計數; 而 substring 則把負數或其它無效的數看成 0。

'hello world!'.slice(-6, -1) // 'world'
'hello world!'.substring("abc", 5) // 'hello'

substr 的 start 也可接受負數,也表示從字符串尾部計數,這點和 slice 相同;但 substr 的 length 則不能小於 1,不然返回空字符串。  

'hello world!'.substr(-6, 5) // 'world'
'hello world!'.substr(0, -1) // ''

二、Number 類型

JavaScript 中的數字類型只有 Number 一種,Number 類型採用 IEEE754 標準中的 「雙精度浮點數」 來表示一個數字,不區分整數和浮點數 。

2.1 存儲結構

在 IEEE754 中,雙精度浮點數採用 64 位存儲,即 8 個字節表示一個浮點數 。其存儲結構以下圖所示:

指數位能夠經過下面的方法轉換爲使用的指數值:

2.2 數值範圍

從存儲結構中能夠看出, 指數部分的長度是11個二進制,即指數部分能表示的最大值是 2047(211-1),取中間值進行偏移,用來表示負指數,也就是說指數的範圍是 [-1023,1024] 。所以,這種存儲結構可以表示的數值範圍爲 21024 到 2-1023 ,超出這個範圍的數沒法表示 。21024  和 2-1023  轉換爲科學計數法以下所示:

1.7976931348623157 × 10308

5 × 10-324

所以,JavaScript 中能表示的最大值是 1.7976931348623157 × 10308,最小值爲 5 × 10-324

這兩個邊界值能夠分別經過訪問 Number 對象的 MAX_VALUE 屬性和 MIN_VALUE 屬性來獲取:

Number.MAX_VALUE; // 1.7976931348623157e+308
Number.MIN_VALUE; // 5e-324

若是數字超過最大值或最小值,JavaScript 將返回一個不正確的值,這稱爲 「正向溢出(overflow)」 或 「負向溢出(underflow)」 。 

Number.MAX_VALUE+1 == Number.MAX_VALUE; //true
Number.MAX_VALUE+1e292; //Infinity
Number.MIN_VALUE + 1; //1
Number.MIN_VALUE - 3e-324; //0
Number.MIN_VALUE - 2e-324; //5e-324

2.3 數值精度

在 64 位的二進制中,符號位決定了一個數的正負,指數部分決定了數值的大小,小數部分決定了數值的精度。

IEEE754 規定,有效數字第一位默認老是1 。所以,在表示精度的位數前面,還存在一個 「隱藏位」 ,固定爲 1 ,但它不保存在 64 位浮點數之中。也就是說,有效數字老是 1.xx...xx 的形式,其中 xx..xx 的部分保存在 64 位浮點數之中,最長爲52位 。因此,JavaScript 提供的有效數字最長爲 53 個二進制位,其內部實際的表現形式爲:

(-1)^符號位 * 1.xx...xx * 2^指數位

這意味着,JavaScript 能表示並進行精確算術運算的整數範圍爲:[-253-1,253-1],即從最小值 -9007199254740991 到最大值 9007199254740991 之間的範圍 。

Math.pow(2, 53)-1 ; // 9007199254740991
-Math.pow(2, 53)-1 ; // -9007199254740991

能夠經過 Number.MAX_SAFE_INTEGER 和  Number.MIN_SAFE_INTEGER 來分別獲取這個最大值和最小值。 

console.log(Number.MAX_SAFE_INTEGER) ; // 9007199254740991
console.log(Number.MIN_SAFE_INTEGER) ; // -9007199254740991

對於超過這個範圍的整數,JavaScript 依舊能夠進行運算,但卻不保證運算結果的精度。

Math.pow(2, 53) ; // 9007199254740992
Math.pow(2, 53) + 1; // 9007199254740992
9007199254740993; //9007199254740992
90071992547409921; //90071992547409920
0.923456789012345678;//0.9234567890123456

2.4 精度丟失

計算機中的數字都是以二進制存儲的,若是要計算 0.1 + 0.2 的結果,計算機會先把 0.1 和 0.2 分別轉化成二進制,而後相加,最後再把相加獲得的結果轉爲十進制 。

但有一些浮點數在轉化爲二進制時,會出現無限循環 。好比, 十進制的 0.1 轉化爲二進制,會獲得以下結果:

0.0001 1001 1001 1001 1001 1001 1001 1001 …(1001無限循環)

而存儲結構中的尾數部分最多隻能表示 53 位。爲了能表示 0.1,只能模仿十進制進行四捨五入了,但二進制只有 0 和 1 , 因而變爲 0 舍 1 入 。 所以,0.1 在計算機裏的二進制表示形式以下:

0.0001100110011001100110011001100110011001100110011001101

用標準計數法表示以下:

(1)× 2−4 × (1.1001100110011001100110011001100110011001100110011010)2

一樣,0.2 的二進制也能夠表示爲: 

(1)× 2× (1.1001100110011001100110011001100110011001100110011010)

在計算浮點數相加時,須要先進行 「對位」,將較小的指數化爲較大的指數,並將小數部分相應右移:

0.1→ (1)× 2× (0.11001100110011001100110011001100110011001100110011010)2
0.2→ (1)× 2× (1.1001100110011001100110011001100110011001100110011010)2

最終,「0.1 + 0.2」 在計算機裏的計算過程以下:

通過上面的計算過程,0.1 + 0.2 獲得的結果也能夠表示爲:

(1)× 2× (1.0011001100110011001100110011001100110011001100110100)2

而後,經過 JS 將這個二進制結果轉化爲十進制表示:

(-1)**0 * 2**-2 * (0b10011001100110011001100110011001100110011001100110100 * 2**-52); //0.30000000000000004
console.log(0.1 + 0.2) ; // 0.30000000000000004

這是一個典型的精度丟失案例,從上面的計算過程能夠看出,0.1 和 0.2 在轉換爲二進制時就發生了一次精度丟失,而對於計算後的二進制又有一次精度丟失 。所以,獲得的結果是不許確的。

2.5 特殊數值

JavaScript 提供了幾個特殊數值,用於判斷數字的邊界和其餘特性 。以下所示:

  • Number.MAX_VALUE:JavaScript 中的最大值
  • Number.MIN_VALUE:JavaScript 中的最小值
  • Number.MAX_SAFE_INTEGER:最大安全整數,爲 253-1
  • Number.MIN_SAFE_INTEGER:最小安全整數,爲 -(253-1)
  • Number.POSITIVE_INFINITY:對應 Infinity,表明正無窮
  • Number.NEGATIVE_INFINITY:對應 -Infinity,表明負無窮
  • Number.EPSILON:是一個極小的值,用於檢測計算結果是否在偏差範圍內
  • Number.NaN:表示非數字,NaN與任何值都不相等,包括NaN自己
  • Infinity:表示無窮大,分 正無窮 Infinity 和 負無窮 -Infinity

2.6 數值轉換

有 3 個函數能夠把非數值轉換爲數值,分別以下:

Number(value)
parseInt(string [, radix])
parseFloat(string)

Number() 能夠用於任何數據類型,而另兩個函數則專門用於把字符串轉換成數值。

對於字符串而言,Number() 只能對字符串進行總體轉換,而 parseInt() 和 parseFloat() 能夠對字符串進行部分轉換,即只轉換第一個無效字符以前的字符。

對於不一樣數據類型的轉換,Number() 的處理也不盡相同,其轉換規則以下:

【1】若是是 Boolean 值,true 和 false 將分別被轉換爲 1 和 0。 

【2】若是是數字值,只是簡單的傳入和返回。

【3】若是是 null 值,返回 0。

【4】若是是 undefined,返回 NaN。

【5】若是是字符串,遵循下列規則:

  • 若是字符串中只包含數字(包括前面帶正號或負號的狀況),則將其轉換爲十進制數值; 
  • 若是字符串中包含有效的浮點格式,則將其轉換爲對應的浮點數值; 
  • 若是字符串中包含有效的十六進制格式,則將其轉換爲相同大小的十進制整數值; 
  • 若是字符串是空的(不包含任何字符),則將其轉換爲 0; 
  • 若是字符串中包含除上述格式以外的字符,則將其轉換爲 NaN。

【6】若是是對象,則調用對象的 valueOf() 方法,而後依照前面的規則轉換返回的值。若是轉換的結果是 NaN,則調用對象的 toString() 方法,而後再次依照前面的規則轉換返回的字符串值。

須要注意的是:

一元加操做符加號 「+」 和 Number() 具備一樣的做用。

在 ECMAScript 2015 規範中,爲了實現全局模塊化,Number 對象重寫了 parseInt 和 parseFloat 方法,但和對應的全局方法並沒有區別。

Number.parseInt === parseInt; // true
Number.parseFloat === parseFloat; // true

2.7 位運算

位運算做用於最基本的層次上,即按內存中表示數值的位來操做數值。ECMAScript 中的數值以64位雙精度浮點數存儲,但位運算只能做用於整數,所以要先將 64 位的浮點數轉換成 32 位的整數,而後再進行位運算,最後再將計算結果轉換成64位浮點數存儲。常見的位運算有如下幾種:

  • 按位非(NOT):~ 
  • 按位與(AND):& 
  • 按位或(OR): |
  • 按位異或(XOR):^
  • 左移:<<
  • 有符號右移:>> 
  • 無符號右移:>>>

須要注意的是:

「有符號右移」 和 「無符號右移」 只在計算負數的狀況下存在差別,>> 在符號位的右側補0,不移動符號位;而 >>> 是在符號位的左側補0,符號位發生移動和改變。

2.8 四捨五入

JavaScript 對數字進行四捨五入操做的 API 有 ceil,floor,round,toFixed,toPrecision 等,詳細介紹請參考:JavaScript 中的四捨五入

三、Boolean 類型

Boolean 類型只有兩個字面值:true 和 false 。 在 JavaScript 中,全部類型的值均可以轉化爲與 Boolean 等價的值 。轉化規則以下:

  •  全部對象都被看成 true
  •  空字符串被看成 false
  •  null 和 undefined 被看成 false
  •  數字 0 和 NaN 被看成 false 
Boolean([]); //true
Boolean({}); //true
Boolean(undefined); //false
Boolean(null); //false
Boolean('');  //false
Boolean(0);   //false
Boolean(NaN); //false

除 Boolean() 方法能夠返回布爾值外,如下 4 種類型的操做,也會返回布爾值。

 【1】關係操做符:>,>=,<,<=

當關系操做符的操做數使用了非數值時,要進行數據轉換或完成某些奇怪的操做。

  • 若是兩個操做數都是數值,則執行數值比較。
  • 若是兩個操做數都是字符串,則逐個比較二者對應的字符編碼(charCode),直到分出大小爲止 。
  • 若是操做數是其餘基本類型,則調用Number() 將其轉化爲數值,而後進行比較。
  • NaN 與任何值比較,均返回 false 。
  • 若是操做數是對象,則調用對象的 valueOf 方法(若是沒有 valueOf ,就調用 toString 方法),最後用獲得的結果,根據前面的規則執行比較。 
'a' > 'b'; // false, 即 'a'.charCodeAt(0) > 'b'.charCodeAt(0)
2 > '1';  // true, 即 Number('1') = 1
true > 0; //true, 即 Number(true) = 1
undefined > 0; //false, Number(undefined) = NaN
null < 0; //false, Number(null) = NaN
new Date > 100; //true , 即 new Date().valueOf()

 【2】相等操做符: ==,!=,===,!==

== 和 != 操做符都會先轉換操做數,而後再比較它們的相等性。在轉換不一樣的數據類型時,須要遵循下列基本規則:

  • 若是有一個操做數是布爾值,則在比較相等性以前,先調用 Number() 將其轉換爲數值;
  • 若是一個操做數是字符串,另外一個操做數是數值,在比較相等性以前,先調用 Number() 將字符串轉換爲數值;
  • 若是一個操做數是對象,另外一個操做數不是,則調用對象的 valueOf() 方法,用獲得的基本類型值按照前面的規則進行比較;
  • null 和 undefined 是相等的。在比較相等性以前,不能將 null 和 undefined 轉換成其餘任何值。
  • 若是有一個操做數是 NaN,則相等操做符返回 false,而不相等操做符返回 true;
  • 若是兩個操做數都是對象,則比較它們的指針地址。若是都指向同一個對象,則相等操做符返回 true;不然,返回 false。

=== 和 !== 操做符最大的特色是,在比較以前不轉換操做數 。它們的操做規則以下:

  • ===: 類型相同,而且值相等,才返回 true ,不然返回 false 。
  • !== : 類型不一樣,或者值不相等,就返回 true,不然返回 false 。
null === undefined; //false, 類型不一樣,直接返回 false
[] === []; //false ,類型相同,值不相同,指針地址不一樣
a=[],b=a,a===b; //true, 類型相同,值也相等
1 !== '1' ; // true , 值相等,但類型不一樣
[] !== [] ;  // true, 類型相同,但值不相等

 【3】布爾操做符:!

布爾操做符屬於一元操做符,即只有一個分項。其求值過程以下:

  • 對分項求值,獲得一個任意類型值;
  • 使用 Boolean() 把該值轉換爲布爾值 true 或 false;
  • 對布爾值取反,即 true 變 false,false 變 true
!(2+3) ; // false
!(function(){}); //false
!([] && null && ''); //true

利用 ! 的取反的特色,使用 !! 能夠很方便的將一個任意類型值轉換爲布爾值:

console.log(!!0); //false
console.log(!!''); //false
console.log(!!(2+3)); //true
console.log(!!([] && null && '')); //false

須要注意的是:

邏輯與 「&&」 和 邏輯或 「||」 返回的不必定是布爾值,而是包含布爾值在內的任意類型值。 

以下所示:

[] && 1; //1
null && undefined; //null
[] || 1; //[]
null || 1; //1

邏輯操做符屬於短路操做符 。在進行計算以前,會先經過 Boolean() 方法將兩邊的分項轉換爲布爾值,而後分別遵循下列規則進行計算:

  • 邏輯與:從左到右檢測每個分項,返回第一個布爾值爲 false 的分項,並中止檢測 。若是沒有檢測到 false 項,則返回最後一個分項 。
  • 邏輯或:從左到右檢測每個分項,返回第一個布爾值爲 true 的分項,並中止檢測 。若是沒有檢測到 true 項,則返回最後一個分項 。
[] && {} &&  null && 1; //null
[] && {} &&  !null  &&  1 ; //1
null || undefined || 1 || 0; //1
undefined || 0 || function(){}; //function(){}

【4】條件語句:if,while,?

條件語句經過計算表達式返回一個布爾值,而後再根據布爾值的真假,來執行對應的代碼。其計算過程以下:

  • 對錶達式求值,獲得一個任意類型值
  • 使用 Boolean() 將獲得的值轉換爲布爾值 true 或 false
if(arr.length) { }
obj && obj.name ? 'obj.name' : ''
while(arr.length){ }

四、Symbol 類型

Symbol 是 ES6 新增的一種原始數據類型,它的字面意思是:符號、標記。表明獨一無二的值 。

在 ES6 以前,對象的屬性名只能是字符串,這樣會致使一個問題,當經過 mixin 模式爲對象注入新屬性的時候,就可能會和原來的屬性名產生衝突 。而在 ES6 中,Symbol 類型也能夠做爲對象屬性名,凡是屬性名是 Symbol 類型的,就都是獨一無二的,能夠保證不會與其餘屬性名產生衝突。  

Symbol 值經過函數生成,以下所示:

let s = Symbol(); //s是獨一無二的值
typeof s ; // symbol  

和其餘基本類型不一樣的是,Symbol 做爲基本類型,沒有對應的包裝類型,也就是說 Symbol 自己不是對象,而是一個函數。所以,在生成 Symbol 類型值的時候,不能使用 new 操做符 。

Symbol 函數能夠接受一個字符串做爲參數,表示對 Symbol 值的描述,當有多個 Symbol 值時,比較容易區分。

var s1 = Symbol('s1');
var s2 = Symbol('s2');
console.log(s1,s2); // Symbol(s1) Symbol(s2)

注意,Symbol 函數的參數只是表示對當前 Symbol 值的描述,所以,相同參數的 Symbol 函數的返回值也是不相等的。 

用 Symbol 做爲對象的屬性名時,不能直接經過的方式訪問屬性和設置屬性值。由於正常狀況下,引擎會把點後面的屬性名解析成字符串。

var s = Symbol();
var obj = {};
obj.s = 'Jack';
console.log(obj); // {s: "Jack"}
obj[s] = 'Jack';
console.log(obj) ; //{Symbol(): "Jack"}

Symbol 做爲屬性名,該屬性不會出如今 for...in、for...of 循環中,也不會被 Object.keys()、Object.getOwnPropertyNames()、JSON.stringify() 返回。可是,它也不是私有屬性,有一個 Object.getOwnPropertySymbols() 方法,專門獲取指定對象的全部 Symbol 屬性名。 

var obj = {};
var s1 = Symbol('s1');
var s2 = Symbol('s2');
obj[s1] = 'Jack';
obj[s2] = 'Tom';
Object.keys(obj); //[]
for(var i in obj){ 
    console.log(i); //無輸出 
}
Object.getOwnPropertySymbols(obj); //[Symbol(s1), Symbol(s2)]

另外一個新的API,Reflect.ownKeys 方法能夠返回全部類型的鍵名,包括常規鍵名和 Symbol 鍵名。

var obj = {};
var s1 = Symbol('s1');
var s2 = Symbol('s2');
obj[s1] = 'Jack';
obj[s2] = 'Tom';
obj.name = 'Nick';
Reflect.ownKeys(obj); //[Symbol(s1), Symbol(s2),"name"]

有時,咱們但願從新使用同一個 Symbol 值,Symbol.for 方法能夠作到這一點。它接受一個字符串做爲參數,而後全局搜索有沒有以該參數做爲名稱的 Symbol 值。若是有,就返回這個 Symbol 值,不然就新建並返回一個以該字符串爲名稱的 Symbol 值。

var s1 = Symbol.for('foo');
var s2 = Symbol.for('foo');
s1 === s2 //true

Symbol.for() 也能夠生成 Symbol 值,它 和 Symbol() 的區別是:

  • Symbol.for() 首先會在全局環境中查找給定的 key 是否存在,若是存在就返回,不然就建立一個以 key 爲標識的 Symbol 值
  • Symbol.for() 生成的 Symbol 會登記在全局環境中供搜索,而 Symbol() 不會。

所以,Symbol.for()  永遠搜索不到 用 Symbol() 產生的值。

var s = Symbol('foo');
var s1 = Symbol.for('foo');
s === s1; // false

Symbol.keyFor() 方法返回一個已在全局環境中登記的 Symbol 類型值的 key 。

var s1 = Symbol.for('s1');
Symbol.keyFor(s1); //foo
var s2 = Symbol('s2'); //未登記的 Symbol 值
Symbol.keyFor(s2); //undefined  

五、Undefined 類型

Undefined 是 Javascript 中特殊的原始數據類型,它只有一個值,即 undefined,字面意思是:未定義的值 。它的語義是,但願表示一個變量最原始的狀態,而非人爲操做的結果 。 這種原始狀態會在如下 4 種場景中出現:

【1】聲明瞭一個變量,但沒有賦值

var foo;
console.log(foo); //undefined

訪問 foo,返回了 undefined,表示這個變量自從聲明瞭之後,就歷來沒有使用過,也沒有定義過任何有效的值,即處於一種原始而不可用的狀態。

【2】訪問對象上不存在的屬性

console.log(Object.foo); // undefined
var arr = [];
console.log(arr[0]); // undefined

訪問 Object 對象上的 foo 屬性,返回 undefined , 表示Object 上不存在或者沒有定義名爲 foo 的屬性。數組中的元素在內部也屬於對象屬性,訪問下標就等於訪問這個屬性,返回 undefined ,就表示數組中不存在這個元素。

【3】函數定義了形參,但沒有傳遞實參

// 函數定義了形參 a
function fn(a) {
    console.log(a); //undefined
}
fn(); // 未傳遞實參

函數 fn 定義了形參 a, 但 fn 被調用時沒有傳遞參數,所以,fn 運行時的參數 a 就是一個原始的、未被賦值的變量。

【4】使用 void 對錶達式求值

void 0 ; // undefined
void false; // undefined
void []; // undefined
void null; // undefined
void function fn(){} ; // undefined

ECMAScript 明確規定 void 操做符 對任何表達式求值都返回 undefined ,這和函數執行操做後沒有返回值的做用是同樣的,JavaScript 中的函數都有返回值,當沒有 return 操做時,就默認返回一個原始的狀態值,這個值就是 undefined,代表函數的返回值未被定義。

所以,undefined 通常都來自於某個表達式最原始的狀態值,不是人爲操做的結果。固然,你也能夠手動給一個變量賦值 undefined,但這樣作沒有意義,由於一個變量不賦值就是 undefined 。

六、Null 類型

Null 是 Javascript 中特殊的原始數據類型,它只有一個值,即 null,字面意思是:「空值」  。它的語義是,但願表示一個對象被人爲的重置爲空對象,而非一個變量最原始的狀態 。 在內存裏的表示就是,棧中的變量沒有指向堆中的內存對象。當一個對象被賦值了 null 之後,原來的對象在內存中就處於遊離狀態,GC 會擇機回收該對象並釋放內存。所以,若是須要釋放某個對象,就將變量設置爲 null,即表示該對象已經被清空,目前無效狀態。

null 是原始數據類型 Null 中的惟一一個值,但 typeof 會將 null 誤判爲 Object 類型 。 

typeof null == 'object'  

在 JavaScript 中,數據類型在底層都是以二進制形式表示的,二進制的前三位爲 0 會被 typeof 斷定爲對象類型,以下所示:

  • 000 - 對象,數據是對象的應用
  • 1 - 整型,數據是31位帶符號整數
  • 010 - 雙精度類型,數據是雙精度數字
  • 100 - 字符串,數據是字符串
  • 110 - 布爾類型,數據是布爾值

而 null 值的二進制表示全是 0 ,天然前三位固然也是 000,所以,typeof 會誤覺得是對象類型。若是想要知道 null 的真實數據類型,能夠經過下面的方式來獲取。

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

七、Object 類型

在 ECMAScript 規範中,引用類型除 Object 自己外,Date、Array、RegExp 也屬於引用類型 。

引用類型也即對象類型,ECMA262 把對象定義爲:無序屬性的集合,其屬性能夠包含基本值、對象或者函數。 也就是說,對象是一組沒有特定順序的值 。因爲其值的大小會改變,因此不能將其存放在棧中,不然會下降變量查詢速度。所以,對象的值存儲在堆(heap)中,而存儲在變量處的值,是一個指針,指向存儲對象的內存處,即按址訪問。具有這種存儲結構的,均可以稱之爲引用類型 。

7.1 對象拷貝

因爲引用類型的變量只存指針,而對象自己存儲在堆中 。所以,當把一個對象賦值給多個變量時,就至關於把同一個對象地址賦值給了每一個變量指針 。這樣,每一個變量都指向了同一個對象,當經過一個變量修改對象,其餘變量也會同步更新。

var obj = {name:'Jack'};
var obj2 = obj;
obj2.name = 'Tom'
console.log(obj.name,obj2.name); //Tom,Tom

ES6 提供了一個原生方法用於對象的拷貝,即 Object.assign() 。

var obj = {name:'Jack'};
var obj2 = Object.assign({},obj);
obj2.name = 'Tom'
console.log(obj.name,obj2.name); //Jack Tom

須要注意的是,Object.assign() 拷貝的是屬性值。當屬性值是基本類型時,沒有什麼問題 ,但若是該屬性值是一個指向對象的引用,它也只能拷貝那個引用值,而不會拷貝被引用的那個對象。

var obj = {base:{name:'Jack'}};
var obj2 = Object.assign({},obj);
obj2.base.name = 'Tom'
console.log(obj.base.name,obj2.base.name); //Tom Tom  

從結果能夠看出,obj 和 obj2 的屬性 base 指向了同一個對象的引用。所以,Object.assign 僅僅是拷貝了一份對象指針做爲副本 。這種拷貝被稱爲 「一級拷貝」 或 「淺拷貝」

若是要完全的拷貝一個對象做爲副本,二者之間的操做相互不受影響,則能夠經過 JSON 的序列化和反序列化方法來實現 。

var obj = {base:{name:'Jack'}};
var obj2 = JSON.parse(JSON.stringify(obj))
obj2.base.name = 'Tom'
console.log(obj.base.name,obj2.base.name); //Jack Tom

這種拷貝被稱爲 「多級拷貝」 或 「深拷貝」 。 

7.2 屬性類型

ECMA-262 第 5 版定義了一些內部特性(attribute),用以描述對象屬性(property)的各類特徵。ECMA-262 定義這些特性是爲了實現 JavaScript 引擎用的,所以在 JavaScript 中不能直接訪問它們。爲了表示特性是內部值,該規範把它們放在了兩對兒方括號中,例如[[Enumerable]]。 這些內部特性能夠分爲兩種:數據屬性 和 訪問器屬性 。

【1】數據屬性

數據屬性包含一個數據值的位置,在這個位置能夠讀取和寫入值 。數據屬性有4個描述其行爲的內部特性:

  • [[Configurable]]:可否經過 delete 刪除屬性從而從新定義屬性,或者可否把屬性修改成訪問器屬性。該默認值爲 true。
  • [[Enumerable]]:表示可否經過 for-in 循環返回屬性。默認值爲 true。
  • [[Writable]]:可否修改屬性的值。默認值爲 true。
  • [[Value]]:包含這個屬性的數據值。讀取屬性值的時候,從這個位置讀;寫入屬性值的時候,把新值保存在這個位置。默認值爲 undefined 。

要修改屬性默認的特性,必須使用 ECMAScript 5 的 Object.defineProperty() 方法。這個方法接收三個參數:屬性所在的對象、屬性的名字和一個描述符對象。其中,描述符(descriptor)對象的屬性必須是:configurable、enumerable、writable 和 value。設置其中的一或多個值,能夠修改對應的特性值。例如:

var person = {};
Object.defineProperty(person, "name", {
    writable: false,
    value: "Nicholas"
});
console.log(person.name); //"Nicholas"
person.name = "Greg";
console.log(person.name); //"Nicholas"

在調用 Object.defineProperty() 方法時,若是不指定 configurable、enumerable 和 writable 特性,其默認值都是 false 。

【2】訪問器屬性

訪問器屬性不包含數據值,它們包含一對 getter 和 setter 函數(不過,這兩個函數都不是必需的)。在讀取訪問器屬性時,會調用getter 函數,這個函數負責返回有效的值;在寫入訪問器屬性時,會調用setter 函數並傳入新值,這個函數負責決定如何處理數據。訪問器屬性有以下4 個特性。

  • [[Configurable]]:表示可否經過 delete 刪除屬性從而從新定義屬性,或者可否把屬性修改成數據屬性。默認值爲true 。
  • [[Enumerable]]:表示可否經過 for-in 循環返回屬性。默認值爲 true。
  • [[Get]]:在讀取屬性時調用的函數。默認值爲 undefined 。
  • [[Set]]:在寫入屬性時調用的函數。默認值爲 undefined 。

訪問器屬性不能直接定義,也必須使用 Object.defineProperty() 來定義。請看下面的例子:

var book = {
    _year: 2004
};
Object.defineProperty(book, "year", {
    get: function () {
        return this._year;
    },
    set: function (newValue) {
        if (newValue > 2004) {
            this._year = newValue;
            console.log('set new value:'+ newValue)
        }
    }
});
book.year = 2005; //set new value:2005

7.3  Object 新增 API

ECMA-262 第 5 版對 Object 對象進行了加強,包括 defineProperty 在內,共定義了 9 個新的 API:

  • create(prototype[,descriptors]):用於原型鏈繼承。建立一個對象,並把其 prototype 屬性賦值爲第一個參數,同時能夠設置多個 descriptors 。
  • defineProperty(O,Prop,descriptor) :用於定義對象屬性的特性。
  • defineProperties(O,descriptors) :用於同時定義多個屬性的特性。
  • getOwnPropertyDescriptor(O,property):獲取 defineProperty 方法設置的 property 特性。
  • getOwnPropertyNames:獲取全部的屬性名,不包括 prototy 中的屬性,返回一個數組。
  • keys():和 getOwnPropertyNames 方法相似,可是獲取全部的可枚舉的屬性,返回一個數組。
  • preventExtensions(O) :用於鎖住對象屬性,使其不可以拓展,也就是不能增長新的屬性,可是屬性的值仍然能夠更改,也能夠把屬性刪除。
  • Object.seal(O) :把對象密封,也就是讓對象既不能夠拓展也不能夠刪除屬性(把每一個屬性的 configurable 設爲 false),單數屬性值仍然能夠修改。
  • Object.freeze(O) :徹底凍結對象,在 seal 的基礎上,屬性值也不能夠修改(每一個屬性的 wirtable 也被設爲 false)。

 

原創發佈 @一像素 2017.08

 

參考資料:

[1]  javascript類型系統——Number數字類型

[2]  IEEE754 浮點數格式 與 Javascript number 的特性

[3]  該死的IEEE-754浮點數

[4]  ES6 Symbol

相關文章
相關標籤/搜索