[打牢基礎系列]JavaScript的變數和資料型別

前端古力士發表於2019-09-09

1 前言

如果面試問你JavaScript的資料型別有哪些?你可以信誓旦旦的說出Null, Undefined, Boolean, String, Number,Symbol以及Object七種資料型別,問到它們的區別是什麼,你也能說出一二,但是你知道JavaScript的包裝型別嗎?拆箱和裝箱又是?Symbol資料型別有哪些特性?你在什麼時候用到了Symbol資料型別?隱式型別轉換規則有哪些?判斷JavaScript資料型別的方法有哪些?優缺點是?enmmmm...

[打牢基礎系列]JavaScript的變數和資料型別

這篇文章會對上述問題作出解答,並會擴充套件一些那些我們需要知道但卻沒有關注到的知識,讓我們開始學習之旅吧~

2 JavaScript資料型別

2.1 原始型別

  • Null: 只包含一個值: null
  • Undefined: 只包含一個值: undefined
  • Boolean: 包含兩個值, true 和 false
  • String: 一串字元序列
  • Number:整數或浮點數,還有一些特殊值(-Infinity、+Infinity、NaN)
  • Symbol(ES6新增)

(在es10中加入了第七種原始型別BigInt,現已被最新Chrome支援)

2.2 引用型別

  • Object Array, Function, Date, RegExp都是特殊的物件

2.3 原始型別與引用型別的區別

1. 原始型別的值是不可變的,引用型別的值是可變的

// 原始型別
var name = 'muzishuiji';
name.subStr(1, 3);    // uzi
name.slice(2);        // zishuiji
name.toUpperCase();   // MUZISHUIJI
console.log(name);                 // muzishuiji

// 引用型別
var obj = {
    name: 'sss'
}
obj.name = 'muzishuiji'
obj.age = 22;
console.log(obj);     // {name: "muzishuiji", age: 22}  
複製程式碼

2. 原始型別的變數是存放在棧區的,引用型別的變數是在堆記憶體中申請地址存放變數值,然後在棧記憶體中存放該變數在記憶體中的地址.*

  • 原始型別

      var name = 'muzishuiji';
      var age = 22;
      var job = 'teacher';
    複製程式碼

儲存結構如下圖:

[打牢基礎系列]JavaScript的變數和資料型別

  • 引用型別

      var obj1 = {name:'muzishuiji'};
      var obj2 = {name:'wangming'};
      var person3 = {name:'xuliu'};
    複製程式碼

儲存結構如下圖:

[打牢基礎系列]JavaScript的變數和資料型別

3. 原始型別的比較是值的比較,引用型別型別的比較是變數值所在地址的比較:

原始型別的變數在棧中存放的就是對應的變數值, 而引用型別在棧中存放的是變數值所在的地址.

// 原始型別的比較
var a = 'muzishuiji';
var b = 'muzishuiji';
console.log(a === b);       // true

// 引用型別的比較
var obj1 = { name: 'muzishuiji' };
var obj2 = { name: 'muzishuiji' };
console.log(obj1 === obj2);  // false   
複製程式碼

2.3 Symbol型別

Symbol型別是ES6新引入的一種資料型別,它接收一個可選的字串作為描述.當引數為物件時,將呼叫物件的toString()方法, 使用示例如下:

let s1 = Symbol();  // Symbol() 
let s2 = Symbol('muzishuiji');  // Symbol(muzishuiji)
let s3 = Symbol(['sss','aaa']);  // Symbol(sss, aaa)
let s4 = Symbol({name:'muzishuiji'}); // Symbol([object Object])
複製程式碼

2.3.1 Symbol型別的特性

  • 獨一無二的特性

使用Symbol()建立的變數使獨一無二的,因此,比較兩個Symbol()建立的變數總是返回false.

let s5 = Symbol();                      
let s6 = Symbol();                         
console.log(s5 === s6);        // false  
let s7 = Symbol('muzishuiji');
let s8 = Symbol('muzishuiji');  
console.log(s7 === s8);        // false
複製程式碼

js提供了Symbol.for(key)來建立兩個相等的變數,使用給定的key搜尋現有的Symbol,如果找到則返回該Symbol,否則將使用給定的key在全域性Symbol登錄檔中建立一個新的Symbol.

