摘要:如果你還沒讀過上篇上篇和中篇并無依賴關(guān)系,您可以讀過本文之后再閱讀上篇,可戳面試篇寒冬求職季之你必須要懂的原生上小姐姐花了近百個小時才完成這篇文章,篇幅較長,希望大家閱讀時多花點耐心,力求真正的掌握相關(guān)知識點。
互聯(lián)網(wǎng)寒冬之際,各大公司都縮減了HC,甚至是采取了“裁員”措施,在這樣的大環(huán)境之下,想要獲得一份更好的工作,必然需要付出更多的努力。
一年前,也許你搞清楚閉包,this,原型鏈,就能獲得認可。但是現(xiàn)在,很顯然是不行了。本文梳理出了一些面試中有一定難度的高頻原生JS問題,部分知識點可能你之前從未關(guān)注過,或者看到了,卻沒有仔細研究,但是它們卻非常重要。本文將以真實的面試題的形式來呈現(xiàn)知識點,大家在閱讀時,建議不要先看我的答案,而是自己先思考一番。盡管,本文所有的答案,都是我在翻閱各種資料,思考并驗證之后,才給出的(絕非復(fù)制粘貼而來)。但因水平有限,本人的答案未必是最優(yōu)的,如果您有更好的答案,歡迎給我留言。
本文篇幅較長,但是滿滿的都是干貨!并且還埋伏了可愛的表情包,希望小伙伴們能夠堅持讀完。
寫文超級真誠的小姐姐祝愿大家都能找到心儀的工作。
如果你還沒讀過上篇【上篇和中篇并無依賴關(guān)系,您可以讀過本文之后再閱讀上篇】,可戳【面試篇】寒冬求職季之你必須要懂的原生JS(上)
小姐姐花了近百個小時才完成這篇文章,篇幅較長,希望大家閱讀時多花點耐心,力求真正的掌握相關(guān)知識點。
1.說一說JS異步發(fā)展史異步最早的解決方案是回調(diào)函數(shù),如事件的回調(diào),setInterval/setTimeout中的回調(diào)。但是回調(diào)函數(shù)有一個很常見的問題,就是回調(diào)地獄的問題(稍后會舉例說明);
為了解決回調(diào)地獄的問題,社區(qū)提出了Promise解決方案,ES6將其寫進了語言標準。Promise解決了回調(diào)地獄的問題,但是Promise也存在一些問題,如錯誤不能被try catch,而且使用Promise的鏈式調(diào)用,其實并沒有從根本上解決回調(diào)地獄的問題,只是換了一種寫法。
ES6中引入 Generator 函數(shù),Generator是一種異步編程解決方案,Generator 函數(shù)是協(xié)程在 ES6 的實現(xiàn),最大特點就是可以交出函數(shù)的執(zhí)行權(quán),Generator 函數(shù)可以看出是異步任務(wù)的容器,需要暫停的地方,都用yield語句注明。但是 Generator 使用起來較為復(fù)雜。
ES7又提出了新的異步解決方案:async/await,async是 Generator 函數(shù)的語法糖,async/await 使得異步代碼看起來像同步代碼,異步編程發(fā)展的目標就是讓異步邏輯的代碼看起來像同步一樣。
1.回調(diào)函數(shù): callback
//node讀取文件 fs.readFile(xxx, "utf-8", function(err, data) { //code });
回調(diào)函數(shù)的使用場景(包括但不限于):
事件回調(diào)
Node API
setTimeout/setInterval中的回調(diào)函數(shù)
異步回調(diào)嵌套會導(dǎo)致代碼難以維護,并且不方便統(tǒng)一處理錯誤,不能try catch 和 回調(diào)地獄(如先讀取A文本內(nèi)容,再根據(jù)A文本內(nèi)容讀取B再根據(jù)B的內(nèi)容讀取C...)。
fs.readFile(A, "utf-8", function(err, data) { fs.readFile(B, "utf-8", function(err, data) { fs.readFile(C, "utf-8", function(err, data) { fs.readFile(D, "utf-8", function(err, data) { //.... }); }); }); });
2.Promise
Promise 主要解決了回調(diào)地獄的問題,Promise 最早由社區(qū)提出和實現(xiàn),ES6 將其寫進了語言標準,統(tǒng)一了用法,原生提供了Promise對象。
那么我們看看Promise是如何解決回調(diào)地獄問題的,仍然以上文的readFile為例。
function read(url) { return new Promise((resolve, reject) => { fs.readFile(url, "utf8", (err, data) => { if(err) reject(err); resolve(data); }); }); } read(A).then(data => { return read(B); }).then(data => { return read(C); }).then(data => { return read(D); }).catch(reason => { console.log(reason); });
想要運行代碼看效果,請戳(小姐姐使用的是VS的 Code Runner 執(zhí)行代碼): https://github.com/YvetteLau/...
思考一下在Promise之前,你是如何處理異步并發(fā)問題的,假設(shè)有這樣一個需求:讀取三個文件內(nèi)容,都讀取成功后,輸出最終的結(jié)果。有了Promise之后,又如何處理呢?代碼可戳: https://github.com/YvetteLau/...
注: 可以使用 bluebird 將接口 promise化;
引申: Promise有哪些優(yōu)點和問題呢?
3.Generator
Generator 函數(shù)是 ES6 提供的一種異步編程解決方案,整個 Generator 函數(shù)就是一個封裝的異步任務(wù),或者說是異步任務(wù)的容器。異步操作需要暫停的地方,都用 yield 語句注明。
Generator 函數(shù)一般配合 yield 或 Promise 使用。Generator函數(shù)返回的是迭代器。對生成器和迭代器不了解的同學(xué),請自行補習(xí)下基礎(chǔ)。下面我們看一下 Generator 的簡單使用:
function* gen() { let a = yield 111; console.log(a); let b = yield 222; console.log(b); let c = yield 333; console.log(c); let d = yield 444; console.log(d); } let t = gen(); //next方法可以帶一個參數(shù),該參數(shù)就會被當作上一個yield表達式的返回值 t.next(1); //第一次調(diào)用next函數(shù)時,傳遞的參數(shù)無效 t.next(2); //a輸出2; t.next(3); //b輸出3; t.next(4); //c輸出4; t.next(5); //d輸出5;
為了讓大家更好的理解上面代碼是如何執(zhí)行的,我畫了一張圖,分別對應(yīng)每一次的next方法調(diào)用:
仍然以上文的readFile為例,使用 Generator + co庫來實現(xiàn):
const fs = require("fs"); const co = require("co"); const bluebird = require("bluebird"); const readFile = bluebird.promisify(fs.readFile); function* read() { yield readFile(A, "utf-8"); yield readFile(B, "utf-8"); yield readFile(C, "utf-8"); //.... } co(read()).then(data => { //code }).catch(err => { //code });
不使用co庫,如何實現(xiàn)?能否自己寫一個最簡的my_co?請戳: https://github.com/YvetteLau/...
PS: 如果你還不太了解 Generator/yield,建議閱讀ES6相關(guān)文檔。
4.async/await
ES7中引入了 async/await 概念。async其實是一個語法糖,它的實現(xiàn)就是將Generator函數(shù)和自動執(zhí)行器(co),包裝在一個函數(shù)中。
async/await 的優(yōu)點是代碼清晰,不用像 Promise 寫很多 then 鏈,就可以處理回調(diào)地獄的問題。錯誤可以被try catch。
仍然以上文的readFile為例,使用 Generator + co庫來實現(xiàn):
const fs = require("fs"); const bluebird = require("bluebird"); const readFile = bluebird.promisify(fs.readFile); async function read() { await readFile(A, "utf-8"); await readFile(B, "utf-8"); await readFile(C, "utf-8"); //code } read().then((data) => { //code }).catch(err => { //code });
可執(zhí)行代碼,請戳:https://github.com/YvetteLau/...
思考一下 async/await 如何處理異步并發(fā)問題的? https://github.com/YvetteLau/...
如果你有更好的答案或想法,歡迎在這題目對應(yīng)的github下留言:說一說JS異步發(fā)展史
2.談?wù)剬?async/await 的理解,async/await 的實現(xiàn)原理是什么?async/await 就是 Generator 的語法糖,使得異步操作變得更加方便。來張圖對比一下:
async 函數(shù)就是將 Generator 函數(shù)的星號(*)替換成 async,將 yield 替換成await。
我們說 async 是 Generator 的語法糖,那么這個糖究竟甜在哪呢?
1)async函數(shù)內(nèi)置執(zhí)行器,函數(shù)調(diào)用之后,會自動執(zhí)行,輸出最后結(jié)果。而Generator需要調(diào)用next或者配合co模塊使用。
2)更好的語義,async和await,比起星號和yield,語義更清楚了。async表示函數(shù)里有異步操作,await表示緊跟在后面的表達式需要等待結(jié)果。
3)更廣的適用性。co模塊約定,yield命令后面只能是 Thunk 函數(shù)或 Promise 對象,而async 函數(shù)的 await 命令后面,可以是 Promise 對象和原始類型的值。
4)返回值是Promise,async函數(shù)的返回值是 Promise 對象,Generator的返回值是 Iterator,Promise 對象使用起來更加方便。
async 函數(shù)的實現(xiàn)原理,就是將 Generator 函數(shù)和自動執(zhí)行器,包裝在一個函數(shù)里。
具體代碼試下如下(和spawn的實現(xiàn)略有差異,個人覺得這樣寫更容易理解),如果你想知道如何一步步寫出 my_co ,可戳: https://github.com/YvetteLau/...
function my_co(it) { return new Promise((resolve, reject) => { function next(data) { try { var { value, done } = it.next(data); }catch(e){ return reject(e); } if (!done) { //done為true,表示迭代完成 //value 不一定是 Promise,可能是一個普通值。使用 Promise.resolve 進行包裝。 Promise.resolve(value).then(val => { next(val); }, reject); } else { resolve(value); } } next(); //執(zhí)行一次next }); } function* test() { yield new Promise((resolve, reject) => { setTimeout(resolve, 100); }); yield new Promise((resolve, reject) => { // throw Error(1); resolve(10) }); yield 10; return 1000; } my_co(test()).then(data => { console.log(data); //輸出1000 }).catch((err) => { console.log("err: ", err); });
如果你有更好的答案或想法,歡迎在這題目對應(yīng)的github下留言:談?wù)剬?async/await 的理解,async/await 的實現(xiàn)原理是什么?
3.使用 async/await 需要注意什么?await 命令后面的Promise對象,運行結(jié)果可能是 rejected,此時等同于 async 函數(shù)返回的 Promise 對象被reject。因此需要加上錯誤處理,可以給每個 await 后的 Promise 增加 catch 方法;也可以將 await 的代碼放在 try...catch 中。
多個await命令后面的異步操作,如果不存在繼發(fā)關(guān)系,最好讓它們同時觸發(fā)。
//下面兩種寫法都可以同時觸發(fā) //法一 async function f1() { await Promise.all([ new Promise((resolve) => { setTimeout(resolve, 600); }), new Promise((resolve) => { setTimeout(resolve, 600); }) ]) } //法二 async function f2() { let fn1 = new Promise((resolve) => { setTimeout(resolve, 800); }); let fn2 = new Promise((resolve) => { setTimeout(resolve, 800); }) await fn1; await fn2; }
await命令只能用在async函數(shù)之中,如果用在普通函數(shù),會報錯。
async 函數(shù)可以保留運行堆棧。
/** * 函數(shù)a內(nèi)部運行了一個異步任務(wù)b()。當b()運行的時候,函數(shù)a()不會中斷,而是繼續(xù)執(zhí)行。 * 等到b()運行結(jié)束,可能a()早就* 運行結(jié)束了,b()所在的上下文環(huán)境已經(jīng)消失了。 * 如果b()或c()報錯,錯誤堆棧將不包括a()。 */ function b() { return new Promise((resolve, reject) => { setTimeout(resolve, 200) }); } function c() { throw Error(10); } const a = () => { b().then(() => c()); }; a(); /** * 改成async函數(shù) */ const m = async () => { await b(); c(); }; m();
報錯信息如下,可以看出 async 函數(shù)可以保留運行堆棧。
如果你有更好的答案或想法,歡迎在這題目對應(yīng)的github下留言:使用 async/await 需要注意什么?
4.如何實現(xiàn) Promise.race?在代碼實現(xiàn)前,我們需要先了解 Promise.race 的特點:
Promise.race返回的仍然是一個Promise.
它的狀態(tài)與第一個完成的Promise的狀態(tài)相同。它可以是完成( resolves),也可以是失敗(rejects),這要取決于第一個Promise是哪一種狀態(tài)。
如果傳入的參數(shù)是不可迭代的,那么將會拋出錯誤。
如果傳的參數(shù)數(shù)組是空,那么返回的 promise 將永遠等待。
如果迭代包含一個或多個非承諾值和/或已解決/拒絕的承諾,則 Promise.race 將解析為迭代中找到的第一個值。
Promise.race = function (promises) { //promises 必須是一個可遍歷的數(shù)據(jù)結(jié)構(gòu),否則拋錯 return new Promise((resolve, reject) => { if (typeof promises[Symbol.iterator] !== "function") { //真實不是這個錯誤 Promise.reject("args is not iteratable!"); } if (promises.length === 0) { return; } else { for (let i = 0; i < promises.length; i++) { Promise.resolve(promises[i]).then((data) => { resolve(data); return; }, (err) => { reject(err); return; }); } } }); }
測試代碼:
//一直在等待態(tài) Promise.race([]).then((data) => { console.log("success ", data); }, (err) => { console.log("err ", err); }); //拋錯 Promise.race().then((data) => { console.log("success ", data); }, (err) => { console.log("err ", err); }); Promise.race([ new Promise((resolve, reject) => { setTimeout(() => { resolve(100) }, 1000) }), new Promise((resolve, reject) => { setTimeout(() => { resolve(200) }, 200) }), new Promise((resolve, reject) => { setTimeout(() => { reject(100) }, 100) }) ]).then((data) => { console.log(data); }, (err) => { console.log(err); });
引申: Promise.all/Promise.reject/Promise.resolve/Promise.prototype.finally/Promise.prototype.catch 的實現(xiàn)原理,如果還不太會,戳:Promise源碼實現(xiàn)
如果你有更好的答案或想法,歡迎在這題目對應(yīng)的github下留言:如何實現(xiàn) Promise.race?
5.可遍歷數(shù)據(jù)結(jié)構(gòu)的有什么特點?一個對象如果要具備可被 for...of 循環(huán)調(diào)用的 Iterator 接口,就必須在其 Symbol.iterator 的屬性上部署遍歷器生成方法(或者原型鏈上的對象具有該方法)
PS: 遍歷器對象根本特征就是具有next方法。每次調(diào)用next方法,都會返回一個代表當前成員的信息對象,具有value和done兩個屬性。
//如為對象添加Iterator 接口; let obj = { name: "Yvette", age: 18, job: "engineer", [Symbol.iterator]() { const self = this; const keys = Object.keys(self); let index = 0; return { next() { if (index < keys.length) { return { value: self[keys[index++]], done: false }; } else { return { value: undefined, done: true }; } } }; } }; for(let item of obj) { console.log(item); //Yvette 18 engineer }
使用 Generator 函數(shù)(遍歷器對象生成函數(shù))簡寫 Symbol.iterator 方法,可以簡寫如下:
let obj = { name: "Yvette", age: 18, job: "engineer", * [Symbol.iterator] () { const self = this; const keys = Object.keys(self); for (let index = 0;index < keys.length; index++) { yield self[keys[index]];//yield表達式僅能使用在 Generator 函數(shù)中 } } };
原生具備 Iterator 接口的數(shù)據(jù)結(jié)構(gòu)如下。
Array
Map
Set
String
TypedArray
函數(shù)的 arguments 對象
NodeList 對象
ES6 的數(shù)組、Set、Map 都部署了以下三個方法: entries() / keys() / values(),調(diào)用后都返回遍歷器對象。
如果你有更好的答案或想法,歡迎在這題目對應(yīng)的github下留言:可遍歷數(shù)據(jù)結(jié)構(gòu)的有什么特點?
6.requestAnimationFrame 和 setTimeout/setInterval 有什么區(qū)別?使用 requestAnimationFrame 有哪些好處?在 requestAnimationFrame 之前,我們主要使用 setTimeout/setInterval 來編寫JS動畫。
編寫動畫的關(guān)鍵是循環(huán)間隔的設(shè)置,一方面,循環(huán)間隔足夠短,動畫效果才能顯得平滑流暢;另一方面,循環(huán)間隔還要足夠長,才能確保瀏覽器有能力渲染產(chǎn)生的變化。
大部分的電腦顯示器的刷新頻率是60HZ,也就是每秒鐘重繪60次。大多數(shù)瀏覽器都會對重繪操作加以限制,不超過顯示器的重繪頻率,因為即使超過那個頻率用戶體驗也不會提升。因此,最平滑動畫的最佳循環(huán)間隔是 1000ms / 60 ,約為16.7ms。
setTimeout/setInterval 有一個顯著的缺陷在于時間是不精確的,setTimeout/setInterval 只能保證延時或間隔不小于設(shè)定的時間。因為它們實際上只是把任務(wù)添加到了任務(wù)隊列中,但是如果前面的任務(wù)還沒有執(zhí)行完成,它們必須要等待。
requestAnimationFrame 才有的是系統(tǒng)時間間隔,保持最佳繪制效率,不會因為間隔時間過短,造成過度繪制,增加開銷;也不會因為間隔時間太長,使用動畫卡頓不流暢,讓各種網(wǎng)頁動畫效果能夠有一個統(tǒng)一的刷新機制,從而節(jié)省系統(tǒng)資源,提高系統(tǒng)性能,改善視覺效果。
綜上所述,requestAnimationFrame 和 setTimeout/setInterval 在編寫動畫時相比,優(yōu)點如下:
1.requestAnimationFrame 不需要設(shè)置時間,采用系統(tǒng)時間間隔,能達到最佳的動畫效果。
2.requestAnimationFrame 會把每一幀中的所有DOM操作集中起來,在一次重繪或回流中就完成。
3.當 requestAnimationFrame() 運行在后臺標簽頁或者隱藏的 里時,requestAnimationFrame() 會被暫停調(diào)用以提升性能和電池壽命(大多數(shù)瀏覽器中)。
requestAnimationFrame 使用(試試使用requestAnimationFrame寫一個移動的小球,從A移動到B初):
function step(timestamp) { //code... window.requestAnimationFrame(step); } window.requestAnimationFrame(step);
如果你有更好的答案或想法,歡迎在這題目對應(yīng)的github下留言:requestAnimationFrame 和 setTimeout/setInterval 有什么區(qū)別?使用 requestAnimationFrame 有哪些好處?
7.JS 類型轉(zhuǎn)換的規(guī)則是什么?類型轉(zhuǎn)換的規(guī)則三言兩語說不清,真想哇得一聲哭出來~

JS中類型轉(zhuǎn)換分為 強制類型轉(zhuǎn)換 和 隱式類型轉(zhuǎn)換 。
通過 Number()、parseInt()、parseFloat()、toString()、String()、Boolean(),進行強制類型轉(zhuǎn)換。
邏輯運算符(&&、 ||、 !)、運算符(+、-、*、/)、關(guān)系操作符(>、 <、 <= 、>=)、相等運算符(==)或者 if/while 的條件,可能會進行隱式類型轉(zhuǎn)換。
強制類型轉(zhuǎn)換
1.Number() 將任意類型的參數(shù)轉(zhuǎn)換為數(shù)值類型
規(guī)則如下:
如果是布爾值,true和false分別被轉(zhuǎn)換為1和0
如果是數(shù)字,返回自身
如果是 null,返回 0
如果是 undefined,返回 NAN
如果是字符串,遵循以下規(guī)則:
1. 如果字符串中只包含數(shù)字(或者是 `0X` / `0x` 開頭的十六進制數(shù)字字符串,允許包含正負號),則將其轉(zhuǎn)換為十進制 2. 如果字符串中包含有效的浮點格式,將其轉(zhuǎn)換為浮點數(shù)值 3. 如果是空字符串,將其轉(zhuǎn)換為0 4. 如不是以上格式的字符串,均返回 `NaN`
如果是Symbol,拋出錯誤
如果是對象,則調(diào)用對象的 valueOf() 方法,然后依據(jù)前面的規(guī)則轉(zhuǎn)換返回的值。如果轉(zhuǎn)換的結(jié)果是 NaN ,則調(diào)用對象的 toString() 方法,再次依照前面的規(guī)則轉(zhuǎn)換返回的字符串值。
部分內(nèi)置對象調(diào)用默認的 valueOf 的行為:
對象 | 返回值 |
---|---|
Array | 數(shù)組本身(對象類型) |
Boolean | 布爾值(原始類型) |
Date | 從 UTC 1970 年 1 月 1 日午夜開始計算,到所封裝的日期所經(jīng)過的毫秒數(shù) |
Function | 函數(shù)本身(對象類型) |
Number | 數(shù)字值(原始類型) |
Object | 對象本身(對象類型) |
String | 字符串值(原始類型) |
Number("0111"); //111 Number("0X11") //17 Number(null); //0 Number(""); //0 Number("1a"); //NaN Number(-0X11);//-17
2.parseInt(param, radix)
如果第一個參數(shù)傳入的是字符串類型:
忽略字符串前面的空格,直至找到第一個非空字符,如果是空字符串,返回NaN
如果第一個字符不是數(shù)字符號或者正負號,返回NaN
如果第一個字符是數(shù)字/正負號,則繼續(xù)解析直至字符串解析完畢或者遇到一個非數(shù)字符號為止
如果第一個參數(shù)傳入的Number類型:
數(shù)字如果是0開頭,則將其當作八進制來解析(如果是一個八進制數(shù));如果以0x開頭,則將其當作十六進制來解析
如果第一個參數(shù)是 null 或者是 undefined,或者是一個對象類型:
返回 NaN
如果第一個參數(shù)是數(shù)組:
1. 去數(shù)組的第一個元素,按照上面的規(guī)則進行解析
如果第一個參數(shù)是Symbol類型:
1. 拋出錯誤
如果指定radix參數(shù),以radix為基數(shù)進行解析
parseInt("0111"); //111 parseInt(0111); //八進制數(shù) 73 parseInt("");//NaN parseInt("0X11"); //17 parseInt("1a") //1 parseInt("a1"); //NaN parseInt(["10aa","aaa"]);//10 parseInt([]);//NaN; parseInt(undefined);
parseFloat
規(guī)則和parseInt基本相同,接受一個Number類型或字符串,如果是字符串中,那么只有第一個小數(shù)點是有效的。
toString()
規(guī)則如下:
如果是Number類型,輸出數(shù)字字符串
如果是 null 或者是 undefined,拋錯
如果是數(shù)組,那么將數(shù)組展開輸出??諗?shù)組,返回""
如果是對象,返回 [object Object]
如果是Date, 返回日期的文字表示法
如果是函數(shù),輸出對應(yīng)的字符串(如下demo)
如果是Symbol,輸出Symbol字符串
let arry = []; let obj = {a:1}; let sym = Symbol(100); let date = new Date(); let fn = function() {console.log("穩(wěn)住,我們能贏!")} let str = "hello world"; console.log([].toString()); // "" console.log([1, 2, 3, undefined, 5, 6].toString());//1,2,3,,5,6 console.log(arry.toString()); // 1,2,3 console.log(obj.toString()); // [object Object] console.log(date.toString()); // Sun Apr 21 2019 16:11:39 GMT+0800 (CST) console.log(fn.toString());// function () {console.log("穩(wěn)住,我們能贏!")} console.log(str.toString());// "hello world" console.log(sym.toString());// Symbol(100) console.log(undefined.toString());// 拋錯 console.log(null.toString());// 拋錯
String()
String() 的轉(zhuǎn)換規(guī)則與 toString() 基本一致,最大的一點不同在于 null 和 undefined,使用 String 進行轉(zhuǎn)換,null 和 undefined對應(yīng)的是字符串 "null" 和 "undefined"
Boolean
除了 undefined、 null、 false、 ""、 0(包括 +0,-0)、 NaN 轉(zhuǎn)換出來是false,其它都是true.
隱式類型轉(zhuǎn)換
&& 、|| 、 ! 、 if/while 的條件判斷
需要將數(shù)據(jù)轉(zhuǎn)換成 Boolean 類型,轉(zhuǎn)換規(guī)則同 Boolean 強制類型轉(zhuǎn)換
運算符: + - * /
+ 號操作符,不僅可以用作數(shù)字相加,還可以用作字符串拼接。
僅當 + 號兩邊都是數(shù)字時,進行的是加法運算。如果兩邊都是字符串,直接拼接,無需進行隱式類型轉(zhuǎn)換。
除了上面的情況外,如果操作數(shù)是對象、數(shù)值或者布爾值,則調(diào)用toString()方法取得字符串值(toString轉(zhuǎn)換規(guī)則)。對于 undefined 和 null,分別調(diào)用String()顯式轉(zhuǎn)換為字符串,然后再進行拼接。
console.log({}+10); //[object Object]10 console.log([1, 2, 3, undefined, 5, 6] + 10);//1,2,3,,5,610
-、*、/ 操作符針對的是運算,如果操作值之一不是數(shù)值,則被隱式調(diào)用Number()函數(shù)進行轉(zhuǎn)換。如果其中有一個轉(zhuǎn)換除了為NaN,結(jié)果為NaN.
關(guān)系操作符: ==、>、< 、<=、>=
> , < ,<= ,>=
如果兩個操作值都是數(shù)值,則進行數(shù)值比較
如果兩個操作值都是字符串,則比較字符串對應(yīng)的字符編碼值
如果有一方是Symbol類型,拋出錯誤
除了上述情況之外,都進行Number()進行類型轉(zhuǎn)換,然后再進行比較。
注: NaN是非常特殊的值,它不和任何類型的值相等,包括它自己,同時它與任何類型的值比較大小時都返回false。
console.log(10 > {});//返回false. /** *{}.valueOf ---> {} *{}.toString() ---> "[object Object]" ---> NaN *NaN 和 任何類型比大小,都返回 false */
相等操作符:==
如果類型相同,無需進行類型轉(zhuǎn)換。
如果其中一個操作值是 null 或者是 undefined,那么另一個操作符必須為 null 或者 undefined 時,才返回 true,否則都返回 false.
如果其中一個是 Symbol 類型,那么返回 false.
兩個操作值是否為 string 和 number,就會將字符串轉(zhuǎn)換為 number
如果一個操作值是 boolean,那么轉(zhuǎn)換成 number
如果一個操作值為 object 且另一方為 string、number 或者 symbol,是的話就會把 object 轉(zhuǎn)為原始類型再進行判斷(調(diào)用object的valueOf/toString方法進行轉(zhuǎn)換)
對象如何轉(zhuǎn)換成原始數(shù)據(jù)類型
如果部署了 [Symbol.toPrimitive] 接口,那么調(diào)用此接口,若返回的不是基礎(chǔ)數(shù)據(jù)類型,拋出錯誤。
如果沒有部署 [Symbol.toPrimitive] 接口,那么先返回 valueOf() 的值,若返回的不是基礎(chǔ)類型的值,再返回 toString() 的值,若返回的不是基礎(chǔ)類型的值, 則拋出異常。
//先調(diào)用 valueOf, 后調(diào)用 toString let obj = { [Symbol.toPrimitive]() { return 200; }, valueOf() { return 300; }, toString() { return "Hello"; } } //如果 valueOf 返回的不是基本數(shù)據(jù)類型,則會調(diào)用 toString, //如果 toString 返回的也不是基本數(shù)據(jù)類型,會拋出錯誤 console.log(obj + 200); //400
如果你有更好的答案或想法,歡迎在這題目對應(yīng)的github下留言:JS 類型轉(zhuǎn)換的規(guī)則是什么?
8.簡述下對 webWorker 的理解?HTML5則提出了 Web Worker 標準,表示js允許多線程,但是子線程完全受主線程控制并且不能操作dom,只有主線程可以操作dom,所以js本質(zhì)上依然是單線程語言。
web worker就是在js單線程執(zhí)行的基礎(chǔ)上開啟一個子線程,進行程序處理,而不影響主線程的執(zhí)行,當子線程執(zhí)行完之后再回到主線程上,在這個過程中不影響主線程的執(zhí)行。子線程與主線程之間提供了數(shù)據(jù)交互的接口postMessage和onmessage,來進行數(shù)據(jù)發(fā)送和接收。
var worker = new Worker("./worker.js"); //創(chuàng)建一個子線程 worker.postMessage("Hello"); worker.onmessage = function (e) { console.log(e.data); //Hi worker.terminate(); //結(jié)束線程 };
//worker.js onmessage = function (e) { console.log(e.data); //Hello postMessage("Hi"); //向主進程發(fā)送消息 };
僅是最簡示例代碼,項目中通常是將一些耗時較長的代碼,放在子線程中運行。
如果你有更好的答案或想法,歡迎在這題目對應(yīng)的github下留言:簡述下對 webWorker 的理解
9.ES6模塊和CommonJS模塊的差異?ES6模塊在編譯時,就能確定模塊的依賴關(guān)系,以及輸入和輸出的變量。
CommonJS 模塊,運行時加載。
ES6 模塊自動采用嚴格模式,無論模塊頭部是否寫了 "use strict";
require 可以做動態(tài)加載,import 語句做不到,import 語句必須位于頂層作用域中。
ES6 模塊中頂層的 this 指向 undefined,ommonJS 模塊的頂層 this 指向當前模塊。
CommonJS 模塊輸出的是一個值的拷貝,ES6 模塊輸出的是值的引用。
CommonJS 模塊輸出的是值的拷貝,也就是說,一旦輸出一個值,模塊內(nèi)部的變化就影響不到這個值。如:
//name.js var name = "William"; setTimeout(() => name = "Yvette", 200); module.exports = { name }; //index.js const name = require("./name"); console.log(name); //William setTimeout(() => console.log(name), 300); //William
對比 ES6 模塊看一下:
ES6 模塊的運行機制與 CommonJS 不一樣。JS 引擎對腳本靜態(tài)分析的時候,遇到模塊加載命令 import ,就會生成一個只讀引用。等到腳本真正執(zhí)行時,再根據(jù)這個只讀引用,到被加載的那個模塊里面去取值。
//name.js var name = "William"; setTimeout(() => name = "Yvette", 200); export { name }; //index.js import { name } from "./name"; console.log(name); //William setTimeout(() => console.log(name), 300); //Yvette
如果你有更好的答案或想法,歡迎在這題目對應(yīng)的github下留言:ES6模塊和CommonJS模塊的差異?
10.瀏覽器事件代理機制的原理是什么?在說瀏覽器事件代理機制原理之前,我們首先了解一下事件流的概念,早期瀏覽器,IE采用的是事件捕獲事件流,而Netscape采用的則是事件捕獲。"DOM2級事件"把事件流分為三個階段,捕獲階段、目標階段、冒泡階段?,F(xiàn)代瀏覽器也都遵循此規(guī)范。
那么事件代理是什么呢?
事件代理又稱為事件委托,在祖先級DOM元素綁定一個事件,當觸發(fā)子孫級DOM元素的事件時,利用事件冒泡的原理來觸發(fā)綁定在祖先級DOM的事件。因為事件會從目標元素一層層冒泡至document對象。
為什么要事件代理?
添加到頁面上的事件數(shù)量會影響頁面的運行性能,如果添加的事件過多,會導(dǎo)致網(wǎng)頁的性能下降。采用事件代理的方式,可以大大減少注冊事件的個數(shù)。
事件代理的當時,某個子孫元素是動態(tài)增加的,不需要再次對其進行事件綁定。
不用擔(dān)心某個注冊了事件的DOM元素被移除后,可能無法回收其事件處理程序,我們只要把事件處理程序委托給更高層級的元素,就可以避免此問題。
如將頁面中的所有click事件都代理到document上:
addEventListener 接受3個參數(shù),分別是要處理的事件名、處理事件程序的函數(shù)和一個布爾值。布爾值默認為false。表示冒泡階段調(diào)用事件處理程序,若設(shè)置為true,表示在捕獲階段調(diào)用事件處理程序。
document.addEventListener("click", function (e) { console.log(e.target); /** * 捕獲階段調(diào)用調(diào)用事件處理程序,eventPhase是 1; * 處于目標,eventPhase是2 * 冒泡階段調(diào)用事件處理程序,eventPhase是 1; */ console.log(e.eventPhase); });
如果你有更好的答案或想法,歡迎在這題目對應(yīng)的github下留言:瀏覽器事件代理機制的原理是什么?
11.js如何自定義事件?自定義 DOM 事件(不考慮IE9之前版本)
自定義事件有三種方法,一種是使用 new Event(), 另一種是 createEvent("CustomEvent") , 另一種是 new customEvent()
使用 new Event()
獲取不到 event.detail
let btn = document.querySelector("#btn"); let ev = new Event("alert", { bubbles: true, //事件是否冒泡;默認值false cancelable: true, //事件能否被取消;默認值false composed: false }); btn.addEventListener("alert", function (event) { console.log(event.bubbles); //true console.log(event.cancelable); //true console.log(event.detail); //undefined }, false); btn.dispatchEvent(ev);
使用 createEvent("CustomEvent") (DOM3)
要創(chuàng)建自定義事件,可以調(diào)用 createEvent("CustomEvent"),返回的對象有 initCustomEvent 方法,接受以下四個參數(shù):
type: 字符串,表示觸發(fā)的事件類型,如此處的"alert"
bubbles: 布爾值: 表示事件是否冒泡
cancelable: 布爾值,表示事件是否可以取消
detail: 任意值,保存在 event 對象的 detail 屬性中
let btn = document.querySelector("#btn"); let ev = btn.createEvent("CustomEvent"); ev.initCustomEvent("alert", true, true, "button"); btn.addEventListener("alert", function (event) { console.log(event.bubbles); //true console.log(event.cancelable);//true console.log(event.detail); //button }, false); btn.dispatchEvent(ev);
使用 new customEvent() (DOM4)
使用起來比 createEvent("CustomEvent") 更加方便
var btn = document.querySelector("#btn"); /* * 第一個參數(shù)是事件類型 * 第二個參數(shù)是一個對象 */ var ev = new CustomEvent("alert", { bubbles: "true", cancelable: "true", detail: "button" }); btn.addEventListener("alert", function (event) { console.log(event.bubbles); //true console.log(event.cancelable);//true console.log(event.detail); //button }, false); btn.dispatchEvent(ev);
自定義非 DOM 事件(觀察者模式)
EventTarget類型有一個多帶帶的屬性handlers,用于存儲事件處理程序(觀察者)。
addHandler() 用于注冊給定類型事件的事件處理程序;
fire() 用于觸發(fā)一個事件;
removeHandler() 用于注銷某個事件類型的事件處理程序。
function EventTarget(){ this.handlers = {}; } EventTarget.prototype = { constructor:EventTarget, addHandler:function(type,handler){ if(typeof this.handlers[type] === "undefined"){ this.handlers[type] = []; } this.handlers[type].push(handler); }, fire:function(event){ if(!event.target){ event.target = this; } if(this.handlers[event.type] instanceof Array){ const handlers = this.handlers[event.type]; handlers.forEach((handler)=>{ handler(event); }); } }, removeHandler:function(type,handler){ if(this.handlers[type] instanceof Array){ const handlers = this.handlers[type]; for(var i = 0,len = handlers.length; i < len; i++){ if(handlers[i] === handler){ break; } } handlers.splice(i,1); } } } //使用 function handleMessage(event){ console.log(event.message); } //創(chuàng)建一個新對象 var target = new EventTarget(); //添加一個事件處理程序 target.addHandler("message", handleMessage); //觸發(fā)事件 target.fire({type:"message", message:"Hi"}); //Hi //刪除事件處理程序 target.removeHandler("message",handleMessage); //再次觸發(fā)事件,沒有事件處理程序 target.fire({type:"message",message: "Hi"});
如果你有更好的答案或想法,歡迎在這題目對應(yīng)的github下留言:js如何自定義事件?
12.跨域的方法有哪些?原理是什么?知其然知其所以然,在說跨域方法之前,我們先了解下什么叫跨域,瀏覽器有同源策略,只有當“協(xié)議”、“域名”、“端口號”都相同時,才能稱之為是同源,其中有一個不同,即是跨域。
那么同源策略的作用是什么呢?同源策略限制了從同一個源加載的文檔或腳本如何與來自另一個源的資源進行交互。這是一個用于隔離潛在惡意文件的重要安全機制。
那么我們又為什么需要跨域呢?一是前端和服務(wù)器分開部署,接口請求需要跨域,二是我們可能會加載其它網(wǎng)站的頁面作為iframe內(nèi)嵌。
跨域的方法有哪些?常用的跨域方法
jsonp
盡管瀏覽器有同源策略,但是 標簽的 src 屬性不會被同源策略所約束,可以獲取任意服務(wù)器上的腳本并執(zhí)行。jsonp 通過插入script標簽的方式來實現(xiàn)跨域,參數(shù)只能通過url傳入,僅能支持get請求。
實現(xiàn)原理:
Step1: 創(chuàng)建 callback 方法
Step2: 插入 script 標簽
Step3: 后臺接受到請求,解析前端傳過去的 callback 方法,返回該方法的調(diào)用,并且數(shù)據(jù)作為參數(shù)傳入該方法
Step4: 前端執(zhí)行服務(wù)端返回的方法調(diào)用
下面代碼僅為說明 jsonp 原理,項目中請使用成熟的庫。分別看一下前端和服務(wù)端的簡單實現(xiàn):
//前端代碼 function jsonp({url, params, cb}) { return new Promise((resolve, reject) => { //創(chuàng)建script標簽 let script = document.createElement("script"); //將回調(diào)函數(shù)掛在 window 上 window[cb] = function(data) { resolve(data); //代碼執(zhí)行后,刪除插入的script標簽 document.body.removeChild(script); } //回調(diào)函數(shù)加在請求地址上 params = {...params, cb} //wb=b&cb=show let arrs = []; for(let key in params) { arrs.push(`${key}=${params[key]}`); } script.src = `${url}?${arrs.join("&")}`; document.body.appendChild(script); }); } //使用 function sayHi(data) { console.log(data); } jsonp({ url: "http://localhost:3000/say", params: { //code }, cb: "sayHi" }).then(data => { console.log(data); });
//express啟動一個后臺服務(wù) let express = require("express"); let app = express(); app.get("/say", (req, res) => { let {cb} = req.query; //獲取傳來的callback函數(shù)名,cb是key res.send(`${cb}("Hello!")`); }); app.listen(3000);
從今天起,jsonp的原理就要了然于心啦~
cors
jsonp 只能支持 get 請求,cors 可以支持多種請求。cors 并不需要前端做什么工作。
簡單跨域請求:
只要服務(wù)器設(shè)置的Access-Control-Allow-Origin Header和請求來源匹配,瀏覽器就允許跨域
1) 請求的方法是get,head或者post。
2) Content-Type是application/x-www-form-urlencoded, multipart/form-data 或 text/plain中的一個值,或者不設(shè)置也可以,一般默認就是application/x-www-form-urlencoded。
3) 請求中沒有自定義的HTTP頭部,如x-token。(應(yīng)該是這幾種頭部 Accept,Accept-Language,Content-Language,Last-Event-ID,Content-Type)
//簡單跨域請求 app.use((req, res, next) => { res.setHeader("Access-Control-Allow-Origin", "XXXX"); });
帶預(yù)檢(Preflighted)的跨域請求
不滿于簡單跨域請求的,即是帶預(yù)檢的跨域請求。服務(wù)端需要設(shè)置 Access-Control-Allow-Origin (允許跨域資源請求的域) 、 Access-Control-Allow-Methods (允許的請求方法) 和 Access-Control-Allow-Headers (允許的請求頭)
app.use((req, res, next) => { res.setHeader("Access-Control-Allow-Origin", "XXX"); res.setHeader("Access-Control-Allow-Headers", "XXX"); //允許返回的頭 res.setHeader("Access-Control-Allow-Methods", "XXX");//允許使用put方法請求接口 res.setHeader("Access-Control-Max-Age", 6); //預(yù)檢的存活時間 if(req.method === "OPTIONS") { res.end(); //如果method是OPTIONS,不做處理 } });
更多CORS的知識可以訪問: [HTTP訪問控制(CORS)
](https://developer.mozilla.org...
nginx 反向代理
使用nginx反向代理實現(xiàn)跨域,只需要修改nginx的配置即可解決跨域問題。
A網(wǎng)站向B網(wǎng)站請求某個接口時,向B網(wǎng)站發(fā)送一個請求,nginx根據(jù)配置文件接收這個請求,代替A網(wǎng)站向B網(wǎng)站來請求。
nginx拿到這個資源后再返回給A網(wǎng)站,以此來解決了跨域問題。
例如nginx的端口號為 8090,需要請求的服務(wù)器端口號為 3000。(localhost:8090 請求 localhost:3000/say)
nginx配置如下:
server { listen 8090; server_name localhost; location / { root /Users/liuyan35/Test/Study/CORS/1-jsonp; index index.html index.htm; } location /say { rewrite ^/say/(.*)$ /$1 break; proxy_pass http://localhost:3000; add_header "Access-Control-Allow-Origin" "*"; add_header "Access-Control-Allow-Credentials" "true"; add_header "Access-Control-Allow-Methods" "GET, POST, OPTIONS"; } # others }
websocket
Websocket 是 HTML5 的一個持久化的協(xié)議,它實現(xiàn)了瀏覽器與服務(wù)器的全雙工通信,同時也是跨域的一種解決方案。
Websocket 不受同源策略影響,只要服務(wù)器端支持,無需任何配置就支持跨域。
前端頁面在 8080 的端口。
let socket = new WebSocket("ws://localhost:3000"); //協(xié)議是ws socket.onopen = function() { socket.send("Hi,你好"); } socket.onmessage = function(e) { console.log(e.data) }
服務(wù)端 3000端口??梢钥闯鰓ebsocket無需做跨域配置。
let WebSocket = require("ws"); let wss = new WebSocket.Server({port: 3000}); wss.on("connection", function(ws) { ws.on("message", function(data) { console.log(data); //接受到頁面發(fā)來的消息"Hi,你好" ws.send("Hi"); //向頁面發(fā)送消息 }); });
postMessage
postMessage 通過用作前端頁面之前的跨域,如父頁面與iframe頁面的跨域。window.postMessage方法,允許跨窗口通信,不論這兩個窗口是否同源。
話說工作中兩個頁面之前需要通信的情況并不多,我本人工作中,僅使用過兩次,一次是H5頁面中發(fā)送postMessage信息,ReactNative的webview中接收此此消息,并作出相應(yīng)處理。另一次是可輪播的頁面,某個輪播頁使用的是iframe頁面,為了解決滑動的事件沖突,iframe頁面中去監(jiān)聽手勢,發(fā)送消息告訴父頁面是否左滑和右滑。
子頁面向父頁面發(fā)消息
父頁面
window.addEventListener("message", (e) => { this.props.movePage(e.data); }, false);
子頁面(iframe):
if(/*左滑*/) { window.parent && window.parent.postMessage(-1, "*") }else if(/*右滑*/){ window.parent && window.parent.postMessage(1, "*") }
父頁面向子頁面發(fā)消息
父頁面:
let iframe = document.querySelector("#iframe"); iframe.onload = function() { iframe.contentWindow.postMessage("hello", "http://localhost:3002"); }
子頁面:
window.addEventListener("message", function(e) { console.log(e.data); e.source.postMessage("Hi", e.origin); //回消息 });
node 中間件
node 中間件的跨域原理和nginx代理跨域,同源策略是瀏覽器的限制,服務(wù)端沒有同源策略。
node中間件實現(xiàn)跨域的原理如下:
1.接受客戶端請求
2.將請求 轉(zhuǎn)發(fā)給服務(wù)器。
3.拿到服務(wù)器 響應(yīng) 數(shù)據(jù)。
4.將 響應(yīng) 轉(zhuǎn)發(fā)給客戶端。
不常用跨域方法
以下三種跨域方式很少用,如有興趣,可自行查閱相關(guān)資料。
window.name + iframe
location.hash + iframe
document.domain (主域需相同)
如果你有更好的答案或想法,歡迎在這題目對應(yīng)的github下留言:跨域的方法有哪些?原理是什么?
13.js異步加載的方式有哪些?的 defer 屬性,HTML4 中新增
的 async 屬性,HTML5 中新增
標簽打開defer屬性,腳本就會異步加載。渲染引擎遇到這一行命令,就會開始下載外部腳本,但不會等它下載和執(zhí)行,而是直接執(zhí)行后面的命令。
defer 和 async 的區(qū)別在于: defer要等到整個頁面在內(nèi)存中正常渲染結(jié)束,才會執(zhí)行;
async一旦下載完,渲染引擎就會中斷渲染,執(zhí)行這個腳本以后,再繼續(xù)渲染。defer是“渲染完再執(zhí)行”,async是“下載完就執(zhí)行”。
如果有多個 defer 腳本,會按照它們在頁面出現(xiàn)的順序加載。
多個async腳本是不能保證加載順序的。
動態(tài)插入 script 腳本
function downloadJS() { varelement = document.createElement("script"); element.src = "XXX.js"; document.body.appendChild(element); } //何時的時候,調(diào)用上述方法
有條件的動態(tài)創(chuàng)建腳本
如頁面 onload 之后,
如果你有更好的答案或想法,歡迎在這題目對應(yīng)的github下留言:js異步加載的方式有哪些?
14.下面代碼a在什么情況中打印出1?//? if(a == 1 && a == 2 && a == 3) { console.log(1); }
1.在類型轉(zhuǎn)換的時候,我們知道了對象如何轉(zhuǎn)換成原始數(shù)據(jù)類型。如果部署了 [Symbol.toPrimitive],那么返回的就是[Symbol.toPrimitive]()的返回值。當然,我們也可以把此函數(shù)部署在valueOf或者是toString接口上,效果相同。
//利用閉包延長作用域的特性 let a = { [Symbol.toPrimitive]: (function() { let i = 1; return function() { return i++; } })() }
(1). 比較 a == 1 時,會調(diào)用 [Symbol.toPrimitive],此時 i 是 1,相等。
(2). 繼續(xù)比較 a == 2,調(diào)用 [Symbol.toPrimitive],此時 i 是 2,相等。
(3). 繼續(xù)比較 a == 3,調(diào)用 [Symbol.toPrimitive],此時 i 是 3,相等。
2.利用Object.definePropert在window/global上定義a屬性,獲取a屬性時,會調(diào)用get.
let val = 1; Object.defineProperty(window, "a", { get: function() { return val++; } });
3.利用數(shù)組的特性。
var a = [1,2,3]; a.join = a.shift;
數(shù)組的 toString 方法返回一個字符串,該字符串由數(shù)組中的每個元素的 toString() 返回值經(jīng)調(diào)用 join() 方法連接(由逗號隔開)組成。
因此,我們可以重新 join 方法。返回第一個元素,并將其刪除。
如果你有更好的答案或想法,歡迎在這題目對應(yīng)的github下留言:下面代碼a在什么情況中打印出1?
15.下面這段代碼的輸出是什么?function Foo() { getName = function() {console.log(1)}; return this; } Foo.getName = function() {console.log(2)}; Foo.prototype.getName = function() {console.log(3)}; var getName = function() {console.log(4)}; function getName() {console.log(5)}; Foo.getName(); getName(); Foo().getName(); getName(); new Foo.getName(); new Foo().getName(); new new Foo().getName();
說明:一道經(jīng)典的面試題,僅是為了幫助大家回顧一下知識點,加深理解,真實工作中,是不可能這樣寫代碼的,否則,肯定會被打死的。
1.首先預(yù)編譯階段,變量聲明與函數(shù)聲明提升至其對應(yīng)作用域的最頂端。
因此上面的代碼編譯后如下(函數(shù)聲明的優(yōu)先級先于變量聲明):
function Foo() { getName = function() {console.log(1)}; return this; } var getName; function getName() {console.log(5)}; Foo.getName = function() {console.log(2)}; Foo.prototype.getName = function() {console.log(3)}; getName = function() {console.log(4)};
2.Foo.getName();直接調(diào)用Foo上getName方法,輸出2
3.getName();輸出4,getName被重新賦值了
4.Foo().getName();執(zhí)行Foo(),window的getName被重新賦值,返回this;瀏覽器環(huán)境中,非嚴格模式,this 指向 window,this.getName();輸出為1.
如果是嚴格模式,this 指向 undefined,此處會拋出錯誤。
如果是node環(huán)境中,this 指向 global,node的全局變量并不掛在global上,因為global.getName對應(yīng)的是undefined,不是一個function,會拋出錯誤。
5.getName();已經(jīng)拋錯的自然走不動這一步了;繼續(xù)瀏覽器非嚴格模式;window.getName被重新賦過值,此時再調(diào)用,輸出的是1
6.new Foo.getName();考察運算符優(yōu)先級的知識,new 無參數(shù)列表,對應(yīng)的優(yōu)先級是18;成員訪問操作符 . , 對應(yīng)的優(yōu)先級是 19。因此相當于是 new (Foo.getName)();new操作符會執(zhí)行構(gòu)造函數(shù)中的方法,因此此處輸出為 2.
7.new Foo().getName();new 帶參數(shù)列表,對應(yīng)的優(yōu)先級是19,和成員訪問操作符.優(yōu)先級相同。同級運算符,按照從左到右的順序依次計算。new Foo()先初始化 Foo 的實例化對象,實例上沒有g(shù)etName方法,因此需要原型上去找,即找到了 Foo.prototype.getName,輸出3
8.new new Foo().getName(); new 帶參數(shù)列表,優(yōu)先級19,因此相當于是 new (new Foo()).getName();先初始化 Foo 的實例化對象,然后將其原型上的 getName 函數(shù)作為構(gòu)造函數(shù)再次 new ,輸出3
因此最終結(jié)果如下:
Foo.getName(); //2 getName();//4 Foo().getName();//1 getName();//1 new Foo.getName();//2 new Foo().getName();//3 new new Foo().getName();//3
如果你有更好的答案或想法,歡迎在這題目對應(yīng)的github下留言:下面這段代碼的輸出是什么?
16.實現(xiàn)雙向綁定 Proxy 與 Object.defineProperty 相比優(yōu)劣如何?Object.definedProperty 的作用是劫持一個對象的屬性,劫持屬性的getter和setter方法,在對象的屬性發(fā)生變化時進行特定的操作。而 Proxy 劫持的是整個對象。
Proxy 會返回一個代理對象,我們只需要操作新對象即可,而 Object.defineProperty 只能遍歷對象屬性直接修改。
Object.definedProperty 不支持數(shù)組,更準確的說是不支持數(shù)組的各種API,因為如果僅僅考慮arry[i] = value 這種情況,是可以劫持的,但是這種劫持意義不大。而 Proxy 可以支持數(shù)組的各種API。
盡管 Object.defineProperty 有諸多缺陷,但是其兼容性要好于 Proxy.
PS: Vue2.x 使用 Object.defineProperty 實現(xiàn)數(shù)據(jù)雙向綁定,V3.0 則使用了 Proxy.
//攔截器 let obj = {}; let temp = "Yvette"; Object.defineProperty(obj, "name", { get() { console.log("讀取成功"); return temp }, set(value) { console.log("設(shè)置成功"); temp = value; } }); obj.name = "Chris"; console.log(obj.name);
PS: Object.defineProperty 定義出來的屬性,默認是不可枚舉,不可更改,不可配置【無法delete】
我們可以看到 Proxy 會劫持整個對象,讀取對象中的屬性或者是修改屬性值,那么就會被劫持。但是有點需要注意,復(fù)雜數(shù)據(jù)類型,監(jiān)控的是引用地址,而不是值,如果引用地址沒有改變,那么不會觸發(fā)set。
let obj = {name: "Yvette", hobbits: ["travel", "reading"], info: { age: 20, job: "engineer" }}; let p = new Proxy(obj, { get(target, key) { //第三個參數(shù)是 proxy, 一般不使用 console.log("讀取成功"); return Reflect.get(target, key); }, set(target, key, value) { if(key === "length") return true; //如果是數(shù)組長度的變化,返回。 console.log("設(shè)置成功"); return Reflect.set([target, key, value]); } }); p.name = 20; //設(shè)置成功 p.age = 20; //設(shè)置成功; 不需要事先定義此屬性 p.hobbits.push("photography"); //讀取成功;注意不會觸發(fā)設(shè)置成功 p.info.age = 18; //讀取成功;不會觸發(fā)設(shè)置成功
最后,我們再看下對于數(shù)組的劫持,Object.definedProperty 和 Proxy 的差別
Object.definedProperty 可以將數(shù)組的索引作為屬性進行劫持,但是僅支持直接對 arry[i] 進行操作,不支持數(shù)組的API,非常雞肋。
let arry = [] Object.defineProperty(arry, "0", { get() { console.log("讀取成功"); return temp }, set(value) { console.log("設(shè)置成功"); temp = value; } }); arry[0] = 10; //觸發(fā)設(shè)置成功 arry.push(10); //不能被劫持
Proxy 可以監(jiān)聽到數(shù)組的變化,支持各種API。注意數(shù)組的變化觸發(fā)get和set可能不止一次,如有需要,自行根據(jù)key值決定是否要進行處理。
let hobbits = ["travel", "reading"]; let p = new Proxy(hobbits, { get(target, key) { // if(key === "length") return true; //如果是數(shù)組長度的變化,返回。 console.log("讀取成功"); return Reflect.get(target, key); }, set(target, key, value) { // if(key === "length") return true; //如果是數(shù)組長度的變化,返回。 console.log("設(shè)置成功"); return Reflect.set([target, key, value]); } }); p.splice(0,1) //觸發(fā)get和set,可以被劫持 p.push("photography");//觸發(fā)get和set p.slice(1); //觸發(fā)get;因為 slice 是不會修改原數(shù)組的
如果你有更好的答案或想法,歡迎在這題目對應(yīng)的github下留言:實現(xiàn)雙向綁定 Proxy 與 Object.defineProperty 相比優(yōu)劣如何?
17.Object.is() 與比較操作符 ===、== 有什么區(qū)別?以下情況,Object.is認為是相等
兩個值都是 undefined 兩個值都是 null 兩個值都是 true 或者都是 false 兩個值是由相同個數(shù)的字符按照相同的順序組成的字符串 兩個值指向同一個對象 兩個值都是數(shù)字并且 都是正零 +0 都是負零 -0 都是 NaN 都是除零和 NaN 外的其它同一個數(shù)字
Object.is() 類似于 ===,但是有一些細微差別,如下:
NaN 和 NaN 相等
-0 和 +0 不相等
console.log(Object.is(NaN, NaN));//true console.log(NaN === NaN);//false console.log(Object.is(-0, +0)); //false console.log(-0 === +0); //true
Object.is 和 ==差得遠了, == 在類型不同時,需要進行類型轉(zhuǎn)換,前文已經(jīng)詳細說明。
如果你有更好的答案或想法,歡迎在這題目對應(yīng)的github下留言:Object.is() 與比較操作符 ===、== 有什么區(qū)別?
18.什么是事件循環(huán)?Node事件循環(huán)和JS事件循環(huán)的差異是什么?最后一道題留給大家回答,再寫下去,篇幅實在太長。
針對這道題,后面會專門寫一篇文章~
留下你的答案: 什么是事件循環(huán)?Node事件循環(huán)和JS事件循環(huán)的差異是什么?
關(guān)于瀏覽器的event-loop可以看我之前的文章:搞懂瀏覽器的EventLoop
參考文章:https://www.imooc.com/article...
http://es6.ruanyifeng.com/
https://www.imooc.com/article...
https://www.cnblogs.com/Lucky...
https://www.jianshu.com/p/a76...
https://www.v2ex.com/t/351261
后續(xù)寫作計劃(寫作順序不定)
1.《寒冬求職季之你必須要懂的原生JS》(下)
2.《寒冬求職季之你必須要知道的CSS》
3.《寒冬求職季之你必須要懂的前端安全》
4.《寒冬求職季之你必須要懂的一些瀏覽器知識》
5.《寒冬求職季之你必須要知道的性能優(yōu)化》
6.《寒冬求職季之你必須要懂的webpack原理》
針對React技術(shù)棧:
1.《寒冬求職季之你必須要懂的React》系列
2.《寒冬求職季之你必須要懂的ReactNative》系列
本文的寫成耗費了非常多的時間,在這個過程中,我也學(xué)習(xí)到了很多知識,謝謝各位小伙伴愿意花費寶貴的時間閱讀本文,如果本文給了您一點幫助或者是啟發(fā),請不要吝嗇你的贊和Star,您的肯定是我前進的最大動力。https://github.com/YvetteLau/...
關(guān)注小姐姐的公眾號,和小姐姐一起學(xué)前端。
文章版權(quán)歸作者所有,未經(jīng)允許請勿轉(zhuǎn)載,若此文章存在違規(guī)行為,您可以聯(lián)系管理員刪除。
轉(zhuǎn)載請注明本文地址:http://systransis.cn/yun/103758.html
摘要:半路出家的前端程序員應(yīng)該不在少數(shù),我也是其中之一。年,馮馮同事兼師兄看我寫太費勁,跟我說對面樓在找,問我要不要學(xué),說出來可能有點丟人,但是在那之前,我真得不知道什么是,什么是。 半路出家的前端程序員應(yīng)該不在少數(shù),我也是其中之一。 為何會走向前端 非計算機專業(yè)的我,畢業(yè)之后,就職于一家電力行業(yè)公司,做過設(shè)備調(diào)試、部門助理、測試,也寫過一段時間的QT,那三年的時間,最難過的不是工作忙不忙,...
摘要:循環(huán)可以使用的范圍包括數(shù)組和結(jié)構(gòu)某些類似數(shù)組的對象對象,以及字符串。只能遍歷數(shù)組,不能中斷,返回值是修改后的數(shù)組。除了之外,等,也有同樣的問題。聲明一個只讀的常量。這在語法上,稱為暫時性死區(qū)。暫時性死區(qū)也意味著不再是一個百分百安全的操作。 互聯(lián)網(wǎng)寒冬之際,各大公司都縮減了HC,甚至是采取了裁員措施,在這樣的大環(huán)境之下,想要獲得一份更好的工作,必然需要付出更多的努力。 一年前,也許你搞清楚閉包...
摘要:只能遍歷數(shù)組,不能中斷,返回值是修改后的數(shù)組。這在語法上,稱為暫時性死區(qū)。作用域鏈無論是還是查詢,都會在當前的作用域開始查找,如果沒有找到,就會向上級作用域繼續(xù)查找目標標識符,每次上升一個作用域,一直到全局作用域為止。 互聯(lián)網(wǎng)寒冬之際,各大公司都縮減了HC,甚至是采取了裁員措施,在這樣的大環(huán)境之下,想要獲得一份更好的工作,必然需要付出更多的努力。 一年前,也許你搞清楚閉包,this,原...
摘要:手冊網(wǎng)超級有用的前端基礎(chǔ)技術(shù)面試問題收集前端面試題目及答案匯總史上最全前端面試題含答案常見前端面試題及答案經(jīng)典面試題及答案精選總結(jié)前端面試過程中最容易出現(xiàn)的問題前端面試題整理騰訊前端面試經(jīng)驗前端基礎(chǔ)面試題部分最新前端面試題攻略前端面試前端入 手冊網(wǎng):http://www.shouce.ren/post/index 超級有用的前端基礎(chǔ)技術(shù)面試問題收集:http://www.codec...
摘要:手冊網(wǎng)超級有用的前端基礎(chǔ)技術(shù)面試問題收集前端面試題目及答案匯總史上最全前端面試題含答案常見前端面試題及答案經(jīng)典面試題及答案精選總結(jié)前端面試過程中最容易出現(xiàn)的問題前端面試題整理騰訊前端面試經(jīng)驗前端基礎(chǔ)面試題部分最新前端面試題攻略前端面試前端入 手冊網(wǎng):http://www.shouce.ren/post/index 超級有用的前端基礎(chǔ)技術(shù)面試問題收集:http://www.codec...
閱讀 2524·2021-11-19 09:59
閱讀 2053·2019-08-30 15:55
閱讀 962·2019-08-29 13:30
閱讀 1367·2019-08-26 10:18
閱讀 3111·2019-08-23 18:36
閱讀 2416·2019-08-23 18:25
閱讀 1194·2019-08-23 18:07
閱讀 463·2019-08-23 17:15