- JS全書:JavaScript Web前端開發(fā)指南
- 高鵬
- 3907字
- 2020-09-18 10:29:15
3.3 數(shù)據(jù)類型
3.3.1 內(nèi)存空間
在講數(shù)據(jù)類型之前,先來了解一下JavaScript中的內(nèi)存空間。
JavaScript并沒有區(qū)分棧內(nèi)存與堆內(nèi)存,但為了理解JavaScript的數(shù)據(jù)結(jié)構(gòu),可以將JavaScript的內(nèi)存空間看作是由棧內(nèi)存與堆內(nèi)存組成的。
棧內(nèi)存中存儲的是基本數(shù)據(jù)類型與引用數(shù)據(jù)類型的地址。
堆內(nèi)存中存儲的是引用數(shù)據(jù)類型的值,JavaScript不允許直接訪問堆內(nèi)存中的數(shù)據(jù),只能通過棧內(nèi)存中存儲的引用數(shù)據(jù)類型的地址來訪問這些值,如圖3-1所示。
3.3.2 基本數(shù)據(jù)類型與引用數(shù)據(jù)類型
JavaScript中有7種數(shù)據(jù)類型,具體如下。
- Number
- String
- Boolean
- null
- undefined
- Object
- Symbol
其中Number、String、Boolean、null、undefined屬于基本數(shù)據(jù)類型,基本數(shù)據(jù)類型的數(shù)據(jù)是按值操作的,操作的是保存在變量中的實際值;Object和Symbol屬于引用數(shù)據(jù)類型,常見的數(shù)組、對象、函數(shù)等都是引用數(shù)據(jù)類型,操作的是保存在變量中的地址。

圖3-1 內(nèi)存空間
例如,我們參加面試時經(jīng)常遇到的一個問題,具體如下。
let a = {name:1}; let b = a; console.log(b); // > {name:1}
a.name = 2; console.log(b); // > {name:2}
上述示例中,將變量a的值賦值給變量b,實際上是將a中所存的地址賦值給變量b,這兩個地址在堆內(nèi)存中對應的是一個值,因此,對變量a的屬性name進行修改時,實際上修改的是堆內(nèi)存中的值,盡管沒有直接操作變量b,但變量b的值卻發(fā)生了改變。
再看下面的示例。
let a = {name:1}; let b = a; console.log(b); // > {name:1}
b = {name:1}; a.name = 2; console.log(b); // > {name:1}
上述代碼中,對變量a的屬性name進行修改前,為變量b重新賦值了另外一個值,此時變量b與變量a中所存的地址已經(jīng)不同,因此,對變量a的修改不影響變量b。
實際上,在ECMAscript中還有一種數(shù)據(jù)類型——Reference。Reference類型在JavaScript中并不存在,其作用是用來描述解釋諸如delete、typeof和賦值操作符之類的操作符的行為的。
3.3.3 淺拷貝與深拷貝
在上面的示例中,我們直接把變量a的值復制給變量b,這樣會導致一個問題——修改變量a的值也會對變量b的值造成影響。這就需要我們?nèi)椭埔粋€對象,而不是直接賦值。
復制分為淺復制與深復制,通常稱作“淺拷貝”與“深拷貝”。
淺拷貝只對對象的第一層鍵值進行復制,如果其中某個鍵存儲的是引用類型的數(shù)據(jù),復制的將會是該鍵所存儲的地址,那么,很顯然,以下面的示例為例,淺拷貝會導致a.tag和b.tag指向的是同一個地址。

而深拷貝不同,深拷貝將復制對象中的所有鍵值,以保證得到的對象圖中不包含原有對象圖中對任何對象的引用。
深拷貝是將所有鍵值都進行復制,所以,在遇到引用類型的數(shù)據(jù)時,再次調(diào)用淺拷貝方法即可。這種在函數(shù)內(nèi)調(diào)用函數(shù)本身的方式,稱作“遞歸”,后面的章節(jié)中還會講到。現(xiàn)在,修改上面的代碼,具體如下。

