JS高階技巧(簡潔版)

李博bluemind發表於2018-06-01

高階函式

由於在JS中,所有的函式都是物件,所以使用函式指標十分簡單,也是這些東西使JS函式有趣且強大

安全的型別檢測

JS內建的型別檢測機制並不是完全可靠的

typeof

操作符返回一個字串,表示未經計算的運算元的型別,在大多數情況下很靠譜,但是當然還有例外

正規表示式

typeof /s/ === `function`; // Chrome 1-12 , 不符合 ECMAScript 5.1
typeof /s/ === `object`; // Firefox 5+ , 符合 ECMAScript 5.1
複製程式碼

NULL

typeof null === `object`; // 從一開始出現JavaScript就是這樣的
複製程式碼

在 JavaScript 最初的實現中,JavaScript 中的值是由一個表示型別的標籤和實際資料值表示的。物件的型別標籤是 0。由於 null 代表的是空指標(大多數平臺下值為 0x00),因此,null的型別標籤也成為了 0,typeof null就錯誤的返回了object

instanceof

運算子用來測試一個物件在其原型鏈中是否存在一個建構函式的 prototype 屬性

語法

object instanceof constructor(要檢測的物件 instanceof 建構函式)

但是在瀏覽器中,我們的指令碼可能需要在多個視窗之間進行互動。多個視窗意味著多個全域性環境,不同的全域性環境擁有不同的全域性物件,從而擁有不同的內建型別建構函式。這可能會引發一些問題。

[] instanceof window.frames[0].Array        //false
複製程式碼

因為 Array.prototype !== window.frames[0].Array.prototype,因此你必須使用 Array.isArray(myObj) 或者 Object.prototype.toString.call(myObj) === “[object Array]”來判斷myObj是否是陣列

解決以上兩個問題的方案就是Object.prototype.toString

Object.prototype.toString

方法返回一個表示該物件的字串

可以通過toString() 來獲取每個物件的型別。為了每個物件都能通過 Object.prototype.toString() 來檢測,需要以 Function.prototype.call() 或者 Function.prototype.apply()的形式來呼叫,傳遞要檢查的物件作為第一個引數,稱為thisArg

var toString = Object.prototype.toString;

toString.call(new Date); // [object Date]
toString.call(new String); // [object String]
toString.call(Math); // [object Math]
toString.call(/s/); // [object RegExp]
toString.call([]); // [object Array]

//Since JavaScript 1.8.5
toString.call(undefined); // [object Undefined]
toString.call(null); // [object Null]
複製程式碼

作用域安全的建構函式

建構函式其實就是一個使用new操作符呼叫的函式。當使用new呼叫時,建構函式內用到的this物件會指向新建立的物件例項

function Person(name, age){
    this.name = name;
    this.age = age;
}

let person = new Person("addone", 20);

person.name // addone
複製程式碼

當你使用new操作符的時候,就會建立一個新的Person物件,同時分配這些屬性,但是如果你沒有使用new

let person = Person("addone", 20);

person1.name // Cannot read property `name` of undefined
window.name // addone
複製程式碼

這是因為this是在執行時確認的,當你沒有使用new,那麼this在當前情況下就被解析成了window,屬性就被分配到window上了

作用域安全的建構函式在進行更改前,首先確認this物件是正確型別的例項,如果不是,就建立新的物件並且返回

function Person(name, age){
    if(this instanceof Person){
        this.name = name;
        this.age = age;     
    }else{
        return new Person(name, age);
    }
}

let person1 = new Person("addone", 20);
person1.name // addone

let person2 = Person("addone", 20);
person2.name // addone
複製程式碼

this instanceof Person檢查了this物件是不是Person的例項,如果是則繼續,不是則呼叫new

惰性載入函式

假如你要寫一個函式,裡面有一些判斷語句

function foo(){
    if(a != b){
        console.log(`aaa`)
    }else{
        console.log(`bbb`)
    }
}
複製程式碼

如果你的a和b是不變的,那麼這個函式不論執行多少次,結果都是不變的,但是每次執行還要進行if判斷,這就造成了不必要的浪費。

惰性載入表示函式執行的分支只會發生一次,這裡有兩種解決方式。

