您好,登錄后才能下訂單哦!
本篇內容介紹了“JavaScript的高級技巧有哪些”的有關知識,在實際案例的操作過程中,不少人都會遇到這樣的困境,接下來就讓小編帶領大家學習一下如何處理這些情況吧!希望大家仔細閱讀,能夠學有所成!
1. 安全的類型檢測
這個問題是怎么安全地檢測一個變量的類型,例如判斷一個變量是否為一個數組。通常的做法是使用instanceof,如下代碼所示:
let data = [1, 2, 3]; console.log(data instanceof Array); //true
但是上面的判斷在一定條件下會失敗——就是在iframe里面判斷一個父窗口的變量的時候。寫個demo驗證一下,如下主頁面的main.html:
<script> window.global = { arrayData: [1, 2, 3] } console.log("parent arrayData installof Array: " + (window.global.arrayData instanceof Array)); </script> <iframe src="iframe.html"></iframe>
在iframe.html判斷一下父窗口的變量類型:
<script> console.log("iframe window.parent.global.arrayData instanceof Array: " + (window.parent.global.arrayData instanceof Array)); </script>
在iframe里面使用window.parent得到父窗口的全局window對象,這個不管跨不跨域都沒有問題,進而可以得到父窗口的變量,然后用instanceof判斷。***運行結果如下:
可以看到父窗口的判斷是正確的,而子窗口的判斷是false,因此一個變量明明是Array,但卻不是Array,這是為什么呢?既然這個是父子窗口才會有的問題,于是試一下把Array改成父窗口的Array,即window.parent.Array,如下圖所示:
這次返回了true,然后再變換一下其它的判斷,如上圖,***可以知道根本原因是上圖***一個判斷:
Array !== window.parent.Array
它們分別是兩個函數,父窗口定義了一個,子窗口又定義了一個,內存地址不一樣,內存地址不一樣的Object等式判斷不成立,而window.parent.arrayData.constructor返回的是父窗口的Array,比較的時候是在子窗口,使用的是子窗口的Array,這兩個Array不相等,所以導致判斷不成立。
那怎么辦呢?
由于不能使用Object的內存地址判斷,可以使用字符串的方式,因為字符串是基本類型,字符串比較只要每個字符都相等就好了。ES5提供了這么一個方法Object.prototype.toString,我們先小試牛刀,試一下不同變量的返回值:
可以看到如果是數組返回”[object Array]”, ES5對這個函數 是這么規定的:
也就是說這個函數的返回值是“[object ”開頭,后面帶上變量類型的名稱和右括號。因此既然它是一個標準語法規范,所以可以用這個函數安全地判斷變量是不是數組。
可以這么寫:
Object.prototype.toString.call([1, 2, 3]) === "[object Array]"
注意要使用call,而不是直接調用,call的***個參數是context執行上下文,把數組傳給它作為執行上下文。
有一個比較有趣的現象是ES6的class也是返回function:
所以可以知道class也是用function實現的原型,也就是說class和function本質上是一樣的,只是寫法上不一樣。
那是不是說不能再使用instanceof判斷變量類型了?不是的,當你需要檢測父頁面的變量類型就得使用這種方法, 本頁面的變量還是可以使用instanceof或者constructor的方法判斷 ,只要你能確保這個變量不會跨頁面。因為對于大多數人來說,很少會寫iframe的代碼,所以沒有必要搞一個比較麻煩的方式,還是用簡單的方式就好了。
2. 惰性載入函數
有時候需要在代碼里面做一些兼容性判斷,或者是做一些UA的判斷,如下代碼所示:
//UA的類型 getUAType: function() { let ua = window.navigator.userAgent; if (ua.match(/renren/i)) { return 0; } else if (ua.match(/MicroMessenger/i)) { return 1; } else if (ua.match(/weibo/i)) { return 2; } return -1; }
這個函數的作用是判斷用戶是在哪個環境打開的網頁,以便于統計哪個渠道的效果比較好。
這種類型的判斷都有一個特點,就是它的結果是死的,不管執行判斷多少次,都會返回相同的結果,例如用戶的UA在這個網頁不可能會發生變化(除了調試設定的之外)。所以為了優化,才有了惰性函數一說,上面的代碼可以改成:
//UA的類型 getUAType: function() { let ua = window.navigator.userAgent; if(ua.match(/renren/i)) { pageData.getUAType = () => 0; return 0; } else if(ua.match(/MicroMessenger/i)) { pageData.getUAType = () => 1; return 1; } else if(ua.match(/weibo/i)) { pageData.getUAType = () => 2; return 2; } return -1; }
在每次判斷之后,把getUAType這個函數重新賦值,變成一個新的function,而這個function直接返回一個確定的變量,這樣以后的每次獲取都不用再判斷了,這就是惰性函數的作用。你可能會說這么幾個判斷能優化多少時間呢,這么點時間對于用戶來說幾乎是沒有區別的呀。確實如此,但是作為一個有追求的碼農,還是會想辦法盡可能優化自己的代碼,而不是只是為了完成需求完成功能。并且當你的這些優化累積到一個量的時候就會發生質變。我上大學的時候C++的老師舉了一個例子,說有個系統比較慢找她去看一下,其中她做的一個優化是把小數的雙精度改成單精度,***是快了不少。
但其實上面的例子我們有一個更簡單的實現,那就是直接搞個變量存起來就好了:
let ua = window.navigator.userAgent; let UAType = ua.match(/renren/i) ? 0 : ua.match(/MicroMessenger/i) ? 1 : ua.match(/weibo/i) ? 2 : -1;
連函數都不用寫了,缺點是即使沒有使用到UAType這個變量,也會執行一次判斷,但是我們認為這個變量被用到的概率還是很高的。
我們再舉一個比較有用的例子,由于Safari的無痕瀏覽會禁掉本地存儲,因此需要搞一個兼容性判斷:
Data.localStorageEnabled = true; // Safari的無痕瀏覽會禁用localStorage try{ window.localStorage.trySetData = 1; } catch(e) { Data.localStorageEnabled = false; } setLocalData: function(key, value) { if (Data.localStorageEnabled) { window.localStorage[key] = value; } else { util.setCookie("_L_" + key, value, 1000); } }
在設置本地數據的時候,需要判斷一下是不是支持本地存儲,如果是的話就用localStorage,否則改用cookie。可以用惰性函數改造一下:
setLocalData: function(key, value) { if(Data.localStorageEnabled) { util.setLocalData = function(key, value){ return window.localStorage[key]; } } else { util.setLocalData = function(key, value){ return util.getCookie("_L_" + key); } } return util.setLocalData(key, value); }
這里可以減少一次if/else的判斷,但好像不是特別實惠,畢竟為了減少一次判斷,引入了一個惰性函數的概念,所以你可能要權衡一下這種引入是否值得,如果有三五個判斷應該還是比較好的。
3. 函數綁定
有時候要把一個函數當作參數傳遞給另一個函數執行,此時函數的執行上下文往往會發生變化,如下代碼:
class DrawTool { constructor() { this.points = []; } handleMouseClick(event) { this.points.push(event.latLng); } init() { $map.on('click', this.handleMouseClick); } }
click事件的執行回調里面this不是指向了DrawTool的實例了,所以里面的this.points將會返回undefined。***種解決方法是使用閉包,先把this緩存一下,變成that:
class DrawTool { constructor() { this.points = []; } handleMouseClick(event) { this.points.push(event.latLng); } init() { let that = this; $map.on('click', event => that.handleMouseClick(event)); } }
由于回調函數是用that執行的,而that是指向DrawTool的實例子,因此就沒有問題了。相反如果沒有that它就用的this,所以就要看this指向哪里了。
因為我們用了箭頭函數,而箭頭函數的this還是指向父級的上下文,因此這里不用自己創建一個閉包,直接用this就可以:
init() { $map.on('click', event => this.handleMouseClick(event)); }
這種方式更加簡單,第二種方法是使用ES5的bind函數綁定,如下代碼:
init() { $map.on('click', this.handleMouseClick.bind(this)); }
這個bind看起來好像很神奇,但其實只要一行代碼就可以實現一個bind函數:
Function.prototype.bind = function(context) { return () => this.call(context); }
就是返回一個函數,這個函數的this是指向的原始函數,然后讓它call(context)綁定一下執行上下文就可以了。
4. 柯里化
柯里化就是函數和參數值結合產生一個新的函數,如下代碼,假設有一個curry的函數:
function add(a, b) { return a + b; } let add1 = add.curry(1); console.log(add1(5)); // 6 console.log(add1(2)); // 3
怎么實現這樣一個curry的函數?它的重點是要返回一個函數,這個函數有一些閉包的變量記錄了創建時的默認參數,然后執行這個返回函數的時候,把新傳進來的參數和默認參數拼一下變成完整參數列表去調原本的函數,所以有了以下代碼:
Function.prototype.curry = function() { let defaultArgs = arguments; let that = this; return function(){ return that.apply(null, arguments.concat(defulatArgs)); } }
但是由于參數不是一個數組,沒有concat函數,所以需要把偽數組轉成一個偽數組,可以用Array.prototype.slice:
Function.prototype.curry = function() { let slice = Array.prototype.slice; let defaultArgs = slice.call(arguments); let that = this; return function() { return that.apply(null, arguments.concat(slice.call(defulatArgs))); } }
現在舉一下柯里化一個有用的例子,當需要把一個數組降序排序的時候,需要這樣寫:
let data = [1,5,2,3,10]; data.sort((a, b) => b - a); // [10, 5, 3, 2, 1]
給sort傳一個函數的參數,但是如果你的降序操作比較多,每次都寫一個函數參數還是有點煩的,因此可以用柯里化把這個參數固化起來:
Array.prototype.sortDescending = Array.prototype.sort.curry((a, b) => b - a);
這樣就方便多了:
let data = [1,5,2,3,10]; data.sortDescending(); console.log(data); // [10, 5, 3, 2, 1]
5. 防止篡改對象
有時候你可能怕你的對象被誤改了,所以需要把它保護起來。
(1)—Object.seal防止新增和刪除屬性
如下代碼,當把一個對象seal之后,將不能添加和刪除屬性:
當使用嚴格模式將會拋異常:
(2)Object.freeze凍結對象
這個是不能改屬性值,如下圖所示:
同時可以使用Object.isFrozen、Object.isSealed、Object.isExtensible判斷當前對象的狀態。
(3)defineProperty凍結單個屬性
如下圖所示,設置enumable/writable為false,那么這個屬性將不可遍歷和寫:
6. 定時器
怎么實現一個JS版的sleep函數?因為在C/C++/Java等語言是有sleep函數,但是JS沒有。sleep函數的作用是讓線程進入休眠,當到了指定時間后再重新喚起。你不能寫個while循環然后不斷地判斷當前時間和開始時間的差值是不是到了指定時間了,因為這樣會占用CPU,就不是休眠了。
這個實現比較簡單,我們可以使用setTimeout + 回調:
function sleep(millionSeconds, callback) { setTimeout(callback, millionSeconds); } // sleep 2秒 sleep(2000, () => console.log("sleep recover"));
但是使用回調讓我的代碼不能夠和平常的代碼一樣像瀑布流一樣寫下來,我得搞一個回調函數當作參數傳值。于是想到了Promise,現在用Promise改寫一下:
function sleep(millionSeconds) { return new Promise(resolve => setTimeout(resolve, millionSeconds)); } sleep(2000).then(() => console.log("sleep recover"));
但好像還是沒有辦法解決上面的問題,仍然需要傳遞一個函數參數。
雖然使用Promise本質上是一樣的,但是它有一個resolve的參數,方便你告訴它什么時候異步結束,然后它就可以執行then了,特別是在回調比較復雜的時候,使用Promise還是會更加的方便。
ES7新增了兩個新的屬性async/await用于處理的異步的情況,讓異步代碼的寫法就像同步代碼一樣,如下async版本的sleep:
function sleep(millionSeconds) { return new Promise(resolve => setTimeout(resolve, millionSeconds)); } async function init() { await sleep(2000); console.log("sleep recover"); } init();
相對于簡單的Promise版本,sleep的實現還是沒變。不過在調用sleep的前面加一個await,這樣只有sleep這個異步完成了,才會接著執行下面的代碼。同時需要把代碼邏輯包在一個async標記的函數里面,這個函數會返回一個Promise對象,當里面的異步都執行完了就可以then了:
init().then(() => console.log("init finished"));
ES7的新屬性讓我們的代碼更加地簡潔優雅。
關于定時器還有一個很重要的話題,那就是setTimeout和setInterval的區別。如下圖所示:
setTimeout是在當前執行單元都執行完才開始計時,而setInterval是在設定完計時器后就立馬計時。可以用一個實際的例子做說明,這個例子我在《JS與多線程》這篇文章里面提到過,這里用代碼實際地運行一下,如下代碼所示:
let scriptBegin = Date.now(); fun1(); fun2(); // 需要執行20ms的工作單元 function act(functionName) { console.log(functionName, Date.now() - scriptBegin); let begin = Date.now(); while(Date.now() - begin < 20); } function fun1() { let fun3 = () => act("fun3"); setTimeout(fun3, 0); act("fun1"); } function fun2() { act("fun2 - 1"); var fun4 = () => act("fun4"); setInterval(fun4, 20); act("fun2 - 2"); }
這個代碼的執行模型是這樣的:
控制臺輸出:
與上面的模型分析一致。
接著再討論***一個話題,函數節流
7. 函數節流throttling
節流的目的是為了不想觸發執行得太快,如:
—監聽input觸發搜索
—監聽resize做響應式調整
—監聽mousemove調整位置
我們先看一下,resize/mousemove事件1s種能觸發多少次,于是寫了以下驅動代碼:
let begin = 0; let count = 0; window.onresize = function() { count++; let now = Date.now(); if (!begin) { begin = now; return; } if((now - begin) % 3000 < 60) { console.log(now - begin, count / (now - begin) * 1000); } };
當把窗口拉得比較快的時候,resize事件大概是1s觸發40次:
需要注意的是,并不是說你拉得越快,觸發得就越快。實際情況是,拉得越快觸發得越慢,因為拉動的時候頁面需要重繪,變化得越快,重繪的次數也就越多,所以導致觸發得更少了。
mousemove事件在我的電腦的Chrome上1s大概觸發60次:
如果你需要監聽resize事件做DOM調整的話,這個調整比較費時,1s要調整40次,這樣可能會響應不過來,并且不需要調整得這么頻繁,所以要節流。
怎么實現一個節流呢,書里是這么實現的:
function throttle(method, context) { clearTimeout(method.tId); method.tId = setTimeout(function() { method.call(context); }, 100); }
每次執行都要setTimeout一下,如果觸發得很快就把上一次的setTimeout清掉重新setTimeout,這樣就不會執行很快了。但是這樣有個問題,就是這個回調函數可能永遠不會執行,因為它一直在觸發,一直在清掉tId,這樣就有點尷尬,上面代碼的本意應該是100ms內最多觸發一次,而實際情況是可能永遠不會執行。
把上面的代碼稍微改造一下:
function throttle(method, context) { if (method.tId) { return; } method.tId = setTimeout(function() { method.call(context); method.tId = 0; }, 100); }
這個實現就是正確的,每100ms最多執行一次回調,原理是在setTimeout里面把tId給置成0,這樣能讓下一次的觸發執行。實際實驗一下:
大概每100ms就執行一次,這樣就達到我們的目的。
但是這樣有一個小問題,就是每次執行都是要延遲100ms,有時候用戶可能就是***化了窗口,只觸發了一次resize事件,但是這次還是得延遲100ms才能執行,假設你的時間是500ms,那就得延遲半秒,因此這個實現不太理想。
需要優化,如下代碼所示:
function throttle(method, context) { // 如果是***次觸發,立刻執行 if (typeof method.tId === "undefined") { method.call(context); } if (method.tId) { return; } method.tId = setTimeout(function() { method.call(context); method.tId = 0; }, 100); }
先判斷是否為***次觸發,如果是的話立刻執行。這樣就解決了上面提到的問題,但是這個實現還是有問題,因為它只是全局的***次,用戶***化之后,隔了一會又取消***化了就又有延遲了,并且***次觸發會執行兩次。那怎么辦呢?
筆者想到了一個方法:
function throttle(method, context) { if (!method.tId) { method.call(context); method.tId = 1; setTimeout(() => method.tId = 0, 100); } }
每次觸發的時候立刻執行,然后再設定一個計時器,把tId置成0,實際的效果如下:
這個實現比之前的實現還要簡潔,并且能夠解決延遲的問題。
“JavaScript的高級技巧有哪些”的內容就介紹到這里了,感謝大家的閱讀。如果想了解更多行業相關的知識可以關注億速云網站,小編將為大家輸出更多高質量的實用文章!
免責聲明:本站發布的內容(圖片、視頻和文字)以原創、轉載和分享為主,文章觀點不代表本網站立場,如果涉及侵權請聯系站長郵箱:is@yisu.com進行舉報,并提供相關證據,一經查實,將立刻刪除涉嫌侵權內容。