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