== VS ===
Q:涉及面試題:== 和 === 有什么區(qū)別?
- ==:如果對比雙方的類型不一樣的話,就會進(jìn)行類型轉(zhuǎn)換
- ===:判斷兩者類型和值是否相同。
例子??:
假如我們需要對比x和y是否相同,就會進(jìn)行如下判斷流程:
[圖片上傳失敗...(image-de4c8f-1618542354617)]
首先會判斷兩者類型是否相同。相同的話就是比大??;類型不相同的話,那么就會進(jìn)行類型轉(zhuǎn)換
會先判斷 是否 在 對比null和undefined,是的話就會返回true;也就是 null == undefined 輸出 true
判斷兩者類型是否為string 和 number,是的話,就會將 字符串 轉(zhuǎn)換為 number
<pre class="cm-s-default" style="color: rgb(89, 89, 89); margin: 0px; padding: 0px; background: none 0% 0% / auto repeat scroll padding-box border-box rgba(0, 0, 0, 0);">1 == '1' ↓ 1 == 1</pre>
- 判斷其中一方是否為boolean,是的話就會把boolean轉(zhuǎn)為number,再進(jìn)行判斷
<pre class="cm-s-default" style="color: rgb(89, 89, 89); margin: 0px; padding: 0px; background: none 0% 0% / auto repeat scroll padding-box border-box rgba(0, 0, 0, 0);">'1' == true ↓ '1' == 1 ↓ 1 == 1 //輸出 true '2' == true ↓ '2' == 1 ↓ 2 == 1 //輸出 false </pre>
- 判斷其中一方是否為object且另一方為string、number或者symbol,是的話就會把object轉(zhuǎn)為原始類型再進(jìn)行判斷
<pre class="cm-s-default" style="color: rgb(89, 89, 89); margin: 0px; padding: 0px; background: none 0% 0% / auto repeat scroll padding-box border-box rgba(0, 0, 0, 0);">'1' == { name: 'yck' } ↓ '1' == '[object Object]'</pre>
思考題:看完了上面的步驟,對于 [] == ![] 你是否能正確寫出答案呢?
過程如下:
- ![] 就是 !true 也就是 false
- [] == false
- [] == 0
- '' == 0
- 0 == 0
更多的內(nèi)容可以參考標(biāo)準(zhǔn)文檔。
閉包
Q: 什么是閉包?
閉包的定義:函數(shù) A 內(nèi)部有一個函數(shù) B,函數(shù) B 可以訪問到函數(shù) A 中的變量,那么函數(shù) B 就是閉包
閉包存在的意義:就是讓我們可以間接訪問函數(shù)內(nèi)部的變量。
<pre class="cm-s-default" style="color: rgb(89, 89, 89); margin: 0px; padding: 0px; background: none 0% 0% / auto repeat scroll padding-box border-box rgba(0, 0, 0, 0);">function A() { let a = 1 window.B = function () { console.log(a) } } A() B() // 1</pre>
很多人對于閉包的解釋可能是函數(shù)嵌套了函數(shù),然后返回一個函數(shù)。其實這個解釋是不完整的,就比如我上面這個例子就可以反駁這個觀點。
經(jīng)典面試題,循環(huán)中使用閉包解決 var 定義函數(shù)的問題
<pre class="cm-s-default" style="color: rgb(89, 89, 89); margin: 0px; padding: 0px; background: none 0% 0% / auto repeat scroll padding-box border-box rgba(0, 0, 0, 0);">for (var i = 1; i <= 5; i++) { setTimeout(function timer() { console.log(i) }, i * 1000) } // 6 6 6 6 6 6</pre>
首先因為setTimeout是個異步函數(shù),所以會先把循環(huán)全部執(zhí)行完畢,由因為是var聲明的,所以這時候i就是 6 了,所以會輸出一堆 6。
解決辦法有三種
第一種是使用閉包的方式 - 立即執(zhí)行函數(shù)
<pre class="cm-s-default" style="color: rgb(89, 89, 89); margin: 0px; padding: 0px; background: none 0% 0% / auto repeat scroll padding-box border-box rgba(0, 0, 0, 0);">for (var i = 1; i <= 5; i++) { ;(function(j) { setTimeout(function timer() { console.log(j) }, j * 1000) })(i) } </pre>
在上述代碼中,我們首先使用了立即執(zhí)行函數(shù)將i傳入函數(shù)內(nèi)部,這個時候值就被固定在了參數(shù)j上面不會改變,當(dāng)下次執(zhí)行timer這個閉包的時候,就可以使用外部函數(shù)的變量j,從而達(dá)到目的。
第二種就是使用setTimeout的第三個參數(shù),這個參數(shù)會被當(dāng)成timer函數(shù)的參數(shù)傳入
<pre class="cm-s-default" style="color: rgb(89, 89, 89); margin: 0px; padding: 0px; background: none 0% 0% / auto repeat scroll padding-box border-box rgba(0, 0, 0, 0);">for (var i = 1; i <= 5; i++) { setTimeout( function timer(j) { console.log(j) }, i * 1000, i ) }</pre>
第三種就是使用let定義i了來解決問題了,這個也是最為推薦的方式
<pre class="cm-s-default" style="color: rgb(89, 89, 89); margin: 0px; padding: 0px; background: none 0% 0% / auto repeat scroll padding-box border-box rgba(0, 0, 0, 0);">for (let i = 1; i <= 5; i++) { setTimeout(function timer() { console.log(i) }, i * 1000) }</pre>
深淺拷貝
Q:什么是淺拷貝?如何實現(xiàn)淺拷貝?什么是深拷貝?如何實現(xiàn)深拷貝?
對象類型在賦值的過程中其實是復(fù)制了地址,從而會導(dǎo)致改變了一方其他也都被改變的情況。
通常在開發(fā)中我們不希望出現(xiàn)這樣的問題,我們可以使用淺拷貝來解決這個情況。注意:淺拷貝只解決了第一層的問題,如果接下去的值中還有對象的話,那么就又回到最開始的話題了,兩者享有相同的地址。要解決這個問題,我們就得使用深拷貝了
<pre class="cm-s-default" style="color: rgb(89, 89, 89); margin: 0px; padding: 0px; background: none 0% 0% / auto repeat scroll padding-box border-box rgba(0, 0, 0, 0);">let a = { age: 1 } let b = a a.age = 2 console.log(b.age) // 2</pre>
淺拷貝
- Object.assign: let b = Object.assign({}, a)
- ... 展開運算符:let b = { ...a }
首先可以通過 Object.assign 來解決這個問題,很多人認(rèn)為這個函數(shù)是用來深拷貝的。其實并不是
Object.assign只會拷貝所有的屬性值到新的對象中,如果屬性值是對象的話,拷貝的是地址,所以并不是深拷貝。
<pre class="cm-s-default" style="color: rgb(89, 89, 89); margin: 0px; padding: 0px; background: none 0% 0% / auto repeat scroll padding-box border-box rgba(0, 0, 0, 0);">let a = { age: 1 } let b = Object.assign({}, a) a.age = 2 console.log(b.age) // 1</pre>
另外我們還可以通過展開運算符...來實現(xiàn)淺拷貝
<pre class="cm-s-default" style="color: rgb(89, 89, 89); margin: 0px; padding: 0px; background: none 0% 0% / auto repeat scroll padding-box border-box rgba(0, 0, 0, 0);">let a = { age: 1 } let b = { ...a } a.age = 2 console.log(b.age) // 1</pre>
通常淺拷貝就能解決大部分問題了,但是當(dāng)我們遇到如下情況就可能需要使用到深拷貝了
<pre class="cm-s-default" style="color: rgb(89, 89, 89); margin: 0px; padding: 0px; background: none 0% 0% / auto repeat scroll padding-box border-box rgba(0, 0, 0, 0);">let a = { age: 1, jobs: { first: 'FE' } } let b = { ...a } a.jobs.first = 'native' console.log(b.jobs.first) // native</pre>
淺拷貝只解決了第一層的問題,如果接下去的值中還有對象的話,那么就又回到最開始的話題了,兩者享有相同的地址。要解決這個問題,我們就得使用深拷貝了。
深拷貝
這個問題通??梢酝ㄟ^ JSON.parse(JSON.stringify(object)) 來解決。
<pre class="cm-s-default" style="color: rgb(89, 89, 89); margin: 0px; padding: 0px; background: none 0% 0% / auto repeat scroll padding-box border-box rgba(0, 0, 0, 0);">let a = { age: 1, jobs: { first: 'FE' } } let b = JSON.parse(JSON.stringify(a)) a.jobs.first = 'native' console.log(b.jobs.first) // FE</pre>
但是該方法也是有局限性的:
- 會忽略undefined
- 會忽略symbol
- 不能序列化函數(shù)
- 不能解決循環(huán)引用的對象
<pre class="cm-s-default" style="color: rgb(89, 89, 89); margin: 0px; padding: 0px; background: none 0% 0% / auto repeat scroll padding-box border-box rgba(0, 0, 0, 0);">let obj = { a: 1, b: { c: 2, d: 3, }, } obj.c = obj.b obj.e = obj.a obj.b.c = obj.c obj.b.d = obj.b obj.b.e = obj.b.c let newObj = JSON.parse(JSON.stringify(obj)) console.log(newObj)</pre>
如果你有這么一個循環(huán)引用對象,你會發(fā)現(xiàn)并不能通過該方法實現(xiàn)深拷貝
[圖片上傳失敗...(image-d235ca-1618542354616)]
在遇到函數(shù)、undefined****或者****symbol的時候,該對象也不能正常的序列化
<pre class="cm-s-default" style="color: rgb(89, 89, 89); margin: 0px; padding: 0px; background: none 0% 0% / auto repeat scroll padding-box border-box rgba(0, 0, 0, 0);">let a = { age: undefined, sex: Symbol('male'), jobs: function() {}, name: 'yck' } let b = JSON.parse(JSON.stringify(a)) console.log(b) // {name: "yck"} </pre>
你會發(fā)現(xiàn)在上述情況中,該方法會忽略掉函數(shù)和undefined和symbol。
但是在通常情況下,復(fù)雜數(shù)據(jù)都是可以序列化的,所以這個函數(shù)可以解決大部分問題。
如果你所需拷貝的對象含有內(nèi)置類型并且不包含函數(shù),可以使用MessageChannel
<pre class="cm-s-default" style="color: rgb(89, 89, 89); margin: 0px; padding: 0px; background: none 0% 0% / auto repeat scroll padding-box border-box rgba(0, 0, 0, 0);">function structuralClone(obj) { return new Promise(resolve => { const { port1, port2 } = new MessageChannel() port2.onmessage = ev => resolve(ev.data) port1.postMessage(obj) }) } var obj = { a: 1, b: { c: 2 } } obj.b.d = obj.b // 注意該方法是異步的 // 可以處理 undefined 和循環(huán)引用對象 const test = async () => { const clone = await structuralClone(obj) console.log(clone) } test()</pre>
當(dāng)然你可能想自己來實現(xiàn)一個深拷貝,但是其實實現(xiàn)一個深拷貝是很困難的,需要我們考慮好多種邊界情況,比如原型鏈如何處理、DOM 如何處理等等,所以這里我們實現(xiàn)的深拷貝只是簡易版,并且我其實更推薦使用lodash 的深拷貝函數(shù)。
自己實現(xiàn)一個深拷貝
<pre class="cm-s-default" style="color: rgb(89, 89, 89); margin: 0px; padding: 0px; background: none 0% 0% / auto repeat scroll padding-box border-box rgba(0, 0, 0, 0);">function deepClone(obj) { function isObject(o) { return (typeof o === 'object' || typeof o === 'function') && o !== null } if (!isObject(obj)) { throw new Error('非對象') } let isArray = Array.isArray(obj) let newObj = isArray ? [...obj] : { ...obj } Reflect.ownKeys(newObj).forEach(key => { newObj[key] = isObject(obj[key]) ? deepClone(obj[key]) : obj[key] }) return newObj } let obj = { a: [1, 2, 3], b: { c: 2, d: 3 } } let newObj = deepClone(obj) newObj.b.c = 1 console.log(obj.b.c) // 2</pre>
原型
Q:如何理解原型?如何理解原型鏈?
當(dāng)我們創(chuàng)建一個對象時let obj = { age: 25 },我們可以發(fā)現(xiàn)能使用很多種函數(shù),但是我們明明沒有定義過它們,對于這種情況你是否有過疑惑?
[圖片上傳失敗...(image-b77be5-1618542354616)]
當(dāng)我們在瀏覽器中打印obj時你會發(fā)現(xiàn),在obj上居然還有一個proto屬性,那么看來之前的疑問就和這個屬性有關(guān)系了。
其實每個 JS 對象都有proto屬性,這個屬性指向了原型。這個屬性在現(xiàn)在來說已經(jīng)不推薦直接去使用它了,這只是瀏覽器在早期為了讓我們訪問到內(nèi)部屬性[[prototype]]來實現(xiàn)的一個東西。
講到這里好像還是沒有弄明白什么是原型,接下來讓我們再看看proto里面有什么吧。
[圖片上傳失敗...(image-1ea8e0-1618542354616)]
看到這里你應(yīng)該明白了,原型也是一個對象,并且這個對象中包含了很多函數(shù),所以我們可以得出一個結(jié)論:對于obj來說,可以通過proto找到一個原型對象,在該對象中定義了很多函數(shù)讓我們來使用。
在上面的圖中我們還可以發(fā)現(xiàn)一個constructor屬性,也就是構(gòu)造函數(shù)
[圖片上傳失敗...(image-41d937-1618542354616)]
打開constructor屬性我們又可以發(fā)現(xiàn)其中還有一個prototype屬性,并且這個屬性對應(yīng)的值和先前我們在proto中看到的一模一樣。
所以我們又可以得出一個結(jié)論:原型的constructor屬性指向構(gòu)造函數(shù),構(gòu)造函數(shù)又通過prototype屬性指回原型,但是并不是所有函數(shù)都具有這個屬性,F(xiàn)unction.prototype.bind()就沒有這個屬性。
其實原型就是那么簡單,接下來我們再來看一張圖,相信這張圖能讓你徹底明白原型和原型鏈
[圖片上傳失敗...(image-84da7-1618542354616)]
其實原型鏈就是多個對象通過proto的方式連接了起來。
為什么obj可以訪問到valueOf函數(shù),就是因為obj通過原型鏈找到了valueOf函數(shù)。
總結(jié)起來就是以下幾點:
- Object是所有對象的爸爸,所有對象都可以通過proto找到它
- Function是所有函數(shù)的爸爸,所有函數(shù)都可以通過proto找到它
- 函數(shù)的prototype是一個對象
- 對象的proto屬性指向原型,proto將對象和原型連接起來組成了原型鏈