js數據類型很簡單,卻也不簡單

最近腦子裏有冒出「多看點書」的想法,但我我的不是很喜歡翻閱紙質書籍,另外一方面也是由於我能抽出來看書的時間比較瑣碎,因此就乾脆用app看電子書了(若是有比較完整的閱讀時間,仍是建議看紙質書籍,排版看起來更舒服點)。考慮到平時工做遇到的大部分問題仍是javascript強相關的,因而我選擇從《Javascript權威指南第6版》開始。javascript

Javascript權威指南第6版

數據類型有哪些?

javascript的數據類型分爲兩大類,一類是原始類型(primitive type),一類是對象類型(object type)。java

原始類型

原始類型又稱爲基本類型,分爲Number, String, Boolean, Undefined, Null幾類。比較特殊的是,undefinedUndefined類型中的惟一一個值;一樣地,nullNull類型中的惟一一個值。git

除此以外,ES6引入了一個比較特殊的原始類型Symbol,用於表示一個獨一無二的值,具體使用方法能夠看阮一峯老師的ECMAScript6入門,或者直接翻閱MDN,我平時看MDN比較多,感受比較權威,API也很完善。程序員

爲何說Symbol是原始類型,而不是對象類型呢?由於咱們知道,大部分程序員都是沒有對象的,那麼要想找到女友,最快的辦法就是new一個。es6

const options = {
    '性格': '好',
    '顏值': '高',
    '對我': '好'
}
const gf = new GirlFriend(options) // new一個女友

皮一下

好了,不皮了,回到正題,意思就是,Symbol是沒有構造函數constructor的,不能經過new Symbol()得到實例。github

可是獲取symbol類型的值是經過調用Symbol函數獲得的。正則表達式

const symbol1 = Symbol('Tusi')

Symbol值是惟一的,因此下面的等式是不成立的。數組

Symbol(1) === Symbol(1) // false

對象類型

對象類型也叫引用類型,簡單地理解呢,對象就是鍵值對key:value的集合。常見的對象類型有Object, Array, Function, Date, RegExp等。promise

除了這些,Javascript還有蠻蠻多的全局對象,具體見JavaScript 標準內置對象。可是全局對象並不意味着它就是一種對象類型,就好比JSON是一個全局對象,可是它不是一種類型,這一點要搞清楚。瀏覽器

前面說了,對象能夠new出來,因此對象類型都有構造函數,Object類型對應的構造函數是Object()Array類型對應的構造函數是Array(),再也不贅述。

var obj = new Object() // 不過咱們通常也不會這麼寫一個普通對象
var arr1 = new Array(1) // 建立一個length是1的空數組
var arr2 = new Array(1, 2) // 建立數組[1, 2]

棧內存和堆內存

棧內存的優點是,存取速度比堆內存要快,充分考慮這一點,實際上是能夠優化代碼性能的。

棧內存

原始類型是按值訪問的,其值存儲在棧內存中,所佔內存大小是已知的或是有範圍的;

對基本類型變量的從新賦值,其本質上是進行壓棧操做,寫入新的值,並讓變量指向一塊棧頂元素(大概意思是這樣,可是v8等引擎有沒有作這方面的優化,就要細緻去看了)

var a = 1; // 壓棧,1成爲棧頂元素,其值賦給變量a
a = 2; // 壓棧,2成爲棧頂元素,並賦值給變量a(內存地址變了)

堆內存

而對象類型是按引用訪問的,經過指針訪問對象。

指針是一個地址值,相似於基本類型,存儲於棧內存中,是變量訪問對象的中間媒介。

而對象自己存儲在堆內存中,其佔用內存大小是可變的,未知的。

舉例以下:

var b = { name: 'Tusi' }

運行這行代碼,會在堆內存中開闢一段內存空間,存儲對象{name: 'Tusi'},同時聲明一個指針,其值爲上述對象的內存地址,指針賦值給引用變量b,意味着b引用了上述對象。

對象能夠新增或刪除屬性,因此說對象類型佔用的內存大小通常是未知的。

b.age = 18; // 對象新增了age屬性

那麼,按引用訪問是什麼意思呢?

個人理解是:對引用變量進行對象操做,其本質上改變的是引用變量所指向的堆內存地址中的對象自己。

這就意味着,若是有兩個或兩個以上的引用變量指向同一個對象,那麼對其中一個引用變量的對象操做,會影響指向該對象的其餘引用變量。

var b = { name: 'Tusi' }; // 建立對象,變量b指向該對象
var c = b; // 聲明變量c,指向與b一致
b.age = 18; // 經過變量b修改對象
// 產生反作用,c受到影響
console.log(c); // {name: "Tusi", age: 18}

考慮到對象操做的反作用,咱們會在業務代碼中常用深拷貝來規避這個問題。

數據類型的判斷

判斷數據類型是很是重要的基礎設施之一,那麼如何判斷數據類型呢?請接着往下看。

typeof

javascript自己提供了typeof運算符,能夠輔助咱們判斷數據類型。

typeof操做符返回一個字符串,表示未經計算的操做數的類型。

typeof的運算結果以下,引用自MDN typeof

