前端面试-进阶篇
一、JS
#2 bind、call、apply 區(qū)別
- call?和?apply?都是為了解決改變?this?的指向。作用都是相同的,只是傳參的方式不同。
- 除了第一個(gè)參數(shù)外,call?可以接收一個(gè)參數(shù)列表,apply?只接受一個(gè)參數(shù)數(shù)組
bind?和其他兩個(gè)方法作用也是一致的,只是該方法會(huì)返回一個(gè)函數(shù)。并且我們可以通過(guò)?bind?實(shí)現(xiàn)柯里化
#3 如何實(shí)現(xiàn)一個(gè) bind 函數(shù)
對(duì)于實(shí)現(xiàn)以下幾個(gè)函數(shù),可以從幾個(gè)方面思考
- 不傳入第一個(gè)參數(shù),那么默認(rèn)為?window
- 改變了?this?指向,讓新的對(duì)象可以執(zhí)行該函數(shù)。那么思路是否可以變成給新的對(duì)象添加一個(gè)函數(shù),然后在執(zhí)行完以后刪除?
#4 如何實(shí)現(xiàn)一個(gè) call 函數(shù)
Function.prototype.myCall = function (context) {var context = context || window// 給 context 添加一個(gè)屬性// getValue.call(a, 'yck', '24') => a.fn = getValuecontext.fn = this// 將 context 后面的參數(shù)取出來(lái)var args = [...arguments].slice(1)// getValue.call(a, 'yck', '24') => a.fn('yck', '24')var result = context.fn(...args)// 刪除 fndelete context.fnreturn result }#5 如何實(shí)現(xiàn)一個(gè) apply 函數(shù)
Function.prototype.myApply = function (context) {var context = context || windowcontext.fn = thisvar result// 需要判斷是否存儲(chǔ)第二個(gè)參數(shù)// 如果存在,就將第二個(gè)參數(shù)展開(kāi)if (arguments[1]) {result = context.fn(...arguments[1])} else {result = context.fn()}delete context.fnreturn result }#6 簡(jiǎn)單說(shuō)下原型鏈?
- 每個(gè)函數(shù)都有?prototype?屬性,除了?Function.prototype.bind(),該屬性指向原型。
- 每個(gè)對(duì)象都有?__proto__?屬性,指向了創(chuàng)建該對(duì)象的構(gòu)造函數(shù)的原型。其實(shí)這個(gè)屬性指向了?[[prototype]],但是?[[prototype]]是內(nèi)部屬性,我們并不能訪問(wèn)到,所以使用?_proto_來(lái)訪問(wèn)。
- 對(duì)象可以通過(guò)?__proto__?來(lái)尋找不屬于該對(duì)象的屬性,__proto__?將對(duì)象連接起來(lái)組成了原型鏈。
#7 怎么判斷對(duì)象類(lèi)型
- 可以通過(guò)?Object.prototype.toString.call(xx)。這樣我們就可以獲得類(lèi)似?[object Type]?的字符串。
- instanceof?可以正確的判斷對(duì)象的類(lèi)型,因?yàn)閮?nèi)部機(jī)制是通過(guò)判斷對(duì)象的原型鏈中是不是能找到類(lèi)型的?prototype
#8 箭頭函數(shù)的特點(diǎn)
function a() {return () => {return () => {console.log(this)}} } console.log(a()()())箭頭函數(shù)其實(shí)是沒(méi)有?this?的,這個(gè)函數(shù)中的?this?只取決于他外面的第一個(gè)不是箭頭函數(shù)的函數(shù)的?this。在這個(gè)例子中,因?yàn)檎{(diào)用?a?符合前面代碼中的第一個(gè)情況,所以?this?是window。并且?this一旦綁定了上下文,就不會(huì)被任何代碼改變
#9 This
function foo() {console.log(this.a) } var a = 1 foo()var obj = {a: 2,foo: foo } obj.foo()// 以上兩者情況 `this` 只依賴(lài)于調(diào)用函數(shù)前的對(duì)象,優(yōu)先級(jí)是第二個(gè)情況大于第一個(gè)情況// 以下情況是優(yōu)先級(jí)最高的,`this` 只會(huì)綁定在 `c` 上,不會(huì)被任何方式修改 `this` 指向 var c = new foo() c.a = 3 console.log(c.a)// 還有種就是利用 call,apply,bind 改變 this,這個(gè)優(yōu)先級(jí)僅次于 new#10 async、await 優(yōu)缺點(diǎn)
async?和?await?相比直接使用?Promise?來(lái)說(shuō),優(yōu)勢(shì)在于處理 then 的調(diào)用鏈,能夠更清晰準(zhǔn)確的寫(xiě)出代碼。缺點(diǎn)在于濫用?await?可能會(huì)導(dǎo)致性能問(wèn)題,因?yàn)?await?會(huì)阻塞代碼,也許之后的異步代碼并不依賴(lài)于前者,但仍然需要等待前者完成,導(dǎo)致代碼失去了并發(fā)性
下面來(lái)看一個(gè)使用?await?的代碼。
var a = 0 var b = async () => {a = a + await 10console.log('2', a) // -> '2' 10a = (await 10) + aconsole.log('3', a) // -> '3' 20 } b() a++ console.log('1', a) // -> '1' 1- 首先函數(shù)b?先執(zhí)行,在執(zhí)行到?await 10?之前變量?a?還是?0,因?yàn)樵?await?內(nèi)部實(shí)現(xiàn)了?generators?,generators?會(huì)保留堆棧中東西,所以這時(shí)候?a = 0?被保存了下來(lái)
- 因?yàn)?await?是異步操作,遇到await就會(huì)立即返回一個(gè)pending狀態(tài)的Promise對(duì)象,暫時(shí)返回執(zhí)行代碼的控制權(quán),使得函數(shù)外的代碼得以繼續(xù)執(zhí)行,所以會(huì)先執(zhí)行?console.log('1', a)
- 這時(shí)候同步代碼執(zhí)行完畢,開(kāi)始執(zhí)行異步代碼,將保存下來(lái)的值拿出來(lái)使用,這時(shí)候?a = 10
- 然后后面就是常規(guī)執(zhí)行代碼了
#11 generator 原理
Generator?是?ES6中新增的語(yǔ)法,和?Promise?一樣,都可以用來(lái)異步編程
// 使用 * 表示這是一個(gè) Generator 函數(shù) // 內(nèi)部可以通過(guò) yield 暫停代碼 // 通過(guò)調(diào)用 next 恢復(fù)執(zhí)行 function* test() {let a = 1 + 2;yield 2;yield 3; } let b = test(); console.log(b.next()); // > { value: 2, done: false } console.log(b.next()); // > { value: 3, done: false } console.log(b.next()); // > { value: undefined, done: true }從以上代碼可以發(fā)現(xiàn),加上?*的函數(shù)執(zhí)行后擁有了?next?函數(shù),也就是說(shuō)函數(shù)執(zhí)行后返回了一個(gè)對(duì)象。每次調(diào)用?next?函數(shù)可以繼續(xù)執(zhí)行被暫停的代碼。以下是?Generator?函數(shù)的簡(jiǎn)單實(shí)現(xiàn)
// cb 也就是編譯過(guò)的 test 函數(shù) function generator(cb) {return (function() {var object = {next: 0,stop: function() {}};return {next: function() {var ret = cb(object);if (ret === undefined) return { value: undefined, done: true };return {value: ret,done: false};}};})(); } // 如果你使用 babel 編譯后可以發(fā)現(xiàn) test 函數(shù)變成了這樣 function test() {var a;return generator(function(_context) {while (1) {switch ((_context.prev = _context.next)) {// 可以發(fā)現(xiàn)通過(guò) yield 將代碼分割成幾塊// 每次執(zhí)行 next 函數(shù)就執(zhí)行一塊代碼// 并且表明下次需要執(zhí)行哪塊代碼case 0:a = 1 + 2;_context.next = 4;return 2;case 4:_context.next = 6;return 3;// 執(zhí)行完畢case 6:case "end":return _context.stop();}}}); }#12 Promise
- Promise?是?ES6?新增的語(yǔ)法,解決了回調(diào)地獄的問(wèn)題。
- 可以把?Promise看成一個(gè)狀態(tài)機(jī)。初始是?pending?狀態(tài),可以通過(guò)函數(shù)?resolve?和?reject,將狀態(tài)轉(zhuǎn)變?yōu)?resolved?或者?rejected?狀態(tài),狀態(tài)一旦改變就不能再次變化。
- then?函數(shù)會(huì)返回一個(gè)?Promise?實(shí)例,并且該返回值是一個(gè)新的實(shí)例而不是之前的實(shí)例。因?yàn)?Promise?規(guī)范規(guī)定除了?pending?狀態(tài),其他狀態(tài)是不可以改變的,如果返回的是一個(gè)相同實(shí)例的話,多個(gè)?then?調(diào)用就失去意義了。 對(duì)于?then?來(lái)說(shuō),本質(zhì)上可以把它看成是?flatMap
#13 如何實(shí)現(xiàn)一個(gè) Promise
// 三種狀態(tài) const PENDING = "pending"; const RESOLVED = "resolved"; const REJECTED = "rejected"; // promise 接收一個(gè)函數(shù)參數(shù),該函數(shù)會(huì)立即執(zhí)行 function MyPromise(fn) {let _this = this;_this.currentState = PENDING;_this.value = undefined;// 用于保存 then 中的回調(diào),只有當(dāng) promise// 狀態(tài)為 pending 時(shí)才會(huì)緩存,并且每個(gè)實(shí)例至多緩存一個(gè)_this.resolvedCallbacks = [];_this.rejectedCallbacks = [];_this.resolve = function (value) {if (value instanceof MyPromise) {// 如果 value 是個(gè) Promise,遞歸執(zhí)行return value.then(_this.resolve, _this.reject)}setTimeout(() => { // 異步執(zhí)行,保證執(zhí)行順序if (_this.currentState === PENDING) {_this.currentState = RESOLVED;_this.value = value;_this.resolvedCallbacks.forEach(cb => cb());}})};_this.reject = function (reason) {setTimeout(() => { // 異步執(zhí)行,保證執(zhí)行順序if (_this.currentState === PENDING) {_this.currentState = REJECTED;_this.value = reason;_this.rejectedCallbacks.forEach(cb => cb());}})}// 用于解決以下問(wèn)題// new Promise(() => throw Error('error))try {fn(_this.resolve, _this.reject);} catch (e) {_this.reject(e);} }MyPromise.prototype.then = function (onResolved, onRejected) {var self = this;// 規(guī)范 2.2.7,then 必須返回一個(gè)新的 promisevar promise2;// 規(guī)范 2.2.onResolved 和 onRejected 都為可選參數(shù)// 如果類(lèi)型不是函數(shù)需要忽略,同時(shí)也實(shí)現(xiàn)了透?jìng)?/ Promise.resolve(4).then().then((value) => console.log(value))onResolved = typeof onResolved === 'function' ? onResolved : v => v;onRejected = typeof onRejected === 'function' ? onRejected : r => throw r;if (self.currentState === RESOLVED) {return (promise2 = new MyPromise(function (resolve, reject) {// 規(guī)范 2.2.4,保證 onFulfilled,onRjected 異步執(zhí)行// 所以用了 setTimeout 包裹下setTimeout(function () {try {var x = onResolved(self.value);resolutionProcedure(promise2, x, resolve, reject);} catch (reason) {reject(reason);}});}));}if (self.currentState === REJECTED) {return (promise2 = new MyPromise(function (resolve, reject) {setTimeout(function () {// 異步執(zhí)行onRejectedtry {var x = onRejected(self.value);resolutionProcedure(promise2, x, resolve, reject);} catch (reason) {reject(reason);}});}));}if (self.currentState === PENDING) {return (promise2 = new MyPromise(function (resolve, reject) {self.resolvedCallbacks.push(function () {// 考慮到可能會(huì)有報(bào)錯(cuò),所以使用 try/catch 包裹try {var x = onResolved(self.value);resolutionProcedure(promise2, x, resolve, reject);} catch (r) {reject(r);}});self.rejectedCallbacks.push(function () {try {var x = onRejected(self.value);resolutionProcedure(promise2, x, resolve, reject);} catch (r) {reject(r);}});}));} }; // 規(guī)范 2.3 function resolutionProcedure(promise2, x, resolve, reject) {// 規(guī)范 2.3.1,x 不能和 promise2 相同,避免循環(huán)引用if (promise2 === x) {return reject(new TypeError("Error"));}// 規(guī)范 2.3.2// 如果 x 為 Promise,狀態(tài)為 pending 需要繼續(xù)等待否則執(zhí)行if (x instanceof MyPromise) {if (x.currentState === PENDING) {x.then(function (value) {// 再次調(diào)用該函數(shù)是為了確認(rèn) x resolve 的// 參數(shù)是什么類(lèi)型,如果是基本類(lèi)型就再次 resolve// 把值傳給下個(gè) thenresolutionProcedure(promise2, value, resolve, reject);}, reject);} else {x.then(resolve, reject);}return;}// 規(guī)范 2.3.3.3.3// reject 或者 resolve 其中一個(gè)執(zhí)行過(guò)得話,忽略其他的let called = false;// 規(guī)范 2.3.3,判斷 x 是否為對(duì)象或者函數(shù)if (x !== null && (typeof x === "object" || typeof x === "function")) {// 規(guī)范 2.3.3.2,如果不能取出 then,就 rejecttry {// 規(guī)范 2.3.3.1let then = x.then;// 如果 then 是函數(shù),調(diào)用 x.thenif (typeof then === "function") {// 規(guī)范 2.3.3.3then.call(x,y => {if (called) return;called = true;// 規(guī)范 2.3.3.3.1resolutionProcedure(promise2, y, resolve, reject);},e => {if (called) return;called = true;reject(e);});} else {// 規(guī)范 2.3.3.4resolve(x);}} catch (e) {if (called) return;called = true;reject(e);}} else {// 規(guī)范 2.3.4,x 為基本類(lèi)型resolve(x);} }#14 == 和 ===區(qū)別,什么情況用 ==
這里來(lái)解析一道題目?[] == ![] // -> true?,下面是這個(gè)表達(dá)式為何為?true?的步驟
// [] 轉(zhuǎn)成 true,然后取反變成 false [] == false // 根據(jù)第 8 條得出 [] == ToNumber(false) [] == 0 // 根據(jù)第 10 條得出 ToPrimitive([]) == 0 // [].toString() -> '' '' == 0 // 根據(jù)第 6 條得出 0 == 0 // -> true===用于判斷兩者類(lèi)型和值是否相同。 在開(kāi)發(fā)中,對(duì)于后端返回的?code,可以通過(guò)?==去判斷
#15 基本數(shù)據(jù)類(lèi)型和引?類(lèi)型在存儲(chǔ)上的差別
前者存儲(chǔ)在棧上,后者存儲(chǔ)在堆上
#16 瀏覽器 Eventloop 和 Node 中的有什么區(qū)別
眾所周知 JS 是門(mén)非阻塞單線程語(yǔ)言,因?yàn)樵谧畛?JS 就是為了和瀏覽器交互而誕生的。如果 JS 是門(mén)多線程的語(yǔ)言話,我們?cè)诙鄠€(gè)線程中處理 DOM 就可能會(huì)發(fā)生問(wèn)題(一個(gè)線程中新加節(jié)點(diǎn),另一個(gè)線程中刪除節(jié)點(diǎn)),當(dāng)然可以引入讀寫(xiě)鎖解決這個(gè)問(wèn)題。
- JS?在執(zhí)行的過(guò)程中會(huì)產(chǎn)生執(zhí)行環(huán)境,這些執(zhí)行環(huán)境會(huì)被順序的加入到執(zhí)行棧中。如果遇到異步的代碼,會(huì)被掛起并加入到?Task(有多種?task) 隊(duì)列中。一旦執(zhí)行棧為空,Event Loop?就會(huì)從?Task?隊(duì)列中拿出需要執(zhí)行的代碼并放入執(zhí)行棧中執(zhí)行,所以本質(zhì)上來(lái)說(shuō)?JS?中的異步還是同步行為
- 以上代碼雖然?setTimeout?延時(shí)為?0,其實(shí)還是異步。這是因?yàn)?HTML5?標(biāo)準(zhǔn)規(guī)定這個(gè)函數(shù)第二個(gè)參數(shù)不得小于?4?毫秒,不足會(huì)自動(dòng)增加。所以?setTimeout還是會(huì)在?script end?之后打印。
- 不同的任務(wù)源會(huì)被分配到不同的?Task隊(duì)列中,任務(wù)源可以分為 微任務(wù)(microtask) 和 宏任務(wù)(macrotask)。在 ES6 規(guī)范中,microtask?稱(chēng)為 jobs,macrotask?稱(chēng)為?task。
- 以上代碼雖然?setTimeout?寫(xiě)在?Promise?之前,但是因?yàn)?Promise?屬于微任務(wù)而?setTimeout屬于宏任務(wù),所以會(huì)有以上的打印。
- 微任務(wù)包括?process.nextTick?,promise?,Object.observe,MutationObserver
- 宏任務(wù)包括?script?,?setTimeout?,setInterval,setImmediate?,I/O?,UI renderin
很多人有個(gè)誤區(qū),認(rèn)為微任務(wù)快于宏任務(wù),其實(shí)是錯(cuò)誤的。因?yàn)楹耆蝿?wù)中包括了?script?,瀏覽器會(huì)先執(zhí)行一個(gè)宏任務(wù),接下來(lái)有異步代碼的話就先執(zhí)行微任務(wù)
所以正確的一次 Event loop 順序是這樣的
- 執(zhí)行同步代碼,這屬于宏任務(wù)
- 執(zhí)行棧為空,查詢(xún)是否有微任務(wù)需要執(zhí)行
- 執(zhí)行所有微任務(wù)
- 必要的話渲染?UI
- 然后開(kāi)始下一輪?Event loop,執(zhí)行宏任務(wù)中的異步代碼
通過(guò)上述的?Event loop?順序可知,如果宏任務(wù)中的異步代碼有大量的計(jì)算并且需要操作?DOM?的話,為了更快的 界面響應(yīng),我們可以把操作?DOM?放入微任務(wù)中
#17 setTimeout 倒計(jì)時(shí)誤差
JS?是單線程的,所以?setTimeout?的誤差其實(shí)是無(wú)法被完全解決的,原因有很多,可能是回調(diào)中的,有可能是瀏覽器中的各種事件導(dǎo)致。這也是為什么頁(yè)面開(kāi)久了,定時(shí)器會(huì)不準(zhǔn)的原因,當(dāng)然我們可以通過(guò)一定的辦法去減少這個(gè)誤差。
// 以下是一個(gè)相對(duì)準(zhǔn)備的倒計(jì)時(shí)實(shí)現(xiàn) var period = 60 * 1000 * 60 * 2 var startTime = new Date().getTime(); var count = 0 var end = new Date().getTime() + period var interval = 1000 var currentInterval = intervalfunction loop() {count++var offset = new Date().getTime() - (startTime + count * interval); // 代碼執(zhí)行所消耗的時(shí)間var diff = end - new Date().getTime()var h = Math.floor(diff / (60 * 1000 * 60))var hdiff = diff % (60 * 1000 * 60)var m = Math.floor(hdiff / (60 * 1000))var mdiff = hdiff % (60 * 1000)var s = mdiff / (1000)var sCeil = Math.ceil(s)var sFloor = Math.floor(s)currentInterval = interval - offset // 得到下一次循環(huán)所消耗的時(shí)間console.log('時(shí):'+h, '分:'+m, '毫秒:'+s, '秒向上取整:'+sCeil, '代碼執(zhí)行時(shí)間:'+offset, '下次循環(huán)間隔'+currentInterval) // 打印 時(shí) 分 秒 代碼執(zhí)行時(shí)間 下次循環(huán)間隔setTimeout(loop, currentInterval) }setTimeout(loop, currentInterval)#18 數(shù)組降維
[1, [2], 3].flatMap(v => v) // -> [1, 2, 3]如果想將一個(gè)多維數(shù)組徹底的降維,可以這樣實(shí)現(xiàn)
const flattenDeep = (arr) => Array.isArray(arr)? arr.reduce( (a, b) => [...a, ...flattenDeep(b)] , []): [arr]flattenDeep([1, [[2], [3, [4]], 5]])#19 深拷貝
這個(gè)問(wèn)題通常可以通過(guò)?JSON.parse(JSON.stringify(object))?來(lái)解決
let a = {age: 1,jobs: {first: 'FE'} } let b = JSON.parse(JSON.stringify(a)) a.jobs.first = 'native' console.log(b.jobs.first) // FE但是該方法也是有局限性的:
- 會(huì)忽略?undefined
- 會(huì)忽略?symbol
- 不能序列化函數(shù)
- 不能解決循環(huán)引用的對(duì)象
在遇到函數(shù)、?undefined?或者?symbol?的時(shí)候,該對(duì)象也不能正常的序列化
let a = {age: undefined,sex: Symbol('male'),jobs: function() {},name: 'yck' } let b = JSON.parse(JSON.stringify(a)) console.log(b) // {name: "yck"}但是在通常情況下,復(fù)雜數(shù)據(jù)都是可以序列化的,所以這個(gè)函數(shù)可以解決大部分問(wèn)題,并且該函數(shù)是內(nèi)置函數(shù)中處理深拷貝性能最快的。當(dāng)然如果你的數(shù)據(jù)中含有以上三種情況下,可以使用?lodash?的深拷貝函數(shù)
#20 typeof 于 instanceof 區(qū)別
typeof?對(duì)于基本類(lèi)型,除了?null都可以顯示正確的類(lèi)型
typeof 1 // 'number' typeof '1' // 'string' typeof undefined // 'undefined' typeof true // 'boolean' typeof Symbol() // 'symbol' typeof b // b 沒(méi)有聲明,但是還會(huì)顯示 undefinedtypeof?對(duì)于對(duì)象,除了函數(shù)都會(huì)顯示?object
typeof [] // 'object' typeof {} // 'object' typeof console.log // 'function'對(duì)于?null?來(lái)說(shuō),雖然它是基本類(lèi)型,但是會(huì)顯示?object,這是一個(gè)存在很久了的?Bug
typeof null // 'object'instanceof?可以正確的判斷對(duì)象的類(lèi)型,因?yàn)閮?nèi)部機(jī)制是通過(guò)判斷對(duì)象的原型鏈中是不是能找到類(lèi)型的?prototype
我們也可以試著實(shí)現(xiàn)一下 instanceof function instanceof(left, right) {// 獲得類(lèi)型的原型let prototype = right.prototype// 獲得對(duì)象的原型left = left.__proto__// 判斷對(duì)象的類(lèi)型是否等于類(lèi)型的原型while (true) {if (left === null)return falseif (prototype === left)return trueleft = left.__proto__} }#二、瀏覽器
#1 cookie和localSrorage、session、indexDB 的區(qū)別
| 數(shù)據(jù)生命周期 | 一般由服務(wù)器生成,可以設(shè)置過(guò)期時(shí)間 | 除非被清理,否則一直存在 | 頁(yè)面關(guān)閉就清理 | 除非被清理,否則一直存在 |
| 數(shù)據(jù)存儲(chǔ)大小 | 4K | 5M | 5M | 無(wú)限 |
| 與服務(wù)端通信 | 每次都會(huì)攜帶在 header 中,對(duì)于請(qǐng)求性能影響 | 不參與 | 不參與 | 不參與 |
從上表可以看到,cookie?已經(jīng)不建議用于存儲(chǔ)。如果沒(méi)有大量數(shù)據(jù)存儲(chǔ)需求的話,可以使用?localStorage和?sessionStorage?。對(duì)于不怎么改變的數(shù)據(jù)盡量使用?localStorage?存儲(chǔ),否則可以用?sessionStorage?存儲(chǔ)。
對(duì)于?cookie,我們還需要注意安全性
| value | 如果用于保存用戶(hù)登錄態(tài),應(yīng)該將該值加密,不能使用明文的用戶(hù)標(biāo)識(shí) |
| http-only | 不能通過(guò)?JS訪問(wèn)?Cookie,減少?XSS攻擊 |
| secure | 只能在協(xié)議為?HTTPS?的請(qǐng)求中攜帶 |
| same-site | 規(guī)定瀏覽器不能在跨域請(qǐng)求中攜帶?Cookie,減少?CSRF?攻擊 |
#2 怎么判斷頁(yè)面是否加載完成?
- Load?事件觸發(fā)代表頁(yè)面中的?DOM,CSS,JS,圖片已經(jīng)全部加載完畢。
- DOMContentLoaded?事件觸發(fā)代表初始的?HTML?被完全加載和解析,不需要等待?CSS,JS,圖片加載
#3 如何解決跨域
因?yàn)闉g覽器出于安全考慮,有同源策略。也就是說(shuō),如果協(xié)議、域名或者端口有一個(gè)不同就是跨域,Ajax請(qǐng)求會(huì)失敗。
我們可以通過(guò)以下幾種常用方法解決跨域的問(wèn)題
JSONP
JSONP?的原理很簡(jiǎn)單,就是利用?<script>標(biāo)簽沒(méi)有跨域限制的漏洞。通過(guò)?<script>標(biāo)簽指向一個(gè)需要訪問(wèn)的地址并提供一個(gè)回調(diào)函數(shù)來(lái)接收數(shù)據(jù)當(dāng)需要通訊時(shí)
<script src="http://domain/api?param1=a¶m2=b&callback=jsonp"></script> <script>function jsonp(data) {console.log(data)} </script>JSONP?使用簡(jiǎn)單且兼容性不錯(cuò),但是只限于?get?請(qǐng)求
- 在開(kāi)發(fā)中可能會(huì)遇到多個(gè)?JSONP?請(qǐng)求的回調(diào)函數(shù)名是相同的,這時(shí)候就需要自己封裝一個(gè)?JSONP,以下是簡(jiǎn)單實(shí)現(xiàn)
CORS
- ORS需要瀏覽器和后端同時(shí)支持。IE 8?和?9?需要通過(guò)?XDomainRequest?來(lái)實(shí)現(xiàn)。
- 瀏覽器會(huì)自動(dòng)進(jìn)行?CORS?通信,實(shí)現(xiàn)CORS通信的關(guān)鍵是后端。只要后端實(shí)現(xiàn)了?CORS,就實(shí)現(xiàn)了跨域。
- 服務(wù)端設(shè)置?Access-Control-Allow-Origin?就可以開(kāi)啟?CORS。 該屬性表示哪些域名可以訪問(wèn)資源,如果設(shè)置通配符則表示所有網(wǎng)站都可以訪問(wèn)資源。
document.domain
- 該方式只能用于二級(jí)域名相同的情況下,比如?a.test.com?和?b.test.com?適用于該方式。
- 只需要給頁(yè)面添加?document.domain = 'test.com'?表示二級(jí)域名都相同就可以實(shí)現(xiàn)跨域
postMessage
這種方式通常用于獲取嵌入頁(yè)面中的第三方頁(yè)面數(shù)據(jù)。一個(gè)頁(yè)面發(fā)送消息,另一個(gè)頁(yè)面判斷來(lái)源并接收消息
// 發(fā)送消息端 window.parent.postMessage('message', 'http://test.com'); // 接收消息端 var mc = new MessageChannel(); mc.addEventListener('message', (event) => {var origin = event.origin || event.originalEvent.origin;if (origin === 'http://test.com') {console.log('驗(yàn)證通過(guò)')} });#4 什么是事件代理
如果一個(gè)節(jié)點(diǎn)中的子節(jié)點(diǎn)是動(dòng)態(tài)生成的,那么子節(jié)點(diǎn)需要注冊(cè)事件的話應(yīng)該注冊(cè)在父節(jié)點(diǎn)上
<ul id="ul"><li>1</li><li>2</li><li>3</li><li>4</li><li>5</li> </ul> <script>let ul = document.querySelector('#ul')ul.addEventListener('click', (event) => {console.log(event.target);}) </script>- 事件代理的方式相對(duì)于直接給目標(biāo)注冊(cè)事件來(lái)說(shuō),有以下優(yōu)點(diǎn)
- 節(jié)省內(nèi)存
- 不需要給子節(jié)點(diǎn)注銷(xiāo)事件
#5 Service worker
service worker
Service workers?本質(zhì)上充當(dāng)Web應(yīng)用程序與瀏覽器之間的代理服務(wù)器,也可以在網(wǎng)絡(luò)可用時(shí)作為瀏覽器和網(wǎng)絡(luò)間的代理。它們旨在(除其他之外)使得能夠創(chuàng)建有效的離線體驗(yàn),攔截網(wǎng)絡(luò)請(qǐng)求并基于網(wǎng)絡(luò)是否可用以及更新的資源是否駐留在服務(wù)器上來(lái)采取適當(dāng)?shù)膭?dòng)作。他們還允許訪問(wèn)推送通知和后臺(tái)同步API
目前該技術(shù)通常用來(lái)做緩存文件,提高首屏速度,可以試著來(lái)實(shí)現(xiàn)這個(gè)功能
// index.js if (navigator.serviceWorker) {navigator.serviceWorker.register("sw.js").then(function(registration) {console.log("service worker 注冊(cè)成功");}).catch(function(err) {console.log("servcie worker 注冊(cè)失敗");}); } // sw.js // 監(jiān)聽(tīng) `install` 事件,回調(diào)中緩存所需文件 self.addEventListener("install", e => {e.waitUntil(caches.open("my-cache").then(function(cache) {return cache.addAll(["./index.html", "./index.js"]);})); });// 攔截所有請(qǐng)求事件 // 如果緩存中已經(jīng)有請(qǐng)求的數(shù)據(jù)就直接用緩存,否則去請(qǐng)求數(shù)據(jù) self.addEventListener("fetch", e => {e.respondWith(caches.match(e.request).then(function(response) {if (response) {return response;}console.log("fetch source");})); });打開(kāi)頁(yè)面,可以在開(kāi)發(fā)者工具中的?Application?看到?Service Worker已經(jīng)啟動(dòng)了
#6 瀏覽器緩存
緩存對(duì)于前端性能優(yōu)化來(lái)說(shuō)是個(gè)很重要的點(diǎn),良好的緩存策略可以降低資源的重復(fù)加載提高網(wǎng)頁(yè)的整體加載速度。
- 通常瀏覽器緩存策略分為兩種:強(qiáng)緩存和協(xié)商緩存。
強(qiáng)緩存
實(shí)現(xiàn)強(qiáng)緩存可以通過(guò)兩種響應(yīng)頭實(shí)現(xiàn):Expires?和?Cache-Control?。強(qiáng)緩存表示在緩存期間不需要請(qǐng)求,state code?為?200
Expires: Wed, 22 Oct 2018 08:41:00 GMTExpires?是?HTTP / 1.0?的產(chǎn)物,表示資源會(huì)在Wed,22 Oct 2018 08:41:00 GMT?后過(guò)期,需要再次請(qǐng)求。并且?Expires?受限于本地時(shí)間,如果修改了本地時(shí)間,可能會(huì)造成緩存失效。
Cache-control: max-age=30- Cache-Control?出現(xiàn)于?HTTP / 1.1,優(yōu)先級(jí)高于?Expires?。該屬性表示資源會(huì)在?30?秒后過(guò)期,需要再次請(qǐng)求。
協(xié)商緩存
- 如果緩存過(guò)期了,我們就可以使用協(xié)商緩存來(lái)解決問(wèn)題。協(xié)商緩存需要請(qǐng)求,如果緩存有效會(huì)返回?304。
- 協(xié)商緩存需要客戶(hù)端和服務(wù)端共同實(shí)現(xiàn),和強(qiáng)緩存一樣,也有兩種實(shí)現(xiàn)方式
Last-Modified 和 If-Modified-Since
- Last-Modified表示本地文件最后修改日期,If-Modified-Since?會(huì)將?Last-Modified?的值發(fā)送給服務(wù)器,詢(xún)問(wèn)服務(wù)器在該日期后資源是否有更新,有更新的話就會(huì)將新的資源發(fā)送回來(lái)。
- 但是如果在本地打開(kāi)緩存文件,就會(huì)造成?Last-Modified被修改,所以在?HTTP / 1.1?出現(xiàn)了?ETag
ETag 和 If-None-Match
ETag?類(lèi)似于文件指紋,If-None-Match?會(huì)將當(dāng)前?ETag發(fā)送給服務(wù)器,詢(xún)問(wèn)該資源?ETag?是否變動(dòng),有變動(dòng)的話就將新的資源發(fā)送回來(lái)。并且?ETag?優(yōu)先級(jí)比?Last-Modified?高
選擇合適的緩存策略
對(duì)于大部分的場(chǎng)景都可以使用強(qiáng)緩存配合協(xié)商緩存解決,但是在一些特殊的地方可能需要選擇特殊的緩存策略
- 對(duì)于某些不需要緩存的資源,可以使用?Cache-control: no-store?,表示該資源不需要緩存
- 對(duì)于頻繁變動(dòng)的資源,可以使用?Cache-Control: no-cache并配合?ETag?使用,表示該資源已被緩存,但是每次都會(huì)發(fā)送請(qǐng)求詢(xún)問(wèn)資源是否更新。
- 對(duì)于代碼文件來(lái)說(shuō),通常使用?Cache-Control: max-age=31536000?并配合策略緩存使用,然后對(duì)文件進(jìn)行指紋處理,一旦文件名變動(dòng)就會(huì)立刻下載新的文件
#7 瀏覽器性能問(wèn)題
重繪(Repaint)和回流(Reflow)
- 重繪和回流是渲染步驟中的一小節(jié),但是這兩個(gè)步驟對(duì)于性能影響很大。
- 重繪是當(dāng)節(jié)點(diǎn)需要更改外觀而不會(huì)影響布局的,比如改變?color就叫稱(chēng)為重繪
- 回流是布局或者幾何屬性需要改變就稱(chēng)為回流。
- 回流必定會(huì)發(fā)生重繪,重繪不一定會(huì)引發(fā)回流。回流所需的成本比重繪高的多,改變深層次的節(jié)點(diǎn)很可能導(dǎo)致父節(jié)點(diǎn)的一系列回流。
所以以下幾個(gè)動(dòng)作可能會(huì)導(dǎo)致性能問(wèn)題:
- 改變?window?大小
- 改變字體
- 添加或刪除樣式
- 文字改變
- 定位或者浮動(dòng)
- 盒模型
很多人不知道的是,重繪和回流其實(shí)和 Event loop 有關(guān)。
- 當(dāng)?Event loop?執(zhí)行完?Microtasks后,會(huì)判斷?document?是否需要更新。- 因?yàn)闉g覽器是?60Hz?的刷新率,每?16ms才會(huì)更新一次。
- 然后判斷是否有resize?或者?scroll?,有的話會(huì)去觸發(fā)事件,所以?resize?和?scroll?事件也是至少 16ms 才會(huì)觸發(fā)一次,并且自帶節(jié)流功能。
- 判斷是否觸發(fā)了?media query
- 更新動(dòng)畫(huà)并且發(fā)送事件
- 判斷是否有全屏操作事件
- 執(zhí)行?requestAnimationFrame回調(diào)
- 執(zhí)行?IntersectionObserver?回調(diào),該方法用于判斷元素是否可見(jiàn),可以用于懶加載上,但是兼容性不好
- 更新界面
- 以上就是一幀中可能會(huì)做的事情。如果在一幀中有空閑時(shí)間,就會(huì)去執(zhí)行?requestIdleCallback?回調(diào)。
減少重繪和回流
使用?translate?替代?top
<div class="test"></div> <style>.test {position: absolute;top: 10px;width: 100px;height: 100px;background: red;} </style> <script>setTimeout(() => {// 引起回流document.querySelector('.test').style.top = '100px'}, 1000) </script>- 使用?visibility?替換?display: none?,因?yàn)榍罢咧粫?huì)引起重繪,后者會(huì)引發(fā)回流(改變了布局)
- 把?DOM?離線后修改,比如:先把?DOM?給?display:none(有一次?Reflow),然后你修改100次,然后再把它顯示出來(lái)
- 不要把?DOM結(jié)點(diǎn)的屬性值放在一個(gè)循環(huán)里當(dāng)成循環(huán)里的變量
- 不要使用?table?布局,可能很小的一個(gè)小改動(dòng)會(huì)造成整個(gè)?table?的重新布局 動(dòng)畫(huà)實(shí)現(xiàn)的速度的選擇,動(dòng)畫(huà)速度越快,回流次數(shù)越多,也可以選擇使用?requestAnimationFrame
- CSS選擇符從右往左匹配查找,避免?DOM?深度過(guò)深
- 將頻繁運(yùn)行的動(dòng)畫(huà)變?yōu)閳D層,圖層能夠阻止該節(jié)點(diǎn)回流影響別的元素。比如對(duì)于?video?標(biāo)簽,瀏覽器會(huì)自動(dòng)將該節(jié)點(diǎn)變?yōu)閳D層。
CDN
靜態(tài)資源盡量使用?CDN?加載,由于瀏覽器對(duì)于單個(gè)域名有并發(fā)請(qǐng)求上限,可以考慮使用多個(gè)?CDN?域名。對(duì)于?CDN?加載靜態(tài)資源需要注意 CDN 域名要與主站不同,否則每次請(qǐng)求都會(huì)帶上主站的?Cookie
使用 Webpack 優(yōu)化項(xiàng)目
- 對(duì)于?Webpack4,打包項(xiàng)目使用?production?模式,這樣會(huì)自動(dòng)開(kāi)啟代碼壓縮
- 使用?ES6?模塊來(lái)開(kāi)啟?tree shaking,這個(gè)技術(shù)可以移除沒(méi)有使用的代碼
- 優(yōu)化圖片,對(duì)于小圖可以使用?base64?的方式寫(xiě)入文件中
- 按照路由拆分代碼,實(shí)現(xiàn)按需加載
#三、Webpack
?
#1 優(yōu)化打包速度
- 減少文件搜索范圍
- 比如通過(guò)別名
- loader?的?test,include & exclude
- Webpack4?默認(rèn)壓縮并行
- Happypack?并發(fā)調(diào)用
- babel?也可以緩存編譯
#2 Babel 原理
- 本質(zhì)就是編譯器,當(dāng)代碼轉(zhuǎn)為字符串生成?AST,對(duì)?AST?進(jìn)行轉(zhuǎn)變最后再生成新的代碼
- 分為三步:詞法分析生成?Token,語(yǔ)法分析生成?AST,遍歷?AST,根據(jù)插件變換相應(yīng)的節(jié)點(diǎn),最后把?AST轉(zhuǎn)換為代碼
#3 如何實(shí)現(xiàn)一個(gè)插件
- 調(diào)用插件?apply?函數(shù)傳入?compiler?對(duì)象
- 通過(guò)?compiler?對(duì)象監(jiān)聽(tīng)事件
比如你想實(shí)現(xiàn)一個(gè)編譯結(jié)束退出命令的插件
apply (compiler) {const afterEmit = (compilation, cb) => {cb()setTimeout(function () {process.exit(0)}, 1000)}compiler.plugin('after-emit', afterEmit) } }module.exports = BuildEndPlugin總結(jié)
- 上一篇: JQuery 总结(8)Ajax 无刷新
- 下一篇: 前端-计算机基础