[打牢基礎系列]JavaScript的變量和數據類型

1 前言

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

這篇文章會對上述問題做出解答,並會擴展一些那些咱們須要知道但卻沒有關注到的知識,讓咱們開始學習之旅吧~jquery

2 JavaScript數據類型

2.1 原始類型

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

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

2.2 引用類型

  • Object Array, Function, Date, RegExp都是特殊的對象

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

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

// 原始類型
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. 原始類型的變量是存放在棧區的,引用類型的變量是在堆內存中申請地址存放變量值,而後在棧內存中存放該變量在內存中的地址.*web

  • 原始類型面試

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

存儲結構以下圖:微信

  • 引用類型koa

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

存儲結構以下圖:異步

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)會作加法運算,其餘狀況下都是作字符串拼接操做.

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與我交流~

擴展閱讀

相關文章
相關標籤/搜索