你盼世界,我盼望你無bug
。Hello 你們好!我是霖呆呆!javascript
幾個月前看過一遍三元大佬的《(建議收藏)原生JS靈魂之問, 請問你能接得住幾個?》系列,當時是利用上下班公交的時間刷的。說下那時的感覺吧,有些知識點還真不知道,就感受好牛批,確實有一種被靈魂拷問的感受。最最可怕的是那時候尚未意識到本身的基礎這麼差,只是死記着一些零散的知識點,沒幾天可能就忘了,總想着何時好好理一下本身的知識體系卻一直沒有付出行動。html
在某一個點上,多是被某篇文章刺激的,忽然讓個人心態發生了很大的改變。那種感受怎麼形容呢...就像是我覺得本身都懂,可是我還不知道本身不懂,而後我還每天期盼着明天會更好。有點以爲本身是井底之蛙吧。等我真正認清了本身以後才知道了沉澱
這個詞的重要性。當我帶着'爲何會這樣?'、'還能夠怎樣?'、'若是這樣會怎樣?'
的問題來回顧以前的一些知識,我發現本身要補充的真的還有不少...前端
這篇文章原本是本身近期再讀《三元-JS靈魂之問》作的一些筆記,可是發現越記越多...也所以引出了我寫的一系列文章,好比JS類型轉換系列、JS繼承系列、this等等。這裏對三元提到的一些題作一些補充說明,使它們變得更適合初中級的小夥伴閱讀吧,同時也是對本身這階段學習的一個鞏固,有寫的不對的地方還請各位大佬指出。vue
寫了一兩年的掘金還沒破Lv4
,看來我要放大招了。在此立個flag
,升Lv4
後爆女裝"呆妹"
,把"她"
放到下篇文章安排一波。已經很卑微了...java
(秉承着對原做者神三元的感謝之情寫的,還請三元的19177
位粉絲不要誤會呀,我本身也是他的一名小粉絲...)git
全部文章均被收入gitHub「niubility-coding-js」中。github
咱們知道若是在代碼中使用:面試
'1'.toString()
// 或者是
true.toString()
複製代碼
都是能夠正常調用的,這是由於toString
它是Object.prototype
上的方法,任何能訪問到Object
原型的元素均可以調用它。數組
而在此處,對於'1'.toString()
至關因而作了一層轉換,將其轉爲了一個"對象"
,這樣就能夠調用toString()
方法了。瀏覽器
也就是這樣:
var s = new Object('1');
s.toString();
s = null;
複製代碼
Object
實例,將s
變爲了String{"1"}
對象Object.prototype
上的實例方法toString()
這一部分三元分析的已經挺多了,我主要是想補充一下1.toString()
爲何就不行。
當咱們在代碼中試圖使用1.toString()
,發現編輯器已經報錯不容許咱們這樣作了。
最開始會有這麼奇怪的想法是由於咱們都忽視了一件事,那就是.
它也是屬於數字裏的一部分啊 😂。
好比1.2
、1.3
。因此當你想要使用1.toString()
的時候,JavaScript
的解釋器會把它做爲數字的一部分,這樣就至關於(1.)toString
了,很顯然這是一段錯誤的代碼。
既然這樣的話,若是我還給代碼一個.
是否是就能夠了,因而我嘗試了一下:
console.log(1.1.toString())
複製代碼
發現它居然能正常打印出來:
"1.1"
複製代碼
這也就再次證實了1.toString()
會將.
歸給1
所屬,而不是歸給toString()
。
固然若是你用的一個變量來承載這個數字的話也是能夠的:
var num = 1;
console.log(num.toString()) // "1"
// 或者
console.log((1).toString()) // "1"
複製代碼
var num = new Number(1) // Number{1}
var str = new String('1') // String{'1'}
var bol = new Boolean(true) // Boolean{true}
var symbol = new Symbol(1) // TypeError
複製代碼
向上面這種使用new Number、new String
等建立的基本數據類型被稱之爲:圍繞原始數據類型建立一個顯式包裝器對象。
通俗點說就是:用new
來建立基本類型的包裝類。
而這種作法在ES6
以後就不被支持了,從new Symbol(1)
報錯就能夠看出來,如今使用的是不帶new
的方式:var symbol = Symbol(1)
,因此它做爲構造函數來講是不完整的。
可是由於歷史遺留的緣由,new Number
仍然能夠這樣用,不過並不推薦。
若是你真的想建立一個 Symbol 包裝器對象 (Symbol wrapper object
),你可使用 Object()
函數:
var sym = Symbol(1)
console.log(typeof sym) // "symbol"
var symObj = Object(sym)
console.log(typeof symObj) // "object"
複製代碼
什麼意思呢 🤔️?
正常來講,instanceof
是用來判斷某個對象的原型鏈上是否可以查找到某個構造函數的原型對象。
來看看通俗點的簡介:
a instanceof B
實例對象a instanceof 構造函數B
檢測a
的原型鏈(__proto__)
上是否有B.prototype
,有則返回true
,不然返回false
。
那麼它能夠用來判斷基本數據類型嗎?
也就是說我定義了一個var num = 1
,我能夠用instanceof
來判斷它是一個number
類型的變量嗎?
若是你試圖這樣寫:
var num = 1;
console.log(num instanceof Number) // false
複製代碼
發現結果是false
。
此時你能夠用Symbol.hasInstance
來實現一個自定義instanceof
的行爲。
首先想一想咱們是要實現一個什麼功能?
a instanceof B
複製代碼
左側的a
是一個變量,而右側的B
是一個構造函數,而class
的本質也是一個構造函數。
因此在這個需求中,咱們能夠定義一個叫作MyNumber
的類,在其裏面封裝一層,暴露一個靜態方法用來判斷數據類型:
class MyNumber {
static [Symbol.hasInstance](instance) {
return typeof instance === 'number'
}
}
var num = 1;
console.log(num instanceof MyNumber) // true
複製代碼
MyNumber
中定義了一個名爲Symbol.hasInstance
的靜態方法instance
typeof
判斷是不是number
類型這裏比較難理解的就是Symbol.hasInstance
了,第一次接觸它也不知道它是個啥 😂。找了一波MDN 上對它的介紹:
用於判斷某對象是否爲某構造器的實例。
而後試着寫了幾個案例,發現也不用把它想的那麼複雜,你就簡單理解,當咱們在使用instanceof
的時候,可以自定義右側構造函數(類)它的instanceof
驗證方式就能夠了。
就像是上面👆那個案例同樣,我在MyNumber
重寫了靜態方法Symbol.hasInstance
,讓它的驗證方式變成type instance === 'number'
。
那麼爲何是靜態方法呢(也就是在方法前面加上static
),經過閱讀《🔥【何不三連】比繼承家業還要簡單的JS繼承題-封裝篇(牛刀小試)》咱們知道,靜態方法是掛載在MyNumber
這個類上的方法,所以咱們甚至能夠把下面的內容換一種寫法:
console.log(num instanceof MyNumber) // true
// 換成:
console.log(MyNumber[Symbol.hasInstance](num)) // true
複製代碼
看到了吧,它其實就是一個方法名而已,而這個方法由於是靜態的,在MyNumber
上的,所以咱們能夠用MyNumber[Symbol.hasInstance]
這種方式調用。
想一想,若是沒有static
這個關鍵字呢?
沒有static
的話,定義在類裏的方法就至關因而掛載到類的原型對象上,那麼若是咱們想要使用它,一種就是直接用MyNumber.prototype
調用,還有一種就是使用new MyNumber()
生成一個實例來調用:
class MyNumber {
[Symbol.hasInstance](instance) { // 沒有 static
return typeof instance === 'number'
}
}
var num = 1
console.log(num instanceof new MyNumber()) // true
console.log(num instanceof MyNumber.prototype) // true
// 轉化爲:
console.log(MyNumber.prototype[Symbol.hasInstance](num)) // true
console.log(new MyNumber()[Symbol.hasInstance](num)) // true
複製代碼
因此如今回過頭來看看:
class MyNumber {
static [Symbol.hasInstance](instance) {
return typeof instance === 'number'
}
}
var num = 1;
console.log(num instanceof MyNumber) // true
複製代碼
是否是就好理解多了呢?
那麼假如我如今想要你實現一個用instanceof
判斷是否是數組的類MyArray
,該如何去寫呢?
思考🤔...
唔...上答案:
class MyArray {
static [Symbol.hasInstance](instance) {
return Array.isArray(instance);
}
}
console.log([] instanceof MyArray); // true
複製代碼
上面咱們說到了instanceof
是用來判斷某個對象的原型鏈上是否可以查找到某個構造函數的原型對象。
而且是會沿着原型鏈一層一層的向上查找,直到到達原型鏈的末位。
那這個過程具體是怎樣的呢?讓咱們來看一個例子🌰:
function Parent () {
this.name = 'parent'
}
function Child () {
this.sex = 'boy'
}
Child.prototype = new Parent()
var child1 = new Child()
console.log(child1 instanceof Child)
console.log(child1 instanceof Parent)
console.log(child1 instanceof Object)
複製代碼
結果爲:
true
true
true
複製代碼
這裏其實用到了原型鏈繼承,Chind
繼承於Parent
,並且三個構造函數的原型對象都存在於child1
的原型鏈上。
也就是說,左邊的child1
它會向它的原型鏈中不停的查找,看有沒有右邊那個構造函數的原型對象。
例如child1 instanceof Child
的查找順序:
child1 -> child1.__proto__ -> Child.prototype
複製代碼
child1 instanceof Parent
的查找順序:
child1 -> child1.__proto__ -> Child.prototype
-> Child.prototype.__proto__ -> Parent.prototype
複製代碼
還不理解?
不要緊,我還有大招:
我在上面👆原型鏈繼承的思惟導圖上加了三個查找路線。
被⭕️標記的一、二、3
分別表明的是Child、Parent、Object
的原型對象。
好滴,一張圖簡潔明瞭。之後再碰到instanceof
這種東西,按照我圖上的查找路線來查找就能夠了 😁 ~
既然說到了instanceof
,那麼就不得不提一下isPrototypeOf
這個方法了。
它屬於Object.prototype
上的方法,這點你能夠將Object.prototype
打印在控制檯中看看。
isPrototypeOf()
的用法和instanceof
相反。
它是用來判斷指定對象object1
是否存在於另外一個對象object2
的原型鏈中,是則返回true
,不然返回false
。
例如仍是上面👆這道題,咱們將要打印的內容改一下:
function Parent () {
this.name = 'parent'
}
function Child () {
this.sex = 'boy'
}
Child.prototype = new Parent()
var child1 = new Child()
console.log(Child.prototype.isPrototypeOf(child1))
console.log(Parent.prototype.isPrototypeOf(child1))
console.log(Object.prototype.isPrototypeOf(child1))
複製代碼
這裏輸出的依然是三個true
:
true
true
true
複製代碼
判斷的方式只要把原型鏈繼承instanceof查找思惟導圖這張圖反過來查找便可。
更多關於instanceOf
的內容能夠戳這裏👇:
💦【何不三連】作完這48道題完全弄懂JS繼承(1.7w字含辛整理-返璞歸真)
+0
和-0
的判斷不一樣NaN
和NaN
的判斷不一樣console.log(+0 === -0) // true
console.log(Object.is(+0, -0)) // false
console.log(NaN === NaN) // false
console.log(Object.is(NaN, NaN)) // true
複製代碼
其實不須要特地的去記,你只須要理解,Object.is()
在===
的基礎上修復了這些特殊狀況的失誤。
也就是說:+0
和-0
本就該不相等的,這點從1 / +0
爲Infinity
,1 / -0
爲-Infinity
上能夠看出。
(可是+0
和0
是沒有區別的)
而NaN
表示的都是非數字,因此應該是相等的。
所以Object.is()
的內部實際上是作了一些修復的處理:
function is (x, y) {
if (x === y) {
return x !== 0 || 1 / x === 1 / y;
} else {
return x !== x && y !== y;
}
}
複製代碼
這裏能夠怎樣理解呢 🤔️?
x === y
的時候,就是用來處理+0, -0, 0
的特殊狀況。在這個判斷中,首先會判斷出x和y
是否是都是+0, -0, 0
中的其中一個(由於咱們知道不管是這三個中的哪個和0
進行全等比較,結果都會是true
,因此x !== 0
就會是false
),而根據||
的短路原則,前面一項爲false
,那麼最終的結果取決於後面一項。
因此x !== 0
至關因而把x, y
爲+0, -0, 0
的狀況推給了1 / x === 1 / y
,用它的結果來決定最後的結果。
(在三元的原文中這段判斷是這樣寫的return x !== 0 || y !== 0 || 1 /x === 1 / y
,比我這裏多了一個|| y !== 0
。經評論區小夥伴matteokjh提示,以爲能夠把|| y !== 0
的判斷省略掉,另外MDN上對它的polyfill
也是沒有這一步的,我想了一下:由於能進入到x === y
裏,那麼若是x !== 0
成立的話,那麼y !== 0
確定也成立了,貌似是能夠省去。另外我把省去的代碼執行了一下,和沒有省去時的執行結果同樣的,因此我感受可行。)
並且咱們又知道1 / +0
爲Infinity
,1 / -0
爲-Infinity
,因此若是x, y
爲+0或者-0
的時候是不相等的,以此來實現Object.is(+0, -0)的結果爲false
。
x !== y
的時候,就是用來處理NaN, NaN
的特殊狀況。由於咱們知道NaN !== NaN
的,那麼若是x
和y
都不和它們本身相等的話,說明兩個都是NaN
了,而若是都是NaN
的話,Object.is(NaN, NaN)
的結果爲true
。
在此以前,我翻了不少關於toString()
的資料,大多都是介紹了它的用法,可是它真正存在於哪裏呢?
可能比較常見的一種說法是它存在於Object
的原型對象中,也就是Object.prototype
上,那麼對於基本數據類型,Number、String、Boolean、 Symbol、BigInt
呢?它們自身有這個方法嗎?或者它們的原型對象上有嗎?
本着一探到底的精神,我打印出了Number
和Number.prototype
:
console.log(Number)
console.log(Number.prototype)
複製代碼
而後我發現了幾件事:
Number
只是一個構造函數,打印出來顯示的會是源代碼Number.prototype
上確實也有toString()
Number.prototype.__proto__
也就是Object.prototype
上也有toString()
而後我又試了一下String、Boolean、Symbol
發現結果也和上面同樣。
其實不難理解,看過《💦【何不三連】作完這48道題完全弄懂JS繼承(1.7w字含辛整理-返璞歸真)》的小夥伴都知道,全部對象的原型鏈到最後都會指向Object.prototype
,算是都"繼承"了Object
的對象實例,所以都能使用toString()
方法,可是對於不一樣的內置對象爲了能實現更適合自身的功能需求,都會重寫該方法,因此你能夠看到Number.prototype
上也會有該方法。
因此咱們能夠先得出第一個結論:
null、undefined
之外的其它數據類型(基本數據類型+引用數據類型),它們構造函數的原型對象上都有toString()
方法toString()
會覆蓋Object
原型對象上的toString()
方法(固然,等你看到後面你就會發現這種說法其實並不太準確,可是大多數時候咱們都只是關心誰能夠用它,而不是它存在於哪裏)
這個問題,其實在上面👆已經給出答案了,全部對象除了null、undefined
之外的任何值均可以調用toString()
方法,一般狀況下它的返回結果和String
同樣。
其實這裏,咱們最容易搞混的就是String
和toString
。
以前老是爲了將某個類型轉爲字符串胡亂的用這兩個屬性。
String
是一個相似於Function
這樣的對象,它既能夠當成對象來用,用它上面的靜態方法,也能夠當成一個構造函數來用,建立一個String
對象toString
它是除了null、undefined
以外的數據類型都有的方法,一般狀況下它的返回結果和String
同樣。可能你們看的比較多的一種用法是這樣的:
Object.prototype.toString.call({ name: 'obj' }) // '[object Object]'
複製代碼
先來點硬知識,Object.prototype.toString
這個方法會根據這個對象的[[class]]
內部屬性,返回由 "[object " 和 class 和 "]"
三個部分組成的字符串。
啥意思?[[class]]
內部屬性是個啥 🤔️?
這裏你還真別想多,你就按字面意思來理解它就行了,想一想,class
英文單詞的意思->類
。
那好,我就認爲它表明的是一類事物就好了。
就好比
[[class]]
是Array
[[class]]
是String
arguments
是一類,它的[[class]]
是Arguments
另外,關於[[class]]
的種類是很是多的,你也不須要記住所有,只須要知道一些經常使用的,基本的,好理解的就能夠了。
因此回到Object.prototype.toString.call()
這種調用方式來,如今你能夠理解它的做用了吧,它可以幫助咱們準確的判斷某個數據類型,也就是辨別出是數組仍是數字仍是函數,仍是NaN
。😊
另外鑑於它的返回結果是"[object Object]"
這樣的字符串,並且前面的"[object ]"
這八個字符串都是固定的(包括"t"
後面的空格),因此咱們是否是能夠封裝一個方法來只拿到"Object"
這樣的字符串呢?
很簡單,上代碼:
function getClass (obj) {
let typeString = Object.prototype.toString.call(obj); // "[object Object]"
return typeString.slice(8, -1);
}
複製代碼
能夠看到,我給這個函數命名爲getClass
,這也就呼應了它本來的做用,是爲了拿到對象的[[class]]
內部屬性。
另外,在拿到了"[object Object]"
字符串以後,是用了一個.slice(8, -1)
的字符串截取功能,去除了前八個字符"[object ]"
和最後一個"]"
。
如今讓咱們來看看一些常見的數據類型吧:
function getClass(obj) {
let typeString = Object.prototype.toString.call(obj); // "[object Array]"
return typeString.slice(8, -1);
}
console.log(getClass(new Date)) // Date
console.log(getClass(new Map)) // Map
console.log(getClass(new Set)) // Set
console.log(getClass(new String)) // String
console.log(getClass(new Number)) // Number
console.log(getClass(true)) // Boolean
console.log(getClass(NaN)) // Number
console.log(getClass(null)) // Null
console.log(getClass(undefined)) // Undefined
console.log(getClass(Symbol(42))) // Symbol
console.log(getClass({})) // Object
console.log(getClass([])) // Array
console.log(getClass(function() {})) // Function
console.log(getClass(document.getElementsByTagName('p'))) // HTMLCollection
console.log(getClass(arguments)) // Arguments
複製代碼
"霖呆呆,這麼多,這是人乾的事嗎?"
"性平氣和,記住一些經常使用的就好了..."
"啪!"
好滴👌,經過剛剛的學習,咱們瞭解到了,toString.call
這種方式是爲了獲取某個變量更加具體的數據類型。
咦~說到數據類型,咱們原來不是有一個typeof
嗎?它和toString.call()
又啥區別?
首先幫你們回顧一下typeof
它的顯示規則:
number、string
這種),除了null
均可以顯示正確的類型null
由於歷史版本的緣由被錯誤的判斷爲了"object"
object、array
這種),除了函數都會顯示爲"object"
function
因此呀,typeof
的缺點很明顯啊,我如今有一個對象和一個數組,或者一個日期對象,我想要仔細的區分它,用typeof
確定是不能實現的,由於它們獲得的都是"object"
。
因此,採用咱們封裝的getClass()
顯然是一個很好的選擇。
在不一樣的數據類型調用toString()
會有什麼不一樣呢?
這裏我主要是分爲兩大塊來講:
對於基本數據類型來調用它,超級簡單的,你就想着就是把它的原始值換成了字符串而已:
console.log('1'.toString()) // '1'
console.log(1.1.toString()) // '1.1'
console.log(true.toString()) // 'true'
console.log(Symbol(1).toString()) // 'Symbol(1)'
console.log(10n.toString()) // '10'
複製代碼
因此對於基本數據類型:
比較難的部分是引用類型調用toString()
,並且咱們知道引用類型根據[[class]]
的不一樣是分了不少類的,好比有Object
、Array
、Date
等等。
那麼不一樣類之間的toString()
是否也不一樣呢 🤔️?
沒錯,不一樣版本的toString
主要是分爲:
toString
方法是將每一項轉換爲字符串而後再用","
鏈接{name: 'obj'}
這種)轉爲字符串都會變爲"[object Object]"
函數(class)、正則
會被轉爲源代碼字符串日期
會被轉爲本地時區的日期字符串toString
會返回原始值的字符串Symbol.toStringTag
內置屬性的對象在調用時會變爲對應的標籤"[object Map]"
(Symbol.toStringTag
屬性下一題會問到)
例如🌰:
console.log([].toString()) // ""
console.log([1, 2].toString()) // "1,2"
console.log({}.toString()) // "[object Object]"
console.log({name: 'obj'}.toString()) // "[object Object]"
console.log(class A {}.toString()) // "class A {}"
console.log(function () {}.toString()) // "function () {}"
console.log(/(\[|\])/g.toString()) // "/(\[|\])/g1"
console.log(new Date().toString()) // "Fri Mar 27 2020 12:33:16 GMT+0800 (中國標準時間)"
console.log(new Object(true).toString()) // "true"
console.log(new Object(1).toString()) // "1"
console.log(new Object(BigInt(10)).toString()) // "10"
console.log(new Map().toString()) // "[object Map]"
console.log(new Set().toString()) // "[object Set]"
複製代碼
《Symbol.toStringTag》上是這樣描述它的:
該Symbol.toStringTag
公知的符號是在建立對象的默認字符串描述中使用的字符串值屬性。它由該Object.prototype.toString()
方法在內部訪問。
看不懂不要緊,你這樣理解就能夠了,它其實就是決定了剛剛咱們提到全部數據類型中[[class]]
這個內部屬性是什麼。
好比數字,咱們前面獲得的[[class]]
是Number
,那我就能夠理解爲數字這個類它的Symbol.toStringTag
返回的就是Number
。
只不過在以前咱們用到的Number、String、Boolean
中並無Symbol.toStringTag
這個內置屬性,它是在咱們使用toString.call()
調用的時候纔將其辨別返回。
而剛剛咱們剛剛在第五問看到的new Map()
,讓咱們把它打印出來看看。
console.log(new Map())
複製代碼
能夠看到Symbol.toStringTag
它是確確實實存在於Map.prototype
上的,也就是說它是Map、Set
內置的一個屬性,所以當咱們直接調用toString()
的時候,就會返回"[object Map]"
了。
額,咱們是否是就能夠這樣理解呢?
Symbol.toStringTag
內置屬性的類型在調用toString()
的時候至關因而String(obj)
這樣調用轉換爲相應的字符串Symbol.toStringTag
內置屬性的類型在調用toString()
的時候會返回相應的標籤(也就是"[object Map]"
這樣的字符串)咱們經常使用的帶有Symbol.toStringTag
內置屬性的對象有:
console.log(new Map().toString()) // "[object Map]"
console.log(new Set().toString()) // "[object Set]"
console.log(Promise.resolve().toString()) // "[object Promise]"
複製代碼
而它最主要的功能就是和Symbol.hasInstance
同樣,能夠容許咱們自定義標籤。
(Symbol.hasInsance
的做用是自定義instanceof
的返回值)
什麼是自定義標籤呢 🤔️?
也就是說,假如咱們如今建立了一個類,而且用toString.call()
調用它的實例對象是會有以下結果:
class Super {}
console.log(Object.prototype.toString.call(new Super())) // "[object Object]"
複製代碼
很好理解,由於產生的new Super()
是一個對象嘛,因此打印出的會是"[object Object]"
。
可是如今有了Symbol.toStringTag
以後,咱們能夠改後面的"Object"
。
好比我重寫一下:
class Super {
get [Symbol.toStringTag] () {
return 'Validator'
}
}
console.log(Object.prototype.toString.call(new Super())) // "[object Validator]"
複製代碼
這就是Symbol.toStringTag
的厲害之處,它可以容許咱們自定義標籤。
可是有一點要注意了,Symbol.toStringTag
重寫的是new Super()
這個實例對象的標籤,而不是重寫Super
這個類的標籤,也就是說這裏有區別的:
class Super {
get [Symbol.toStringTag] () {
return 'Validator'
}
}
console.log(Object.prototype.toString.call(Super)) // "[object Function]"
console.log(Object.prototype.toString.call(new Super())) // "[object Validator]"
複製代碼
由於Super
它自己仍是一個函數,只有Super
產生的實例對象纔會用到咱們的自定義標籤。
總結一下Symbol.toStringTag
:
Map、Set、Promise
Object.prototype.toString.call()
的返回結果更多關於toString
和Symbol.toStringTag
的內容能夠戳這裏👇:
【精】從206個console.log()徹底弄懂數據類型轉換的前世此生(上)
由於類型轉換算是讓人比較頭疼的一部分,因此對於這一塊我也專門寫了系列文章,基本上覆蓋了面試可能會問到的知識點,傳送門:
【精】從206個console.log()徹底弄懂數據類型轉換的前世此生(上)
【精】從206個console.log()徹底弄懂數據類型轉換的前世此生(下)
(下篇寫的挺好的沒人看難受😣)
valueOf()
默認是返回它自己1970 年 1 月 1 日以來的毫秒數
(相似於1585370128307
)。例子🌰:
console.log('1'.valueOf()) // '1'
console.log(1.1.valueOf()) // 1.1
console.log([].valueOf()) // []
console.log({}.valueOf()) // {}
console.log(['1'].valueOf()) // ['1']
console.log(function () {}.valueOf()) // ƒ () {}
console.log(/(\[|\])/g.valueOf()) // /(\[|\])/g
console.log(new Date().valueOf()) // 1585370128307
複製代碼
當咱們在將對象轉換爲原始類型或者進行==比較的時候,會調用內置的ToPrimitive
函數。
好比:
console.log(String({})) // 對象轉字符串,結果爲 "[object Object]"
console.log(Number([1, 2])) // 對象轉數字,結果爲 NaN
console.log([] == ![]) // true
複製代碼
以上結果的由來都通過了ToPrimitive
函數。
先讓咱們來看看它的函數語法:
ToPrimitive(input, PreferredType?)
複製代碼
參數:
input
,表示要處理的輸入值PerferredType
,指望轉換的類型,能夠看到語法後面有個問號,表示是非必填的。它只有兩個可選值,Number
和String
。而它對於傳入參數的處理是比較複雜的,讓咱們來看看流程圖:
根據流程圖,咱們得出了這麼幾個信息:
(總結來源《冴羽-JavaScript深刻之頭疼的類型轉換(上)》)
上面👆的圖其實只是看着很複雜,細心的小夥伴可能會發現,在圖裏紅框裱起來的地方,只有toString()
和valueOf()
方法的執行順序不一樣而已。
若是 PreferredType 是 String 的話,就先執行 toString()
方法
若是 PreferredType 是 Number 的話,就先執行 valueOf()
方法
(霖呆呆建議你先本身在草稿紙上將這幅流程圖畫一遍)
來點例子鞏固一下:
console.log(String({})) // "[object Object]"
複製代碼
對於這個簡單的轉換咱們能夠把它換成toPrimitive
的僞代碼看看:
toPrimitive({}, 'string')
複製代碼
OK👌,來回顧一下剛剛的轉換規則:
input
是{}
,是一個引用類型,PerferredType
爲string
toString()
方法,也就是{}.toString()
{}.toString()
的結果爲"[object Object]"
,是一個字符串,爲基本數據類型,而後返回,到此結束。哇~
是否是一切都說得通了,好像不難吧 😁。
沒錯,當使用String()
方法的時候,JS
引擎內部的執行順序確實是這樣的,不過有一點和剛剛提到的步驟不同,那就是最後返回結果的時候,其實會將最後的基本數據類型再轉換爲字符串返回。
也就是說上面👆的第三步咱們得拆成兩步來:
{}.toString()
的結果爲"[object Object]"
,是一個字符串,爲基本數據類型"[object Object]"
字符串再作一次字符串的轉換而後返回。(由於"[object Object]"
已是字符串了,因此原樣返回,這裏看不出有什麼區別)將最後的結果再轉換爲字符串返回這一步,其實很好理解啊。你想一想,我調用String
方法那就是爲了獲得一個字符串啊,你要是給我返回一個number、null
啊什麼的,那不是隔壁老王乾的事嘛~
剛剛咱們說了對象轉字符串也就是toPrimitive(object, 'string')
的狀況,
那麼對象轉數字就是toPrimitive(object, 'number')
。
區別就是轉數字會先調用valueOf()
後調用toString()
。
例子🌰:
console.log(Number({}))
console.log(Number([]))
console.log(Number([0]))
console.log(Number([1, 2]))
console.log(Number(new Date()))
複製代碼
對於Number({})
:
{}
,所以調用valueOf()
方法,該方法在題7.1
中已經提到過了,它除了日期對象的其它引用類型調用都是返回它自己,因此這裏仍是返回了對象{}
valueOf()
返回的值仍是對象,因此繼續調用toString()
方法,而{}
調用toString()
的結果爲字符串"[object Object]"
,是一個基本數據類型"[object Object]"
轉爲數字爲NaN
,因此結果爲NaN
對於Number([])
:
[]
,所以調用valueOf()
方法,返回它自身[]
[]
繼續調用toString()
方法,而空數組轉爲字符串是爲""
""
轉爲數字0
返回對於Number([0])
:
[0]
轉爲字符串是爲"0"
,最後在轉爲數字0
返回對於Number([1, 2])
:
[1, 2]
,因此調用valueOf()
方法返回的是數組自己[1,2]
toString()
方法,此時被轉換爲了"1,2"
字符串"1,2"
字符串最後被轉爲數字爲NaN
,因此結果爲NaN
對於Number(new Date())
:
new Date()
,所以調用valueOf()
,在題目7.2
中已經說了,日期類型調用valueOf()
是會返回一個毫秒數1585413652137
結果:
console.log(Number({})) // NaN
console.log(Number([])) // 0
console.log(Number([0])) // 0
console.log(Number([1, 2])) // NaN
console.log(Number(new Date())) // 1585413652137
複製代碼
咱們都知道,當數組在進行轉字符串的時候,會把裏面的每一項都轉爲字符串而後再進行","
拼接返回。
那麼爲何會有","
拼接這一步呢?難道toString()
在調用的時候還會調用join()
方法嗎?
爲了驗證個人想法💡,我作了一個實驗,重寫了一個數組的join()
方法:
var arr = [1, 2]
arr['join'] = function () {
let target = [...this]
return target.join('~')
}
console.log(String(arr))
複製代碼
重寫的join
函數中,this
表示的就是調用的這個數組arr
。
而後將返回值改成"~"
拼接,結果答案居然是:
"1~2"
複製代碼
也就是說在String(arr)
的過程當中,它確實是隱式調用了join
方法。
可是當咱們重寫了toString()
以後,就不會管這個重寫的join
了:
var arr = [1, 2]
arr['toString'] = function () {
let target = [...this]
return target.join('*')
}
arr['join'] = function () {
let target = [...this]
return target.join('~')
}
console.log(String(arr)) // "1*2"
複製代碼
能夠看出toString()
的優先級仍是比join()
高的。
如今咱們又能夠得出一個結論:
對象若是是數組的話,當咱們不重寫其toString()
方法,在轉換爲字符串類型的時候,默認實現就是將調用join()
方法的返回值做爲toString()
的返回值。
當咱們在進行對象轉原始值的時候,會隱式調用內部的ToPrimitive
方法,按照前面說的那種方式進行轉換。
而Symbol.toPrimitive
屬性是能幫助咱們重寫toPrimitive
,以此來更改轉換的結果。
讓咱們來看看它的總結:
toString、valueOf、Symbol.toPrimitive
方法,Symbol.toPrimitive
的優先級是最高的Symbol.toPrimitive
函數返回的值不是基礎數據類型(也就是原始值),就會報錯Symbol.toPrimitive
接收一個字符串參數hint
,它表示要轉換到的原始值的預期類型,一共有'number'、'string'、'default'
三種選項String()
調用時,hint
爲'string'
;使用Number()
時,hint
爲'number'
hint
參數的值從開始調用的時候就已經肯定了來看個例子🌰:
var b = {
toString () {
console.log('toString')
return '1'
},
valueOf () {
console.log('valueOf')
return [1, 2]
},
[Symbol.toPrimitive] (hint) {
console.log('symbol')
if (hint === 'string') {
console.log('string')
return '1'
}
if (hint === 'number') {
console.log('number')
return 1
}
if (hint === 'default') {
console.log('default')
return 'default'
}
}
}
console.log(String(b))
console.log(Number(b))
複製代碼
這道題重寫了toString、valueOf、Symbol.toPrimitive
三個屬性,經過上面👆的題目咱們已經知道了只要有Symbol.toPrimitive
在,前面兩個屬性就被忽略了,因此咱們不用管它們。
而對於Symbol.toPrimitive
,我將三種hint
的狀況都寫上了,若是按照個人設想的話,在調用String(b)
的時候應該是要打印出string
的,調用Number(b)
打印出number
,結果也正如我所預想的同樣:
'string'
'1'
'number'
1
複製代碼
其實在實際中咱們被考的比較多的可能就是用==
來比較判斷兩個不一樣類型的變量是否相等。
而全等===
的狀況比較簡單,通常不太會考,由於全等的條件就是:若是類型相等值也相等才認爲是全等,並不會涉及到類型轉換。
可是==
的狀況就相對複雜了,先給你們看幾個比較眼熟的題哈:
console.log([] == ![]) // true
console.log({} == true) // false
console.log({} == "[object Object]") // true
複製代碼
怎樣?這幾題是否是常常看到呀 😁,下面就讓咱們一個一個來看。
首先,咱們仍是得清楚幾個概念,這個是硬性規定的,不看的話咱無法繼續下去啊。
當使用==
進行比較的時候,會有如下轉換規則(判斷規則):
2 == 3
確定是爲false
的了null、undefined
,則另外一方必須爲null或者undefined
才爲true
,也就是null == undefined
爲true
或者null == null
爲true
,由於undefined
派生於null
String
,是的話則把String
轉爲Number
再來比較Boolean
,是的話則將Boolean
轉爲Number
再來比較ToNumber
的轉換形式來進行比較(實際上它的hint
是default
,也就是toPrimitive(obj, 'default')
,可是default
的轉換規則和number
很像)在一些文章中,會說道:
若是其中一方爲Object,且另外一方爲String、Number或者Symbol,會將Object轉換成字符串,再進行比較
(摘自《神三元-(建議收藏)原生JS靈魂之問, 請問你能接得住幾個?(上)》中的3. == 和 ===有什麼區別?
)
這樣認爲其實也能夠,由於想一想toPrimitive(obj, 'number')
的過程:
valueOf()
方法valueOf()
方法的返回值是基本數據類型則直接返回,若不是則繼續調用toString()
toString()
的返回值是基本數據類型則返回,不然報錯。能夠看到,首先是會執行valueOf()
的,可是引用類型執行valueOf()
方法,除了日期類型,其它狀況都是返回它自己,也就是說執行完valueOf()
以後,仍是一個引用類型而且是它自己。那麼咱們是否是就能夠將valueOf()
這一步給省略掉,認爲它是直接執行toString()
的,這樣作起題來也快了不少。
對於幾種經常使用運算符的類型轉換:
-、*、/、%
這四種都會把符號兩邊轉成數字來進行運算+
因爲不只是數字運算符,仍是字符串的鏈接符,因此分爲兩種狀況:+b
這種狀況至關於轉換爲數字)對象的+
號類型轉換:
+
號字符串鏈接的時候,toPrimitive
的參數hint
是default
,可是default
的執行順序和number
同樣都是先判斷有沒有valueOf
,有的話執行valueOf
,而後判斷valueof
後的返回值,如果是引用類型則繼續執行toString
。(相似題4.5
和4.6
)+
號字符串鏈接的時候,優先調用toString()
方法。(相似題4.7
)這道題相信你們看的不會少,除了重寫valueOf()
你還會哪些解法呢?
解法一:重寫valueOf()
這個解法是利用了:當對象在進行==
比較的時候實際是會先執行valueOf()
,如果valueOf()
的返回值是基本數據類型就返回,不然仍是引用類型的話就會繼續調用toString()
返回,而後判斷toString()
的返回值,如果返回值爲基本數據類型就返回,不然就報錯。
如今valueOf()
每次返回的是一個數字類型,因此會直接返回。
// 1
var a = {
value: 0,
valueOf () {
return ++this.value
}
}
if (a == 1 && a == 2 && a == 3) {
console.log('成立')
}
複製代碼
解法二:重寫valueOf()
和toString()
var a = {
value: 0,
valueOf () {
return {}
},
toString () {
return ++this.value
}
}
if (a == 1 && a == 2 && a == 3) {
console.log('成立')
}
複製代碼
原理就是解法一的原理,只不過用到了當valueOf()
的返回值是引用類型的時候會繼續調用toString()
。
這裏你甚至均可以不用重寫valueOf()
,由於除了日期對象其它對象在調用valueOf()
的時候都是返回它自己。
也就是說你也能夠這樣作:
var a = {
value: 0,
toString () {
return ++this.value
}
}
if (a == 1 && a == 2 && a == 3) {
console.log('成立')
}
複製代碼
解法三:重寫Symbol.toPrimitive
想一想是否是還能夠用Symbol.toPrimitive
來解呢?
結合題3.10
咱們知道,當對象在進行==
比較的時候,Symbol.toPrimitive
接收到的參數hint
是"defalut"
,那麼咱們只須要這樣重寫:
var a = {
value: 0,
[Symbol.toPrimitive] (hint) {
if (hint === 'default') {
return ++this.value
}
}
}
if (a == 1 && a == 2 && a == 3) {
console.log('成立')
}
複製代碼
這樣結果也是能夠的。
解法四:定義class並重寫valueOf()
固然你還能夠用class
來寫:
class A {
constructor () {
this.value = 0
}
valueOf () {
return ++this.value
}
}
var a = new A()
if (a == 1 && a == 2 && a == 3) {
console.log('成立')
}
複製代碼
解法五:利用數組轉爲字符串會隱式調用join()
什麼 ? 還有別的解法嗎?並且我看解法五的題目有點沒看懂啊。
讓咱們回過頭去看看題4.3
,那裏提到了當數組在進行轉字符串的時候,調用toString()
的結果其實就是調用join
的結果。
那和這道題有什麼關係?來看看答案:
let a = [1, 2, 3]
a['join'] = function () {
return this.shift()
}
if (a == 1 && a == 2 && a == 3) {
console.log('成立')
}
複製代碼
由於咱們知道,對象若是是數組的話,當咱們不重寫其toString()
方法,在轉換爲字符串類型的時候,默認實現就是將調用join()
方法的返回值做爲toString()
的返回值。
因此這裏咱們重寫了a
的join
方法,而此次重寫作了兩件事情:
a
執行a.shift()
方法,咱們知道這會影響原數組a
的,將第一項去除因此當咱們在執行a == 1
這一步的時候,因爲隱式調用了a['join']
方法,因此會執行上面👆說的那兩件事情,後面的a == 2
和a == 3
同理。
解法六:定義class繼承Array並重寫join()
對於解法五咱們一樣能夠用class
來實現
class A extends Array {
join = this.shift
}
var a = new A(1, 2, 3)
if (a == 1 && a == 2 && a == 3) {
console.log('成立')
}
複製代碼
這種寫法比較酷🆒,可是第一次看可能不太能懂。
A
這個類經過extends
繼承於Array
,這樣經過new A
建立的就是一個數組A
重寫了join
方法,join = this.shift
就至關因而join = function () { return this.shift() }
a == xxx
的時候,都會隱式調用咱們自定義的join
方法,執行和解法五同樣的操做。這道題看着和上面那道有點像,不過這裏判斷的條件是全等的。
咱們知道全等的條件:
false
,這點和==
不一樣,==
會發生隱式類型轉換而對於上面👆一題的解法咱們都是利用了==
會發生隱式類型轉換這一點,顯然若是再用它來解決這道題是不能實現的。
想一想當咱們在進行a === xxx
判斷的時候,實際上就是調用了a
這個數據而已,也就是說咱們要在調用這個數據以前,作一些事情,來達到咱們的目的。
不知道這樣說有沒有讓你想到些什麼 🤔️?或許你和呆呆同樣會想到Vue
大名鼎鼎的數據劫持 😁。
想一想在Vue2.x
中不就是利用了Object.defineProperty()
方法從新定義data
中的全部屬性,那麼在這裏咱們一樣也能夠利用它來劫持a
,修改a
變量的get
屬性。
var value = 1;
Object.defineProperty(window, "a", {
get () {
return this.value++;
}
})
if (a === 1 && a === 2 && a === 3) {
console.log('成立')
}
複製代碼
這裏實際就作了這麼幾件事情:
Object.defineProperty()
方法劫持全局變量window
上的屬性a
a
的時候將value
自增,並返回自增後的值(其實我還想着用Proxy
來進行數據劫持,代理一下window
,將它用new Proxy()
處理一下,可是對於window
對象好像沒有效果...)
解法二
怎麼辦 😂,一碰到這種題我又想到了數組...
var arr = [1, 2, 3];
Object.defineProperty(window, "a", {
get () {
return this.arr.shift()
}
})
if (a === 1 && a === 2 && a === 3) {
console.log('成立')
}
複製代碼
中了shift()
的毒...固然,這樣也是能夠實現的。
解法三
還有就是EnoYao大佬那裏看來的騷操做:
var aᅠ = 1;
var a = 2;
var ᅠa = 3;
if (aᅠ == 1 && a == 2 && ᅠa == 3) {
console.log("成立");
}
複製代碼
說來慚愧...a
的先後隱藏的字符我打不來 😂...
這道有趣的題是從LINGLONG的一篇《【js小知識】[]+ {} =?/{} +[] =?(關於加號的隱式類型轉換)》那裏看來的。
(PS: pick一波玲瓏,這位小姐姐的文章寫的都挺好的,不過熱度都不高,你們能夠支持一下呀 😁)
OK👌,來看看題目是這樣的:
在控制檯(好比瀏覽器的控制檯)輸入:
{}+[]
複製代碼
的結果會是什麼 🤔️?
咦~這道題應該很簡單吧,根據前面的類型轉換原則,+
兩邊都轉換爲字符串,{}
轉爲"[object Object]"
,[]
轉爲""
,拼接的結果就是:
console.log({}+[]) // "[object Object]"
複製代碼
可是注意這裏的題目,是要在控制檯輸出哦。
此時我把這段代碼在控制檯輸出結果發現答案居然和預期的不同:
{}+[]
0
複製代碼
也就是說{}
被忽略了,直接執行了+[]
,結果爲0
。
知道緣由的我眼淚掉了下來,原來它和以前提到的1.toString()
有點像,也是由於JS
對於代碼解析的緣由,在控制檯或者終端中,JS
會認爲大括號{}
開頭的是一個空的代碼塊,這樣看着裏面沒有內容就會忽略它了。
因此只執行了+[]
,將其轉換爲0
。
若是咱們換個順序的話就不會有這種問題:
(圖片來源:juejin.im/post/5e6055…)
爲了證明這一點,咱們能夠把{}
當成空對象來調用一些對象的方法,看會有什麼效果:
(控制檯或者終端)
{}.toString()
複製代碼
如今的{}
依舊被認爲是代碼塊而不是一個對象,因此會報錯:
Uncaught SyntaxError: Unexpected token '.'
複製代碼
解決辦法能夠用一個()
將它擴起來:
({}).toString
複製代碼
不過這東西在實際中用的很少,我能想到的一個就是在項目中(好比我用的vue
),而後定義props
的時候,若是其中一個屬性的默認值你是想要定義爲一個空對象的話,就會用到:
props: {
target: {
type: Object,
default: () => ({})
}
}
複製代碼
對於JS
繼承我也寫了一個系列「封裝|繼承|多態」,這裏是傳送門:
具體的例子還有題目在文章中都已經說的很清楚了,這裏我就只列舉一下各個繼承的優缺點以及僞代碼。
僞代碼:
Child.prototype = new Parent()
複製代碼
思惟導圖:
優勢:
缺點:
Child.prototype = new Parent()
這樣的語句後面僞代碼:
function Child () {
Parent.call(this, ...arguments)
}
複製代碼
思惟導圖:
優勢:
缺點:
僞代碼:
// 構造繼承
function Child () {
Parent.call(this, ...arguments)
}
// 原型鏈繼承
Child.prototype = new Parent()
// 修正constructor
Child.prototype.constructor = Child
複製代碼
思惟導圖:
實現方式:
優勢:
缺點:
僞代碼:
// 構造繼承
function Child () {
Parent.call(this, ...arguments)
}
// 原型式繼承
Child.prototype = Object.create(Parent.prototype)
// 修正constructor
Child.prototype.constructor = Child
複製代碼
思惟導圖:
寄生組合繼承算是ES6
以前一種比較完美的繼承方式吧。
它避免了組合繼承中調用兩次父類構造函數,初始化兩次實例屬性的缺點。
因此它擁有了上述全部繼承方式的優勢:
instanceOf
和isPrototypeOf
方法僞代碼:
var child = Object.create(parent)
複製代碼
實現方式:
該方法的原理是建立一個構造函數,構造函數的原型指向對象,而後調用 new 操做符建立實例,並返回這個實例,本質是一個淺拷貝。
在ES5
以後能夠直接使用Object.create()
方法來實現,而在這以前就只能手動實現一個了(如題目6.2
)。
優勢:
缺點:
僞代碼:
function createAnother (original) {
var clone = Object.create(original);; // 經過調用 Object.create() 函數建立一個新對象
clone.fn = function () {}; // 以某種方式來加強對象
return clone; // 返回這個對象
}
複製代碼
實現方式:
優勢:
缺點:
僞代碼:
function Child () {
Parent.call(this)
OtherParent.call(this)
}
Child.prototype = Object.create(Parent.prototype)
Object.assign(Child.prototype, OtherParent.prototype)
Child.prototype.constructor = Child
複製代碼
思惟導圖:
僞代碼:
class Child extends Parent {
constructor (...args) {
super(...args)
}
}
複製代碼
ES6中的繼承:
extends
關鍵字來實現繼承,且繼承的效果相似於寄生組合繼承extends
實現繼承不必定要constructor
和super
,由於沒有的話會默認產生並調用它們extends
後面接着的目標不必定是class
,只要是個有prototype
屬性的函數就能夠了ES5繼承和ES6繼承的區別:
ES5
中的繼承(例如構造繼承、寄生組合繼承) ,實質上是先創造子類的實例對象this
,而後再將父類的屬性和方法添加到this
上(使用的是Parent.call(this)
)。ES6
中卻不是這樣的,它實質是先創造父類的實例對象this
(也就是使用super()
),而後再用子類的構造函數去修改this
。知識無價,支持原創。
參考文章:
你盼世界,我盼望你無bug
。這篇文章就介紹到這裏。
因爲開篇已經說了太多話了這裏就不說了🙊。
喜歡霖呆呆的小夥還但願能夠關注霖呆呆的公衆號 LinDaiDai
或者掃一掃下面的二維碼👇👇👇.
我會不定時的更新一些前端方面的知識內容以及本身的原創文章🎉
你的鼓勵就是我持續創做的主要動力 😊.
相關推薦:
《【建議星星】要就來45道Promise面試題一次爽到底(1.1w字用心整理)》
《【建議👍】再來40道this面試題酸爽繼續(1.2w字用手整理)》
《【何不三連】比繼承家業還要簡單的JS繼承題-封裝篇(牛刀小試)》