這裡給大家分享我在網上總結出來的一些知識,希望對大家有所幫助 第一問:安全類型檢測——typeof和instanceof 區別以及缺陷,以及解決方案 這兩個方法都可以用來判斷變數類型 區別:前者是判斷這個變數是什麼類型,後者是判斷這個變數是不是某種類型,返回的是布爾值 (1)typeof 缺陷: 1 ...
這裡給大家分享我在網上總結出來的一些知識,希望對大家有所幫助
第一問:安全類型檢測——typeof和instanceof 區別以及缺陷,以及解決方案
這兩個方法都可以用來判斷變數類型
區別:前者是判斷這個變數是什麼類型,後者是判斷這個變數是不是某種類型,返回的是布爾值
(1)typeof
缺陷:
1.不能判斷變數具體的數據類型比如數組、正則、日期、對象,因為都會返回object,不過可以判斷function,如果檢測對象是正則表達式的時候,在Safari和Chrome中使用typeof的時候會錯誤的返回"function",其他的瀏覽器返回的是object.
2.判斷null的時候返回的是一個object,這是js的一個缺陷,判斷NaN的時候返回是number
(2)instanceof 可以用來檢測這個變數是否為某種類型,返回的是布爾值,並且可以判斷這個變數是否為某個函數的實例,它檢測的是對象的原型
let num = 1 num instanceof Number // false num = new Number(1) num instanceof Number // true
明明都是num,而且都是1,只是因為第一個不是對象,是基本類型,所以直接返回false,而第二個是封裝成對象,所以true。
這裡要嚴格註意這個問題,有些說法是檢測目標的__proto__與構造函數的prototype相同即返回true,這是不嚴謹的,檢測的一定要是對象才行,如:
let num = 1 num.__proto__ === Number.prototype // true num instanceof Number // false num = new Number(1) num.proto === Number.prototype // true num instanceof Number // true num.proto === (new Number(1)).proto // true
此外,instanceof還有另外一個缺點:如果一個頁面上有多個框架,即有多個全局環境,那麼我在a框架里定義一個Array,然後在b框架里去用instanceof去判斷,那麼該array的原型鏈上不可能找到b框架里的array,則會判斷該array不是一個array。
解決方案:使用Object.prototype.toString.call(value) 方法去調用對象,得到對象的構造函數名。可以解決instanceof的跨框架問題,缺點是對用戶自定義的類型,它只會返回[object Object]
第二問:既然提到了instanceof,那手寫實現下instanceof吧
// [1,2,3] instanceof Array ---- true // L instanceof R // 變數R的原型 存在於 變數L的原型鏈上 function instance_of(L,R){ // 驗證如果為基本數據類型,就直接返回false const baseType = ['string', 'number','boolean','undefined','symbol'] if(baseType.includes(typeof(L))) { return false } let RP = R.prototype; //取 R 的顯示原型 L = L.__proto__; //取 L 的隱式原型 while(true){ // 無線迴圈的寫法(也可以使 for(;;) ) if(L === null){ //找到最頂層 return false; } if(L === RP){ //嚴格相等 return true; } L = L.__proto__; //沒找到繼續向上一層原型鏈查找 } }
第三問:作用域安全的構造函數--當我們new一個構造函數的時候可以獲得一個實例,要是我們忘記寫new了呢?
例如
function Person(){ this.name = "小紅" } p = Person();
這會發生什麼問題?,怎麼解決
這樣直接使用,this會映射到全局對象window上。解決方法可以是:首先確認this對象是正確類型的實例。如果不是,那麼會創建新的實例並返回。請看下麵的例子
function Person(){ if(this instanceof Person){ this.name = "小紅" }else{ return new Person() } } p = Person();
第四問:談一下惰性載入函數
在JavaScript代碼中,由於瀏覽器之間行為的差異,多數JavaScript代碼包含了大量的if語句,以檢查瀏覽器特性,解決不同瀏覽器的相容問題。例如添加事件的函數:
function addEvent (element, type, handler) { if (element.addEventListener) { element.addEventListener(type, handler, false); } else if (element.attachEvent) { element.attachEvent("on" + type, handler); } else { element["on" + type] = handler; } }每次調用addEvent()的時候,都要對瀏覽器所支持的能力仔細檢查。首先檢查是否支持addEventListener方法,如果不支持再檢查是否支持attachEvent方法,如果還不支持,就用DOM 0級的方法添加事件。在調用addEvent()過程中,每次這個過程都要走一遍。其實,瀏覽器支持其中的一種方法就會一直支持他,就沒有必要再進行其他分支的檢測了,也就是說if語句不必每次都執行,代碼可以運行的更快一些。解決的方案稱之為惰性載入。 所謂惰性載入,就是說函數的if分支只會執行一次,之後調用函數時,直接進入所支持的分支代碼。有兩種實現惰性載入的方式,第一種事函數在第一次調用時,對函數本身進行二次處理,該函數會被覆蓋為符合分支條件的函數,這樣對原函數的調用就不用再經過執行的分支了,我們可以用下麵的方式使用惰性載入重寫addEvent()。
function addEvent (type, element, handler) { if (element.addEventListener) { addEvent = function (type, element, handler) { element.addEventListener(type, handler, false); } } else if(element.attachEvent){ addEvent = function (type, element, handler) { element.attachEvent('on' + type, handler); } } else{ addEvent = function (type, element, handler) { element['on' + type] = handler; } } return addEvent(type, element, handler); }
在這個惰性載入的addEvent()中,if語句的每個分支都會為addEvent變數賦值,有效覆蓋了原函數。最後一步便是調用了新賦函數。下一次調用addEvent()的時候,便會直接調用新賦值的函數,這樣就不用再執行if語句了。
第二種實現惰性載入的方式是在聲明函數時就指定適當的函數。這樣在第一次調用函數時就不會損失性能了,只在代碼載入時會損失一點性能。一下就是按照這一思路重寫的addEvent()。
var addEvent = (function () { if (document.addEventListener) { return function (type, element, fun) { element.addEventListener(type, fun, false); } } else if (document.attachEvent) { return function (type, element, fun) { element.attachEvent('on' + type, fun); } } else { return function (type, element, fun) { element['on' + type] = fun; } } })();
這個例子中使用的技巧是創建一個匿名的自執行函數,通過不同的分支以確定應該使用那個函數實現,實際的邏輯都一樣,不一樣的地方就是使用了函數表達式(使用了var定義函數)和新增了一個匿名函數,另外每個分支都返回一個正確的函數,並立即將其賦值給變數addEvent。
惰性載入函數的優點只執行一次if分支,避免了函數每次執行時候都要執行if分支和不必要的代碼,因此提升了代碼性能,至於那種方式更合適,就要看您的需求而定了。
第五問:談一下函數節流
概念:限制一個函數在一定時間內只能執行一次。
主要實現思路 就是通過 setTimeout 定時器,通過設置延時時間,在第一次調用時,創建定時器,先設定一個變數true,寫入需要執行的函數。第二次執行這個函數時,會判斷變數是否true,是則返回。當第一次的定時器執行完函數最後會設定變數為false。那麼下次判斷變數時則為false,函數會依次運行。目的在於在一定的時間內,保證多次函數的請求只執行最後一次調用。
函數節流的代碼實現
function throttle(fn,wait){ var timer = null; return function(){ var context = this; var args = arguments; if(!timer){ timer = setTimeout(function(){ fn.apply(context,args); timer = null; },wait) } } } function handle(){ console.log(Math.random()); } window.addEventListener("mousemove",throttle(handle,1000));
函數節流的應用場景(throttle)
- DOM 元素的拖拽功能實現(mousemove)
- 高頻點擊提交,表單重覆提交
- 搜索聯想(keyup)
- 計算滑鼠移動的距離(mousemove)
- 監聽滾動事件,比如是否滑到底部自動載入更多,用throttle來判斷
- 射擊游戲的 mousedown/keydown 事件(單位時間只能發射一顆子彈)
- 監聽滾動事件判斷是否到頁面底部自動載入更多:給 scroll 加了 debounce 後,只有用戶停止滾動後,- - 才會判斷是否到了頁面底部;如果是 throttle 的話,只要頁面滾動就會間隔一段時間判斷一次.
第六問:談一下函數防抖
概念:函數防抖(debounce),就是指觸發事件後,在 n 秒內函數只能執行一次,如果觸發事件後在 n 秒內又觸發了事件,則會重新計算函數延執行時間。
函數防抖的要點,是需要一個 setTimeout 來輔助實現,延遲運行需要執行的代碼。如果方法多次觸發,則把上次記錄的延遲執行代碼用 clearTimeout 清掉,重新開始計時。若計時期間事件沒有被重新觸發,等延遲時間計時完畢,則執行目標代碼。
函數防抖的代碼實現
function debounce(fn,wait){ var timer = null; return function(){ if(timer !== null){ clearTimeout(timer); } timer = setTimeout(fn,wait); } } function handle(){ console.log(Math.random()); } window.addEventListener("resize",debounce(handle,1000));
函數防抖的使用場景 函數防抖一般用在什麼情況之下呢?一般用在,連續的事件只需觸發一次回調的場合。具體有:
- 搜索框搜索輸入。只需用戶最後一次輸入完,再發送請求;
- 用戶名、手機號、郵箱輸入驗證;
- 瀏覽器視窗大小改變後,只需視窗調整完後,再執行 resize 事件中的代碼,防止重覆渲染。
目前遇到過的用處就是這些,理解了原理與實現思路,小伙伴可以把它運用在任何需要的場合,提高代碼質量。
第七問:談一下requestAnimationFrame
動畫原理 : 眼前所看到圖像正在以每秒60次的頻率刷新,由於刷新頻率很高,因此你感覺不到它在刷新。而動畫本質就是要讓人眼看到圖像被刷新而引起變化的視覺效果,這個變化要以連貫的、平滑的方式進行過渡。 那怎麼樣才能做到這種效果呢?
刷新頻率為60Hz的屏幕每16.7ms刷新一次,我們在屏幕每次刷新前,將圖像的位置向左移動一個像素,即1px。這樣一來,屏幕每次刷出來的圖像位置都比前一個要差1px,因此你會看到圖像在移動;由於我們人眼的視覺停留效應,當前位置的圖像停留在大腦的印象還沒消失,緊接著圖像又被移到了下一個位置,因此你才會看到圖像在流暢的移動,這就是視覺效果上形成的動畫。
與setTimeout相比較:
理解了上面的概念以後,我們不難發現,setTimeout 其實就是通過設置一個間隔時間來不斷的改變圖像的位置,從而達到動畫效果的。但我們會發現,利用seTimeout實現的動畫在某些低端機上會出現卡頓、抖動的現象。 這種現象的產生有兩個原因:
-
setTimeout的執行時間並不是確定的。在Javascript中, setTimeout 任務被放進了非同步隊列中,只有當主線程上的任務執行完以後,才會去檢查該隊列里的任務是否需要開始執行,因此 setTimeout 的實際執行時間一般要比其設定的時間晚一些。
-
刷新頻率受屏幕解析度和屏幕尺寸的影響,因此不同設備的屏幕刷新頻率可能會不同,而 setTimeout只能設置一個固定的時間間隔,這個時間不一定和屏幕的刷新時間相同。
以上兩種情況都會導致setTimeout的執行步調和屏幕的刷新步調不一致,從而引起丟幀現象
requestAnimationFrame:與setTimeout相比,requestAnimationFrame最大的優勢是由系統來決定回調函數的執行時機。具體一點講,如果屏幕刷新率是60Hz,那麼回調函數就每16.7ms被執行一次,如果刷新率是75Hz,那麼這個時間間隔就變成了1000/75=13.3ms,換句話說就是,requestAnimationFrame的步伐跟著系統的刷新步伐走。它能保證回調函數在屏幕每一次的刷新間隔中只被執行一次,這樣就不會引起丟幀現象,也不會導致動畫出現卡頓的問題。
除此之外,requestAnimationFrame還有以下兩個優勢:
-
CPU節能:使用setTimeout實現的動畫,當頁面被隱藏或最小化時,setTimeout 仍然在後臺執行動畫任務,由於此時頁面處於不可見或不可用狀態,刷新動畫是沒有意義的,完全是浪費CPU資源。而requestAnimationFrame則完全不同,當頁面處理未激活的狀態下,該頁面的屏幕刷新任務也會被系統暫停,因此跟著系統步伐走的requestAnimationFrame也會停止渲染,當頁面被激活時,動畫就從上次停留的地方繼續執行,有效節省了CPU開銷。
-
函數節流:在高頻率事件(resize,scroll等)中,為了防止在一個刷新間隔內發生多次函數執行,使用requestAnimationFrame可保證每個刷新間隔內,函數只被執行一次,這樣既能保證流暢性,也能更好的節省函數執行的開銷。一個刷新間隔內函數執行多次時沒有意義的,因為顯示器每16.7ms刷新一次,多次繪製並不會在屏幕上體現出來。
第八問:web計時,你知道該怎麼計算首屏,白屏時間嗎?
白屏時間: 白屏時間指的是瀏覽器開始顯示內容的時間。因此我們只需要知道是瀏覽器開始顯示內容的時間點,即頁面白屏結束時間點即可獲取到頁面的白屏時間。
計算白屏時間 因此,我們通常認為瀏覽器開始渲染 標簽或者解析完 標簽的時刻就是頁面白屏結束的時間點。
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>白屏</title> <script type="text/javascript"> // 不相容performance.timing 的瀏覽器,如IE8 window.pageStartTime = Date.now(); </script> <!-- 頁面 CSS 資源 --> <link rel="stylesheet" href="common.css"> <link rel="stylesheet" href="page.css"> <script type="text/javascript"> // 白屏時間結束點 window.firstPaint = Date.now(); </script> </head> <body> <!-- 頁面內容 --> </body> </html>
因此白屏時間則可以這樣計算出:
可使用 Performance API 時
:
白屏時間 = firstPaint - performance.timing.navigationStart;
不可使用 Performance API 時
:
白屏時間 = firstPaint - pageStartTime; //雖然我們知道這並不准確,畢竟DNS解析,tcp三次握手等都沒計算入內。
首屏時間: 首屏時間是指用戶打開網站開始,到瀏覽器首屏內容渲染完成的時間。對於用戶體驗來說,首屏時間是用戶對一個網站的重要體驗因素。通常一個網站,如果首屏時間在5秒以內是比較優秀的,10秒以內是可以接受的,10秒以上就不可容忍了。超過10秒的首屏時間用戶會選擇刷新頁面或立刻離開。
通常計算首屏的方法有
-
首屏模塊標簽標記法
-
統計首屏內載入最慢的圖片的時間
-
自定義首屏內容計演算法
1、首屏模塊標簽標記法
首屏模塊標簽標記法,通常適用於首屏內容不需要通過拉取數據才能生存以及頁面不考慮圖片等資源載入的情況。我們會在 HTML 文檔中對應首屏內容的標簽結束位置,使用內聯的 JavaScript 代碼記錄當前時間戳。如下所示:
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>首屏</title> <script type="text/javascript"> window.pageStartTime = Date.now(); </script> <link rel="stylesheet" href="common.css"> <link rel="stylesheet" href="page.css"> </head> <body> <!-- 首屏可見模塊1 --> <div class="module-1"></div> <!-- 首屏可見模塊2 --> <div class="module-2"></div> <script type="text/javascript"> window.firstScreen = Date.now(); </script> <!-- 首屏不可見模塊3 --> <div class="module-3"></div> <!-- 首屏不可見模塊4 --> <div class="module-4"></div> </body> </html>
此時首屏時間等於 firstScreen - performance.timing.navigationStart;
事實上首屏模塊標簽標記法 在業務中的情況比較少,大多數頁面都需要通過介面拉取數據才能完整展示,因此我們會使用 JavaScript 腳本來判斷首屏頁面內容載入情況。
2、統計首屏內圖片完成載入的時間
通常我們首屏內容載入最慢的就是圖片資源,因此我們會把首屏內載入最慢的圖片的時間當做首屏的時間。
由於瀏覽器對每個頁面的 TCP 連接數有限制,使得並不是所有圖片都能立刻開始下載和顯示。因此我們在 DOM樹 構建完成後將會去遍歷首屏內的所有圖片標簽,並且監聽所有圖片標簽 onload 事件,最終遍歷圖片標簽的載入時間的最大值,並用這個最大值減去 navigationStart 即可獲得近似的首屏時間。
此時首屏時間等於 載入最慢的圖片的時間點 - performance.timing.navigationStart; //首屏時間嘗試: //1,獲取首屏基線高度 //2,計算出基線dom元素之上的所有圖片元素 //3,所有圖片onload之後為首屏顯示時間 //
function getOffsetTop(ele) { var offsetTop = ele.offsetTop; if (ele.offsetParent !== null) { offsetTop += getOffsetTop(ele.offsetParent); } return offsetTop; } var firstScreenHeight = win.screen.height; var firstScreenImgs = []; var isFindLastImg = false; var allImgLoaded = false; var t = setInterval(function() { var i, img; if (isFindLastImg) { if (firstScreenImgs.length) { for (i = 0; i < firstScreenImgs.length; i++) { img = firstScreenImgs[i]; if (!img.complete) { allImgLoaded = false; break; } else { allImgLoaded = true; } } } else { allImgLoaded = true; } if (allImgLoaded) { collect.add({ firstScreenLoaded: startTime - Date.now() }); clearInterval(t); } } else { var imgs = body.querySelector('img'); for (i = 0; i<imgs.length; i++) { img = imgs[i]; var imgOffsetTop = getOffsetTop(img); if (imgOffsetTop > firstScreenHeight) { isFindLastImg = true; break; } else if (imgOffsetTop <= firstScreenHeight && !img.hasPushed) { img.hasPushed = 1; firstScreenImgs.push(img); } } } }, 0); doc.addEventListener('DOMContentLoaded', function() { var imgs = body.querySelector('img'); if (!imgs.length) { isFindLastImg = true; } }); win.addEventListener('load', function() { allImgLoaded = true; isFindLastImg = true; if (t) { clearInterval(t); } collect.log(collect.global); });
解釋一下思路,大概就是判斷首屏有沒有圖片,如果沒圖片就用domready時間,如果有圖,分2種情況,圖在首屏,圖不在首屏,如果在則收集,並判斷載入狀態,載入完畢之後則首屏完成載入,如果首屏沒圖,找到首屏下麵的圖,立刻觸發首屏完畢。可以想象這麼做前端收集是不准的,但是可以確保最晚不會超過win load,所以應該還算有些意義。。沒辦法,移動端很多瀏覽器不支持performance api,所以土辦法前端收集,想出這麼個黑魔法,在基線插入節點收集也是個辦法,但是不友好,而且現在手機屏幕這麼多。。
3、自定義模塊內容計演算法
由於統計首屏內圖片完成載入的時間比較複雜。因此我們在業務中通常會通過自定義模塊內容,來簡化計算首屏時間。如下麵的做法:
- 忽略圖片等資源載入情況,只考慮頁面主要 DOM
- 只考慮首屏的主要模塊,而不是嚴格意義首屏線以上的所有內容
實際上用performance.timing來計算首屏載入時間與白屏時間非常簡單與精確。不過目前只支持IE10和chrome 貼下其API的使用
var navigationStart = performance.timing.navigationStart; //1488984540668 console.log(navigationStart); //Wed Mar 08 2017 22:49:44 GMT+0800 (中國標準時間) console.log(new Date(new Date(navigationStart))); 複製代碼 redirectStart:到當前頁面的重定向開始的時間。但只有在重定向的頁面來自同一個域時這個屬性才會有值;否則,值為0 redirectEnd:到當前頁面的重定向結束的時間。但只有在重定向的頁面來自同一個域時這個屬性才會有值;否則,值為0 console.log(performance.timing.redirectStart);//0 console.log(performance.timing.redirectEnd);//0 fetchStart:開始通過HTTP GET取得頁面的時間 console.log(performance.timing.fetchStart);//1488984540668 domainLookupStart:開始査詢當前頁面DNS的時間,如果使用了本地緩存或持久連接,則與fetchStart值相等 domainLookupEnd:査詢當前頁面DNS結束的時間,如果使用了本地緩存或持久連接,則與fetchStart值相等 console.log(performance.timing.domainLookupStart);//1488984540670 console.log(performance.timing.domainLookupEnd);//1488984540671 connectStart:瀏覽器嘗試連接伺服器的時間 secureConnectionStart:瀏覽器嘗試以SSL方式連接伺服器的時間。不使用SSL方式連接時,這個屬性的值為0 connectEnd:瀏覽器成功連接到伺服器的時間 console.log(performance.timing.connectStart);//1488984540671 console.log(performance.timing.secureConnectionStart);//0 console.log(performance.timing.connectEnd);//1488984540719 requestStart:瀏覽器開始請求頁面的時間 responseStart:瀏覽器接收到頁面第一位元組的時間 responseEnd:瀏覽器接收到頁面所有內容的時間 console.log(performance.timing.requestStart);//1488984540720 console.log(performance.timing.responseStart);//1488984540901 console.log(performance.timing.responseEnd);//1488984540902 unloadEventStart:前一個頁面的unload事件開始的時間。但只有在前一個頁面與當前頁面來自同一個域時這個屬性才會有值;否則,值為0 unloadEventEnd:前一個頁面的unload事件結束的時間。但只有在前一個頁面與當前頁面來自同一個域時這個屬性才會有值;否則,值為0 console.log(performance.timing.unloadEventStart);//1488984540902 console.log(performance.timing.unloadEventEnd);//1488984540903 domLoading:document.readyState變為"loading"的時間,即開始解析DOM樹的時間 domInteractive:document.readyState變為"interactive"的時間,即完成完成解析DOM樹的時間 domContentLoadedEventStart:發生DOMContentloaded事件的時間,即開始載入網頁內資源的時間 domContentLoadedEventEnd:DOMContentLoaded事件已經發生且執行完所有事件處理程式的時間,網頁內資源載入完成的時間 domComplete:document.readyState變為"complete"的時間,即DOM樹解析完成、網頁內資源準備就緒的時間 console.log(performance.timing.domLoading);//1488984540905 console.log(performance.timing.domInteractive);//1488984540932 console.log(performance.timing.domContentLoadedEventStart);//1488984540932 console.log(performance.timing.domContentLoadedEventEnd);//1488984540932 console.log(performance.timing.domComplete);//1488984540932 loadEventStart:發生load事件的時間,也就是load回調函數開始執行的時間 loadEventEnd:load事件已經發生且執行完所有事件處理程式的時間 console.log(performance.timing.loadEventStart);//1488984540933 console.log(performance.timing.loadEventEnd);//1488984540933
第九問:你知道web Worker嗎?
多線程技術在服務端技術中已經發展的很成熟了,而在Web端的應用中卻一直是雞肋 在新的標準中,提供的新的WebWork API,讓前端的非同步工作變得異常簡單。 使用:創建一個Worker對象,指向一個js文件,然後通過Worker對象往js文件發送消息,js文件內部的處理邏輯,處理完畢後,再發送消息回到當前頁面,純非同步方式,不影響當前主頁面渲染。
<!DOCTYPE html> <html xmlns="http://www.w3.org/1999/xhtml"> <head> <meta http-equiv="Content-Type" content="text/html; charset=utf-8"/> <title></title> <script type="text/javascript"> //創建線程 work對象 var work = new Worker("work.js"); //work文件中不要存在跟ui代碼 //發送消息 work.postMessage("100"); // 監聽消息 work.onmessage = function(event) { alert(event.data); }; </script> </head> <body> </body> </html>
work.js
onmessage = function (event) { //從1加到num var num = event.data; var result = 0; for (var i = 1; i <= num; i++) { result += i; } postMessage(result); }