let s1 = Symbol.for('muzishuiji');
let s2 = Symbol.for('muzishuiji');
console.log(s1 === s2); // true
複製程式碼
  • 原始型別,不能使用new操作符建立

使用new 操作符來建立Symbol變數會報錯,因為Symbol()返回的不是一個變數,而是一個Symbol型別的值,所以禁止把它當做建構函式使用.

new Symbol(); // Uncaught TypeError: Symbol is not a constructor
複製程式碼

這個時候你不會不會有些奇怪,平時我們使用new 操作符來呼叫一個普通的函式(不是嚴格意義上的建構函式),並不會給我們丟擲這樣的錯誤:

function Person() {
    console.log('muzishuiji');
}
var person1 = new Person(); // muzishuiji, 並沒有報錯
複製程式碼

那麼Symbol函式是怎麼知道我是用來new 操作符,並給我丟擲錯誤呢?我做了以下實驗:

function Person() {
    if(this instanceof Person) {
        throw Error("Person is not a constructor");  // Uncaught TypeError: Symbol is not a constructor
    }
}
var person1 = new Person();
複製程式碼

是的,報錯了,可見Symbol函式是通過判斷當前物件是不是Symbol的例項來判斷我們有沒有使用new 操作符(js中的new操作符的原理).

  • 不可列舉

使用Symbol建立的屬性名是不可列舉的,使用for...in, Object.keys(), Object.getOwnPropertyNames()等方法是無法獲取的.可以呼叫Object.getOwnPropertySymbols()和Reflect.ownKeys()來獲取物件的Symbol屬性.

var obj = {
    name:'muzishuiji',
    [Symbol('age')]: 18
}
Object.getOwnPropertyNames(obj);   // ["name"]
Object.keys(obj);                  // ["name"]
for (var i in obj) {
    console.log(i);                // name
}
Object.getOwnPropertySymbols(obj)  // [Symbol(age)]
Reflect.ownKeys(obj)               // ["name", Symbol(name)]
複製程式碼

2.3.2 Symbol型別的使用場景

  • 使用Symbol來定義屬性名,防止屬性汙染

有時候我們想給一個物件新增屬性名,但又擔心和別的同事重名,我們可以這樣:

const symKey1 = Symbol('name');
var obj = {
    name: '1223'
}
obj[symKey1] = '1478'
複製程式碼

Symbol建立的變數獨一無二的特性有效避免了屬性汙染.

  • 使用Symbol定義類的私有屬性或方法

      (function(){
          var AGE_SY = Symbol()
          var GET_NAME = Symbol()
          class Animal {
              constructor(name, age) {
                  this.name = name
                  this[AGE_SY] = age
              }
              [GET_NAME]() {
                  console.log(this.name)
              }
          }
      })()
      var animal1 = new Animal('muzishuiji', 18);
      // 我們不能直接獲取到內部定義的變數AGE_SY和GET_NAME ,所以也就不能直接訪問Animal類的AGE_SY屬性和GET_NAME方法
      // 但這裡的私有屬性不是嚴格意義上的的私有屬性,因為我們仍然可以通過這樣的操作來訪問
      animal1[Object.getOwnPropertySymbols(animal1)[0]];   // 18  
    複製程式碼
  • 建立常量

      // 通常我們會這樣,我們需要根據不同的傳入值做不同的處理
      const TYPE_ONE = 'red'
      const TYPE_TWO = 'green'
      const TYPE_THERE = 'blue'
      function handleSome(resource) {
          switch(resource.type) {
              case TYPE_AUDIO:
                  // do something
                  break;
              case TYPE_VIDEO:
                  // do something
                  break;
              break
                  // do something
                  break;
          }
      }
      handleSome('red')
    
      // 使用Symbol我們可以這樣,不必費勁心思思考列舉值用什麼
      const TYPE_ONE = Symbol()
      const TYPE_TWO = Symbol()
      const TYPE_THERE = Symbol()
      function handleSome(resource) {
          switch(resource.type) {
              case TYPE_AUDIO:
                  // do something
                  break;
              case TYPE_VIDEO:
                  // do something
                  break;
              break
                  // do something
                  break;
          }
      }
      handleSome(TYPE_ONE)   // 這樣就可以處理對應TYPE_ONE的程式碼邏輯啦
    複製程式碼