由于數(shù)組的concat方法是淺拷貝,因此需要將它替換成for循環(huán)。
對于深拷貝,其實有一種極為簡單的方式,示例如下。
let a = {name:1, tag:['js', 'html', 'css']}; let b = JSON.parse(JSON.stringify(a));
console.log(b); // > {name:1, tag:['js', 'html', 'css']};
a.tag[0] = 'javascript';
console.log(b); // > {name:1, tag:['js', 'html', 'css']};
console.log(a.tag === b.tag); // > false
這個方法要求被復制的對象必須是一個標準的JSON字符串。此外,這個方法將會忽略target中包含的undefined、正則表達式、函數(shù)等數(shù)值,示例如下。
let a = {name:/1/, tag:[undefined, 'html', 'css']}; let b = JSON.parse(JSON.stringify(a));
console.log(b); // > {name:{}, tag:[null, 'html', 'css']};
3.3.4 typeof與instanceof
在上面的淺拷貝函數(shù)中,我們用到了兩個操作符進行類型檢查——typeof和instanceof。其中,typeof操作符是最常用的類型檢查方式,該操作符返回一個字符串,表示被操作值的數(shù)據(jù)類型,示例如下。

對于基本數(shù)據(jù)類型與Symbol數(shù)據(jù)類型的檢查,typeof操作符完全可以勝任,但對于引用類型的數(shù)據(jù),typeof就不那么可靠了。
JavaScript中內(nèi)置的對象和自定義的對象,使用typeof返回的都是'object',示例如下。

因此,在上文的淺拷貝函數(shù)中,我們借助了instanceof運算符來判斷其原型,instanceof運算符返回一個布爾值,表示一個對象的原型是否存在于另一個構(gòu)造函數(shù)(ES6中的class聲明的類也是構(gòu)造函數(shù),關(guān)于構(gòu)造函數(shù)與原型的問題會在后續(xù)的章節(jié)中講到,目前可暫不理會)的原型鏈中。
語法:
object instanceof constructor;
object為要檢查的對象,constructor為構(gòu)造函數(shù)。
示例代碼:
console.log([] instanceof Array); // > true console.log([] instanceof Number); // > false
由此,我們就可以判斷這個對象到底是何種類型,示例如下。

你可能已經(jīng)發(fā)現(xiàn),除了基本數(shù)據(jù)類型、null、undefined和Symbol類型,target instanceof Object總是返回true,這是因為原型鏈的機制引起的,同樣,在這里你不需要深入思考原型和原型鏈的問題。
3.3.5 類型轉(zhuǎn)換
類型轉(zhuǎn)換分為兩種——隱式類型轉(zhuǎn)換和強制類型轉(zhuǎn)換。隱式類型轉(zhuǎn)換發(fā)生在不同類型的數(shù)據(jù)運算時,例如常見的算術(shù)運算(不包含遞增遞減),比較運算中,有些函數(shù)也會對入?yún)⑦M行隱式類型轉(zhuǎn)換。這里主要以相等(==)為例講解,便于稍后與嚴格相等(===)進行區(qū)分,使大家對相等與嚴格相等有更清晰的認識。
1. ==的比較
a == b,比較a、b兩個表達式的值,會在比較過程中對兩個表達式的值進行隱式類型轉(zhuǎn)換(null除外),比較后返回true或false表示兩個表達式的值是否相等。
其比較過程如下。
① 如果類型相同,則返回嚴格相等比較后的結(jié)果。
② null和undefined比較返回true。
③ 將其中的String類型轉(zhuǎn)換為Number后再次進行==比較,并返回比較后的結(jié)果。
④ 將其中的Boolean類型轉(zhuǎn)換為Number后再次進行==比較,并返回比較后的結(jié)果。
⑤ 如果a類型為String/Number/Symbol,b類型為Object,則將b轉(zhuǎn)化為基礎數(shù)據(jù)類型后再次進行==比較,并返回比較后的結(jié)果。
⑥ 如果b類型為String/Number/Symbol,a類型為Object,則將a轉(zhuǎn)化為基礎數(shù)據(jù)類型后再次進行==比較,并返回比較后的結(jié)果。
⑦ 否則返回false。
其中,對象轉(zhuǎn)化為基礎數(shù)據(jù)類型的方式如下。
① 轉(zhuǎn)化為字符串時,如果有toString方法,則調(diào)用后返回,否則調(diào)用valueOf方法并返回。
② 轉(zhuǎn)化為數(shù)字時,如果有valueOf方法,則調(diào)用后返回,否則調(diào)用toString方法并返回。
示例代碼如下。

