官术网_书友最值得收藏!

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ū)別。
主站蜘蛛池模板: 吴桥县| 左云县| 清流县| 灵武市| 同江市| 衡水市| 靖边县| 大荔县| 江油市| 临沭县| 淮南市| 长丰县| 扎鲁特旗| 太仓市| 同江市| 安塞县| 淮阳县| 宿州市| 肃宁县| 丰原市| 临泽县| 泰兴市| 莆田市| 宽甸| 嘉兴市| 巫溪县| 都昌县| 花莲县| 新巴尔虎左旗| 乌兰县| 雅安市| 香港| 正安县| 芦溪县| 遵义市| 台南县| 中阳县| 永清县| 临汾市| 桐城市| 盱眙县|