摘要:本文從底層原理到實(shí)際應(yīng)用詳細(xì)介紹了中的變量和類型相關(guān)知識。內(nèi)存空間又被分為兩種,棧內(nèi)存與堆內(nèi)存。一個值能作為對象屬性的標(biāo)識符這是該數(shù)據(jù)類型僅有的目的。
導(dǎo)讀
變量和類型是學(xué)習(xí)JavaScript最先接觸到的東西,但是往往看起來最簡單的東西往往還隱藏著很多你不了解、或者容易犯錯的知識,比如下面幾個問題:
JavaScript中的變量在內(nèi)存中的具體存儲形式是什么?
0.1+0.2為什么不等于0.3?發(fā)生小數(shù)計(jì)算錯誤的具體原因是什么?
Symbol的特點(diǎn),以及實(shí)際應(yīng)用場景是什么?
[] == ![]、[undefined] == false為什么等于true?代碼中何時會發(fā)生隱式類型轉(zhuǎn)換?轉(zhuǎn)換的規(guī)則是什么?
如何精確的判斷變量的類型?
如果你還不能很好的解答上面的問題,那說明你還沒有完全掌握這部分的知識,那么請好好閱讀下面的文章吧。
本文從底層原理到實(shí)際應(yīng)用詳細(xì)介紹了JavaScript中的變量和類型相關(guān)知識。
一、JavaScript數(shù)據(jù)類型ECMAScript標(biāo)準(zhǔn)規(guī)定了7種數(shù)據(jù)類型,其把這7種數(shù)據(jù)類型又分為兩種:原始類型和對象類型。
原始類型
Null:只包含一個值:null
Undefined:只包含一個值:undefined
Boolean:包含兩個值:true和false
Number:整數(shù)或浮點(diǎn)數(shù),還有一些特殊值(-Infinity、+Infinity、NaN)
String:一串表示文本值的字符序列
Symbol:一種實(shí)例是唯一且不可改變的數(shù)據(jù)類型
(在es10中加入了第七種原始類型BigInt,現(xiàn)已被最新Chrome支持)
對象類型
Object:自己分一類絲毫不過分,除了常用的Object,Array、Function等都屬于特殊的對象
二、為什么區(qū)分原始類型和對象類型 2.1 不可變性上面所提到的原始類型,在ECMAScript標(biāo)準(zhǔn)中,它們被定義為primitive values,即原始值,代表值本身是不可被改變的。
以字符串為例,我們在調(diào)用操作字符串的方法時,沒有任何方法是可以直接改變字符串的:
var str = "ConardLi"; str.slice(1); str.substr(1); str.trim(1); str.toLowerCase(1); str[0] = 1; console.log(str); // ConardLi
在上面的代碼中我們對str調(diào)用了幾個方法,無一例外,這些方法都在原字符串的基礎(chǔ)上產(chǎn)生了一個新字符串,而非直接去改變str,這就印證了字符串的不可變性。
那么,當(dāng)我們繼續(xù)調(diào)用下面的代碼:
str += "6" console.log(str); // ConardLi6
你會發(fā)現(xiàn),str的值被改變了,這不就打臉了字符串的不可變性么?其實(shí)不然,我們從內(nèi)存上來理解:
在JavaScript中,每一個變量在內(nèi)存中都需要一個空間來存儲。
內(nèi)存空間又被分為兩種,棧內(nèi)存與堆內(nèi)存。
棧內(nèi)存:
存儲的值大小固定
空間較小
可以直接操作其保存的變量,運(yùn)行效率高
由系統(tǒng)自動分配存儲空間
JavaScript中的原始類型的值被直接存儲在棧中,在變量定義時,棧就為其分配好了內(nèi)存空間。
由于棧中的內(nèi)存空間的大小是固定的,那么注定了存儲在棧中的變量就是不可變的。
在上面的代碼中,我們執(zhí)行了str += "6"的操作,實(shí)際上是在棧中又開辟了一塊內(nèi)存空間用于存儲"ConardLi6",然后將變量str指向這塊空間,所以這并不違背不可變性的特點(diǎn)。
2.2 引用類型堆內(nèi)存:
存儲的值大小不定,可動態(tài)調(diào)整
空間較大,運(yùn)行效率低
無法直接操作其內(nèi)部存儲,使用引用地址讀取
通過代碼進(jìn)行分配空間
相對于上面具有不可變性的原始類型,我習(xí)慣把對象稱為引用類型,引用類型的值實(shí)際存儲在堆內(nèi)存中,它在棧中只存儲了一個固定長度的地址,這個地址指向堆內(nèi)存中的值。
var obj1 = {name:"ConardLi"} var obj2 = {age:18} var obj3 = function(){...} var obj4 = [1,2,3,4,5,6,7,8,9]
由于內(nèi)存是有限的,這些變量不可能一直在內(nèi)存中占用資源,這里推薦下這篇文章JavaScript中的垃圾回收和內(nèi)存泄漏,這里告訴你JavaScript是如何進(jìn)行垃圾回收以及可能會發(fā)生內(nèi)存泄漏的一些場景。
當(dāng)然,引用類型就不再具有不可變性了,我們可以輕易的改變它們:
obj1.name = "ConardLi6"; obj2.age = 19; obj4.length = 0; console.log(obj1); //{name:"ConardLi6"} console.log(obj2); // {age:19} console.log(obj4); // []
以數(shù)組為例,它的很多方法都可以改變它自身。
pop() 刪除數(shù)組最后一個元素,如果數(shù)組為空,則不改變數(shù)組,返回undefined,改變原數(shù)組,返回被刪除的元素
push()向數(shù)組末尾添加一個或多個元素,改變原數(shù)組,返回新數(shù)組的長度
shift()把數(shù)組的第一個元素刪除,若空數(shù)組,不進(jìn)行任何操作,返回undefined,改變原數(shù)組,返回第一個元素的值
unshift()向數(shù)組的開頭添加一個或多個元素,改變原數(shù)組,返回新數(shù)組的長度
reverse()顛倒數(shù)組中元素的順序,改變原數(shù)組,返回該數(shù)組
sort()對數(shù)組元素進(jìn)行排序,改變原數(shù)組,返回該數(shù)組
splice()從數(shù)組中添加/刪除項(xiàng)目,改變原數(shù)組,返回被刪除的元素
下面我們通過幾個操作來對比一下原始類型和引用類型的區(qū)別:
2.3 復(fù)制當(dāng)我們把一個變量的值復(fù)制到另一個變量上時,原始類型和引用類型的表現(xiàn)是不一樣的,先來看看原始類型:
var name = "ConardLi"; var name2 = name; name2 = "code秘密花園"; console.log(name); // ConardLi;
內(nèi)存中有一個變量name,值為ConardLi。我們從變量name復(fù)制出一個變量name2,此時在內(nèi)存中創(chuàng)建了一個塊新的空間用于存儲ConardLi,雖然兩者值是相同的,但是兩者指向的內(nèi)存空間完全不同,這兩個變量參與任何操作都互不影響。
復(fù)制一個引用類型:
var obj = {name:"ConardLi"}; var obj2 = obj; obj2.name = "code秘密花園"; console.log(obj.name); // code秘密花園
當(dāng)我們復(fù)制引用類型的變量時,實(shí)際上復(fù)制的是棧中存儲的地址,所以復(fù)制出來的obj2實(shí)際上和obj指向的堆中同一個對象。因此,我們改變其中任何一個變量的值,另一個變量都會受到影響,這就是為什么會有深拷貝和淺拷貝的原因。
2.4 比較當(dāng)我們在對兩個變量進(jìn)行比較時,不同類型的變量的表現(xiàn)是不同的:
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。
2.5 值傳遞和引用傳遞借助下面的例子,我們先來看一看什么是值傳遞,什么是引用傳遞:
let name = "ConardLi"; function changeValue(name){ name = "code秘密花園"; } changeValue(name); console.log(name);
執(zhí)行上面的代碼,如果最終打印出來的name是"ConardLi",沒有改變,說明函數(shù)參數(shù)傳遞的是變量的值,即值傳遞。如果最終打印的是"code秘密花園",函數(shù)內(nèi)部的操作可以改變傳入的變量,那么說明函數(shù)參數(shù)傳遞的是引用,即引用傳遞。
很明顯,上面的執(zhí)行結(jié)果是"ConardLi",即函數(shù)參數(shù)僅僅是被傳入變量復(fù)制給了的一個局部變量,改變這個局部變量不會對外部變量產(chǎn)生影響。
let obj = {name:"ConardLi"}; function changeValue(obj){ obj.name = "code秘密花園"; } changeValue(obj); console.log(obj.name); // code秘密花園
上面的代碼可能讓你產(chǎn)生疑惑,是不是參數(shù)是引用類型就是引用傳遞呢?
首先明確一點(diǎn),ECMAScript中所有的函數(shù)的參數(shù)都是按值傳遞的。
同樣的,當(dāng)函數(shù)參數(shù)是引用類型時,我們同樣將參數(shù)復(fù)制了一個副本到局部變量,只不過復(fù)制的這個副本是指向堆內(nèi)存中的地址而已,我們在函數(shù)內(nèi)部對對象的屬性進(jìn)行操作,實(shí)際上和外部變量指向堆內(nèi)存中的值相同,但是這并不代表著引用傳遞,下面我們再按一個例子:
let obj = {}; function changeValue(obj){ obj.name = "ConardLi"; obj = {name:"code秘密花園"}; } changeValue(obj); console.log(obj.name); // ConardLi
可見,函數(shù)參數(shù)傳遞的并不是變量的引用,而是變量拷貝的副本,當(dāng)變量是原始類型時,這個副本就是值本身,當(dāng)變量是引用類型時,這個副本是指向堆內(nèi)存的地址。所以,再次記住:
ECMAScript中所有的函數(shù)的參數(shù)都是按值傳遞的。三、分不清的null和undefined
在原始類型中,有兩個類型Null和Undefined,他們都有且僅有一個值,null和undefined,并且他們都代表無和空,我一般這樣區(qū)分它們:
null
表示被賦值過的對象,刻意把一個對象賦值為null,故意表示其為空,不應(yīng)有值。
所以對象的某個屬性值為null是正常的,null轉(zhuǎn)換為數(shù)值時值為0。
undefined
表示“缺少值”,即此處應(yīng)有一個值,但還沒有定義,
如果一個對象的某個屬性值為undefined,這是不正常的,如obj.name=undefined,我們不應(yīng)該這樣寫,應(yīng)該直接delete obj.name。
undefined轉(zhuǎn)為數(shù)值時為NaN(非數(shù)字值的特殊值)
JavaScript是一門動態(tài)類型語言,成員除了表示存在的空值外,還有可能根本就不存在(因?yàn)榇娌淮嬖谥辉谶\(yùn)行期才知道),這就是undefined的意義所在。對于JAVA這種強(qiáng)類型語言,如果有"undefined"這種情況,就會直接編譯失敗,所以在它不需要一個這樣的類型。
四、不太熟的Symbol類型Symbol類型是ES6中新加入的一種原始類型。
每個從Symbol()返回的symbol值都是唯一的。一個symbol值能作為對象屬性的標(biāo)識符;這是該數(shù)據(jù)類型僅有的目的。
下面來看看Symbol類型具有哪些特性。
4.1 Symbol的特性1.獨(dú)一無二
直接使用Symbol()創(chuàng)建新的symbol變量,可選用一個字符串用于描述。當(dāng)參數(shù)為對象時,將調(diào)用對象的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
我們用兩個相同的字符串創(chuàng)建兩個Symbol變量,它們是不相等的,可見每個Symbol變量都是獨(dú)一無二的。
如果我們想創(chuàng)造兩個相等的Symbol變量,可以使用Symbol.for(key)。
使用給定的key搜索現(xiàn)有的symbol,如果找到則返回該symbol。否則將使用給定的key在全局symbol注冊表中創(chuàng)建一個新的symbol。
var sym1 = Symbol.for("ConardLi"); var sym2 = Symbol.for("ConardLi"); console.log(sym1 === sym2); // true
2.原始類型
注意是使用Symbol()函數(shù)創(chuàng)建symbol變量,并非使用構(gòu)造函數(shù),使用new操作符會直接報(bào)錯。
new Symbol(); // Uncaught TypeError: Symbol is not a constructor
我們可以使用typeof運(yùn)算符判斷一個Symbol類型:
typeof Symbol() === "symbol" typeof Symbol("ConardLi") === "symbol"
3.不可枚舉
當(dāng)使用Symbol作為對象屬性時,可以保證對象不會出現(xiàn)重名屬性,調(diào)用for...in不能將其枚舉出來,另外調(diào)用Object.getOwnPropertyNames、Object.keys()也不能獲取Symbol屬性。
可以調(diào)用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)]4.2 Symbol的應(yīng)用場景
下面是幾個Symbol在程序中的應(yīng)用場景。
應(yīng)用一:防止XSS
在React的ReactElement對象中,有一個$$typeof屬性,它是一個Symbol類型的變量:
var REACT_ELEMENT_TYPE = (typeof Symbol === "function" && Symbol.for && Symbol.for("react.element")) || 0xeac7;
ReactElement.isValidElement函數(shù)用來判斷一個React組件是否是有效的,下面是它的具體實(shí)現(xiàn)。
ReactElement.isValidElement = function (object) { return typeof object === "object" && object !== null && object.$$typeof === REACT_ELEMENT_TYPE; };
可見React渲染時會把沒有$$typeof標(biāo)識,以及規(guī)則校驗(yàn)不通過的組件過濾掉。
如果你的服務(wù)器有一個漏洞,允許用戶存儲任意JSON對象, 而客戶端代碼需要一個字符串,這可能會成為一個問題:
// JSON let expectedTextButGotJSON = { type: "div", props: { dangerouslySetInnerHTML: { __html: "/* put your exploit here */" }, }, }; let message = { text: expectedTextButGotJSON };{message.text}
而JSON中不能存儲Symbol類型的變量,這就是防止XSS的一種手段。
應(yīng)用二:私有屬性
借助Symbol類型的不可枚舉,我們可以在類中模擬私有屬性,控制變量讀寫:
const privateField = Symbol(); class myClass { constructor(){ this[privateField] = "ConardLi"; } getField(){ return this[privateField]; } setField(val){ this[privateField] = val; } }
應(yīng)用三:防止屬性污染
在某些情況下,我們可能要為對象添加一個屬性,此時就有可能造成屬性覆蓋,用Symbol作為對象屬性可以保證永遠(yuǎn)不會出現(xiàn)同名屬性。
例如下面的場景,我們模擬實(shí)現(xiàn)一個call方法:
Function.prototype.myCall = function (context) { if (typeof this !== "function") { return undefined; // 用于防止 Function.prototype.myCall() 直接調(diào)用 } context = context || window; const fn = Symbol(); context[fn] = this; const args = [...arguments].slice(1); const result = context[fn](...args); delete context[fn]; return result; }
我們需要在某個對象上臨時調(diào)用一個方法,又不能造成屬性污染,Symbol是一個很好的選擇。
五、不老實(shí)的Number類型為什么說Number類型不老實(shí)呢,相信大家都多多少少的在開發(fā)中遇到過小數(shù)計(jì)算不精確的問題,比如0.1+0.2!==0.3,下面我們來追本溯源,看看為什么會出現(xiàn)這種現(xiàn)象,以及該如何避免。
下面是我實(shí)現(xiàn)的一個簡單的函數(shù),用于判斷兩個小數(shù)進(jìn)行加法運(yùn)算是否精確:
function judgeFloat(n, m) { const binaryN = n.toString(2); const binaryM = m.toString(2); console.log(`${n}的二進(jìn)制是 ${binaryN}`); console.log(`${m}的二進(jìn)制是 ${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}的二進(jìn)制是${binaryMN}`); console.log(`${accuracyMN}的二進(jìn)制是 ${accuracyBinaryMN}`); console.log(`${n}+${m}的二進(jìn)制再轉(zhuǎn)成十進(jìn)制是${to10(binaryMN)}`); console.log(`${accuracyMN}的二進(jìn)制是再轉(zhuǎn)成十進(jìn)制是${to10(accuracyBinaryMN)}`); console.log(`${n}+${m}在js中計(jì)算是${(to10(binaryMN) === to10(accuracyBinaryMN)) ? "" : "不"}準(zhǔn)確的`); } 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);5.1 精度丟失
計(jì)算機(jī)中所有的數(shù)據(jù)都是以二進(jìn)制存儲的,所以在計(jì)算時計(jì)算機(jī)要把數(shù)據(jù)先轉(zhuǎn)換成二進(jìn)制進(jìn)行計(jì)算,然后在把計(jì)算結(jié)果轉(zhuǎn)換成十進(jìn)制。
由上面的代碼不難看出,在計(jì)算0.1+0.2時,二進(jìn)制計(jì)算發(fā)生了精度丟失,導(dǎo)致再轉(zhuǎn)換成十進(jìn)制后和預(yù)計(jì)的結(jié)果不符。
5.2 對結(jié)果的分析—更多的問題0.1和0.2的二進(jìn)制都是以1100無限循環(huán)的小數(shù),下面逐個來看JS幫我們計(jì)算所得的結(jié)果:
0.1的二進(jìn)制:
0.0001100110011001100110011001100110011001100110011001101
0.2的二進(jìn)制:
0.001100110011001100110011001100110011001100110011001101
理論上講,由上面的結(jié)果相加應(yīng)該::
0.0100110011001100110011001100110011001100110011001100111
實(shí)際JS計(jì)算得到的0.1+0.2的二進(jìn)制
0.0100110011001100110011001100110011001100110011001101
看到這里你可能會產(chǎn)生更多的問題:
為什么 js計(jì)算出的 0.1的二進(jìn)制 是這么多位而不是更多位???5.3 js對二進(jìn)制小數(shù)的存儲方式為什么 js計(jì)算的(0.1+0.2)的二進(jìn)制和我們自己計(jì)算的(0.1+0.2)的二進(jìn)制結(jié)果不一樣呢???
為什么 0.1的二進(jìn)制 + 0.2的二進(jìn)制 != 0.3的二進(jìn)制???
小數(shù)的二進(jìn)制大多數(shù)都是無限循環(huán)的,JavaScript是怎么來存儲他們的呢?
在ECMAScript?語言規(guī)范中可以看到,ECMAScript中的Number類型遵循IEEE 754標(biāo)準(zhǔn)。使用64位固定長度來表示。
事實(shí)上有很多語言的數(shù)字類型都遵循這個標(biāo)準(zhǔn),例如JAVA,所以很多語言同樣有著上面同樣的問題。
所以下次遇到這種問題不要上來就噴JavaScript...
有興趣可以看看下這個網(wǎng)站http://0.30000000000000004.com/,是的,你沒看錯,就是http://0.30000000000000004.com/!!!
5.4 IEEE 754IEEE754標(biāo)準(zhǔn)包含一組實(shí)數(shù)的二進(jìn)制表示法。它有三部分組成:
符號位
指數(shù)位
尾數(shù)位
三種精度的浮點(diǎn)數(shù)各個部分位數(shù)如下:
JavaScript使用的是64位雙精度浮點(diǎn)數(shù)編碼,所以它的符號位占1位,指數(shù)位占11位,尾數(shù)位占52位。
下面我們在理解下什么是符號位、指數(shù)位、尾數(shù)位,以0.1為例:
它的二進(jìn)制為:0.0001100110011001100...
為了節(jié)省存儲空間,在計(jì)算機(jī)中它是以科學(xué)計(jì)數(shù)法表示的,也就是
1.100110011001100... X 2-4
如果這里不好理解可以想一下十進(jìn)制的數(shù):
1100的科學(xué)計(jì)數(shù)法為11 X 102
所以:
符號位就是標(biāo)識正負(fù)的,1表示負(fù),0表示正;
指數(shù)位存儲科學(xué)計(jì)數(shù)法的指數(shù);
尾數(shù)位存儲科學(xué)計(jì)數(shù)法后的有效數(shù)字;
所以我們通常看到的二進(jìn)制,其實(shí)是計(jì)算機(jī)實(shí)際存儲的尾數(shù)位。
5.5 js中的toString(2)由于尾數(shù)位只能存儲52個數(shù)字,這就能解釋toString(2)的執(zhí)行結(jié)果了:
如果計(jì)算機(jī)沒有存儲空間的限制,那么0.1的二進(jìn)制應(yīng)該是:
0.00011001100110011001100110011001100110011001100110011001...
科學(xué)計(jì)數(shù)法尾數(shù)位
1.1001100110011001100110011001100110011001100110011001...
但是由于限制,有效數(shù)字第53位及以后的數(shù)字是不能存儲的,它遵循,如果是1就向前一位進(jìn)1,如果是0就舍棄的原則。
0.1的二進(jìn)制科學(xué)計(jì)數(shù)法第53位是1,所以就有了下面的結(jié)果:
0.0001100110011001100110011001100110011001100110011001101
0.2有著同樣的問題,其實(shí)正是由于這樣的存儲,在這里有了精度丟失,導(dǎo)致了0.1+0.2!=0.3。
事實(shí)上有著同樣精度問題的計(jì)算還有很多,我們無法把他們都記下來,所以當(dāng)程序中有數(shù)字計(jì)算時,我們最好用工具庫來幫助我們解決,下面是兩個推薦使用的開源庫:
number-precision
mathjs/
5.6 JavaScript能表示的最大數(shù)字由與IEEE 754雙精度64位規(guī)范的限制:
指數(shù)位能表示的最大數(shù)字:1023(十進(jìn)制)
尾數(shù)位能表達(dá)的最大數(shù)字即尾數(shù)位都位1的情況
所以JavaScript能表示的最大數(shù)字即位
1.111...X 21023 這個結(jié)果轉(zhuǎn)換成十進(jìn)制是1.7976931348623157e+308,這個結(jié)果即為Number.MAX_VALUE。
5.7 最大安全數(shù)字JavaScript中Number.MAX_SAFE_INTEGER表示最大安全數(shù)字,計(jì)算結(jié)果是9007199254740991,即在這個數(shù)范圍內(nèi)不會出現(xiàn)精度丟失(小數(shù)除外),這個數(shù)實(shí)際上是1.111...X 252。
我們同樣可以用一些開源庫來處理大整數(shù):
node-bignum
node-bigint
其實(shí)官方也考慮到了這個問題,bigInt類型在es10中被提出,現(xiàn)在Chrome中已經(jīng)可以使用,使用bigInt可以操作超過最大安全數(shù)字的數(shù)字。
六、還有哪些引用類型在ECMAScript中,引用類型是一種數(shù)據(jù)結(jié)構(gòu),用于將數(shù)據(jù)和功能組織在一起。
我們通常所說的對象,就是某個特定引用類型的實(shí)例。
在ECMAScript關(guān)于類型的定義中,只給出了Object類型,實(shí)際上,我們平時使用的很多引用類型的變量,并不是由Object構(gòu)造的,但是它們原型鏈的終點(diǎn)都是Object,這些類型都屬于引用類型。
Array 數(shù)組
Date 日期
RegExp 正則
Function 函數(shù)
6.1 包裝類型為了便于操作基本類型值,ECMAScript還提供了幾個特殊的引用類型,他們是基本類型的包裝類型:
Boolean
Number
String
注意包裝類型和原始類型的區(qū)別:
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
引用類型和包裝類型的主要區(qū)別就是對象的生存期,使用new操作符創(chuàng)建的引用類型的實(shí)例,在執(zhí)行流離開當(dāng)前作用域之前都一直保存在內(nèi)存中,而自基本類型則只存在于一行代碼的執(zhí)行瞬間,然后立即被銷毀,這意味著我們不能在運(yùn)行時為基本類型添加屬性和方法。
var name = "ConardLi" name.color = "red"; console.log(name.color); // undefined6.2 裝箱和拆箱
裝箱轉(zhuǎn)換:把基本類型轉(zhuǎn)換為對應(yīng)的包裝類型
拆箱操作:把引用類型轉(zhuǎn)換為基本類型
既然原始類型不能擴(kuò)展屬性和方法,那么我們是如何使用原始類型調(diào)用方法的呢?
每當(dāng)我們操作一個基礎(chǔ)類型時,后臺就會自動創(chuàng)建一個包裝類型的對象,從而讓我們能夠調(diào)用一些方法和屬性,例如下面的代碼:
var name = "ConardLi"; var name2 = name.substring(2);
實(shí)際上發(fā)生了以下幾個過程:
創(chuàng)建一個String的包裝類型實(shí)例
在實(shí)例上調(diào)用substring方法
銷毀實(shí)例
也就是說,我們使用基本類型調(diào)用方法,就會自動進(jìn)行裝箱和拆箱操作,相同的,我們使用Number和Boolean類型時,也會發(fā)生這個過程。
從引用類型到基本類型的轉(zhuǎn)換,也就是拆箱的過程中,會遵循ECMAScript規(guī)范規(guī)定的toPrimitive原則,一般會調(diào)用引用類型的valueOf和toString方法,你也可以直接重寫toPeimitive方法。一般轉(zhuǎn)換成不同類型的值遵循的原則不同,例如:
引用類型轉(zhuǎn)換為Number類型,先調(diào)用valueOf,再調(diào)用toString
引用類型轉(zhuǎn)換為String類型,先調(diào)用toString,再調(diào)用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
除了程序中的自動拆箱和自動裝箱,我們還可以手動進(jìn)行拆箱和裝箱操作。我們可以直接調(diào)用包裝類型的valueOf或toString,實(shí)現(xiàn)拆箱操作:
var name =new Number("123"); console.log( typeof name.valueOf() ); //number console.log( typeof name.toString() ); //string七、類型轉(zhuǎn)換
因?yàn)?b>JavaScript是弱類型的語言,所以類型轉(zhuǎn)換發(fā)生非常頻繁,上面我們說的裝箱和拆箱其實(shí)就是一種類型轉(zhuǎn)換。
類型轉(zhuǎn)換分為兩種,隱式轉(zhuǎn)換即程序自動進(jìn)行的類型轉(zhuǎn)換,強(qiáng)制轉(zhuǎn)換即我們手動進(jìn)行的類型轉(zhuǎn)換。
強(qiáng)制轉(zhuǎn)換這里就不再多提及了,下面我們來看看讓人頭疼的可能發(fā)生隱式類型轉(zhuǎn)換的幾個場景,以及如何轉(zhuǎn)換:
7.1 類型轉(zhuǎn)換規(guī)則如果發(fā)生了隱式轉(zhuǎn)換,那么各種類型互轉(zhuǎn)符合下面的規(guī)則:
7.2 if語句和邏輯語句在if語句和邏輯語句中,如果只有單個變量,會先將變量轉(zhuǎn)換為Boolean值,只有下面幾種情況會轉(zhuǎn)換成false,其余被轉(zhuǎn)換成true:
null undefined "" NaN 0 false7.3 各種運(yùn)數(shù)學(xué)算符
我們在對各種非Number類型運(yùn)用數(shù)學(xué)運(yùn)算符(- * /)時,會先將非Number類型轉(zhuǎn)換為Number類型;
1 - true // 0 1 - null // 1 1 * undefined // NaN 1 - {} // 1 2 * ["5"] // 10
注意+是個例外,執(zhí)行+操作符時:
1.當(dāng)一側(cè)為String類型,被識別為字符串拼接,并會優(yōu)先將另一側(cè)轉(zhuǎn)換為字符串類型。
2.當(dāng)一側(cè)為Number類型,另一側(cè)為原始類型,則將原始類型轉(zhuǎn)換為Number類型。
3.當(dāng)一側(cè)為Number類型,另一側(cè)為引用類型,將引用類型和Number類型轉(zhuǎn)換成字符串后拼接。
123 + "123" // 123123 (規(guī)則1) 123 + null // 123 (規(guī)則2) 123 + true // 124 (規(guī)則2) 123 + {} // 123[object Object] (規(guī)則3)7.4 ==
使用==時,若兩側(cè)類型相同,則比較結(jié)果和===相同,否則會發(fā)生隱式轉(zhuǎn)換,使用==時發(fā)生的轉(zhuǎn)換可以分為幾種不同的情況(只考慮兩側(cè)類型不同):
1.NaN
NaN和其他任何類型比較永遠(yuǎn)返回false(包括和他自己)。
NaN == NaN // false
2.Boolean
Boolean和其他任何類型比較,Boolean首先被轉(zhuǎn)換為Number類型。
true == 1 // true true == "2" // false true == ["1"] // true true == ["2"] // false
這里注意一個可能會弄混的點(diǎn):undefined、null和Boolean比較,雖然undefined、null和false都很容易被想象成假值,但是他們比較結(jié)果是false,原因是false首先被轉(zhuǎn)換成0:
undefined == false // false null == false // false
3.String和Number
String和Number比較,先將String轉(zhuǎn)換為Number類型。
123 == "123" // true "" == 0 // true
4.null和undefined
null == undefined比較結(jié)果是true,除此之外,null、undefined和其他任何結(jié)果的比較值都為false。
null == undefined // true null == "" // false null == 0 // false null == false // false undefined == "" // false undefined == 0 // false undefined == false // false
5.原始類型和引用類型
當(dāng)原始類型和引用類型做比較時,對象類型會依照ToPrimitive規(guī)則轉(zhuǎn)換為原始類型:
"[object Object]" == {} // true "1,2,3" == [1, 2, 3] // true
來看看下面這個比較:
[] == ![] // true
!的優(yōu)先級高于==,![]首先會被轉(zhuǎn)換為false,然后根據(jù)上面第三點(diǎn),false轉(zhuǎn)換成Number類型0,左側(cè)[]轉(zhuǎn)換為0,兩側(cè)比較相等。
[null] == false // true [undefined] == false // true
根據(jù)數(shù)組的ToPrimitive規(guī)則,數(shù)組元素為null或undefined時,該元素被當(dāng)做空字符串處理,所以[null]、[undefined]都會被轉(zhuǎn)換為0。
所以,說了這么多,推薦使用===來判斷兩個值是否相等...
7.5 一道有意思的面試題一道經(jīng)典的面試題,如何讓:a == 1 && a == 2 && a == 3。
根據(jù)上面的拆箱轉(zhuǎn)換,以及==的隱式轉(zhuǎn)換,我們可以輕松寫出答案:
const a = { value:[3,2,1], valueOf: function() {return this.value.pop(); }, }八、判斷JavaScript數(shù)據(jù)類型的方式 8.1 typeof
適用場景
typeof操作符可以準(zhǔn)確判斷一個變量是否為下面幾個原始類型:
typeof "ConardLi" // string typeof 123 // number typeof true // boolean typeof Symbol() // symbol typeof undefined // undefined
你還可以用它來判斷函數(shù)類型:
typeof function(){} // function
不適用場景
當(dāng)你用typeof來判斷引用類型時似乎顯得有些乏力了:
typeof [] // object typeof {} // object typeof new Date() // object typeof /^d*$/; // object
除函數(shù)外所有的引用類型都會被判定為object。
另外typeof null === "object"也會讓人感到頭痛,這是在JavaScript初版就流傳下來的bug,后面由于修改會造成大量的兼容問題就一直沒有被修復(fù)...
8.2 instanceofinstanceof操作符可以幫助我們判斷引用類型具體是什么類型的對象:
[] instanceof Array // true new Date() instanceof Date // true new RegExp() instanceof RegExp // true
我們先來回顧下原型鏈的幾條規(guī)則:
1.所有引用類型都具有對象特性,即可以自由擴(kuò)展屬性
2.所有引用類型都具有一個__proto__(隱式原型)屬性,是一個普通對象
3.所有的函數(shù)都具有prototype(顯式原型)屬性,也是一個普通對象
4.所有引用類型__proto__值指向它構(gòu)造函數(shù)的prototype
5.當(dāng)試圖得到一個對象的屬性時,如果變量本身沒有這個屬性,則會去他的__proto__中去找
[] instanceof Array 實(shí)際上是判斷Foo.prototype是否在[]的原型鏈上。
所以,使用instanceof來檢測數(shù)據(jù)類型,不會很準(zhǔn)確,這不是它設(shè)計(jì)的初衷:
[] instanceof Object // true function(){} instanceof Object // true
另外,使用instanceof也不能檢測基本數(shù)據(jù)類型,所以instanceof并不是一個很好的選擇。
8.3 toString上面我們在拆箱操作中提到了toString函數(shù),我們可以調(diào)用它實(shí)現(xiàn)從引用類型的轉(zhuǎn)換。
每一個引用類型都有toString方法,默認(rèn)情況下,toString()方法被每個Object對象繼承。如果此方法在自定義對象中未被覆蓋,toString() 返回 "[object type]",其中type是對象的類型。
const obj = {}; obj.toString() // [object Object]
注意,上面提到了如果此方法在自定義對象中未被覆蓋,toString才會達(dá)到預(yù)想的效果,事實(shí)上,大部分引用類型比如Array、Date、RegExp等都重寫了toString方法。
我們可以直接調(diào)用Object原型上未被覆蓋的toString()方法,使用call來改變this指向來達(dá)到我們想要的效果。
8.4 jquery我們來看看jquery源碼中如何進(jìn)行類型判斷:
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"這樣的判斷。
參考http://www.ecma-international...
https://while.dev/articles/ex...
https://github.com/mqyqingfen...
https://juejin.im/post/5bc5c7...
https://juejin.im/post/5bbda2...
《JS高級程序設(shè)計(jì)》
小結(jié)希望你閱讀本篇文章后可以達(dá)到以下幾點(diǎn):
了解JavaScript中的變量在內(nèi)存中的具體存儲形式,可對應(yīng)實(shí)際場景
搞懂小數(shù)計(jì)算不精確的底層原因
了解可能發(fā)生隱式類型轉(zhuǎn)換的場景以及轉(zhuǎn)換原則
掌握判斷JavaScript數(shù)據(jù)類型的方式和底層原理
文中如有錯誤,歡迎在評論區(qū)指正,如果這篇文章幫助到了你,歡迎點(diǎn)贊和關(guān)注。
想閱讀更多優(yōu)質(zhì)文章、可關(guān)注我的github博客,你的star?、點(diǎn)贊和關(guān)注是我持續(xù)創(chuàng)作的動力!
推薦關(guān)注我的微信公眾號【code秘密花園】,每天推送高質(zhì)量文章,我們一起交流成長。
關(guān)注公眾號后回復(fù)【加群】拉你進(jìn)入優(yōu)質(zhì)前端交流群。
文章版權(quán)歸作者所有,未經(jīng)允許請勿轉(zhuǎn)載,若此文章存在違規(guī)行為,您可以聯(lián)系管理員刪除。
轉(zhuǎn)載請注明本文地址:http://specialneedsforspecialkids.com/yun/109908.html
摘要:接著下一個例子賦予副本新的地址可見,函數(shù)參數(shù)傳遞的并不是變量的引用,而是變量拷貝的副本,當(dāng)變量是原始類型時,這個副本就是值本身,當(dāng)變量是引用類型是,這個副本是指向堆內(nèi)存的地址。 轉(zhuǎn)載自ConardLi: 《【JS進(jìn)階】 你真的掌握變量和類型了嗎》 公眾號: code秘密花園 1. JavaScript數(shù)據(jù)類型 ECMAScript標(biāo)準(zhǔn)規(guī)定了7種數(shù)據(jù)類型,這些數(shù)據(jù)類型分為原始類型和對象...
摘要:跨域請求詳解從繁至簡前端掘金什么是為什么要用是的一種使用模式,可用于解決主流瀏覽器的跨域數(shù)據(jù)訪問的問題。異步編程入門道典型的面試題前端掘金在界中,開發(fā)人員的需求量一直居高不下。 jsonp 跨域請求詳解——從繁至簡 - 前端 - 掘金什么是jsonp?為什么要用jsonp?JSONP(JSON with Padding)是JSON的一種使用模式,可用于解決主流瀏覽器的跨域數(shù)據(jù)訪問的問題...
摘要:瀏覽器的主要組成包括有調(diào)用堆棧,事件循環(huán),任務(wù)隊(duì)列和。好了,現(xiàn)在有了前面這些知識,我們可以看一下這道題的講解過程實(shí)現(xiàn)步驟調(diào)用會將函數(shù)放入調(diào)用堆棧。由于調(diào)用堆棧是空的,事件循環(huán)將選擇回調(diào)并將其推入調(diào)用堆棧進(jìn)行處理。進(jìn)程再次重復(fù),堆棧不會溢出。 JavaScript是前端開發(fā)中非常重要的一門語言,瀏覽器是他主要運(yùn)行的地方。JavaScript是一個非常有意思的語言,但是他有很多一些概念,大...
摘要:什么是中的調(diào)用棧調(diào)用棧就像是程序當(dāng)前執(zhí)行的日志。當(dāng)函數(shù)執(zhí)行結(jié)束時,將從調(diào)用棧中出去。了解全局和局部執(zhí)行上下文是掌握作用域和閉包的關(guān)鍵。總結(jié)引擎創(chuàng)建執(zhí)行上下文,全局存儲器和調(diào)用棧。 原文作者:Valentino 原文鏈接:https://www.valentinog.com/blog/js-execution-context-call-stack 什么是Javascript中的執(zhí)行上下文...
摘要:這樣就改進(jìn)了代碼的性能,看代碼將保存在局部變量中所以啊,我們在開發(fā)中,如果在函數(shù)中會經(jīng)常用到全局變量,把它保存在局部變量中避免使用語句用語句延長了作用域,查找變量同樣費(fèi)時間,這個我們一般不會用到,所以不展開了。 本來在那片編寫可維護(hù)性代碼文章后就要總結(jié)這篇代碼性能文章的,耽擱了幾天,本來也是決定每天都要更新一篇文章的,因?yàn)橐郧扒废绿鄸|西沒總結(jié),學(xué)過的東西沒去總結(jié)真的很快就忘記了...
閱讀 2405·2021-11-11 16:54
閱讀 1210·2021-09-22 15:23
閱讀 3653·2021-09-07 09:59
閱讀 2002·2021-09-02 15:41
閱讀 3289·2021-08-17 10:13
閱讀 3051·2019-08-30 15:53
閱讀 1241·2019-08-30 13:57
閱讀 1214·2019-08-29 15:16