在函式被呼叫時再處理函式

function foo(){
    if(a != b){
        foo = function(){
            console.log(`aaa`)
        }
    }else{
        foo = function(){
            console.log(`bbb`)
        }
    }
    return foo();
}
複製程式碼

這樣進入每個分支後都會對foo進行賦值,覆蓋了之前的函式,之後每次呼叫foo就不會再執行if判斷

在宣告函式時就指定適當的函式

var foo = (function foo(){
    if(a != b){
        return function(){
            console.log(`aaa`)
        }
    }else{
        return function(){
            console.log(`bbb`)
        }
    }
})();
複製程式碼

這裡建立一個匿名,自執行的函式,用來確定應該使用哪一個函式來實現。

惰性函式的優點就是隻在第一次執行分支時犧牲一點點效能

函式繫結

請使用fun.bind(thisArg[, arg1[, arg2[, …]]])

thisArg

當繫結函式被呼叫時,該引數會作為原函式執行時的 this 指向。當使用new 操作符呼叫繫結函式時,該引數無效

arg1,arg2,…

當繫結函式被呼叫時,這些引數將置於實參之前傳遞給被繫結的方法

返回

由指定的this值和初始化引數改造的原函式拷貝

一個例子

let person = {
   name: `addone`,
   click: function(e){
       console.log(this.name)
   }
}

let btn = document.getElementById(`btn`);
EventUtil.addHandle(btn, `click`, person.click);
複製程式碼

這裡建立了一個person物件,然後將person.click方法分配給DOM按鈕的事件處理程式,當你點選按按鈕時,會列印出undefiend,原因是執行時this指向了DOM按鈕而不是person

解決方案: 將this強行指向person

 EventUtil.addHandle(btn, `click`, person.click.bind(person));
複製程式碼

函式柯里化

函式柯里化是把接受多個引數的函式轉變成接受單一引數的函式

function add(num1, num2){
    return num1 + num2;
}
function curryAdd(num2){
    return add(1, num2);
}
add(2, 3) // 5
curryAdd(2) // 3
複製程式碼

這個例子用來方便理解柯里化的概念

下面是建立函式柯里化的通用方式

function curry(fn){
    var args = Array.prototype.slice.call(arguments, 1);
    return function(){
        let innerArgs = Array.prototype.slice.call(arguments);
        let finalArgs = args.concat(innerArgs);
        return fn.apply(null, finalArgs);
    }
}
複製程式碼

第一個引數是要進行柯里化的函式,其他引數是要傳入的值。這裡使用Array.prototype.slice.call(arguments, 1)來獲取第一個引數後的所有引數(外部)。在返回的函式中,同樣呼叫Array.prototype.slice.call(arguments)讓innerArgs來存放所有的引數(內部),然後用concat將內部外部引數組合,用apply傳遞給函式

function add(num1, num2){
    return num1 + num2;
}
let curryAdd1 = curry(add, 1);
curryAdd1(2); // 3

let curryAdd2 = curry(add, 1, 2);
curryAdd2(); // 3
複製程式碼

防篡改物件

Javascript中任何物件都可以被同一環境中執行的程式碼修改,所以開發人員有時候需要定義防篡改物件(tamper-proof object) 來保護自己

不可擴充套件物件

預設情況下所有物件都是可以擴充套件的(新增屬性和方法)

let person = { name: `addone` };
person.age = 20;
複製程式碼

第二行為person物件擴充套件了age屬性,當然你可以阻止這一行為,使用Object.preventExtensions()

let person = { name: `addone` };
Object.preventExtensions(person);
person.age = 20;

person.age // undefined
複製程式碼

你還可以用Object.isExtensible()來判斷物件是不是可擴充套件的

let person = { name: `addone` };
Object.isExtensible(person); // true

Object.preventExtensions(person);
Object.isExtensible(person); // false
複製程式碼

請記住這是不可擴充套件!!,即不能新增屬性或方法

密封的物件

密封物件不可擴充套件,且不能刪除屬性和方法

let person = { name: `addone` };
Object.seal(person);

person.age = 20;
delete person.name;

person.age // undefined
person.name // addone
複製程式碼

相對的也有Object.isSealed()來判斷是否密封