各類型的數(shù)據(jù)按表3-1的方式轉(zhuǎn)換為數(shù)字、布爾值、字符串方式。
表3-1 數(shù)據(jù)類型轉(zhuǎn)換

這里有一道有趣的題目,試著思考下面的代碼輸出的是true還是false,相信這個題目會加深你對==比較的認識。

答案已經(jīng)很明顯了,最終輸出是true,首先a == 1,左邊的表達式是一個對象,右邊的表達式是一個數(shù)字,因此會調(diào)用對象的valueOf()方法,valueOf()方法將a.value的值自增后,返回自增前的值,此時valueOf()返回1,1 == 1返回true,a.value的值已經(jīng)自增為2,然后進行a == 2的比較,過程同上,最終,整個表達式的返回值為true。
2. ===的比較
a === b,比較a、b兩個表達式的類型和值是否相等,比較后返回true或false表示兩個表達式的值是否相等,其比較過程如下。
① 如果類型不同,返回false。
② 類型相同,均為Number。
A其中含有NaN,返回false。
B其中不含NaN,值相同,返回true。
C+0、-0和0之間比較返回true。
D否則返回false。
③ 均為undefined,返回true。
④ 均為null,返回true。
⑤ 均為String,如果兩個表達式的值長度相同,且相應索引對應的編碼單元相同,返回true,否則返回false。
⑥ 均為Boolean,如果兩個表達式的值都是true或false,返回true,否則返回false。
⑦ 均為Symbol,如果兩個表達式的值都是相同的Symbol值,返回true,否則返回false。
⑧ 均為Object,如果兩個表達式的值指向同一個對象,則返回true,否則返回false。
示例代碼如下:

現(xiàn)在來看一個常見的面試題。

除了隱式類型轉(zhuǎn)換,還可以使用強制類型轉(zhuǎn)換來處理轉(zhuǎn)換值的類型,ECMAScript中可用的3種強制類型轉(zhuǎn)換如下。
- Boolean(value):把給定的值轉(zhuǎn)換成布爾值。
- Number(value):把給定的值轉(zhuǎn)換成數(shù)字。
- String(value):把給定的值轉(zhuǎn)換成字符串。
其結(jié)果與上面的數(shù)據(jù)類型轉(zhuǎn)換表結(jié)果一致。
示例代碼:
Number(false); // -> 0 Boolean(""); // -> false String(null); // => "null"
此外,ECMAScript還提供了兩種字符串轉(zhuǎn)換成數(shù)字的方法——parseInt()和parseFloat(),前者把字符串轉(zhuǎn)換成整數(shù)后返回,后者把字符串轉(zhuǎn)換成一個浮點數(shù)后返回。
語法:
parseInt(string, radix); parseFloat(string);
string表示需要被轉(zhuǎn)換為數(shù)字的值,這兩個方法會對這個值從頭到尾進行測試,在遇到非有效的字符時停止(對于parseFloat來說,只有遇到的第一個小數(shù)點是有效字符,如果遇到兩個小數(shù)點,第二個小數(shù)點將被作為非有效字符處理),此時,再將之前測試成功的字符轉(zhuǎn)換成數(shù)字后返回,如果沒有測試成功的字符,則返回NaN,示例如下。

如果傳入的string不是一個字符串,這兩個方法都會嘗試將其轉(zhuǎn)換為字符串后再進行數(shù)字的轉(zhuǎn)換,示例如下。