2.4 包裝型別

2.4.1 基本包裝型別

ECMAScript還提供了3個特殊的引用型別: Boolean, Number和String.這些型別具有與各自的基本型別相似的特殊行為.實際上,每當讀取一個基本型別值的時候,後臺就會建立一個對應的基本包裝型別的物件,從而讓我們能夠呼叫一些方法來操作這些資料.

var s1 = 'some text';
var s2 = s1.substring(2);  // "me text"
複製程式碼

事實上,s1是基本型別不是物件,從邏輯上講它是沒有方法的,它是所以呼叫substring方法,是因為後臺幫我們做了裝箱的操作,當第二行程式碼訪問s1時,訪問過程處於一種讀取模式,也就是要從記憶體中讀取這個字串的值,而在讀取模式訪問字串時,後臺會自動完成下列操作:

  • (1) 建立String型別的一個例項;

  • (2) 在例項上呼叫指定的方法;

  • (3) 銷燬這個例項;

翻譯成程式碼是這樣的:

// 紅寶書上傳入的是字串"some text",我覺得其實就是傳入的s1的值建立了一個臨時的包裝型別的變數.
var tempS1 = new String(s1);
var s2 = tempS1.sunString();
tempS1 = null;
複製程式碼

上面三個步驟也分別適用於Boolean和Number型別對應的布林值和數值.

引用型別與基本包裝型別的主要區別就是物件的生存期,使用new操作符建立的引用型別的例項,在執行流離開當前作用域之前都一直儲存在記憶體中.而自動建立的基本型別的物件,則只存在於一行程式碼的執行瞬間,然後立即被銷燬,這意味著我們不能在執行時為基本型別值新增屬性和方法.來看下面的例子:

var s1 = 'some text';
s1.color = 'red'
alert(s1.color);  // undefined
複製程式碼

在嘗試訪問s1的color屬性的時候,第二行建立的String物件已經被銷燬了,第三行程式碼又建立新的String物件,而該物件沒有color屬性.

可以顯式地呼叫Boolean,Number和String來建立基本包裝型別的物件,不過,應該在必要的情況下這樣做,因為這種做法很容易讓人分不清自己是在處理基本型別還是引用型別的值.

2.4.2 裝箱和拆箱

  • 裝箱: 把基本型別轉換成對應的包裝型別
  • 拆箱: 把引用型別轉換為基本型別

裝箱的操作也就是上面2.4.1介紹的基本操作型別在呼叫相關方法時後臺為我們執行的操作.

從引用型別到基本型別的轉換,也就是拆箱的過程中,會遵循ECMAScript規範規定的toPrimitive原則,一般會呼叫引用型別的valueOf和toString方法,你也可以直接重寫toPeimitive方法。一般會根據想要轉換的目標資料型別, string or number,來執行相應的轉換操作.

// 自定義valueOf和toString, 返回對應值
const obj = {
    valueOf: () => { return 123; },
    toString: () => { return 'muzishuiji'; }
}
console.log(obj - 1);      // 目標型別number, 結果: 122
console.log(obj + '11');   // 目標型別string, 結果: "12311"

// 自定義toPrimitive 
const obj1 = {
    [Symbol.toPrimitive]: () => { return 123; }
}
console.log(obj1 - 1);    // 目標型別number, 結果: 122
console.log(obj1 + '11'); // 目標型別string, 結果: "12311" 

// 自定義valueOf和toString, 返回不能被正常轉換的值
const obj2 = {
    valueOf: () => { return {}; },
    toString: () => { return {}; }
}
console.log(obj2 - 1);    // Uncaught TypeError: Cannot convert object to primitive value
console.log(obj2 + '11'); // Uncaught TypeError: Cannot convert object to primitive value
複製程式碼

和手動建立包裝型別一樣,我們也可以通過手動呼叫包裝型別或者引用型別的valueOf或toString,實現拆箱操作:

