前端工程師吃飯的家伙峻村,深度麸折、廣度一樣都不能差。
一粘昨、JavaScript 規(guī)定了幾種語言類型
7 種基本數(shù)據(jù)類型:BigInt垢啼、Symbol、Undefined张肾、Null芭析、Boolean、Number和String
1 種復(fù)雜數(shù)據(jù)類型:Object
二吞瞪、JavaScript 對象的底層數(shù)據(jù)結(jié)構(gòu)是什么
通過 V8 的源碼嘗試分析 Object 的實現(xiàn):V8 里面所有的數(shù)據(jù)類型的根父類都是 Object馁启,Object 派生 HeapObject,提供存儲基本功能芍秆,往下的 JSReceiver 用于原型查找惯疙,再往下的 JSObject 就是 JS 里面的 Object,Array/Function/Date 等繼承于 JSObject妖啥。左邊的 FixedArray 是實際存儲數(shù)據(jù)的地方霉颠。
在創(chuàng)建一個 JSObject 之前,會先把讀到的 Object 的文本屬性序列化成 constant_properties迹栓,如下的 data:
var data = {
name: 'yin',
age: 18,
'-school-': 'high school'
}
會被序列成:
../../v8/src/runtime/runtime-literals.cc 72 constant_properties:
0xdf9ed2aed19: [FixedArray]
– length: 6
[0]: 0x1b5ec69833d1 <String[4]: name>
[1]: 0xdf9ed2aec51 <String[3]: yin>
[2]: 0xdf9ed2aec71 <String[3]: age>
[3]: 18
[4]: 0xdf9ed2aec91 <String[8]: -school->
[5]: 0xdf9ed2aecb1 <String[11]: high school>
它是一個 FixedArray掉分,一共有 6 個元素,由于 data 總共是有 3 個屬性克伊,每個屬性有一個 key 和一個 value泥技,所以 Array 就有 6 個毅贮。第一個元素是第一個 key实辑,第二個元素是第一個 value浊洞,第三個元素是第二個 key,第四個元素是第二個 key犁跪,依次類推椿息。
Object 提供了一個 Print()的函數(shù),把它用來打印對象的信息非常有幫助坷衍。上面的輸出有兩種類型的數(shù)據(jù)寝优,一種是 String 類型,第二種是整型類型的枫耳。
FixedArray 是 V8 實現(xiàn)的一個類似于數(shù)組的類乏矾,它表示一段連續(xù)的內(nèi)存。
參考自:https://www.rrfed.com/2017/04/04/chrome-object/
三、Symbol 類型在實際開發(fā)中的應(yīng)用钻心、可手動實現(xiàn)一個簡單的 Symbol
應(yīng)用場景 1:使用 Symbol 來作為對象屬性名(key)
在這之前凄硼,我們通常定義或訪問對象的屬性時都是使用字符串,比如下面的代碼:
let obj = {
abc: 123,
hello: 'world'
}
obj['abc'] // 123
obj['hello'] // 'world'
而現(xiàn)在捷沸,Symbol 可同樣用于對象屬性的定義和訪問:
const PROP_NAME = Symbol()
const PROP_AGE = Symbol()
let obj = {
[PROP_NAME]: '一斤代碼'
}
obj[PROP_AGE] = 18
obj[PROP_NAME] // '一斤代碼'
obj[PROP_AGE] // 18
隨之而來的是另一個非常值得注意的問題:就是當(dāng)使用了 Symbol 作為對象的屬性 key 后摊沉,在對該對象進(jìn)行 key 的枚舉時,會有什么不同痒给?在實際應(yīng)用中说墨,我們經(jīng)常會需要使用 Object.keys()或者 for...in 來枚舉對象的屬性名,那在這方面侈玄,Symbol 類型的 key 表現(xiàn)的會有什么不同之處呢婉刀?來看以下示例代碼:
let obj = {
[Symbol('name')]: '一斤代碼',
age: 18,
title: 'Engineer'
}
Object.keys(obj) // ['age', 'title']
for (let p in obj) {
console.log(p) // 分別會輸出:'age' 和 'title'
}
Object.getOwnPropertyNames(obj) // ['age', 'title']
由上代碼可知吟温,Symbol 類型的 key 是不能通過 Object.keys()或者 for...in 來枚舉的序仙,它未被包含在對象自身的屬性名集合(property names)之中。所以鲁豪,利用該特性潘悼,我們可以把一些不需要對外操作和訪問的屬性使用 Symbol 來定義。
也正因為這樣一個特性爬橡,當(dāng)使用 JSON.stringify()將對象轉(zhuǎn)換成 JSON 字符串的時候治唤,Symbol 屬性也會被排除在輸出內(nèi)容之外:
JSON.stringify(obj) // {"age":18,"title":"Engineer"}
我們可以利用這一特點來更好的設(shè)計我們的數(shù)據(jù)對象,讓“對內(nèi)操作”和“對外選擇性輸出”變得更加優(yōu)雅糙申。
然而宾添,這樣的話,我們就沒辦法獲取以 Symbol 方式定義的對象屬性了么柜裸?非也缕陕。還是會有一些專門針對 Symbol 的 API,比如:
// 使用Object的API
Object.getOwnPropertySymbols(obj) // [Symbol(name)]
// 使用新增的反射API
Reflect.ownKeys(obj) // [Symbol(name), 'age', 'title']
應(yīng)用場景 2:使用 Symbol 來替代常量
先來看一下下面的代碼疙挺,是不是在你的代碼里經(jīng)常會出現(xiàn)扛邑?
const TYPE_AUDIO = 'AUDIO'
const TYPE_VIDEO = 'VIDEO'
const TYPE_IMAGE = 'IMAGE'
function handleFileResource(resource) {
switch (resource.type) {
case TYPE_AUDIO:
playAudio(resource)
break
case TYPE_VIDEO:
playVideo(resource)
break
case TYPE_IMAGE:
previewImage(resource)
break
default:
throw new Error('Unknown type of resource')
}
}
如上面的代碼中那樣,我們經(jīng)常定義一組常量來代表一種業(yè)務(wù)邏輯下的幾個不同類型铐然,我們通常希望這幾個常量之間是唯一的關(guān)系蔬崩,為了保證這一點,我們需要為常量賦一個唯一的值(比如這里的'AUDIO'搀暑、'VIDEO'沥阳、 'IMAGE'),常量少的時候還算好自点,但是常量一多桐罕,你可能還得花點腦子好好為他們?nèi)€好點的名字。
現(xiàn)在有了 Symbol,我們大可不必這么麻煩了:
const TYPE_AUDIO = Symbol()
const TYPE_VIDEO = Symbol()
const TYPE_IMAGE = Symbol()
這樣定義冈绊,直接就保證了三個常量的值是唯一的了侠鳄!是不是挺方便的呢。
應(yīng)用場景 3:使用 Symbol 定義類的私有屬性/方法
我們知道在 JavaScript 中死宣,是沒有如 Java 等面向?qū)ο笳Z言的訪問控制關(guān)鍵字 private 的伟恶,類上所有定義的屬性或方法都是可公開訪問的。因此這對我們進(jìn)行 API 的設(shè)計時造成了一些困擾毅该。
而有了 Symbol 以及模塊化機(jī)制博秫,類的私有屬性和方法才變成可能。例如:
在文件 a.js 中
const PASSWORD = Symbol()
class Login {
constructor(username, password) {
this.username = username
this[PASSWORD] = password
}
checkPassword(pwd) {
return this[PASSWORD] === pwd
}
}
export default Login
在文件 b.js 中
import Login from './a'
const login = new Login('admin', '123456')
login.checkPassword('123456') // true
login.PASSWORD // oh!no!
login[PASSWORD] // oh!no!
login['PASSWORD'] // oh!no!
由于 Symbol 常量 PASSWORD 被定義在 a.js 所在的模塊中眶掌,外面的模塊獲取不到這個 Symbol挡育,也不可能再創(chuàng)建一個一模一樣的 Symbol 出來(因為 Symbol 是唯一的),因此這個 PASSWORD 的 Symbol 只能被限制在 a.js 內(nèi)部使用朴爬,所以使用它來定義的類屬性是沒有辦法被模塊外訪問到的即寒,達(dá)到了一個私有化的效果。
手動實現(xiàn) Symbol:
(function() {
var root = this;
var generateName = (function(){
var postfix = 0;
return function(descString){
postfix++;
return '@@' + descString + '_' + postfix
}
})()
var SymbolPolyfill = function Symbol(description) {
if (this instanceof SymbolPolyfill) throw new TypeError('Symbol is not a constructor');
var descString = description === undefined ? undefined : String(description)
var symbol = Object.create({
toString: function() {
return this.__Name__;
},
valueOf: function() {
return this;
}
})
Object.defineProperties(symbol, {
'__Description__': {
value: descString,
writable: false,
enumerable: false,
configurable: false
},
'__Name__': {
value: generateName(descString),
writable: false,
enumerable: false,
configurable: false
}
});
return symbol;
}
var forMap = {};
Object.defineProperties(SymbolPolyfill, {
'for': {
value: function(description) {
var descString = description === undefined ? undefined : String(description)
return forMap[descString] ? forMap[descString] : forMap[descString] = SymbolPolyfill(descString);
},
writable: true,
enumerable: false,
configurable: true
},
'keyFor': {
value: function(symbol) {
for (var key in forMap) {
if (forMap[key] === symbol) return key;
}
},
writable: true,
enumerable: false,
configurable: true
}
});
root.SymbolPolyfill = SymbolPolyfill;
四召噩、JavaScript 中的變量在內(nèi)存中的具體存儲形式
棧內(nèi)存和堆內(nèi)存
JavaScript 中的變量分為基本類型和引用類型
基本類型是保存在棧內(nèi)存中的簡單數(shù)據(jù)段母赵,它們的值都有固定的大小,保存在椌叩危空間凹嘲,通過按值訪問
引用類型是保存在堆內(nèi)存中的對象,值大小不固定构韵,棧內(nèi)存中存放的該對象的訪問地址指向堆內(nèi)存中的對象周蹭,JavaScript 不允許直接訪問堆內(nèi)存中的位置,因此操作對象時疲恢,實際操作對象的引用
結(jié)合代碼與圖來理解
let a1 = 0 // 棧內(nèi)存
let a2 = 'this is string' // 棧內(nèi)存
let a3 = null // 棧內(nèi)存
let b = { x: 10 } // 變量b存在于棧中凶朗,{ x: 10 }作為對象存在于堆中
let c = [1, 2, 3] // 變量c存在于棧中,[1, 2, 3]作為對象存在于堆中
當(dāng)我們要訪問堆內(nèi)存中的引用數(shù)據(jù)類型時
- 從棧中獲取該對象的地址引用
- 再從堆內(nèi)存中取得我們需要的數(shù)據(jù)
基本類型發(fā)生復(fù)制行為
let a = 20
let b = a
b = 30
console.log(a) // 20
結(jié)合下面圖進(jìn)行理解:
在棧內(nèi)存中的數(shù)據(jù)發(fā)生復(fù)制行為時冈闭,系統(tǒng)會自動為新的變量分配一個新值俱尼,最后這些變量都是相互獨(dú)立互不影響的
引用類型發(fā)生復(fù)制行為
let a = { x: 10, y: 20 }
let b = a
b.x = 5
console.log(a.x) // 5
- 引用類型的復(fù)制,同樣為新的變量 b 分配一個新的值萎攒,保存在棧內(nèi)存中遇八,不同的是,這個值僅僅是引用類型的一個地址指針
- 他們兩個指向同一個值耍休,也就是地址指針相同刃永,在堆內(nèi)存中訪問到的具體對象實際上是同一個
- 因此改變 b.x 時,a.x 也發(fā)生了變化羊精,這就是引用類型的特性
結(jié)合下圖理解
總結(jié)
五斯够、基本類型對應(yīng)的內(nèi)置對象,以及他們之間的裝箱拆箱操作
JS 中的內(nèi)置函數(shù)(對象)
String()、Number()读规、Boolean()抓督、RegExp()、Date()束亏、Error()铃在、Array()、Function()碍遍、Object()定铜、symbol();類似于對象的構(gòu)造函數(shù)
1、這些內(nèi)置函數(shù)構(gòu)造的變量都是封裝了基本類型值的對象如:
var a = new String('abb') // typeof(a)=object
除了利用 Function()構(gòu)造的變量通過 typeof 輸出為 function 外其他均為 object
2怕敬、為了知道構(gòu)造的變量的真實類型可以利用:
Object.prototype.toString.call([1, 2, 3]) // "[object,array]"
后面的一個值即為傳入?yún)?shù)的類型
3揣炕、如果有常量形式(即利用基本數(shù)據(jù)類型)賦值給變量就不要用該方式來定義變量
裝箱
就是把基本類型轉(zhuǎn)變?yōu)閷?yīng)的對象。裝箱分為隱式和顯示
- 隱式裝箱: 每當(dāng)讀取一個基本類型的值時东跪,后臺會創(chuàng)建一個該基本類型所對應(yīng)的對象畸陡。在這個基本類型上調(diào)用方法,其實是在這個基本類型對象上調(diào)用方法越庇。這個基本類型的對象是臨時的罩锐,它只存在于方法調(diào)用那一行代碼執(zhí)行的瞬間奉狈,執(zhí)行方法后立刻被銷毀卤唉。
let num = 123
num.toFixed(2) // '123.00'//上方代碼在后臺的真正步驟為
var c = new Number(123)
c.toFixed(2)
c = null
(1)創(chuàng)建一個 Number 類型的實例。
(2)在實例上調(diào)用方法仁期。
(3)銷毀實例桑驱。
- 顯式裝箱: 通過內(nèi)置對象 Boolean、Object跛蛋、String 等可以對基本類型進(jìn)行顯示裝箱熬的。
var obj = new String('123')
拆箱
拆箱與裝箱相反,把對象轉(zhuǎn)變?yōu)榛绢愋偷闹瞪藜丁2鹣溥^程內(nèi)部調(diào)用了抽象操作 ToPrimitive 押框。該操作接受兩個參數(shù),第一個參數(shù)是要轉(zhuǎn)變的對象理逊,第二個參數(shù) PreferredType 是對象被期待轉(zhuǎn)成的類型橡伞。第二個參數(shù)不是必須的,默認(rèn)該參數(shù)為 number晋被,即對象被期待轉(zhuǎn)為數(shù)字類型
-
Number 轉(zhuǎn)化為對象
1.先調(diào)用對象自身的 valueOf 方法兑徘。如果返回原始類型的值,則直接對該值使用 Number 函數(shù)羡洛,返回結(jié)果挂脑。
2.如果 valueOf 返回的還是對象,繼續(xù)調(diào)用對象自身的 toString 方法。如果 toString 返回原始類型的值崭闲,則對該值使用 Number 函數(shù)肋联,返回結(jié)果。
3.如果 toString 返回的還是對象刁俭,報錯牺蹄。
Number([1]); //1
轉(zhuǎn)換演變:
[1].valueOf(); // [1];
[1].toString(); // '1';Number('1'); //1
-
String 轉(zhuǎn)化為對象
1.先調(diào)用對象自身的 toString 方法。如果返回原始類型的值薄翅,則對該值使用 String 函數(shù)沙兰,返回結(jié)果。
2.如果 toString 返回的是對象翘魄,繼續(xù)調(diào)用 valueOf 方法鼎天。如果 valueOf 返回原始類型的值,則對該值使用 String 函數(shù)暑竟,返回結(jié)果斋射。
3.如果 valueOf 返回的還是對象,報錯但荤。
String([1,2]) //"1,2"
轉(zhuǎn)化演變:
[1,2].toString(); //"1,2"
String("1,2"); //"1,2"
-
Boolean 轉(zhuǎn)化對象
Boolean 轉(zhuǎn)換對象很特別罗岖,除了以下六個值轉(zhuǎn)換為 false,其他都為 true
undefined null false 0(包括+0和-0) NaN 空字符串('')
Boolean(undefined) //false
Boolean(null) //false
Boolean(false) //false
Boolean(0) //false
Boolean(NaN) //false
Boolean('') //false
Boolean([]) //true
Boolean({}) //true
Boolean(new Date()) //true
六腹躁、理解值類型和引用類型
a 聲明變量時不同的內(nèi)存分配:
1)原始值:存儲在棧(stack)中的簡單數(shù)據(jù)段桑包,也就是說,它們的值直接存儲在變量訪問的位置纺非。
這是因為這些原始類型占據(jù)的空間是固定的哑了,所以可將他們存儲在較小的內(nèi)存區(qū)域 – 棧中。這樣存儲便于迅速查尋變量的值烧颖。
2)引用值:存儲在堆(heap)中的對象弱左,也就是說,存儲在變量處的值是一個指針(point)炕淮,指向存儲對象的內(nèi)存地址拆火。
這是因為:引用值的大小會改變,所以不能把它放在棧中涂圆,否則會降低變量查尋的速度们镜。相反,放在變量的棾俗郏空間中的值是該對象存儲在堆中的地址憎账。
地址的大小是固定的,所以把它存儲在棧中對變量性能無任何負(fù)面影響卡辰。
b 不同的內(nèi)存分配機(jī)制也帶來了不同的訪問機(jī)制
1)在 JS 中是不允許直接訪問保存在堆內(nèi)存中的對象的胞皱,所以在訪問一個對象時邪意,首先得到的是這個對象在堆內(nèi)存中的地址,然后再按照這個地址去獲得這個對象中的值反砌,這就是傳說中的按引用訪問雾鬼。
2)而原始類型的值則是可以直接訪問到的。
c 復(fù)制變量時的不同
1)原始值:在將一個保存著原始值的變量復(fù)制給另一個變量時宴树,會將原始值的副本賦值給新變量策菜,此后這兩個變量是完全獨(dú)立的,他們只是擁有相同的值而已酒贬,彼此都是獨(dú)立的又憨。
2)引用值:在將一個保存著對象內(nèi)存地址的變量復(fù)制給另一個變量時,會把這個內(nèi)存地址賦值給新變量锭吨,也就是說這兩個變量都指向了堆內(nèi)存中的同一個對象蠢莺,他們中任何一個作出的改變都會反映在另一個身上。(復(fù)制對象時并不會在堆內(nèi)存中新生成一個一模一樣的對象零如,只是多了一個保存指向這個對象指針的變量罷了)
d 參數(shù)傳遞的不同(把實參復(fù)制給形參的過程)
首先我們應(yīng)該明確一點:ECMAScript 中所有函數(shù)的參數(shù)都是按值來傳遞的躏将。
但是為什么涉及到原始類型與引用類型的值時仍然有區(qū)別呢?還不就是因為內(nèi)存分配時的差別考蕾。
1)原始值:只是把變量里的值傳遞給參數(shù)祸憋,之后參數(shù)和這個變量互不影響。
2)引用值:對象變量它里面的值是這個對象在堆內(nèi)存中的內(nèi)存地址肖卧,這一點你要時刻銘記在心蚯窥!
因此它傳遞的值也就是這個內(nèi)存地址,這也就是為什么函數(shù)內(nèi)部對這個參數(shù)的修改會體現(xiàn)在外部的原因了喜命,因為它們都指向同一個對象沟沙。
七、null 和 undefined 的區(qū)別
定義
Null 類型:Null 類型也只有一個特殊的值——null壁榕。從邏輯角度來看,null 值表示一個空對象指針赎瞎。
Undefined 類型:Undefined 類型只有一個值牌里,即特殊的 undefined。在使用 var 聲明變量但未對其加以初始化時务甥,這個變量的值就是 undefined牡辽。
null 和 undefined 的應(yīng)用場景
null 表示"沒有對象",即該處不應(yīng)該有值敞临。典型用法是:
(1) 作為函數(shù)的參數(shù)态辛,表示該函數(shù)的參數(shù)不是對象。
(2) 作為對象原型鏈的終點挺尿。
console.log(null instanceof Object) // false
undefined 表示"缺少值"奏黑,就是此處應(yīng)該有一個值炊邦,但是還沒有定義。典型用法是:
(1)變量被聲明了熟史,但沒有賦值時馁害,就等于 undefined。
(2) 調(diào)用函數(shù)時蹂匹,應(yīng)該提供的參數(shù)沒有提供碘菜,該參數(shù)等于 undefined。
(3)對象沒有賦值的屬性限寞,該屬性的值為 undefined忍啸。
(4)函數(shù)沒有返回值時,默認(rèn)返回 undefined履植。
Number 轉(zhuǎn)換的值
Number(null)輸出為 0, Number(undefined)輸出為 NaN
八吊骤、至少可以說出三種判斷 JavaScript 數(shù)據(jù)類型的方式,以及他們的優(yōu)缺點静尼,如何準(zhǔn)確的判斷數(shù)組類型
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ù)...
instanceof
instanceof
操作符可以幫助我們判斷引用類型具體是什么類型的對象:
;[] 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
實際上是判斷Array.prototype
是否在[]
的原型鏈上普舆。
所以恬口,使用instanceof
來檢測數(shù)據(jù)類型,不會很準(zhǔn)確沼侣,這不是它設(shè)計的初衷:
[] instanceof Object // true
function(){} instanceof Object // true
另外祖能,使用instanceof
也不能檢測基本數(shù)據(jù)類型,所以instanceof
并不是一個很好的選擇蛾洛。
toString
上面我們在拆箱操作中提到了toString
函數(shù)养铸,我們可以調(diào)用它實現(xiàn)從引用類型的轉(zhuǎn)換。
每一個引用類型都有
toString
方法轧膘,默認(rèn)情況下钞螟,toString()
方法被每個Object
對象繼承。如果此方法在自定義對象中未被覆蓋谎碍,toString()
返回"[object type]"
鳞滨,其中type
是對象的類型。
const obj = {}
obj.toString() // [object Object]
注意蟆淀,上面提到了如果此方法在自定義對象中未被覆蓋
拯啦,toString
才會達(dá)到預(yù)想的效果澡匪,事實上,大部分引用類型比如Array提岔、Date仙蛉、RegExp
等都重寫了toString
方法。
我們可以直接調(diào)用Object
原型上未被覆蓋的toString()
方法碱蒙,使用call
來改變this
指向來達(dá)到我們想要的效果荠瘪。
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
取得類型赛惩。
判斷數(shù)組類型可以用 Array.isArray(value)
或者 Object.prototype.toString.call(value)
哀墓。
九、可能發(fā)生隱式類型轉(zhuǎn)換的場景以及轉(zhuǎn)換原則喷兼,應(yīng)如何避免或巧妙應(yīng)用
因為JavaScript
是弱類型的語言篮绰,所以類型轉(zhuǎn)換發(fā)生非常頻繁,上面我們說的裝箱和拆箱其實就是一種類型轉(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)換:
類型轉(zhuǎn)換規(guī)則
如果發(fā)生了隱式轉(zhuǎn)換藕筋,那么各種類型互轉(zhuǎn)符合下面的規(guī)則:
if 語句和邏輯語句
在if
語句和邏輯語句中纵散,如果只有單個變量,會先將變量轉(zhuǎn)換為Boolean
值隐圾,只有下面幾種情況會轉(zhuǎn)換成false
伍掀,其余被轉(zhuǎn)換成true
:
null
undefined
;('')
NaN
0
false
各種運(yùn)數(shù)學(xué)算符
我們在對各種非Number
類型運(yùn)用數(shù)學(xué)運(yùn)算符(- * /
)時,會先將非Number
類型轉(zhuǎn)換為Number
類型;
1 - true // 0
1 - null // 1
1 * undefined // NaN
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)
==
使用==
時,若兩側(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
這里注意一個可能會弄混的點:
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ù)上面第三點阐斜,false
轉(zhuǎn)換成Number
類型0
,左側(cè)[]
轉(zhuǎn)換為0
放吩,兩側(cè)比較相等智听。
;([null] == false[undefined]) == // true
false // true
根據(jù)數(shù)組的ToPrimitive
規(guī)則,數(shù)組元素為null
或undefined
時渡紫,該元素被當(dāng)做空字符串處理到推,所以[null]、[undefined]
都會被轉(zhuǎn)換為0
惕澎。
所以莉测,說了這么多,推薦使用===
來判斷兩個值是否相等...
一道有意思的面試題
一道經(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()
}
}
十董朝、出現(xiàn)小數(shù)精度丟失的原因,JavaScript 可以存儲的最大數(shù)字干跛、最大安全數(shù)字子姜,JavaScript 處理大數(shù)字的方法、避免精度丟失的方法
出現(xiàn)小數(shù)精度丟失的原因
計算機(jī)的二進(jìn)制實現(xiàn)和位數(shù)限制有些數(shù)無法有限表示楼入。就像一些無理數(shù)不能有限表示哥捕,如 圓周率 3.1415926...牧抽,1.3333... 等。JS 遵循 IEEE 754 規(guī)范遥赚,采用雙精度存儲(double precision)扬舒,占用 64 bit。如圖
意義
1 位用來表示符號位
11 位用來表示指數(shù)
52 位表示尾數(shù)
浮點數(shù)凫佛,比如
1
2
0.1 >> 0.0001 1001 1001 1001…(1001 無限循環(huán))
0.2 >> 0.0011 0011 0011 0011…(0011 無限循環(huán))
此時只能模仿十進(jìn)制進(jìn)行四舍五入了讲坎,但是二進(jìn)制只有 0 和 1 兩個,于是變?yōu)?0 舍 1 入御蒲。這即是計算機(jī)中部分浮點數(shù)運(yùn)算時出現(xiàn)誤差衣赶,丟失精度的根本原因。
JS 的最大和最小安全值可以這樣獲得:
console.log(Number.MAX_SAFE_INTEGER) //9007199254740991
console.log(Number.MIN_SAFE_INTEGER) //-9007199254740991
對于整數(shù)厚满,前端出現(xiàn)問題的幾率可能比較低府瞄,畢竟很少有業(yè)務(wù)需要需要用到超大整數(shù),只要運(yùn)算結(jié)果不超過 Math.pow(2, 53) 就不會丟失精度碘箍。如果實在是超過最大安全數(shù)字了遵馆,那就用 BigInt(Number)計算。
對于小數(shù)丰榴,前端出現(xiàn)問題的幾率還是很多的货邓,尤其在一些電商網(wǎng)站涉及到金額等數(shù)據(jù)。解決方式:把小數(shù)放到位整數(shù)(乘倍數(shù))四濒,再縮小回原來倍數(shù)(除倍數(shù))换况,也就是說,盡量在業(yè)務(wù)中避免處理小數(shù)