ECMAScirpt 變量有兩種不同的數據類型:基本類型,引用類型。也有其他的叫法,比如原始類型和對象類型等。
1、內置類型
JavaScript 有七種內置類型:
• 空值(null)
• 未定義(undefined)
• 布爾值(boolean)
• 數字(number)
• 字符串(string)
• 對象(object)
• 符號(symbol, ES6 中新增)
除對象之外,其他統稱為“基本類型”。 對象稱為“引用類型”。
我們可以用 typeof 運算符來查看值的類型,它返回的是類型的字符串值。有意思的是,這七種類型和它們的字符串值並不一一對應:
typeof undefined === "undefined"; // true typeof true === "boolean"; // true typeof 42 === "number"; // true typeof "42" === "string"; // true typeof { life: 42 } === "object"; // true // ES6中新加入的類型 typeof Symbol() === "symbol"; // true
以上六種類型均有同名的字符串值與之對應。 你可能注意到 null 類型不在此列。它比較特殊, typeof 對它的處理有問題:
typeof null === "object"; // true
按照類型的定義,這里正確的返回結果應該是 "null"。但這個 bug 由來已久,在 JavaScript 中已經存在了將近二十年,也許永遠也不會修復,因為這牽涉到太多的 Web 系統,“修復”它會產生更多的bug,令許多系統無法正常工作。
所以在日常開發中,為了更好的檢測null類型,我們需要使用復合條件來檢測 null 值的類型:
var a = null; (!a && typeof a === "object"); // true
null 是基本類型中唯一的一個“假值”類型, typeof對它的返回值為 "object"。
這里還有一張情況,看下面:
typeof function a(){ /* .. */ } === "function"; // true
或許這里你會說js的內置類型沒有function類型呀,為什么這里typeof會返回"function"。在介紹里面,function類型實際上是 object 的一個“子類型”。具體來說,函數是“可調用對象”,它有一個內部屬性 [[Call]],該屬性使其可以被調用。
函數不僅是對象,還可以擁有屬性。例如:
function a(b,c) { /* .. */ } a.length; // 2 函數對象的 length 屬性是其聲明的參數的個數 a.name; // "a" 函數對象的 name 屬性返回函數名
再來看看數組。 JavaScript 支持數組 ,數組也是對象,它也是 object 的一個“子類型”,數組的元素按數字順序來進行索引(而非普通像對象那樣通過字符串鍵值),其 length 屬性是元素的個數。
typeof [1,2,3] === "object"; // true
我們來比較一下下面的代碼:
typeof null; // "object" typeof []; // "object" typeof {}; // "object"
返回的都是"object",那這樣我們要怎么知道區分對象,數組和null呢?
其實方法有很多,這里羅列幾種給大家。
1、instanceof
instanceof 是用來判斷 A 是否為 B 的實例對,表達式為:A instanceof B,如果A是B的實例,則返回true,否則返回false。 在這里需要特別注意的是:instanceof檢測的是原型。其內部原理大致可以這樣理解:
instanceof (A,B) = { var L = A.__proto__; var R = B.prototype; if(L === R) { //A的內部屬性__proto__指向B的原型對象 return true; } return false; }
雖然上述代碼很不嚴謹也有一些問題,但是從上述過程可以看出,當 A 的 __proto__ 指向 B 的 prototype 時,就認為A就是B的實例,我們再來看幾個例子:
[] instanceof Array; // true {} instanceof Object; // true new Date() instanceof Date; // true function Person(){}; new Person() instanceof Person; [] instanceof Object; // true
細心的你可能發現了 [] instanceof Array 和 [] instanceof Object 竟然都返回 true。為什么?回想一下前面我講過的“instanceof檢測的是原型”,在剛才的例子里面就涉及到了原型和原型鏈的概念。如果對原型和原型鏈不了解的同學,可以先去看看相關知識,這個知識點是十分重要的,不管是以后工作還是面試,都是非常重要的!那么。。。現在繼續賺回來看我們的例子。從原型鏈上來說 [].__proto__ 指向 Array.prototype, 而 Array.prototype.__proto__ 又指向了Object.prototype,Object.prototype.__proto__ 指向了null,標志着原型鏈的結束。所以按照 instanceof 的判斷規則,[] 就是Object的實例。當然,類似的new Date()、new Person() 也會形成這樣一條原型鏈,例如:new Date() instanceof Object 也會返回 true。因此,instanceof 只能用來判斷兩個對象是否屬於原型鏈的關系, 而不能獲取對象的具體類型。
2、Object.prototype.toString.call
最強利器!!
Object.prototype.toString.call('') ; // "[object String]" Object.prototype.toString.call(1) ; // "[object Number]" Object.prototype.toString.call(true) ; // "[object Boolean]" Object.prototype.toString.call(undefined) ; // "[object Undefined]" Object.prototype.toString.call(null) ; // "[object Null]" Object.prototype.toString.call(new Function()) ; // "[object Function]" Object.prototype.toString.call(new Date()) ; // [object Date]" Object.prototype.toString.call([]) ; // "[object Array]" Object.prototype.toString.call({}) ; //"[object Object]"
基本上所有對象的類型都可以通過這個方法獲取到。也可以簡寫成 toString.call ,例如 toString.call({ }),toString.call([ ])
2、值和類型
JavaScript 中的變量是沒有類型的, 只有值才有。變量可以隨時持有任何類型的值。 ----《你所不知道的JavaScript(中)》P6
JavaScript 不做“類型強制”,也就是說,JavaScript 擁有動態類型,這意味着相同的變量可用作不同的類型。語言引擎不要求變量總是持有與其初始值同類型的值。一個變量可以現在被賦值為字符串類型值,隨后又被賦值為數字類型值。
42 的類型為 number,並且無法更改。而 "42" 的類型為 string。數字 42 可以通過強制類型轉換(coercion)為字符串 "42" 。
在對變量執行 typeof 操作時,得到的結果並不是該變量的類型,而是該變量持有的值的類型,因為 JavaScript 中的變量沒有類型。
var a = 42; typeof a; // "number"
a = true; typeof a; // "boolean"
3、值和引用
開頭曾說過,ECMAScirpt 變量有兩種不同的數據類型:基本類型,引用類型。為什么有這兩種區分呢?我們先來看下面的例子:
var a = 2; var b = a; b++; a; // 2 b; // 3
var c = [1,2,3]; var d = c; d.push( 4 ); c; // [1,2,3,4] d; // [1,2,3,4]
在JavaScript中,簡單值(即標量基本類型值, scalar primitive) 總是通過值復制的方式來賦值 / 傳遞,包括null、 undefined、字符串、數字、布爾和 ES6 中的 symbol。 復合值(compound value)——對象(包括數組和封裝對象,參見第 3 章)和函數,則總
是通過引用復制的方式來賦值 / 傳遞。
上例中 2 是一個標量基本類型值,所以變量 a 持有該值的一個復本, b 持有它的另一個復本。 b 更改時, a 的值保持不變。c 和 d 則分別指向同一個復合值 [1,2,3] 的兩個不同引用。請注意, c 和 d 僅僅是指向值[1,2,3],並非持有。所以它們更改的是同一個值(如調用 .push(4))。隨后它們都指向更改后的新值 [1,2,3,4]。
由於引用指向的是值本身而非變量,所以一個引用無法更改另一個引用的指向。
var a = [1,2,3]; var b = a; a; // [1,2,3] b; // [1,2,3] // 然后 b = [4,5,6]; a; // [1,2,3] b; // [4,5,6]
代碼前兩句使得a、b都是對復合值 [1,2,3] 的兩個不同引用,所以代碼第三第四句輸出的結果就都是值[1,2,3]。代碼第五句b=[4,5,6] 的執行使得變量b重新指向了另一個復合值,而不會修改的原先的復合值,也不會不影響 a 指向值 [1,2,3],所以a輸出的值依舊是復合值[1,2,3]。
有了上面的例子之后,再來看看下面一段代碼,
function foo(x) { x.push( 4 ); x; // [1,2,3,4] // 然后 x = [4,5,6]; x.push( 7 ); x; // [4,5,6,7] } var a = [1,2,3]; foo( a ); a; // 是[1,2,3,4],不是[4,5,6,7]
我們向函數傳遞 a 的時候,實際是將引用 a 的一個復本賦值給 x,而 a 仍然指向 [1,2,3]。在函數中我們可以通過引用 x 來更改數組的值(push(4) 之后變為 [1,2,3,4])。但 x =[4,5,6] 並不影響 a 的指向,所以 a 仍然指向 [1,2,3,4]。
我們不能通過引用 x 來更改引用 a 的指向,只能更改 a 和 x 共同指向的值 。如果要將 a 的值變為 [4,5,6,7],必須更改 x 指向的數組,而不是為 x 賦值一個新的數組。
function foo(x) { x.push( 4 ); x; // [1,2,3,4] // 然后 x.length = 0; // 清空數組 x.push( 4, 5, 6, 7 ); x; // [4,5,6,7] } var a = [1,2,3]; foo( a ); a; // 是[4,5,6,7],不是[1,2,3,4]
x.length = 0 和 x.push(4,5,6,7) 並沒有創建一個新的數組,而是更改了當前的數組。於是 a 指向的值變成了 [4,5,6,7]。
關於引用類型的比較和基本類型的比較,我們來看下面的代碼:
//基礎類型的比較 var a = 1; var b = 1; a===b; // true //引用類型比較 var c = [1]; var d = c; var e = [1]; c===d; // true c===e; // false
基礎類型的比較是值的比較,只要值相等,就返回 true,而引用類型的比較是兩個引用的比較,上面例子中c、d都是對復合值 [1] 的應用,他們都指向同一個值,所以返回 true ,雖然 e 所指的值也是 [1],但是這個 [1] 和 c 所指的 [1] 不一定,c和e是兩個不同的引用,就像C++里面所說的指針,雖然兩個指針所指的內存區域存放的值一樣,但是這兩個指針所指的地址不一樣,在機器看來這就是兩個完全不同的變量,所以在比較的時候就返回 false 。
總之請謹記:我們無法自行決定使用值復制還是引用復制,一切由值的類型來決定。