var num =new Number("123");
console.log(num.valueOf(), typeof num.valueOf()); // 123 "number"
console.log(num.toString(), typeof num.toString()); // "123" "string"
const obj = {
    valueOf: () => { return 123; },
    toString: () => { return 'muzishuiji'; }
}
obj.toString();    // 'muzishuiji'
obj.valueOf();     //  123
複製程式碼

3 JavaScript的型別轉換

我們都知道JavaScript是一種弱型別的語言,js宣告變數並沒有預先確定的型別,變數的型別就是其值的型別,也就是說我們可以通過賦值來隨意的修改變數的型別,重新賦值的過程其實就是在後臺為我們執行了強制型別轉換的操作.這一特性使js的編碼變得更靈活,但同時也帶來了程式碼的不穩定性和不可預測性,所以熟知JavaScript的型別轉換規則,可以讓一定程度上避免寫出意外的bug.

JavaScript的型別轉換分為強制型別轉換和隱式型別轉換.

3.1 強制型別轉換

3.1.1 ToPrimitive

ToPrimitive(obj,type);
複製程式碼

ToPrimitive方法接收兩個引數,需要轉換的物件和期望轉換成的資料型別,第二個引數可選.

  • type為string:
  1. 先呼叫obj的toString方法,如果為原始值,則返回,否則進行第2步;

  2. 呼叫obj的valueOf方法,如果為原始值,則返回,否則進行第3步;

  3. 否則,丟擲錯誤.

  • type為number
  1. 先呼叫obj的valueOf方法,如果為原始值,則返回,否則進行第2步;

  2. 呼叫obj的toString方法,如果為原始值,則返回,否則進行第3步;

  3. 否則,丟擲錯誤.

  • type引數為空
  1. 該物件為Date,則預設轉換成string型別
  2. 否則,預設轉換成number型別

Date資料型別特殊說明:對於Date資料型別,我們更多期望獲得的是其轉為時間後的字串,而非毫秒值(時間戳),如果為number,則會取到對應的毫秒值,顯然字串使用更多。 其他型別物件按照取值的型別操作即可。

注意, 隱式型別某個引用型別轉換為原始值就是在後臺呼叫ToPrimitive方法, 轉換邏輯就和type引數為空時的轉換邏輯一致.

3.1.2 toString

Object.prototype.toString()方法返回該物件的字串表示

每個物件都有一個 toString() 方法,當物件被表示為文字值時或者當以期望字串的方式引用物件時,該方法被自動呼叫。

3.1.3 valueOf

Object.prototype.valueOf()方法返回指定物件的原始值。

JavaScript 呼叫 valueOf() 方法用來把物件轉換成原始型別的值(數值、字串和布林值)。但是和toString方法一樣,我們很少需要自己呼叫這些函式,這些方法一般都會在發生資料型別轉換的時候被 JavaScript 自動呼叫。

不同內建物件的 valueOf 實現:

  • String => 返回字串值
  • Number => 返回數字值
  • Date => 返回一個數字,即時間值,字串中內容是依賴於具體實現的
  • Boolean => 返回Boolean的this值
  • Array => 預設返回自身
  • Object => 預設返回自身 我們可以通過重寫物件的valueOf方法來讓它返回我們想要的結果

程式碼展示如下:

var str = "123";
str.valueOf();  // "123"

var num = 456;
num.valueOf();  // 456

var date = new Date();
date.valueOf(); // 1567998675017

var arr = [1,2,3,4];
arr.valueOf();  // [1,2,3,4]

var obj = new Object({valueOf:()=>{
    return 'muzishuiji'
}})
console.log(obj.valueOf());   // "muzishuiji"
複製程式碼

3.1.4 Number

Number運算子轉換規則:

  • null 轉換為0
  • undefined 轉換為NaN
  • true轉換為1, false轉換為0
  • 字串轉換時遵循數字常量轉換規則,轉換失敗返回NaN

如果要呼叫Number方法轉換物件,則會呼叫ToPrimitive轉換,type指定為number

程式碼示例:

Number(null);       // 0
Number(undefined);  // NaN
Number('123');      // 123
Number('456abc');   // 456
Number([1,2,3]);    // NaN
Number({});         // NaN
Number(new Date()); // 1568000206474
複製程式碼