let person = { name: `addone` };
Object.isExtensible(person); // true
Object.isSealed(person); // false

Object.seal(person);
Object.isExtensible(person); // false
Object.isSealed(person); // true
複製程式碼

凍結的物件

這是最嚴格的防篡改級別,凍結的物件即不可擴充套件,又密封,且不能修改

let person = { name: `addone` };
Object.freeze(person);

person.age = 20;
delete person.name;
person.name = `addtwo`

person.age // undefined
person.name // addone
複製程式碼

同樣也有Object.isFrozen來檢測

let person = { name: `addone` };
Object.isExtensible(person); // true
Object.isSealed(person); // false
Object.isFrozen(person); // false

Object.freeze(person);
Object.isExtensible(person); // false
Object.isSealed(person); // true
Object.isFrozen(person); // true
複製程式碼

以上三種方法在嚴格模式下進行錯誤操作均會導致丟擲錯誤

高階定時器

閱讀前提

大概理解setTimeout的基本執行機制和js事件機制

重複的定時器

當你使用setInterval重複定義多個定時器的時候,可能會出現某個定時器程式碼在程式碼再次被新增到執行佇列之前還沒有完成執行,導致定時器程式碼連續執行多次。

機智Javascript引擎解決了這個問題,使用setInterval()的時候,僅當沒有該定時器的其他程式碼例項時,才會將定時器程式碼新增到佇列中。但這還會導致一些問題:

  • 某些間隔被跳過
  • 間隔可能比預期的小

為了避免這個兩個問題,你可以使用鏈式setTimeout()呼叫

setTimeout(function(){
    TODO();
    
    setTimeout(arguments.callee, interval);
}, interval)
複製程式碼

arguments.callee獲取了當前執行函式的引用,然後為其設定另外一個定時器,這樣就確保在下一次定時器程式碼執行前,必須等待指定的間隔。

Yielding Processes

瀏覽器對長時間執行的指令碼進行了制約,如果程式碼執行超過特定的時間或者特定語句數量就不會繼續執行。

如果你發現某個迴圈佔用了大量的時間,那麼對於下面這兩個問題

  • 該處理是否必須同步完成?
  • 資料是否必須按順序完成?

如果你的兩個答案都是”否”,那麼你可以使用一種叫做陣列分塊(array chunking) 的技術。基本思路是為要處理的專案建立一個佇列,然後使用定時器取出下一個要出處理的專案進行處理,然後再設定另一個定時器。

function chunk(array, process, context){
    setTimeout(function(){
        // 取出下一個專案進行處理
        let item = array.shift();
        process.call(item);
        
        if(array.length > 0){
            setTimeout(arguments.callee, 100);
        }
    }, 100)
}
複製程式碼

這裡接受三個引數,要處理的陣列,處理的函式,執行該函式的環境(可選),這裡設定間隔100ms是個效果不錯的選擇

如果你一個函式需要50ms以上時間完成,那麼最好看看能否將任務分割成一系列可以使用定時器的小任務

函式節流(Throttle)

節流的目的是防止某些操作執行的太快。比如在調整瀏覽器大小的時候會出發onresize事件,如果在其內部進行一些DOM操作,這種高頻率的更愛可能會使瀏覽器崩潰。為了避免這種情況,可以採取函式節流的方式。

function throttle(method, context){
    clearTimeout(method.tId);
    method.tId = setTimeout(function(){
        method.call(context);
    }, 100)
}
複製程式碼

這裡接受兩個引數,要執行的函式,執行的環境。執行時先清除之前的定時器,然後將當前定時器賦值給方法的tId,之後呼叫call來確定函式的執行環境。

一個應用的例子

function resizeDiv(){
    let div = document.getElementById(`div`);
    div.style.height = div.offsetWidth + "px";
}

window.onresize = function(){
    throttle(resizeDiv);
}
複製程式碼

這個就不用講了吧2333

文章參考於《JavaScript高階程式設計(第三版)》

如果你覺得我的理解有問題或者整理的太簡略,那麼我強烈安利你自己去讀一下這本書~

原文釋出時間:2018-05-14

原文作者:AddOneG

本文來源掘金如需轉載請緊急聯絡作者


相關文章