掃二維碼與項目經理溝通
我們在微信上24小時期待你的聲音
解答本文疑問/技術咨詢/運營咨詢/技術建議/互聯(lián)網交流
前言

創(chuàng)新互聯(lián)建站專業(yè)為企業(yè)提供晉源網站建設、晉源做網站、晉源網站設計、晉源網站制作等企業(yè)網站建設、網頁設計與制作、晉源企業(yè)網站模板建站服務,10年晉源做網站經驗,不只是建網站,更提供有價值的思路和整體網絡服務。
作為JavaScript的入門知識點,Js數據類型在整個JavaScript的學習過程中其實尤為重要。最常見的是邊界數據類型條件判斷問題。
我們將通過這幾個方面來了解數據類型:
概念
undefined、Null、Boolean、String、Number、Symbol、BigInt為基礎類型;
Object為引用類型,其中包括Array、RegExp、Date、Math、Function這幾種常見的類型。
數據類型大致分為兩類來進行存儲。
基礎類型存儲在棧內存,被引用或拷貝時,會創(chuàng)建一個完全相等的變量。
引用類型存儲在堆內存,存儲的是地址,多個引用指向同一個地址,這里會涉及一個“共享”的概念。
示例題一:
- let a = {
- name:"maomin",
- age:"20"
- }
- let b = a;
- console.log(a.name); // "maomin"
- b.name = "haojie";
- console.log(a.name); // "haojie"
- console.log(b.name); // "haojie"
這里就體現了引用類型的“共享”的特性,即這兩個值都存在同一塊內存中共享,一個發(fā)生了改變,另外一個也隨之跟著變化。
示例題二:
- let a = {
- name: 'maomin',
- age: 20
- }
- function change(o) {
- o.age = 24;
- o = {
- name: 'haojie',
- age: 30
- }
- return o;
- }
- let b = change(a);
- console.log(b.age); // 30
- console.log(a.age); // 24
函數傳參進來的 o,傳遞的是對象在堆中的內存地址值,通過調用 o.age = 24確實改變了 a 對象的 age 屬性;但是代碼中{name:'haojie',age:30}卻又把 o 變成了另一個內存地址,將{name: 'haojie', age: 30}存入其中,最后返回 b 的值就變成了 {name: 'haojie', age: 30}。
其實,上面兩個例子很顯明地闡述了在Vue.js組件中data屬性必須是一個函數而不是一個對象,每個實例可以維護一份被返回對象的獨立的拷貝。
數據類型檢測
第一種檢測方法:typeof
- typeof 1 // 'number'
- typeof '1' // 'string'
- typeof undefined // 'undefined'
- typeof true // 'boolean'
- typeof Symbol() // 'symbol'
- typeof null // 'object'
- typeof [] // 'object'
- typeof {} // 'object'
- typeof console // 'object'
- typeof console.log // 'function'
可以看到,前 6 個都是基礎數據類型,而為什么第 6 個 null 的typeof是 object 呢?這里要和你強調一下,雖然 typeof null會輸出 object,但這只是 JS 存在的一個悠久 Bug,不代表 null 就是引用數據類型,并且 null 本身也不是對象。因此,null 在 typeof 之后返回的是有問題的結果,不能作為判斷 null 的方法。如果你需要在 if 語句中判斷是否為null,直接通過 ===null來判斷就好。
此外還要注意,引用數據類型 Object,用 typeof 來判斷的話,除了 function 會正確判斷以外,其余都是 object,是無法判斷出來的。
第二種檢測方法:instanceof
我們 new 一個對象,那么這個新對象就是它原型鏈繼承上面的對象了,通過 instanceof我們能判斷這個對象是否是之前那個構造函數生成的對象,這樣就基本可以判斷出這個新對象的數據類型。
- let Car = function() {}
- let benz = new Car();
- benz instanceof Car; // true
- let car = new String('maomin');
- car instanceof String; // true
- let str = 'haojie';
- str instanceof String; // false
我們自己可以實現一個 instanceof 的底層實現:
- function myInstanceof(target, typeObj) {
- // 這里先用typeof來判斷基礎數據類型,如果是,直接返回false
- if(typeof target !== 'object' || target === null) return false;
- // getProtypeOf是Object對象自帶的API,能夠拿到參數的原型對象
- let proto = Object.getPrototypeOf(target);
- while(true) { //循環(huán)往下尋找,直到找到相同的原型對象
- if(proto === null) return false;
- if(proto === typeObj.prototype) return true;//找到相同原型對象,返回true
- proto = Object.getPrototypeof(proto);
- }
- }
- // 驗證一下自己實現的myInstanceof
- console.log(myInstanceof(new String('maomin'), String)); // true
- console.log(myInstanceof('maomin', String)); // false
看到上述代碼的實現,我們會總結這兩個方法的差異性:
第三種檢測方法:Object.prototype.toString
toString() 是 Object 的原型方法,調用該方法,可以統(tǒng)一返回格式為 “[object Xxx]” 的字符串,其中 Xxx就是對象的類型,第一個首字母要大寫。對于 Object 對象,直接調用 toString()就能返回"[object Object]";而對于其他對象,則需要通過call來調用,才能返回正確的類型信息。
- Object.prototype.toString({}) // "[object Object]"
- Object.prototype.toString.call({}) // 同上結果,加上call也ok
- Object.prototype.toString.call(1) // "[object Number]"
- Object.prototype.toString.call('1') // "[object String]"
- Object.prototype.toString.call(true) // "[object Boolean]"
- Object.prototype.toString.call(function(){}) // "[object Function]"
- Object.prototype.toString.call(null) //"[object Null]"
- Object.prototype.toString.call(undefined) //"[object Undefined]"
- Object.prototype.toString.call(/123/g) //"[object RegExp]"
- Object.prototype.toString.call(new Date()) //"[object Date]"
- Object.prototype.toString.call([]) //"[object Array]"
- Object.prototype.toString.call(document) //"[object HTMLDocument]"
- Object.prototype.toString.call(window) //"[object Window]"
好,下面我們將實現一個完善的數據類型檢測方法。
- function getType(obj){
- let type = typeof obj;
- if (type !== "object") { // 先進行typeof判斷,如果是基礎數據類型,直接返回
- return type;
- }
- // 對于typeof返回結果是object的,再進行如下的判斷,正則返回結果
- return Object.prototype.toString.call(obj).replace(/^\[object (\S+)\]$/, '$1'); // 注意正則中間有個空格
- }
- /* 代碼驗證,需要注意大小寫,哪些是typeof判斷,哪些是toString判斷?思考下 */
- getType([]) // "Array" typeof []是object,因此toString返回
- getType('123') // "string" typeof 直接返回
- getType(window) // "Window" toString返回
- getType(null) // "Null"首字母大寫,typeof null是object,需toString來判斷
- getType(undefined) // "undefined" typeof 直接返回
- getType() // "undefined" typeof 直接返回
- getType(function(){}) // "function" typeof能判斷,因此首字母小寫
- getType(/123/g) //"RegExp" toString返回
數據類型轉換
在日常的業(yè)務開發(fā)中,經常會遇到 JavaScript 數據類型轉換問題,有的時候需要我們主動進行強制轉換,而有的時候 JavaScript 會進行隱式轉換,隱式轉換的時候就需要我們多加留心。
- '123' == 123 // true
- '' == null // false
- '' == 0 // true
- [] == 0 // true
- [] == '' // true
- [] == ![] // true
- null == undefined // true
- Number(null) // 0
- Number('') // 0
- parseInt(''); // NaN
- {}+10 // 10
- let obj = {
- [Symbol.toPrimitive]() {
- return 200;
- },
- valueOf() {
- return 300;
- },
- toString() {
- return 'Hello';
- }
- }
- console.log(obj + 200); // 400
上面的代碼相信你并不陌生,基本涵蓋了我們平常容易疏漏的一些情況,這就是在做數據類型轉換時經常會遇到的強制轉換和隱式轉換的方式,
強制類型轉換
強制類型轉換方式包括Number()、parseInt()、parseFloat()、toString()、String()、Boolean(),這幾種方法都比較類似,通過字面意思可以很容易理解,都是通過自身的方法來進行數據類型的強制轉換。
Number() 方法的強制轉換規(guī)則
- Number(true); // 1
- Number(false); // 0
- Number('0111'); //111
- Number(null); //0
- Number(''); //0
- Number('1a'); //NaN
- Number(-0X11); //-17
- Number('0X11') //17
其中,上面分別列舉了比較常見的 Number 轉換的例子,它們都會把對應的非數字類型轉換成數字類型,而有一些實在無法轉換成數字的,最后只能輸出NaN的結果。
parseInt() 方法的強制轉換規(guī)則
考慮到用 Number()函數轉換字符串時相對復雜且有點反常規(guī),通常在需要得到整數時可以優(yōu)先使用 parseInt()函數。parseInt()函數更專注于字符串是否包含數值模式。字符串最前面的空格會被忽略,從第一個非空格字符開始轉換。如果第一個字符不是數值字符、加號或減號,parseInt()立即返回 NaN。這意味著空字符串也會返回 NaN(這一點跟 Number()不一樣,它返回 0)。如果第一個字符是數值字符、加號或減號,則繼續(xù)依次檢測每個字符,直到字符串末尾,或碰到非數值字符。比如,"1234blue"會被轉換為 1234,因為"blue"會被完全忽略。類似地,"22.5"會被轉換為22,因為小數點不是有效的整數字符。
假設字符串中的第一個字符是數值字符,parseInt()函數也能識別不同的整數格式(十進制、八進制、十六進制)。換句話說,如果字符串以"0x"開頭,就會被解釋為十六進制整數。如果字符串以"0"開頭,且緊跟著數值字符,在非嚴格模式下會被某些實現解釋為八進制整數。
- let num1 = parseInt("1234blue"); // 1234
- let num2 = parseInt(""); // NaN
- let num3 = parseInt("0xA"); // 10,解釋為十六進制整數
- let num4 = parseInt(22.5); // 22
- let num5 = parseInt("70"); // 70,解釋為十進制值
- let num6 = parseInt("0xf"); // 15,解釋為十六進制整數
不同的數值格式很容易混淆,因此 parseInt()也接收第二個參數,用于指定底數(進制數)。如果知道要解析的值是十六進制,那么可以傳入 16 作為第二個參數,以便正確解析:
- let num = parseInt("0xAF", 16); // 175
事實上,如果提供了十六進制參數,那么字符串前面的"0x"可以省掉:
- let num1 = parseInt("AF", 16); // 175
- let num2 = parseInt("AF"); // NaN
在這個例子中,第一個轉換是正確的,而第二個轉換失敗了。區(qū)別在于第一次傳入了進制數作為參數,告訴 parseInt()要解析的是一個十六進制字符串。而第二個轉換檢測到第一個字符就是非數值字符,隨即自動停止并返回 NaN。
通過第二個參數,可以極大擴展轉換后獲得的結果類型。比如:
- let num1 = parseInt("10", 2); // 2,按二進制解析
- let num2 = parseInt("10", 8); // 8,按八進制解析
- let num3 = parseInt("10", 10); // 10,按十進制解析
- let num4 = parseInt("10", 16); // 16,按十六進制解析
因為不傳底數參數相當于讓 parseInt()自己決定如何解析,所以為避免解析出錯,建議始終傳給它第二個參數。多數情況下解析的應該都是十進制數,此時第二個參數就要傳入 10。
parseFloat() 方法的強制轉換規(guī)則
parseFloat()函數的工作方式跟parseInt()函數類似,都是從位置 0 開始檢測每個字符。同樣,它也是解析到字符串末尾或者解析到一個無效的浮點數值字符為止。這意味著第一次出現的小數點是有效的,但第二次出現的小數點就無效了,此時字符串的剩余字符都會被忽略。因此,"22.34.5"將轉換成 22.34。
parseFloat()函數的另一個不同之處在于,它始終忽略字符串開頭的零。十六進制數值始終會返回 0。因為parseFloat()只解析十進制值,因此不能指定底數。最后,如果字符串表示整數(沒有小數點或者小數點后面只有一個零),則 parseFloat()返回整數。下面是幾個示例:
- let a = parseFloat(true); // NaN
- let num1 = parseFloat("1234blue"); // 1234,按整數解析
- let num2 = parseFloat("0xA"); // 0
- let num3 = parseFloat("22.5"); // 22.5
- let num4 = parseFloat("22.34.5"); // 22.34
- let num5 = parseFloat("0908.5"); // 908.5
- let num6 = parseFloat("3.125e7"); // 31250000
Boolean() 方法的強制轉換規(guī)則
除了undefined、null、 false、 ''、0(包括 +0,-0)、NaN轉換出來是false,其他都是 true。
- Boolean(0) //false
- Boolean(null) //false
- Boolean(undefined) //false
- Boolean(NaN) //false
- Boolean(1) //true
- Boolean(13) //true
- Boolean('12') //true
toString() 方法的強制轉換規(guī)則
這個方法唯一的用途就是返回當前值的字符串等價物。比如:
- let age = 11;
- let ageAsString = age.toString(); // 字符串"11"
- let found = true;
- let foundAsString = found.toString(); // 字符串"true"
toString()方法可見于數值、布爾值、對象和字符串值。(沒錯,字符串值也有 toString()方法,該方法只是簡單地返回自身的一個副本。)null 和 undefined值沒有toString()方法。
多數情況下,toString()不接收任何參數。不過,在對數值調用這個方法時,toString()可以接收一個底數參數,即以什么底數來輸出數值的字符串表示。默認情況下,toString()返回數值的十
進制字符串表示。而通過傳入參數,可以得到數值的二進制、八進制、十六進制,或者其他任何有效基
數的字符串表示,比如:
- let num = 10;
- console.log(num.toString()); // "10"
- console.log(num.toString(2)); // "1010"
- console.log(num.toString(8)); // "12"
- console.log(num.toString(10)); // "10"
- console.log(num.toString(16)); // "a"
這個例子展示了傳入底數參數時,toString()輸出的字符串值也會隨之改變。數值 10 可以輸出為
任意數值格式。注意,默認情況下(不傳參數)的輸出與傳入參數 10 得到的結果相同。
String() 方法的強制轉換規(guī)則
如果你不確定一個值是不是 null 或 undefined,可以使用 String()轉型函數,它始終會返回表示相應類型值的字符串。String()函數遵循如下規(guī)則。
下面看幾個例子:
- let value1 = 10;
- let value2 = true;
- let value3 = null;
- let value4;
- console.log(String(value1)); // "10"
- console.log(String(value2)); // "true"
- console.log(String(value3)); // "null"
- console.log(String(value4)); // "undefined"
這里展示了將 4 個值轉換為字符串的情況:一個數值、一個布爾值、一個 null 和一個 undefined。數值和布爾值的轉換結果與調用toString()相同。因為 null 和 undefined沒有 toString()方法,所以 String()方法就直接返回了這兩個值的字面量文本。
隱式類型轉換
凡是通過邏輯運算符 (&&、 ||、!)、運算符 (+、-、*、/)、關系操作符 (>、 <、 <= 、>=)、相等運算符 (==) 或者 if/while 條件的操作,如果遇到兩個數據類型不一樣的情況,都會出現隱式類型轉換。
下面主要說一下日常用得比較多的“==”和“+”這兩個符號的隱式轉換規(guī)則。
'==' 的隱式類型轉換規(guī)則
- null == undefined // true 規(guī)則2
- null == 0 // false 規(guī)則2
- '' == null // false 規(guī)則2
- '' == 0 // true 規(guī)則4 字符串轉隱式轉換成Number之后再對比
- '123' == 123 // true 規(guī)則4 字符串轉隱式轉換成Number之后再對比
- 0 == false // true e規(guī)則 布爾型隱式轉換成Number之后再對比
- 1 == true // true e規(guī)則 布爾型隱式轉換成Number之后再對比
- var a = {
- value: 0,
- valueOf: function() {
- this.value++;
- return this.value;
- }
- };
- // 注意這里a又可以等于1、2、3
- console.log(a == 1 && a == 2 && a ==3); //true f規(guī)則 Object隱式轉換
- // 注:但是執(zhí)行過3遍之后,再重新執(zhí)行a==3或之前的數字就是false,因為value已經加上去了,這里需要注意一下
'+' 的隱式類型轉換規(guī)則
'+' 號操作符,不僅可以用作數字相加,還可以用作字符串拼接。僅當 '+'號兩邊都是數字時,進行的是加法運算;如果兩邊都是字符串,則直接拼接,無須進行隱式類型轉換。
除了上述比較常規(guī)的情況外,還有一些特殊的規(guī)則,如下所示。
- 1 + 2 // 3 常規(guī)情況
- '1' + '2' // '12' 常規(guī)情況
- // 下面看一下特殊情況
- '1' + undefined // "1undefined" 規(guī)則1,undefined轉換字符串
- '1' + null // "1null" 規(guī)則1,null轉換字符串
- '1' + true // "1true" 規(guī)則1,true轉換字符串
- '1' + 1n // '11' 比較特殊字符串和BigInt相加,BigInt轉換為字符串
- 1 + undefined // NaN 規(guī)則2,undefined轉換數字相加NaN
- 1 + null // 1 規(guī)則2,null轉換為0
- 1 + true // 2 規(guī)則2,true轉換為1,二者相加為2
- 1 + 1n // 錯誤 不能把BigInt和Number類型直接混合相加
- '1' + 3 // '13' 規(guī)則3,字符串拼接
整體來看,如果數據中有字符串,JavaScript 類型轉換還是更傾向于轉換成字符串,因為第三條規(guī)則中可以看到,在字符串和數字相加的過程中最后返回的還是字符串,這里需要關注一下。
Object 的轉換規(guī)則
對象轉換的規(guī)則,會先調用內置的[ToPrimitive]函數,其規(guī)則邏輯如下:
- var obj = {
- value: 1,
- valueOf() {
- return 2;
- },
- toString() {
- return '3'
- },
- [Symbol.toPrimitive]() {
- return 4
- }
- }
- console.log(obj + 1); // 輸出5
- // 因為有Symbol.toPrimitive,就優(yōu)先執(zhí)行這個;如果Symbol.toPrimitive這段代碼刪掉,則執(zhí)行valueOf打印結果為3;如果valueOf也去掉,則調用toString返回'31'(字符串拼接)
- // 再看兩個特殊的case:
- 10 + {}
- // "10[object Object]",注意:{}會默認調用valueOf是{},不是基礎類型繼續(xù)轉換,調用toString,返回結果"[object Object]",于是和10進行'+'運算,按照字符串拼接規(guī)則來,參考'+'的規(guī)則C
- [1,2,undefined,4,5] + 10
- // "1,2,,4,510",注意[1,2,undefined,4,5]會默認先調用valueOf結果還是這個數組,不是基礎數據類型繼續(xù)轉換,也還是調用toString,返回"1,2,,4,5",然后再和10進行運算,還是按照字符串拼接規(guī)則,參考'+'的第3條規(guī)則
總結
1.**數據類型的基本概念:**這是必須掌握的知識點,作為深入理解 JavaScript 的基礎。
2.數據類型的判斷方法:typeof和instanceof,以及Object.prototype.toString 的判斷數據類型、手寫 instanceof 代碼片段,這些是日常開發(fā)中經常會遇到的。
3.**數據類型的轉換方式:**兩種數據類型的轉換方式,日常寫代碼過程中隱式轉換需要多留意,如果理解不到位,很容易引起在編碼過程中的 bug,得到一些意想不到的結果。

我們在微信上24小時期待你的聲音
解答本文疑問/技術咨詢/運營咨詢/技術建議/互聯(lián)網交流