3.1.5 String

String 運算子轉換規則

  • null 轉換為 'null'
  • undefined 轉換為 undefined
  • true 轉換為 'true',false 轉換為 'false'
  • 數字轉換遵循通用規則,極大極小的數字使用指數形式

如果要呼叫String方法轉換物件,則會呼叫ToPrimitive轉換,type指定為string

程式碼示例:

String(null);                // 'null'
String(undefined);           // 'undefined'
String(true)                 // 'true'
String(1)                    // '1'
String(0)                    // '0'
String(Infinity)             // 'Infinity'
String(-Infinity)            // '-Infinity'
String({})                   // '[object Object]'
String([1,[2,3]])            // '1,2,3'
String(['koala',1])          //koala,1
複製程式碼

3.1.5 Boolean

ToBoolean 運算子轉換規則

除了下述 6 個值轉換結果為 false,其他全部為true:

  • undefined
  • null
  • -0
  • 0或+0
  • NaN
  • ''(空字串)

需要說明的一點是 new Boolaen(false)的轉換結果也是true, 因為通過Boolaen方法建立的是一個值為false的變數.

Boolean(undefined) // false
Boolean(null) // false
Boolean(0) // false
Boolean(NaN) // false
Boolean('') // false
Boolean({}) // true
Boolean([]) // true
Boolean(new Boolean(false)) // true
typeof new Boolean(false)   // "object"
複製程式碼

3.2 隱式型別轉換

3.2.1 加法運算子

除加法運算子以外的運算子,如*, / , - 運算子都會預設將符號兩側的資料轉換成數值在進行計算.

'12' + 2;      // '123'
'12' + true;   // '12true'
'12' + false;  // '12false'
'12' + ['1', '2'];   // '121,2 ' 
'12' + {};           "12[object Object]"

12 + null;     // 12
12 + undefined; // NaN
12 + '3';      // '123'     
12 + true;     // 13
12 + false;    // 12
12+['3','4'];       // '123,4'
12 + {};       // '12[object Object]'
複製程式碼

總結規律如下:

  • 當一側為String型別時,另一側也會被轉換成字串型別,做拼接操作;
  • 當一側為Number型別,另一側為非String型別的原始型別時,另一側會被轉換成number型別,做加法運算;
  • 當一側為Number型別,另一側為引用型別時,會將引用型別和Number型別都轉換成字串做拼接操作.

運用排除法可得,使用加法運算子時的隱式型別轉換就是: 除了Number型別 + (Null, Undefined, Boolean,Number)會做加法運算,其他情況下都是做字串拼接操作.

發現了一個例外,{}在加法運算子左側也會做加法運算(加法運算子右側是{}或者function(){}除外).

[打牢基礎系列]JavaScript的變數和資料型別

3.2.2 if語句和邏輯語句

在if語句和邏輯語句中,如果只有單個變數,會先將變數轉換為Boolean值,只有以下情況會被轉成false,其餘會被轉換成true.

null
undefined
''
NaN
0
false
複製程式碼

3.2.3 == 運算子

使用 == 時會發生隱式型別轉換,導致意外的bug出現,我們如果需要做比較運算最好使用 === 嚴格等於運算子.

null == undefined;      // true
NaN == NaN;             // false

true == 1;              // true
true == 'sss';          // false
true == ['44'];         // false
true == {};             // false
false == 0;             // true
false == 'sss';         // false
false == ['44'];        // false
false == {};            // false
'123' == 123;           // true
'' == 0;                // true 
'[object Object]' == {} // true
'1,2,3' == [1, 2, 3]    // true
{} == '1'               // Uncaught SyntaxError: Unexpected token ==
複製程式碼

總結規律如下:

  • null除了跟自己和undefined相比返回true,其他返回false;
  • undefined除了和自己和null相比返回true,其他返回false;
  • NaN和任何值比較都返回false
  • Boolean跟其他型別的值比較,會被轉換為Number型別

true只有和1比較會返回true, false只有和0比較會返回true

  • String和Number比較,現將String轉換為Number

  • 原始型別和引用型別比較