數據類型 運算結果
Undefined "undefined"
Null "object"
Boolean "boolean"
Number "number"
String "string"
Symbol "symbol"
Function "function"
其餘對象 "object"
宿主對象(由JS環境提供,如Nodejs有global,瀏覽器有window) 取決於具體實現

能夠看到,typeof能幫咱們判斷出大部分的數據類型,可是要注意的是:

  1. typeof null的結果也是"object"
  2. 對象的種類不少,typeof獲得的結果沒法判斷出數組,普通對象,其餘特殊對象

那麼如何準確地知道一個變量的數據類型呢?

結合instanceof

instanceof 運算符用於檢測構造函數的 prototype 屬性是否出如今某個實例對象的原型鏈上。

利用instanceof,咱們能夠判斷一個對象是否是某個構造函數的實例。那麼結合typeof,咱們能夠封裝一個基本的判斷數據類型的函數。

基本思想是:首先看typeof是否是返回"object",若是不是,說明是普通數據類型,那麼直接返回typeof運算結果便可;若是是,則須要先把null這個坑貨摘出來,而後依次判斷其餘對象類型。

function getType(val) {
    const type = typeof val;
    if (type === 'object') {
        if (val === null) {
            // null不是對象,因此不能用instanceof判斷
            return 'null'
        } else if (val instanceof Array) {
            return 'array'
        } else if (val instanceof Date) {
            return 'date'
        } else if (// 其餘對象的instanceof判斷) {
            return 'xxx'
        } else if (val instanceof Object) {
            // 全部對象都是Object的實例,因此放最後
            return 'object'
        }
    } else {
        return type
    }
}
// 測試下
getType(Symbol(1)) // "symbol"
getType(null) // "null"
getType(new Date()) // "date"
getType([1, 2, 3]) // "array"
getType({}) // "object"

可是,要把經常使用的對象類型都列舉出來也是有點麻煩的,因此也不算一個優雅的方法。

終極神器toString

有沒有終極解決方案?固然是有的。可是,不是標題中的toString,而是Object.prototype.toString。用上它,不只上面的數據類型都能被判斷出來,並且也能夠判斷ES6引入的一些新的對象類型,好比Map, Set等。

// 利用了Object.prototype.toString和正則表達式的捕獲組
function getType(val) {
    return Object.prototype.toString.call(val).replace(/\[object\s(\w+)\]/, '$1').toLowerCase();
}

getType(new Map()) // "map"
getType(new Set()) // "set"
getType(new Promise((resolve, reject) => {})) // "promise"

爲何普通的調用toString不能判斷數據類型,而Object.prototype.toString能夠呢?

由於Object是基類,而各個派生類,如DateArray等在繼承Object的時候,通常都重寫(overwrite)了toString方法,用以表達自身業務,從而失去了判斷類型的能力。

裝箱和拆箱

首先解釋一下什麼是裝箱和拆箱,把原始類型轉換爲對應的對象類型的操做稱爲裝箱,反之是拆箱。

裝箱

咱們知道,只有對象才能夠擁有屬性和方法,可是咱們在使用一些基本類型數據的時候,卻能夠直接調用它們的一些屬性或方法,這是怎麼回事呢?

var a = 1;
a.toFixed(2); // "1.00"

var b = 'I love study';
b.length; // 12
b.substring(2, 6); // "love"

其實在讀取一些基本類型數據的屬性或方法時,javascript會建立臨時對象(也稱爲「包裝對象」),經過這個臨時對象來讀取屬性或方法。以上代碼等價於:

var a = 1;
var aObj = new Number(a);
aObj.toFixed(2); // "1.00"

var b = 'I love study';
var bObj1 = new String(b);
bObj1.length; // 12
var bObj2 = new String(b);
bObj2.substring(2, 6); // "love"

臨時對象是隻讀的,能夠理解爲它們在發生讀操做後就銷燬了,因此不能給它們定義新的屬性,也不能修改它們現有的屬性。

var c = '123';
c.name = 'jack'; // 給臨時對象加新屬性是無效的
c.name; // undefined
c.length; // 3
c.length = 2; // 修改臨時對象的屬性值,是無效的
c.length; // 3
咱們也能夠顯示地進行裝箱操做,即經過 String(), Number(), Boolean()構造函數來顯示地建立包裝對象。
var b = 'I love study';
var bObj = new String(b);

拆箱

對象的拆箱操做是經過valueOftoString完成的,且看下文。

類型的轉換

javascript在某些場景會自動執行類型轉換操做,而咱們也會根據業務的須要進行數據類型的轉換。類型的轉換規則以下:

類型轉換規則

對象到原始值的轉換

toString

toString()是默認的對象到字符串的轉換方法。

var a = {};
a.toString(); // "[object Object]"

可是不少類都自定義了toString()方法,舉例以下:

  • Array:將數組元素用逗號拼接成字符串做爲返回值。
var a = [1, 2, 3];
a.toString(); // 1,2,3
  • Function:返回一個字符串,字符串的內容是函數源代碼。
  • Date:返回一個日期時間字符串。
