本篇是看的《JS高級程序設計》第23章《高級技巧》作的讀書分享。本篇按照書裏的思路根據本身的理解和經驗,進行擴展延伸,同時指出書裏的一些問題。將會討論安全的類型檢測、惰性載入函數、凍結對象、定時器等話題。javascript
這個問題是怎麼安全地檢測一個變量的類型,例如判斷一個變量是否爲一個數組。一般的作法是使用instanceof,以下代碼所示:html
let data = [1, 2, 3];
console.log(data instanceof Array); //true複製代碼
可是上面的判斷在必定條件下會失敗——就是在iframe裏面判斷一個父窗口的變量的時候。寫個demo驗證一下,以下主頁面的main.html:java
<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.Arraycookie
它們分別是兩個函數,父窗口定義了一個,子窗口又定義了一個,內存地址不同,內存地址不同的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的代碼,因此沒有必要搞一個比較麻煩的方式,仍是用簡單的方式就行了。
有時候須要在代碼裏面作一些兼容性判斷,或者是作一些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的判斷,但好像不是特別實惠,畢竟爲了減小一次判斷,引入了一個惰性函數的概念,因此你可能要權衡一下這種引入是否值得,若是有三五個判斷應該仍是比較好的。
有時候要把一個函數看成參數傳遞給另外一個函數執行,此時函數的執行上下文每每會發生變化,以下代碼:
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)綁定一下執行上下文就能夠了。
柯里化就是函數和參數值結合產生一個新的函數,以下代碼,假設有一個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(this,
defaultArgs.concat(arguments)); }};
複製代碼
可是因爲參數不是一個數組,沒有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(this,
defaultArgs.concat(slice.call(arguments))); }};
複製代碼
如今舉一下柯里化一個有用的例子,當須要把一個數組降序排序的時候,須要這樣寫:
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]複製代碼
有時候你可能怕你的對象被誤改了,因此須要把它保護起來。
(1)Object.seal防止新增和刪除屬性
以下代碼,當把一個對象seal以後,將不能添加和刪除屬性:
當使用嚴格模式將會拋異常:
(2)Object.freeze凍結對象
這個是不能改屬性值,以下圖所示:
同時可使用Object.isFrozen、Object.isSealed、Object.isExtensible判斷當前對象的狀態。
(3)defineProperty凍結單個屬性
以下圖所示,設置enumable/writable爲false,那麼這個屬性將不可遍歷和寫:
怎麼實現一個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");
}
複製代碼
這個代碼的執行模型是這樣的:
控制檯輸出:
與上面的模型分析一致。
接着再討論最後一個話題,函數節流
節流的目的是爲了避免想觸發執行得太快,如:
咱們先看一下,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,實際的效果以下:
這個實現比以前的實現還要簡潔,而且可以解決延遲的問題。
因此經過節流,把執行次數降到了1s執行10次,節流時間也能夠控制,但同時失去了靈敏度,若是你須要高靈敏度就不該該使用節流,例如作一個拖拽的應用。若是拖拽節流了會怎麼樣?用戶會發現拖起來一卡一卡的。
筆者從新看了高程的《高級技巧》的章節結合本身的理解和實踐總結了這麼一篇文章,個人體會是若是看書看博客只是看成睡前讀物看一看其實收穫不是很大,沒有實際地把書裏的代碼實踐一下,沒有結合本身的編碼經驗,就不能用本身的理解去融入這個知識點,從而轉化爲本身的知識。你可能會說我看了以後就會印象啊,有印象仍是好的,可是你花了那麼多時間看了那本書只是獲得了一個印象,你本身都沒有實踐過的印象,這個印象又有多靠譜呢。若是別人問到了這個印象,你可能會回答出一些連不起來的碎片,就會給人一種背書的感受。還有有時候書裏可能會有一些錯誤或者過期的東西,只有實踐了才能出真知。