當原始型別和引用型別做比較時,物件型別會依照ToPrimitive規則轉換為原始型別, {}放在運算子左側會報錯.

4. 判斷JavaScript資料型別的方式

4.1 typeof

typeof多用於判斷一個變數屬於哪個原始型別:

typeof 'muzishuiji'  // string
typeof 123  // number
typeof true  // boolean
typeof Symbol()  // symbol
typeof undefined  // undefined
typeof function() {}  // function
複製程式碼

typeof不能準確判斷引用型別的資料型別:

typeof [] // object
typeof {} // object
typeof new Date() // object
typeof /^\d*$/; // object
typeof null;    // object, 眾所周知的JavaScript的一個bug
複製程式碼

4.2 instanceof

instanceof操作符可以判斷引用型別具體是什麼型別的變數,其主要原理就是監測建構函式的prototype 是否出現在被檢測物件的原型鏈上.

var a = {}
a instanceof Object  // true
[] instanceof Array // true
new Date() instanceof Date // true
new RegExp() instanceof RegExp // true
var b = function() {}
b instanceof Function  // true
複製程式碼

但是有些情況下,instanceof得到的結果也不準確,

[] instanceof Object     // true
var b = function() {}
b instanceof Object      // true
複製程式碼

4.3 Object.prototype.toString.call

Object.prototype.toString.call({})              // '[object Object]'
Object.prototype.toString.call([])              // '[object Array]'
Object.prototype.toString.call(() => {})        // '[object Function]'
Object.prototype.toString.call('seymoe')        // '[object String]'
Object.prototype.toString.call(1)               // '[object Number]'
Object.prototype.toString.call(true)            // '[object Boolean]'
Object.prototype.toString.call(Symbol())        // '[object Symbol]'
Object.prototype.toString.call(null)            // '[object Null]'
Object.prototype.toString.call(undefined)       // '[object Undefined]'

Object.prototype.toString.call(new Date())      // '[object Date]'
Object.prototype.toString.call(Math)            // '[object Math]'
Object.prototype.toString.call(new Set())       // '[object Set]'
Object.prototype.toString.call(new WeakSet())   // '[object WeakSet]'
Object.prototype.toString.call(new Map())       // '[object Map]'
Object.prototype.toString.call(new WeakMap())   // '[object WeakMap]'
複製程式碼

我們可以使用這個方法返回傳入值的準確型別,Object.prototype.toString方法返回的是該函式執行是this指向物件的資料型別,看下面的例子:

var tempFun = Object.prototype.toString;
var aaa = [];  
aaa.tempFun = tempFun;
aaa.tempFun();      //  "[object Array]"

var reg = new RegExp();
reg.tempFun = tempFun;
reg.tempFun();     //  "[object RegExp]"
複製程式碼

所以call函式為我們繫結了Object.prototype.toString函式執行時候的this,呼叫Object.prototype.toString.call(obj);就會返回obj的資料型別了(^_^).

4.4 嘗試實現一個判斷資料型別的工具函式(受jquery原始碼中的型別判斷的啟發)

const classType = {};
const typeArray = ["Boolean", "Number", "String", "Function", "Array", "Date", "RegExp", "Object", "Error", "Symbol"];
typeArray.forEach(type => {
    classType["[object " + type + "]"] = type.toLowerCase();
})
function getType(obj) {
    if ( obj == null ) {
        return obj + "";
    }
    return typeof obj === "object" || typeof obj === "function" ?
    classType[Object.prototype.toString.call(obj) ] || "object" :
    typeof obj;
}
複製程式碼

原始型別直接使用typeof, 引用型別使用Object.prototype.toString.call取得型別, classType來將對應型別的小寫形式存起來,將Object.prototype.toString.call返回的多餘的內容過濾掉,只留下對應型別的小寫形式返回.

結語

在借鑑了前輩們的分享才得以完成這篇JavaScript變數與資料型別的總結,在此過程中,我加入了自己的理解和擴充套件,用比較淺顯的語言來闡述JavaScript的一些概念,以及一些規則對應的原理.如果又發現不對的地方或者解釋不到位的地方,歡迎在下方評論或者加微信lj_de_wei_xin與我交流~

擴充套件閱讀

相關文章