var a = new Date();
a.toString(); // "Sun May 10 2020 11:19:29 GMT+0800 (中國標準時間)"
  • RegExp:返回表示正則表達式直接量的字符串。
var a = /\d+/;
a.toString(); // "/\d+/"

valueOf

valueOf()會默認地返回對象自己,包括Object, Array, Function, RegExp

日期類Date重寫了valueOf()方法,返回一個1970年1月1日以來的毫秒數。

var a = new Date();
a.toString(); // 1589095600419

對象 --> 布爾值

從上表可見,對象(包括數組和函數)轉換爲布爾值都是true

對象 --> 字符串

對象轉字符串的基本規則以下:

  • 若是對象具備toString()方法,則調用這個方法。若是它返回字符串,則做爲轉換的結果;若是它返回其餘原始值,則將原始值轉爲字符串,做爲轉換的結果。
  • 若是對象沒有toString()方法,或toString()不返回原始值(不返回原始值這種狀況好像沒見過,通常是自定義類的toString()方法吧),那麼javascript會調用valueOf()方法。若是存在valueOf()方法而且valueOf()方法返回一個原始值,javascript將這個值轉換爲字符串(若是這個原始值自己不是字符串),做爲轉換的結果。
  • 不然,javascript沒法從toString()valueOf()得到一個原始值,會拋出異常。

對象 --> 數字

與對象轉字符串的規則相似,只不過是優先調用valueOf()

  • 若是對象具備valueOf()方法,且valueOf()返回一個原始值,則javascript將這個原始值轉換爲數字(若是原始值自己不是數字),做爲轉換結果。
  • 不然,若是對象有toString()方法且返回一個原始值,javascript將這個原始值轉換爲數字,做爲轉換結果。
  • 不然,javascript將拋出一個類型錯誤異常。

顯示轉換

使用String(), Number(), Boolean()函數強制轉換類型。

var a = 1;
var b = String(a); // "1"
var c = Boolean(a); // true

隱式轉換

在不一樣的使用場景中,javascript會根據實際狀況進行類型的隱式轉換。舉幾個例子說明下。

加法運算符+

咱們比較熟悉的運算符有算術運算符+, -, *, /,其中比較特殊的是+。由於加法運算符+能夠用於數字加法,也能夠用於字符串鏈接,因此加法運算符的兩個操做數多是類型不一致的。

當兩個操做數類型不一致時,加法運算符+會有以下的運算規則。

  • 若是其中一個運算符是對象,則會遵循對象到原始值的轉換規則,對於非日期對象來講,對象到原始值的轉換基本上是對象到數字的轉換,因此首先調用valueOf(),然而大部分對象的valueOf()返回的值都是對象自己,不是一個原始值,因此最後也是調用toString()去得到原始值。對於日期對象來講,會使用對象到字符串的轉換,因此首先調用toString()
1 + {}; // "1[object Object]"
1 + new Date(); // "1Sun May 10 2020 22:53:24 GMT+0800 (中國標準時間)"
  • 在進行了對象到原始值的轉換後,若是加法運算符+的其中一個操做數是字符串的話,就將另外一個操做數也轉換爲字符串,而後進行字符串鏈接。
var a = {} + false; // "[object Object]false"

var b = 1 + []; // "1"
  • 不然,兩個操做數都將轉換爲數字(或者NaN),而後進行加法操做。
var a = 1 + true; // 2

var b = 1 + undefined; // NaN

var c = 1 + null; // 1

[] == ![]

還有個很經典的例子,就是[] == ![],其結果是true。一看,是否是以爲有點懵,一個值的求反居然還等於這個值!其實仔細分析下過程,就能發現其中的奧祕了。

  1. 首先,咱們要知道運算符的優先級是這樣的,一元運算符!的優先級高於關係運算符==

js運算符優先級

  1. 因此,右側的![]首先會執行,而邏輯非運算符!會首先將其操做數轉爲布爾值,再進行求反。[]轉爲布爾值是true,因此![]的結果是false。此時的比較變成了[] == false
  2. 根據比較規則,若是==的其中一個值是false,則將其轉換爲數字0,再與另外一個操做數比較。此時的比較變成了[] == 0
  3. 接着,再參考比較規則,若是一個值是對象,另外一個值是數字或字符串,則將對象轉爲原始值,再進行比較。左側的[]轉爲原始值是空字符串"",因此此時的比較變成了"" == 0
  4. 最後,若是一個值是數字,另外一個是字符串,先將字符串轉換爲數字,再進行比較。空字符串會轉爲數字000天然是相等的。

搞懂了這個問題,也能夠分析下爲何{} == !{}的結果是false了,這個就比較簡單了。

看到這裏,你還以爲數據類型是簡單的知識點嗎?有興趣深究的朋友能夠翻閱下ES5的權威解釋

最後

數據類型是javascript中很是重要的一部分,搞清楚數據類型的基本知識點,對於學習javascript的後續知識點多有裨益。

另外,寫筆記其實對思考問題頗有幫助,就算只是總結很簡單的基礎知識,也是多有助益。

以上內容是我的筆記和總結,不免有錯誤或遺漏之處,歡迎留言交流。

歡迎交流

相關文章
相關標籤/搜索