這篇文章主要介紹“JS的高級技巧總結(jié)”,在日常操作中,相信很多人在JS的高級技巧總結(jié)問題上存在疑惑,小編查閱了各式資料,整理出簡單好用的操作方法,希望對大家解答”JS的高級技巧總結(jié)”的疑惑有所幫助!接下來,請跟著小編一起來學(xué)習(xí)吧!
專注于為中小企業(yè)提供成都網(wǎng)站建設(shè)、成都網(wǎng)站制作服務(wù),電腦端+手機(jī)端+微信端的三站合一,更高效的管理,為中小企業(yè)宜秀免費(fèi)做網(wǎng)站提供優(yōu)質(zhì)的服務(wù)。我們立足成都,凝聚了一批互聯(lián)網(wǎng)行業(yè)人才,有力地推動(dòng)了上千多家企業(yè)的穩(wěn)健成長,幫助中小企業(yè)通過網(wǎng)站建設(shè)實(shí)現(xiàn)規(guī)模擴(kuò)充和轉(zhuǎn)變。
這個(gè)問題是怎么安全地檢測一個(gè)變量的類型,例如判斷一個(gè)變量是否為一個(gè)數(shù)組。通常的做法是使用instanceof
,如下代碼所示:
let data = [1, 2, 3]; console.log(data instanceof Array); //true
但是上面的判斷在一定條件下會失敗——就是在iframe
里面判斷一個(gè)父窗口的變量的時(shí)候。寫個(gè)demo
驗(yàn)證一下,如下主頁面的main.html
:
在iframe.html
判斷一下父窗口的變量類型:
在iframe
里面使用window.parent
得到父窗口的全局window
對象,這個(gè)不管跨不跨域都沒有問題,進(jìn)而可以得到父窗口的變量,然后用instanceof
判斷。最后運(yùn)行結(jié)果如下:
可以看到父窗口的判斷是正確的,而子窗口的判斷是false
,因此一個(gè)變量明明是Array
,但卻不是Array
,這是為什么呢?既然這個(gè)是父子窗口才會有的問題,于是試一下把Array
改成父窗口的Array
,即window.parent.Array
,如下圖所示:
這次返回了true
,然后再變換一下其它的判斷,如上圖,最后可以知道根本原因是上圖最后一個(gè)判斷:
Array !== window.parent.Array
它們分別是兩個(gè)函數(shù),父窗口定義了一個(gè),子窗口又定義了一個(gè),內(nèi)存地址不一樣,內(nèi)存地址不一樣的Object
等式判斷不成立,而window.parent.arrayData.constructor
返回的是父窗口的Array
,比較的時(shí)候是在子窗口,使用的是子窗口的Array
,這兩個(gè)Array
不相等,所以導(dǎo)致判斷不成立。
那怎么辦呢?
由于不能使用Object
的內(nèi)存地址判斷,可以使用字符串的方式,因?yàn)樽址腔绢愋?,字符串比較只要每個(gè)字符都相等就好了。ES5
提供了這么一個(gè)方法Object.prototype.toString
,我們先小試牛刀,試一下不同變量的返回值:
可以看到如果是數(shù)組返回"[object Array]",ES5對這個(gè)函數(shù)是這么規(guī)定的:
ES5函數(shù)地址:https://262.ecma-international.org/5.1/#sec-15.2.4.2
也就是說這個(gè)函數(shù)的返回值是“[object
”開頭,后面帶上變量類型的名稱和右括號。因此既然它是一個(gè)標(biāo)準(zhǔn)語法規(guī)范,所以可以用這個(gè)函數(shù)安全地判斷變量是不是數(shù)組。
可以這么寫:
Object.prototype.toString.call([1, 2, 3]) === "[object Array]"
注意要使用call
,而不是直接調(diào)用,call
的第一個(gè)參數(shù)是context
執(zhí)行上下文,把數(shù)組傳給它作為執(zhí)行上下文。
有一個(gè)比較有趣的現(xiàn)象是ES6
的class
也是返回function
:
所以可以知道class
也是用function
實(shí)現(xiàn)的原型,也就是說class
和function
本質(zhì)上是一樣的,只是寫法上不一樣。
那是不是說不能再使用instanceof
判斷變量類型了?不是的,當(dāng)你需要檢測父頁面的變量類型就得使用這種方法,本頁面的變量還是可以使用instanceof
或者constructor
的方法判斷,只要你能確保這個(gè)變量不會跨頁面。因?yàn)閷τ诖蠖鄶?shù)人來說,很少會寫iframe
的代碼,所以沒有必要搞一個(gè)比較麻煩的方式,還是用簡單的方式就好了。
有時(shí)候需要在代碼里面做一些兼容性判斷,或者是做一些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; }
這個(gè)函數(shù)的作用是判斷用戶是在哪個(gè)環(huán)境打開的網(wǎng)頁,以便于統(tǒng)計(jì)哪個(gè)渠道的效果比較好。
這種類型的判斷都有一個(gè)特點(diǎn),就是它的結(jié)果是死的,不管執(zhí)行判斷多少次,都會返回相同的結(jié)果,例如用戶的UA
在這個(gè)網(wǎng)頁不可能會發(fā)生變化(除了調(diào)試設(shè)定的之外)。所以為了優(yōu)化,才有了惰性函數(shù)一說,上面的代碼可以改成:
//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
這個(gè)函數(shù)重新賦值,變成一個(gè)新的function
,而這個(gè)function
直接返回一個(gè)確定的變量,這樣以后的每次獲取都不用再判斷了,這就是惰性函數(shù)的作用。你可能會說這么幾個(gè)判斷能優(yōu)化多少時(shí)間呢,這么點(diǎn)時(shí)間對于用戶來說幾乎是沒有區(qū)別的呀。確實(shí)如此,但是作為一個(gè)有追求的碼農(nóng),還是會想辦法盡可能優(yōu)化自己的代碼,而不是只是為了完成需求完成功能。并且當(dāng)你的這些優(yōu)化累積到一個(gè)量的時(shí)候就會發(fā)生質(zhì)變。我上大學(xué)的時(shí)候C++
的老師舉了一個(gè)例子,說有個(gè)系統(tǒng)比較慢找她去看一下,其中她做的一個(gè)優(yōu)化是把小數(shù)的雙精度改成單精度,最后是快了不少。
但其實(shí)上面的例子我們有一個(gè)更簡單的實(shí)現(xiàn),那就是直接搞個(gè)變量存起來就好了:
let ua = window.navigator.userAgent; let UAType = ua.match(/renren/i) ? 0 : ua.match(/MicroMessenger/i) ? 1 : ua.match(/weibo/i) ? 2 : -1;
連函數(shù)都不用寫了,缺點(diǎn)是即使沒有使用到UAType
這個(gè)變量,也會執(zhí)行一次判斷,但是我們認(rèn)為這個(gè)變量被用到的概率還是很高的。
我們再舉一個(gè)比較有用的例子,由于Safari
的無痕瀏覽會禁掉本地存儲,因此需要搞一個(gè)兼容性判斷:
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); } }
在設(shè)置本地?cái)?shù)據(jù)的時(shí)候,需要判斷一下是不是支持本地存儲,如果是的話就用localStorage
,否則改用cookie
??梢杂枚栊院瘮?shù)改造一下:
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
的判斷,但好像不是特別實(shí)惠,畢竟為了減少一次判斷,引入了一個(gè)惰性函數(shù)的概念,所以你可能要權(quán)衡一下這種引入是否值得,如果有三五個(gè)判斷應(yīng)該還是比較好的。
有時(shí)候要把一個(gè)函數(shù)當(dāng)作參數(shù)傳遞給另一個(gè)函數(shù)執(zhí)行,此時(shí)函數(shù)的執(zhí)行上下文往往會發(fā)生變化,如下代碼:
class DrawTool { constructor() { this.points = []; } handleMouseClick(event) { this.points.push(event.latLng); } init() { $map.on('click', this.handleMouseClick); } }
click
事件的執(zhí)行回調(diào)里面this
不是指向了DrawTool
的實(shí)例了,所以里面的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)); } }
由于回調(diào)函數(shù)是用that
執(zhí)行的,而that
是指向DrawTool
的實(shí)例子,因此就沒有問題了。相反如果沒有that
它就用的this
,所以就要看this
指向哪里了。
因?yàn)槲覀冇昧思^函數(shù),而箭頭函數(shù)的this
還是指向父級的上下文,因此這里不用自己創(chuàng)建一個(gè)閉包,直接用this
就可以:
init() { $map.on('click', event => this.handleMouseClick(event));}復(fù)制代碼 這種方式更加簡單,第二種方法是使用ES5的bind函數(shù)綁定,如下代碼: init() { $map.on('click', this.handleMouseClick.bind(this));}
這個(gè)bind
看起來好像很神奇,但其實(shí)只要一行代碼就可以實(shí)現(xiàn)一個(gè)bind函數(shù):
Function.prototype.bind = function(context) { return () => this.call(context); }
就是返回一個(gè)函數(shù),這個(gè)函數(shù)的this
是指向的原始函數(shù),然后讓它call(context)
綁定一下執(zhí)行上下文就可以了。
柯里化就是函數(shù)和參數(shù)值結(jié)合產(chǎn)生一個(gè)新的函數(shù),如下代碼,假設(shè)有一個(gè)curry的函數(shù):
function add(a, b) { return a + b; } let add1 = add.curry(1); console.log(add1(5)); // 6 console.log(add1(2)); // 3
怎么實(shí)現(xiàn)這樣一個(gè)curry
的函數(shù)?它的重點(diǎn)是要返回一個(gè)函數(shù),這個(gè)函數(shù)有一些閉包的變量記錄了創(chuàng)建時(shí)的默認(rèn)參數(shù),然后執(zhí)行這個(gè)返回函數(shù)的時(shí)候,把新傳進(jìn)來的參數(shù)和默認(rèn)參數(shù)拼一下變成完整參數(shù)列表去調(diào)原本的函數(shù),所以有了以下代碼:
Function.prototype.curry = function() { let defaultArgs = arguments; let that = this; return function() { return that.apply(this, defaultArgs.concat(arguments)); }};
但是由于參數(shù)不是一個(gè)數(shù)組,沒有concat函數(shù),所以需要把偽數(shù)組轉(zhuǎn)成一個(gè)偽數(shù)組,可以用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(this, defaultArgs.concat(slice.call(arguments))); }};
現(xiàn)在舉一下柯里化一個(gè)有用的例子,當(dāng)需要把一個(gè)數(shù)組降序排序的時(shí)候,需要這樣寫:
let data = [1,5,2,3,10]; data.sort((a, b) => b - a); // [10, 5, 3, 2, 1]
給sort
傳一個(gè)函數(shù)的參數(shù),但是如果你的降序操作比較多,每次都寫一個(gè)函數(shù)參數(shù)還是有點(diǎn)煩的,因此可以用柯里化把這個(gè)參數(shù)固化起來:
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]
有時(shí)候你可能怕你的對象被誤改了,所以需要把它保護(hù)起來。
(1)Object.seal防止新增和刪除屬性
如下代碼,當(dāng)把一個(gè)對象seal之后,將不能添加和刪除屬性:
當(dāng)使用嚴(yán)格模式將會拋異常:
(2)Object.freeze凍結(jié)對象
這個(gè)是不能改屬性值,如下圖所示:
同時(shí)可以使用Object.isFrozen
、Object.isSealed
、Object.isExtensible
判斷當(dāng)前對象的狀態(tài)。
(3)defineProperty凍結(jié)單個(gè)屬性
如下圖所示,設(shè)置enumable/writable
為false
,那么這個(gè)屬性將不可遍歷和寫:
怎么實(shí)現(xiàn)一個(gè)JS
版的sleep
函數(shù)?因?yàn)樵?code>C/C++/Java等語言是有sleep
函數(shù),但是JS
沒有。sleep
函數(shù)的作用是讓線程進(jìn)入休眠,當(dāng)?shù)搅酥付〞r(shí)間后再重新喚起。你不能寫個(gè)while
循環(huán)然后不斷地判斷當(dāng)前時(shí)間和開始時(shí)間的差值是不是到了指定時(shí)間了,因?yàn)檫@樣會占用CPU
,就不是休眠了。
這個(gè)實(shí)現(xiàn)比較簡單,我們可以使用setTimeout + 回調(diào):
function sleep(millionSeconds, callback) { setTimeout(callback, millionSeconds); } // sleep 2秒 sleep(2000, () => console.log("sleep recover"));
但是使用回調(diào)讓我的代碼不能夠和平常的代碼一樣像瀑布流一樣寫下來,我得搞一個(gè)回調(diào)函數(shù)當(dāng)作參數(shù)傳值。于是想到了Promise
,現(xiàn)在用Promise
改寫一下:
function sleep(millionSeconds) { return new Promise(resolve => setTimeout(resolve, millionSeconds)); } sleep(2000).then(() => console.log("sleep recover"));
但好像還是沒有辦法解決上面的問題,仍然需要傳遞一個(gè)函數(shù)參數(shù)。
雖然使用Promise
本質(zhì)上是一樣的,但是它有一個(gè)resolve
的參數(shù),方便你告訴它什么時(shí)候異步結(jié)束,然后它就可以執(zhí)行then
了,特別是在回調(diào)比較復(fù)雜的時(shí)候,使用Promise
還是會更加的方便。
ES7
新增了兩個(gè)新的屬性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
的實(shí)現(xiàn)還是沒變。不過在調(diào)用sleep
的前面加一個(gè)await
,這樣只有sleep
這個(gè)異步完成了,才會接著執(zhí)行下面的代碼。同時(shí)需要把代碼邏輯包在一個(gè)async
標(biāo)記的函數(shù)里面,這個(gè)函數(shù)會返回一個(gè)Promise
對象,當(dāng)里面的異步都執(zhí)行完了就可以then
了:
init().then(() => console.log("init finished"));
ES7
的新屬性讓我們的代碼更加地簡潔優(yōu)雅。
關(guān)于定時(shí)器還有一個(gè)很重要的話題,那就是setTimeout
和setInterval
的區(qū)別。如下圖所示:
setTimeout
是在當(dāng)前執(zhí)行單元都執(zhí)行完才開始計(jì)時(shí),而setInterval
是在設(shè)定完計(jì)時(shí)器后就立馬計(jì)時(shí)??梢杂靡粋€(gè)實(shí)際的例子做說明,這個(gè)例子我在《JS與多線程》這篇文章里面提到過,這里用代碼實(shí)際地運(yùn)行一下,如下代碼所示:
let scriptBegin = Date.now(); fun1(); fun2(); // 需要執(zhí)行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"); }
這個(gè)代碼的執(zhí)行模型是這樣的:
控制臺輸出:
與上面的模型分析一致。
接著再討論最后一個(gè)話題,函數(shù)節(jié)流
節(jié)流的目的是為了不想觸發(fā)執(zhí)行得太快,如:
監(jiān)聽input
觸發(fā)搜索監(jiān)聽resize
做響應(yīng)式調(diào)整監(jiān)聽mousemove
調(diào)整位置
我們先看一下,resize/mousemove
事件1s
種能觸發(fā)多少次,于是寫了以下驅(qū)動(dòng)代碼:
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); } };
當(dāng)把窗口拉得比較快的時(shí)候,resize
事件大概是1s
觸發(fā)40
次:
需要注意的是,并不是說你拉得越快,觸發(fā)得就越快。實(shí)際情況是,拉得越快觸發(fā)得越慢,因?yàn)槔瓌?dòng)的時(shí)候頁面需要重繪,變化得越快,重繪的次數(shù)也就越多,所以導(dǎo)致觸發(fā)得更少了。
mousemove
事件在我的電腦的Chrome
上1s
大概觸發(fā)60
次:
如果你需要監(jiān)聽resize
事件做DOM
調(diào)整的話,這個(gè)調(diào)整比較費(fèi)時(shí),1s
要調(diào)整40
次,這樣可能會響應(yīng)不過來,并且不需要調(diào)整得這么頻繁,所以要節(jié)流。
怎么實(shí)現(xiàn)一個(gè)節(jié)流呢,書里是這么實(shí)現(xiàn)的:
function throttle(method, context) { clearTimeout(method.tId); method.tId = setTimeout(function() { method.call(context); }, 100); }
每次執(zhí)行都要setTimeout
一下,如果觸發(fā)得很快就把上一次的setTimeout
清掉重新setTimeout
,這樣就不會執(zhí)行很快了。但是這樣有個(gè)問題,就是這個(gè)回調(diào)函數(shù)可能永遠(yuǎn)不會執(zhí)行,因?yàn)樗恢痹谟|發(fā),一直在清掉tId,這樣就有點(diǎn)尷尬,上面代碼的本意應(yīng)該是100ms
內(nèi)最多觸發(fā)一次,而實(shí)際情況是可能永遠(yuǎn)不會執(zhí)行。這種實(shí)現(xiàn)應(yīng)該叫防抖,不是節(jié)流。
把上面的代碼稍微改造一下:
function throttle(method, context) { if (method.tId) { return; } method.tId = setTimeout(function() { method.call(context); method.tId = 0; }, 100); }
這個(gè)實(shí)現(xiàn)就是正確的,每100ms
最多執(zhí)行一次回調(diào),原理是在setTimeout
里面把tId
給置成0
,這樣能讓下一次的觸發(fā)執(zhí)行。實(shí)際實(shí)驗(yàn)一下:
大概每100ms
就執(zhí)行一次,這樣就達(dá)到我們的目的。
但是這樣有一個(gè)小問題,就是每次執(zhí)行都是要延遲100ms
,有時(shí)候用戶可能就是最大化了窗口,只觸發(fā)了一次resize
事件,但是這次還是得延遲100ms
才能執(zhí)行,假設(shè)你的時(shí)間是500ms
,那就得延遲半秒,因此這個(gè)實(shí)現(xiàn)不太理想。
需要優(yōu)化,如下代碼所示:
function throttle(method, context) { // 如果是第一次觸發(fā),立刻執(zhí)行 if (typeof method.tId === "undefined") { method.call(context); } if (method.tId) { return; } method.tId = setTimeout(function() { method.call(context); method.tId = 0; }, 100); }
先判斷是否為第一次觸發(fā),如果是的話立刻執(zhí)行。這樣就解決了上面提到的問題,但是這個(gè)實(shí)現(xiàn)還是有問題,因?yàn)樗皇侨值牡谝淮?,用戶最大化之后,隔了一會又取消最大化了就又有延遲了,并且第一次觸發(fā)會執(zhí)行兩次。那怎么辦呢?
筆者想到了一個(gè)方法:
function throttle(method, context) { if (!method.tId) { method.call(context); method.tId = 1; setTimeout(() => method.tId = 0, 100); } }
每次觸發(fā)的時(shí)候立刻執(zhí)行,然后再設(shè)定一個(gè)計(jì)時(shí)器,把tId
置成0,實(shí)際的效果如下:
這個(gè)實(shí)現(xiàn)比之前的實(shí)現(xiàn)還要簡潔,并且能夠解決延遲的問題。
所以通過節(jié)流,把執(zhí)行次數(shù)降到了1s
執(zhí)行10次,節(jié)流時(shí)間也可以控制,但同時(shí)失去了靈敏度,如果你需要高靈敏度就不應(yīng)該使用節(jié)流,例如做一個(gè)拖拽的應(yīng)用。如果拖拽節(jié)流了會怎么樣?用戶會發(fā)現(xiàn)拖起來一卡一卡的。
到此,關(guān)于“JS的高級技巧總結(jié)”的學(xué)習(xí)就結(jié)束了,希望能夠解決大家的疑惑。理論與實(shí)踐的搭配能更好的幫助大家學(xué)習(xí),快去試試吧!若想繼續(xù)學(xué)習(xí)更多相關(guān)知識,請繼續(xù)關(guān)注創(chuàng)新互聯(lián)網(wǎng)站,小編會繼續(xù)努力為大家?guī)砀鄬?shí)用的文章!