變量和類型是學習JavaScript
最早接觸到的東西,可是每每看起來最簡單的東西每每還隱藏着不少你不瞭解、或者容易犯錯的知識,好比下面幾個問題:html
JavaScript
中的變量在內存中的具體存儲形式是什麼?0.1+0.2
爲何不等於0.3
?發生小數計算錯誤的具體緣由是什麼?Symbol
的特色,以及實際應用場景是什麼?[] == ![]
、[undefined] == false
爲何等於true
?代碼中什麼時候會發生隱式類型轉換?轉換的規則是什麼?若是你還不能很好的解答上面的問題,那說明你尚未徹底掌握這部分的知識,那麼請好好閱讀下面的文章吧。前端
本文從底層原理到實際應用詳細介紹了JavaScript
中的變量和類型相關知識。node
ECMAScript標準規定了7
種數據類型,其把這7
種數據類型又分爲兩種:原始類型和對象類型。react
原始類型jquery
Null
:只包含一個值:null
Undefined
:只包含一個值:undefined
Boolean
:包含兩個值:true
和false
Number
:整數或浮點數,還有一些特殊值(-Infinity
、+Infinity
、NaN
)String
:一串表示文本值的字符序列Symbol
:一種實例是惟一且不可改變的數據類型(在es10
中加入了第七種原始類型BigInt
,現已被最新Chrome
支持)git
對象類型github
Object
:本身分一類絲絕不過度,除了經常使用的Object
,Array
、Function
等都屬於特殊的對象上面所提到的原始類型,在ECMAScript
標準中,它們被定義爲primitive values
,即原始值,表明值自己是不可被改變的。面試
以字符串爲例,咱們在調用操做字符串的方法時,沒有任何方法是能夠直接改變字符串的:typescript
var str = 'ConardLi'; str.slice(1); str.substr(1); str.trim(1); str.toLowerCase(1); str[0] = 1; console.log(str); // ConardLi
在上面的代碼中咱們對str
調用了幾個方法,無一例外,這些方法都在原字符串的基礎上產生了一個新字符串,而非直接去改變str
,這就印證了字符串的不可變性。數組
那麼,當咱們繼續調用下面的代碼:
str += '6' console.log(str); // ConardLi6
你會發現,str
的值被改變了,這不就打臉了字符串的不可變性麼?其實否則,咱們從內存上來理解:
在JavaScript
中,每個變量在內存中都須要一個空間來存儲。
內存空間又被分爲兩種,棧內存與堆內存。
棧內存:
JavaScript
中的原始類型的值被直接存儲在棧中,在變量定義時,棧就爲其分配好了內存空間。
因爲棧中的內存空間的大小是固定的,那麼註定了存儲在棧中的變量就是不可變的。
在上面的代碼中,咱們執行了str += '6'
的操做,其實是在棧中又開闢了一塊內存空間用於存儲'ConardLi6'
,而後將變量str
指向這塊空間,因此這並不違背不可變性的
特色。
堆內存:
相對於上面具備不可變性的原始類型,我習慣把對象稱爲引用類型,引用類型的值實際存儲在堆內存中,它在棧中只存儲了一個固定長度的地址,這個地址指向堆內存中的值。
var obj1 = {name:"ConardLi"} var obj2 = {age:18} var obj3 = function(){...} var obj4 = [1,2,3,4,5,6,7,8,9]
因爲內存是有限的,這些變量不可能一直在內存中佔用資源,這裏推薦下這篇文章
JavaScript中的垃圾回收和內存泄漏,這裏告訴你
JavaScript
是如何進行垃圾回收以及可能會發生內存泄漏的一些場景。
固然,引用類型就再也不具備不可變性
了,咱們能夠輕易的改變它們:
obj1.name = "ConardLi6"; obj2.age = 19; obj4.length = 0; console.log(obj1); //{name:"ConardLi6"} console.log(obj2); // {age:19} console.log(obj4); // []
以數組爲例,它的不少方法均可以改變它自身。
pop()
刪除數組最後一個元素,若是數組爲空,則不改變數組,返回undefined,改變原數組,返回被刪除的元素push()
向數組末尾添加一個或多個元素,改變原數組,返回新數組的長度shift()
把數組的第一個元素刪除,若空數組,不進行任何操做,返回undefined,改變原數組,返回第一個元素的值unshift()
向數組的開頭添加一個或多個元素,改變原數組,返回新數組的長度reverse()
顛倒數組中元素的順序,改變原數組,返回該數組sort()
對數組元素進行排序,改變原數組,返回該數組splice()
從數組中添加/刪除項目,改變原數組,返回被刪除的元素下面咱們經過幾個操做來對比一下原始類型和引用類型的區別:
當咱們把一個變量的值複製到另外一個變量上時,原始類型和引用類型的表現是不同的,先來看看原始類型:
var name = 'ConardLi'; var name2 = name; name2 = 'code祕密花園'; console.log(name); // ConardLi;
內存中有一個變量name
,值爲ConardLi
。咱們從變量name
複製出一個變量name2
,此時在內存中建立了一個塊新的空間用於存儲ConardLi
,雖然二者值是相同的,可是二者指向的內存空間徹底不一樣,這兩個變量參與任何操做都互不影響。
複製一個引用類型:
var obj = {name:'ConardLi'}; var obj2 = obj; obj2.name = 'code祕密花園'; console.log(obj.name); // code祕密花園
當咱們複製引用類型的變量時,實際上覆制的是棧中存儲的地址,因此複製出來的obj2
實際上和obj
指向的堆中同一個對象。所以,咱們改變其中任何一個變量的值,另外一個變量都會受到影響,這就是爲何會有深拷貝和淺拷貝的緣由。
當咱們在對兩個變量進行比較時,不一樣類型的變量的表現是不一樣的:
var name = 'ConardLi'; var name2 = 'ConardLi'; console.log(name === name2); // true var obj = {name:'ConardLi'}; var obj2 = {name:'ConardLi'}; console.log(obj === obj2); // false
對於原始類型,比較時會直接比較它們的值,若是值相等,即返回true
。
對於引用類型,比較時會比較它們的引用地址,雖然兩個變量在堆中存儲的對象具備的屬性值都是相等的,可是它們被存儲在了不一樣的存儲空間,所以比較值爲false
。
藉助下面的例子,咱們先來看一看什麼是值傳遞,什麼是引用傳遞:
let name = 'ConardLi'; function changeValue(name){ name = 'code祕密花園'; } changeValue(name); console.log(name);
執行上面的代碼,若是最終打印出來的name
是'ConardLi'
,沒有改變,說明函數參數傳遞的是變量的值,即值傳遞。若是最終打印的是'code祕密花園'
,函數內部的操做能夠改變傳入的變量,那麼說明函數參數傳遞的是引用,即引用傳遞。
很明顯,上面的執行結果是'ConardLi'
,即函數參數僅僅是被傳入變量複製給了的一個局部變量,改變這個局部變量不會對外部變量產生影響。
let obj = {name:'ConardLi'}; function changeValue(obj){ obj.name = 'code祕密花園'; } changeValue(obj); console.log(obj.name); // code祕密花園
上面的代碼可能讓你產生疑惑,是否是參數是引用類型就是引用傳遞呢?
首先明確一點,ECMAScript
中全部的函數的參數都是按值傳遞的。
一樣的,當函數參數是引用類型時,咱們一樣將參數複製了一個副本到局部變量,只不過複製的這個副本是指向堆內存中的地址而已,咱們在函數內部對對象的屬性進行操做,實際上和外部變量指向堆內存中的值相同,可是這並不表明着引用傳遞,下面咱們再按一個例子:
let obj = {}; function changeValue(obj){ obj.name = 'ConardLi'; obj = {name:'code祕密花園'}; } changeValue(obj); console.log(obj.name); // ConardLi
可見,函數參數傳遞的並非變量的引用
,而是變量拷貝的副本,當變量是原始類型時,這個副本就是值自己,當變量是引用類型時,這個副本是指向堆內存的地址。因此,再次記住:
ECMAScript
中全部的函數的參數都是按值傳遞的。
在原始類型中,有兩個類型Null
和Undefined
,他們都有且僅有一個值,null
和undefined
,而且他們都表明無和空,我通常這樣區分它們:
null
表示被賦值過的對象,刻意把一個對象賦值爲null
,故意表示其爲空,不該有值。
因此對象的某個屬性值爲null
是正常的,null
轉換爲數值時值爲0
。
undefined
表示「缺乏值」,即此處應有一個值,但尚未定義,
若是一個對象的某個屬性值爲undefined
,這是不正常的,如obj.name=undefined
,咱們不該該這樣寫,應該直接delete obj.name
。
undefined
轉爲數值時爲NaN
(非數字值的特殊值)
JavaScript
是一門動態類型語言,成員除了表示存在的空值外,還有可能根本就不存在(由於存不存在只在運行期才知道),這就是undefined
的意義所在。對於JAVA
這種強類型語言,若是有"undefined"
這種狀況,就會直接編譯失敗,因此在它不須要一個這樣的類型。
Symbol
類型是ES6
中新加入的一種原始類型。
每一個從Symbol()返回的symbol值都是惟一的。一個symbol值能做爲對象屬性的標識符;這是該數據類型僅有的目的。
下面來看看Symbol
類型具備哪些特性。
1.獨一無二
直接使用Symbol()
建立新的symbol
變量,可選用一個字符串用於描述。當參數爲對象時,將調用對象的toString()
方法。
var sym1 = Symbol(); // Symbol() var sym2 = Symbol('ConardLi'); // Symbol(ConardLi) var sym3 = Symbol('ConardLi'); // Symbol(ConardLi) var sym4 = Symbol({name:'ConardLi'}); // Symbol([object Object]) console.log(sym2 === sym3); // false
咱們用兩個相同的字符串建立兩個Symbol
變量,它們是不相等的,可見每一個Symbol
變量都是獨一無二的。
若是咱們想創造兩個相等的Symbol
變量,可使用Symbol.for(key)
。
使用給定的key搜索現有的symbol,若是找到則返回該symbol。不然將使用給定的key在全局symbol註冊表中建立一個新的symbol。
var sym1 = Symbol.for('ConardLi'); var sym2 = Symbol.for('ConardLi'); console.log(sym1 === sym2); // true
2.原始類型
注意是使用Symbol()
函數建立symbol
變量,並不是使用構造函數,使用new
操做符會直接報錯。
new Symbol(); // Uncaught TypeError: Symbol is not a constructor
咱們可使用typeof
運算符判斷一個Symbol
類型:
typeof Symbol() === 'symbol' typeof Symbol('ConardLi') === 'symbol'
3.不可枚舉
當使用Symbol
做爲對象屬性時,能夠保證對象不會出現重名屬性,調用for...in
不能將其枚舉出來,另外調用Object.getOwnPropertyNames、Object.keys()
也不能獲取Symbol
屬性。
能夠調用Object.getOwnPropertySymbols()用於專門獲取Symbol屬性。
var obj = { name:'ConardLi', [Symbol('name2')]:'code祕密花園' } Object.getOwnPropertyNames(obj); // ["name"] Object.keys(obj); // ["name"] for (var i in obj) { console.log(i); // name } Object.getOwnPropertySymbols(obj) // [Symbol(name)]
下面是幾個Symbol
在程序中的應用場景。
應用一:防止XSS
在React
的ReactElement
對象中,有一個$$typeof
屬性,它是一個Symbol
類型的變量:
var REACT_ELEMENT_TYPE = (typeof Symbol === 'function' && Symbol.for && Symbol.for('react.element')) || 0xeac7;
ReactElement.isValidElement
函數用來判斷一個React組件是不是有效的,下面是它的具體實現。
ReactElement.isValidElement = function (object) { return typeof object === 'object' && object !== null && object.$$typeof === REACT_ELEMENT_TYPE; };
可見React
渲染時會把沒有$$typeof
標識,以及規則校驗不經過的組件過濾掉。
若是你的服務器有一個漏洞,容許用戶存儲任意JSON
對象, 而客戶端代碼須要一個字符串,這可能會成爲一個問題:
// JSON let expectedTextButGotJSON = { type: 'div', props: { dangerouslySetInnerHTML: { __html: '/* put your exploit here */' }, }, }; let message = { text: expectedTextButGotJSON }; <p> {message.text} </p>
而JSON
中不能存儲Symbol
類型的變量,這就是防止XSS
的一種手段。
應用二:私有屬性
藉助Symbol
類型的不可枚舉,咱們能夠在類中模擬私有屬性,控制變量讀寫:
const privateField = Symbol(); class myClass { constructor(){ this[privateField] = 'ConardLi'; } getField(){ return this[privateField]; } setField(val){ this[privateField] = val; } }
應用三:防止屬性污染
在某些狀況下,咱們可能要爲對象添加一個屬性,此時就有可能形成屬性覆蓋,用Symbol
做爲對象屬性能夠保證永遠不會出現同名屬性。
例以下面的場景,咱們模擬實現一個call
方法:
Function.prototype.myCall = function (context) { if (typeof this !== 'function') { return undefined; // 用於防止 Function.prototype.myCall() 直接調用 } context = context || window; const fn = Symbol(); context[fn] = this; const args = [...arguments].slice(1); const result = context[fn](...args); delete context[fn]; return result; }
咱們須要在某個對象上臨時調用一個方法,又不能形成屬性污染,Symbol
是一個很好的選擇。
爲何說Number
類型不老實呢,相信你們都多多少少的在開發中遇到太小數計算不精確的問題,好比0.1+0.2!==0.3
,下面咱們來追本溯源,看看爲何會出現這種現象,以及該如何避免。
下面是我實現的一個簡單的函數,用於判斷兩個小數進行加法運算是否精確:
function judgeFloat(n, m) { const binaryN = n.toString(2); const binaryM = m.toString(2); console.log(`${n}的二進制是 ${binaryN}`); console.log(`${m}的二進制是 ${binaryM}`); const MN = m + n; const accuracyMN = (m * 100 + n * 100) / 100; const binaryMN = MN.toString(2); const accuracyBinaryMN = accuracyMN.toString(2); console.log(`${n}+${m}的二進制是${binaryMN}`); console.log(`${accuracyMN}的二進制是 ${accuracyBinaryMN}`); console.log(`${n}+${m}的二進制再轉成十進制是${to10(binaryMN)}`); console.log(`${accuracyMN}的二進制是再轉成十進制是${to10(accuracyBinaryMN)}`); console.log(`${n}+${m}在js中計算是${(to10(binaryMN) === to10(accuracyBinaryMN)) ? '' : '不'}準確的`); } function to10(n) { const pre = (n.split('.')[0] - 0).toString(2); const arr = n.split('.')[1].split(''); let i = 0; let result = 0; while (i < arr.length) { result += arr[i] * Math.pow(2, -(i + 1)); i++; } return result; } judgeFloat(0.1, 0.2); judgeFloat(0.6, 0.7);
計算機中全部的數據都是以二進制
存儲的,因此在計算時計算機要把數據先轉換成二進制
進行計算,而後在把計算結果轉換成十進制
。
由上面的代碼不難看出,在計算0.1+0.2
時,二進制
計算髮生了精度丟失,致使再轉換成十進制
後和預計的結果不符。
0.1
和0.2
的二進制都是以1100無限循環的小數,下面逐個來看JS幫咱們計算所得的結果:
0.1的二進制:
0.0001100110011001100110011001100110011001100110011001101
0.2的二進制:
0.001100110011001100110011001100110011001100110011001101
理論上講,由上面的結果相加應該::
0.0100110011001100110011001100110011001100110011001100111
實際JS計算獲得的0.1+0.2的二進制
0.0100110011001100110011001100110011001100110011001101
看到這裏你可能會產生更多的問題:
爲何 js計算出的 0.1的二進制 是這麼多位而不是更多位???爲何 js計算的(0.1+0.2)的二進制和咱們本身計算的(0.1+0.2)的二進制結果不同呢???
爲何 0.1的二進制 + 0.2的二進制 != 0.3的二進制???
小數的二進制
大多數都是無限循環的,JavaScript
是怎麼來存儲他們的呢?
在ECMAScript®語言規範中能夠看到,ECMAScript
中的Number
類型遵循IEEE 754
標準。使用64位固定長度來表示。
事實上有不少語言的數字類型都遵循這個標準,例如JAVA
,因此不少語言一樣有着上面一樣的問題。
因此下次遇到這種問題不要上來就噴JavaScript
...
有興趣能夠看看下這個網站http://0.30000000000000004.com/,是的,你沒看錯,就是http://0.30000000000000004.com/!!!
IEEE754
標準包含一組實數的二進制表示法。它有三部分組成:
三種精度的浮點數各個部分位數以下:
JavaScript
使用的是64位雙精度浮點數編碼,因此它的符號位
佔1
位,指數位佔11
位,尾數位佔52
位。
下面咱們在理解下什麼是符號位
、指數位
、尾數位
,以0.1
爲例:
它的二進制爲:0.0001100110011001100...
爲了節省存儲空間,在計算機中它是以科學計數法表示的,也就是
1.100110011001100...
X 2-4
若是這裏很差理解能夠想一下十進制的數:
1100
的科學計數法爲11
X 102
因此:
符號位
就是標識正負的,1
表示負
,0
表示正
;
指數位
存儲科學計數法的指數;
尾數位
存儲科學計數法後的有效數字;
因此咱們一般看到的二進制,實際上是計算機實際存儲的尾數位。
因爲尾數位只能存儲52
個數字,這就能解釋toString(2)
的執行結果了:
若是計算機沒有存儲空間的限制,那麼0.1
的二進制
應該是:
0.00011001100110011001100110011001100110011001100110011001...
科學計數法尾數位
1.1001100110011001100110011001100110011001100110011001...
可是因爲限制,有效數字第53
位及之後的數字是不能存儲的,它遵循,若是是1
就向前一位進1
,若是是0
就捨棄的原則。
0.1的二進制科學計數法第53位是1,因此就有了下面的結果:
0.0001100110011001100110011001100110011001100110011001101
0.2
有着一樣的問題,其實正是因爲這樣的存儲,在這裏有了精度丟失,致使了0.1+0.2!=0.3
。
事實上有着一樣精度問題的計算還有不少,咱們沒法把他們都記下來,因此當程序中有數字計算時,咱們最好用工具庫來幫助咱們解決,下面是兩個推薦使用的開源庫:
由與IEEE 754
雙精度64位規範的限制:
指數位
能表示的最大數字:1023
(十進制)
尾數位
能表達的最大數字即尾數位都位1
的狀況
因此JavaScript能表示的最大數字即位
1.111...
X 21023 這個結果轉換成十進制是1.7976931348623157e+308
,這個結果即爲Number.MAX_VALUE
。
JavaScript中Number.MAX_SAFE_INTEGER
表示最大安全數字,計算結果是9007199254740991
,即在這個數範圍內不會出現精度丟失(小數除外),這個數其實是1.111...
X 252。
咱們一樣能夠用一些開源庫來處理大整數:
其實官方也考慮到了這個問題,bigInt
類型在es10
中被提出,如今Chrome
中已經可使用,使用bigInt
能夠操做超過最大安全數字的數字。
在
ECMAScript
中,引用類型是一種數據結構,用於將數據和功能組織在一塊兒。
咱們一般所說的對象,就是某個特定引用類型的實例。
在ECMAScript
關於類型的定義中,只給出了Object
類型,實際上,咱們平時使用的不少引用類型的變量,並非由Object
構造的,可是它們原型鏈的終點都是Object
,這些類型都屬於引用類型。
Array
數組Date
日期RegExp
正則Function
函數爲了便於操做基本類型值,ECMAScript
還提供了幾個特殊的引用類型,他們是基本類型的包裝類型:
Boolean
Number
String
注意包裝類型和原始類型的區別:
true === new Boolean(true); // false 123 === new Number(123); // false 'ConardLi' === new String('ConardLi'); // false console.log(typeof new String('ConardLi')); // object console.log(typeof 'ConardLi'); // string
引用類型和包裝類型的主要區別就是對象的生存期,使用new操做符建立的引用類型的實例,在執行流離開當前做用域以前都一直保存在內存中,而自基本類型則只存在於一行代碼的執行瞬間,而後當即被銷燬,這意味着咱們不能在運行時爲基本類型添加屬性和方法。
var name = 'ConardLi' name.color = 'red'; console.log(name.color); // undefined
既然原始類型不能擴展屬性和方法,那麼咱們是如何使用原始類型調用方法的呢?
每當咱們操做一個基礎類型時,後臺就會自動建立一個包裝類型的對象,從而讓咱們可以調用一些方法和屬性,例以下面的代碼:
var name = "ConardLi"; var name2 = name.substring(2);
實際上發生瞭如下幾個過程:
String
的包裝類型實例substring
方法也就是說,咱們使用基本類型調用方法,就會自動進行裝箱和拆箱操做,相同的,咱們使用Number
和Boolean
類型時,也會發生這個過程。
從引用類型到基本類型的轉換,也就是拆箱的過程當中,會遵循ECMAScript規範
規定的toPrimitive
原則,通常會調用引用類型的valueOf
和toString
方法,你也能夠直接重寫toPeimitive
方法。通常轉換成不一樣類型的值遵循的原則不一樣,例如:
Number
類型,先調用valueOf
,再調用toString
String
類型,先調用toString
,再調用valueOf
若valueOf
和toString
都不存在,或者沒有返回基本類型,則拋出TypeError
異常。
const obj = { valueOf: () => { console.log('valueOf'); return 123; }, toString: () => { console.log('toString'); return 'ConardLi'; }, }; console.log(obj - 1); // valueOf 122 console.log(`${obj}ConardLi`); // toString ConardLiConardLi const obj2 = { [Symbol.toPrimitive]: () => { console.log('toPrimitive'); return 123; }, }; console.log(obj2 - 1); // valueOf 122 const obj3 = { valueOf: () => { console.log('valueOf'); return {}; }, toString: () => { console.log('toString'); return {}; }, }; console.log(obj3 - 1); // valueOf // toString // TypeError
除了程序中的自動拆箱和自動裝箱,咱們還能夠手動進行拆箱和裝箱操做。咱們能夠直接調用包裝類型的valueOf
或toString
,實現拆箱操做:
var name =new Number("123"); console.log( typeof name.valueOf() ); //number console.log( typeof name.toString() ); //string
由於JavaScript
是弱類型的語言,因此類型轉換髮生很是頻繁,上面咱們說的裝箱和拆箱其實就是一種類型轉換。
類型轉換分爲兩種,隱式轉換即程序自動進行的類型轉換,強制轉換即咱們手動進行的類型轉換。
強制轉換這裏就再也不多說起了,下面咱們來看看讓人頭疼的可能發生隱式類型轉換的幾個場景,以及如何轉換:
若是發生了隱式轉換,那麼各類類型互轉符合下面的規則:
在if
語句和邏輯語句中,若是隻有單個變量,會先將變量轉換爲Boolean
值,只有下面幾種狀況會轉換成false
,其他被轉換成true
:
null undefined '' NaN 0 false
咱們在對各類非Number
類型運用數學運算符(- * /
)時,會先將非Number
類型轉換爲Number
類型;
1 - true // 0 1 - null // 1 1 * undefined // NaN 1 - {} // 1 2 * ['5'] // 10
注意+
是個例外,執行+
操做符時:
String
類型,被識別爲字符串拼接,並會優先將另外一側轉換爲字符串類型。Number
類型,另外一側爲原始類型,則將原始類型轉換爲Number
類型。Number
類型,另外一側爲引用類型,將引用類型和Number
類型轉換成字符串後拼接。123 + '123' // 123123 (規則1) 123 + null // 123 (規則2) 123 + true // 124 (規則2) 123 + {} // 123[object Object] (規則3)
使用==
時,若兩側類型相同,則比較結果和===
相同,不然會發生隱式轉換,使用==
時發生的轉換能夠分爲幾種不一樣的狀況(只考慮兩側類型不一樣):
NaN
和其餘任何類型比較永遠返回false
(包括和他本身)。
NaN == NaN // false
Boolean
和其餘任何類型比較,Boolean
首先被轉換爲Number
類型。
true == 1 // true true == '2' // false true == ['1'] // true true == ['2'] // false
這裏注意一個可能會弄混的點:undefined、null
和Boolean
比較,雖然undefined、null
和false
都很容易被想象成假值,可是他們比較結果是false
,緣由是false
首先被轉換成0
:
undefined == false // false null == false // false
String
和Number
比較,先將String
轉換爲Number
類型。
123 == '123' // true '' == 0 // true
null == undefined
比較結果是true
,除此以外,null、undefined
和其餘任何結果的比較值都爲false
。
null == undefined // true null == '' // false null == 0 // false null == false // false undefined == '' // false undefined == 0 // false undefined == false // false
當原始類型和引用類型作比較時,對象類型會依照ToPrimitive
規則轉換爲原始類型:
'[object Object]' == {} // true '1,2,3' == [1, 2, 3] // true
來看看下面這個比較:
[] == ![] // true
!
的優先級高於==
,![]
首先會被轉換爲false
,而後根據上面第三點,false
轉換成Number
類型0
,左側[]
轉換爲0
,兩側比較相等。
[null] == false // true [undefined] == false // true
根據數組的ToPrimitive
規則,數組元素爲null
或undefined
時,該元素被當作空字符串處理,因此[null]、[undefined]
都會被轉換爲0
。
因此,說了這麼多,推薦使用===
來判斷兩個值是否相等...
一道經典的面試題,如何讓:a == 1 && a == 2 && a == 3
。
根據上面的拆箱轉換,以及==
的隱式轉換,咱們能夠輕鬆寫出答案:
const a = { value:[3,2,1], valueOf: function() {return this.value.pop(); }, }
適用場景
typeof
操做符能夠準確判斷一個變量是否爲下面幾個原始類型:
typeof 'ConardLi' // 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
除函數外全部的引用類型都會被斷定爲object
。
另外typeof null === 'object'
也會讓人感到頭痛,這是在JavaScript
第一版就流傳下來的bug
,後面因爲修改會形成大量的兼容問題就一直沒有被修復...
instanceof
操做符能夠幫助咱們判斷引用類型具體是什麼類型的對象:
[] instanceof Array // true new Date() instanceof Date // true new RegExp() instanceof RegExp // true
咱們先來回顧下原型鏈的幾條規則:
__proto__
(隱式原型)屬性,是一個普通對象prototype
(顯式原型)屬性,也是一個普通對象__proto__
值指向它構造函數的prototype
__proto__
中去找[] instanceof Array
其實是判斷Foo.prototype
是否在[]
的原型鏈上。
因此,使用instanceof
來檢測數據類型,不會很準確,這不是它設計的初衷:
[] instanceof Object // true function(){} instanceof Object // true
另外,使用instanceof
也不能檢測基本數據類型,因此instanceof
並非一個很好的選擇。
上面咱們在拆箱操做中提到了toString
函數,咱們能夠調用它實現從引用類型的轉換。
每個引用類型都有toString
方法,默認狀況下,toString()
方法被每一個Object
對象繼承。若是此方法在自定義對象中未被覆蓋,toString()
返回"[object type]"
,其中type
是對象的類型。
const obj = {}; obj.toString() // [object Object]
注意,上面提到了若是此方法在自定義對象中未被覆蓋
,toString
纔會達到預想的效果,事實上,大部分引用類型好比Array、Date、RegExp
等都重寫了toString
方法。
咱們能夠直接調用Object
原型上未被覆蓋的toString()
方法,使用call
來改變this
指向來達到咱們想要的效果。
咱們來看看jquery
源碼中如何進行類型判斷:
var class2type = {}; jQuery.each( "Boolean Number String Function Array Date RegExp Object Error Symbol".split( " " ), function( i, name ) { class2type[ "[object " + name + "]" ] = name.toLowerCase(); } ); type: function( obj ) { if ( obj == null ) { return obj + ""; } return typeof obj === "object" || typeof obj === "function" ? class2type[Object.prototype.toString.call(obj) ] || "object" : typeof obj; } isFunction: function( obj ) { return jQuery.type(obj) === "function"; }
原始類型直接使用typeof
,引用類型使用Object.prototype.toString.call
取得類型,藉助一個class2type
對象將字符串多餘的代碼過濾掉,例如[object function]
將獲得array
,而後在後面的類型判斷,如isFunction
直接可使用jQuery.type(obj) === "function"
這樣的判斷。
但願你閱讀本篇文章後能夠達到如下幾點:
JavaScript
中的變量在內存中的具體存儲形式,可對應實際場景JavaScript
數據類型的方式和底層原理文中若有錯誤,歡迎在評論區指正,若是這篇文章幫助到了你,歡迎點贊和關注。
想閱讀更多優質文章、可關注個人github
博客,你的star✨、點贊和關注是我持續創做的動力!
推薦關注個人微信公衆號【code祕密花園】,天天推送高質量文章,咱們一塊兒交流成長。
關注公衆號後回覆【加羣】拉你進入優質前端交流羣。