上面的示例都是基于十進制的,parseInt()支持指定返回數(shù)值的進制,radix就表示這個進制,又稱為“基數(shù)”,它的取值區(qū)間為[2,36]之間的整數(shù),默認為10,表示十進制數(shù)值系統(tǒng)。例如,想要把一個數(shù)值轉(zhuǎn)換成二進制,示例如下。
parseInt(12.11, 2); // -> 1
parseFloat()方法是不支持radix的。
3.3.6 基本包裝類型
在上面的代碼中,有如下一些代碼。
... typeof "" === 'string'; // -> true ... typeof new String() === 'object'; // -> true ...
不要被new String()中的Number迷惑,這里的Number并不是數(shù)據(jù)類型,而是一個構(gòu)造函數(shù),因此其通過new創(chuàng)建的實例是一個對象。
那么,new String()與""有什么區(qū)別呢?示例如下。
let a = ""; let b = new String();
console.log(typeof a); // > "string" console.log(typeof b); // > "object"
console.log(a == b); // > true console.log(a === b); // > false
觀察上面的示例可以發(fā)現(xiàn),兩者的類型不同,一個是字符串,一個是對象,但兩者的值相等,這是因為對象b隱式轉(zhuǎn)換后為0,字符串a(chǎn)轉(zhuǎn)換后也為0,因此兩者相等,但不嚴格相等。
現(xiàn)在,嘗試操作一下這兩個變量,看看這兩個變量會發(fā)生什么改變,示例如下。

上面的示例中,盡管a是一個空字符串,但依然可以通過length屬性去獲取它的長度,就好像它是一個對象(例如b)。既然它是一個“對象”,接下來嘗試給它添加一些自定義的屬性(例如name),在對a和b分別賦予了name屬性后,我們發(fā)現(xiàn)a依然是一個空字符串,似乎沒有發(fā)生任何變化,但對象b中卻多出了一個name屬性,那么,是不是這個name屬性和length一樣,可以訪問,但是不可見呢?然后,我們訪問a和b的name屬性,發(fā)現(xiàn)a中并沒有被添加的name屬性,但之前對a添加name屬性時,確實成功且返回了被添加的值,那么對a添加的name屬性去了哪里呢?
實際上,對a添加name屬性時,執(zhí)行了如下操作:
console.log(a.name = 'js'); // > "js"
//上述代碼可以看作創(chuàng)建String的一個實例,在實例上添加屬性,因此返回"js",并在執(zhí)行結(jié)束 后銷毀這個實例console.log(new String(a).name = 'js'); // > "js"
再看一個示例。

每當操作一個基本數(shù)據(jù)類型的時候,會創(chuàng)建一個對應的基本包裝類型的對象,以便于在基本數(shù)據(jù)類型上直接調(diào)用其屬性和方法,擁有這種特性的數(shù)據(jù)類型稱為“基本包裝類型”。在JavaScript中,基本包裝類型包括3個特殊的引用類型——Boolean、Number和String。
操作基本數(shù)據(jù)類型時,其創(chuàng)建對應的基本包裝類型的對象是在后臺直接調(diào)用的,因此,即便你對構(gòu)造函數(shù)String做出修改,也不影響基本數(shù)據(jù)類型上擁有的屬性和方法,示例如下。

這也是字符串字面量的一個優(yōu)點,關(guān)于字符串字面量將會在下一節(jié)中講解。
練習
- 假設有變量a,嘗試對其數(shù)據(jù)類型進行判斷。
- 了解==與===的區(qū)別。
- Mastering LibGDX Game Development
- Hands-On RESTful Web Services with Go
- Building an RPG with Unity 2018
- Learning OpenStack Networking(Neutron)
- HTML5從入門到精通 (第2版)
- Android開發(fā):從0到1 (清華開發(fā)者書庫)
- Cocos2d-x Game Development Blueprints
- Mastering Apache Storm
- 從程序員角度學習數(shù)據(jù)庫技術(shù)(藍橋杯軟件大賽培訓教材-Java方向)
- 從零開始學UI:概念解析、實戰(zhàn)提高、突破規(guī)則
- HTML5移動前端開發(fā)基礎與實戰(zhàn)(微課版)
- Spring Boot從入門到實戰(zhàn)
- Node.js實戰(zhàn):分布式系統(tǒng)中的后端服務開發(fā)
- Python計算機視覺與深度學習實戰(zhàn)
- 從零開始學Unity游戲開發(fā):場景+角色+腳本+交互+體驗+效果+發(fā)布