利用apply提高程式設計效率的方法總結

ThinkMore發表於2017-10-16

合理的使用apply/call函式能夠幫助我們極大地簡化程式碼,高效地解決問題。本文嘗試總結apply方法的幾種用法,並發現規律,以便在需要時能夠想到該方法。因apply和call方法的用法幾乎相同,差別僅在於引數的傳入方法,故本文僅以apply方法為例。

方法介紹

apply是函式物件原型的一個方法(Function.prototype.apply),它能夠改變函式在執行時的this指向,即,能夠改變函式執行時的執行環境。該函式最多接受兩個引數,第一個引數指定函式執行時的this指向,決定了執行環境,第二個引數為,將要傳入函式的引數組成的陣列或者類陣列物件(如果是call函式,則引數直接傳入)。

下面是一個常見的例子:

var dog = {
    sound: 'wang',
    makeSound: function() {
        console.log(this.sound);
    }
}

var cat = {
    sound: 'miao',
}

dog.makeSound.apply(cat);複製程式碼

例子中分別定義了一個dog物件和一個cat物件,它們都有sound變數,但是dog物件有一個makeSound方法,而cat沒有,如果這裡要求cat也要能夠發聲(makeSound),有兩種直觀的解決方案:

第一,給cat直接賦予一個makeSound方法

var cat = {
    sound: 'miao',
    makeSound: function() {
        console.log(this.sound);
    }
}複製程式碼

第二,利用原型繼承

var Pets = function(sound) { this.sound = sound; }
Pets.prototype.makeSound = function() { console.log(this.sound); }

var dog = new Pets('wang');
var cat = new Pets('miao');
cat.makeSound(); // miao複製程式碼

但是,有了apply,一切都變得很簡單:

dog.makeSound.apply(cat);// miao

cat既不需要有自己的makeSound方法,也不需要和dog繼承自同一個父類,只要makeSound自己能用,那就可以拿來用。
以下的所有用法,其實都是針對apply方法兩個方面的特性而來的:

  1. 可以傳入this,改變函式執行時的執行環境。
  2. apply的第二個引數是函式引數組成的陣列或者類陣列,且被借用函式是以雜湊形式傳參。

幾種用途

1. 類陣列借用陣列方法

類陣列雖然能和陣列一樣使用下標索引,但是它不具有陣列擁有的內建方法,無法直接使用這些方法,幸運的是,類陣列的特性決定了陣列的方法也能在其物件上使用,這就給apply方法發揮的空間:

// 錯誤例子
var addOne = function() {
    return argument.map(function(a) {return a+1;});
}
var arr = addOne(1,2,3,4) // Uncaught TypeError: arguments.map is not a function

// 正確的例子
var addOne = function(){
    var arr = Array.prototype.slice.apply(arguments);
    return arr.map(function(a){return a+1;});
}
addOne(1,2,3,4); // [2,3,4,5]
// 這裡使用call更加簡單
var addOne = function() {
    return Array.prototype.map.call(arguments, function(a){ return a+1; })
}複製程式碼

上面的addOne函式將所有傳入的引數分別加1,然後組成陣列返回,函式的arguments就是由引數構成的一個類陣列,我們無需取出這些引數再一個個加1,再push進陣列,而是將類陣列先轉化為陣列,然後使用陣列的map方法。第一個錯誤的示例表明了類陣列不具有陣列方法,所以報錯。

在DOM操作中,如document.getElementsByClassName, document.querySelectAll等方法拿到的物件都是類陣列,一般來講,只要轉化成陣列型別就會極大地方便我們的操作。

2. 求陣列的最大最小值

求陣列中的最大最小值操作是很常見的,但是,陣列並沒有為我們實現這樣的操作,最直觀的方法就是遍歷陣列,查詢最大最小值,無疑,這種方法不僅笨拙低效,而且效能極差。但是,如果給你的不是一個陣列,而就是一些數字呢?你可能會立即想到Math.max方法。這兩種形式引數的關係恰好就符合我們之前提到的兩個特性之二:函式要求以雜湊形式傳參,apply又要求以陣列或類陣列傳參。

Math.max.apply(null, [1,6,5,3,5]) // 6
Math.min.apply(null, [1,6,5,3,5] ) // 1複製程式碼

3. 準確判斷物件型別

判斷物件型別,我們有typeof函式可用,但是它的判斷並不可靠,比如,對陣列進行typeof操作,返回的卻是"object"。而在Object的原型物件上,有一個toString方法,它作用在不同型別的物件上,返回特定的字串,根據返回值可以準確地判斷物件型別。

Object.prototype.toString.apply([]) // "[object, Array]"
Object.prototype.toString.apply({}) // "[object, Object]"
Object.prototype.toString.apply(undefined) // "[object, Undefined]"
Object.prototype.toString.apply(function(){}) // "[object, Function]"
Object.prototype.toString.apply(document.getElementsByClassName(div)) // "[object, NodeList ]"複製程式碼

該判斷方法可以支援如下型別的判斷:NodeList,Window, Object, String, Infinity, Number(NaN), Function, HTMLDocument, Undefined, Boolean。需要特別注意的是Number型別的判斷,NaNInfinity 也會被識別為Number型別(可用如下規則判斷:1/0 === Infinity, 1/-0 === -Infinity, NaN != NaN)。這裡因為不涉及第二個引數的問題,所以使用call也完全是可以的。

4. 二維陣列的扁平化

先來看看陣列的concat方法的用法:

var a = [1,2,3];
var b = [4,5,6];
var c = a.concat(7,8,9) // [1,2,3,7,8,9]
var d = a.concat(b) // [1, 2, 3, 4, 5, 6]
var f = a.concat(7,8,b,9) //[1, 2, 3, 7, 8, 4, 5, 6, 9]
a // [1,2,3]
b // [4,5,6]複製程式碼

可以發現,concat既可以接受陣列也可以接受雜湊引數,而且最終生成的結果都是一樣的,都是一個一維的陣列,同時,不改變原來的陣列。如果將計算f的引數組成一個陣列,那麼就是一個二維陣列,再結合apply接受陣列作為第二個引數的特性,就可以實現一個二維陣列的扁平化功能了:

var twoDemArr = [[1,2,3], [4,5,6], 7,8,9]
var arr = Array.prototype.concat.apply([],twoDemArr);
arr // [1,2,3,4,5,6,7,8,9]複製程式碼

進一步,我們看看那些接受雜湊引數的陣列方法,如果結合apply會有什麼樣的作用:

push也接受雜湊引數,它將引數推入陣列,並且改變了原陣列,那麼它就可以實現,將一個陣列的元素推入另外一個陣列,並且改變被推入陣列:

var a = [1,2,3]
var b = [4,5,6]
Array.prototype.push.apply(a, b);
a // [1,2,3,4,5,6]複製程式碼

unshiftpush一樣,不過是將元素加在陣列前面:

var a = [1,2,3]
var b = [4,5,6]
Array.prototype.unshift.apply(a, b);
a // [4,5,6,1,2,3]複製程式碼

5. 修正內部函式的this指向

在函式內部定義的函式,如果直接呼叫,則該內部函式的this並不指向外層函式的this,而是指向全域性執行環境,所以呼叫內部函式必須指明其this的指向

// 問題程式碼示例
document.getElementById('div1').onclick = function() {
    alert(this.id) // div1
    var func = function() {
        alert(this.id);
    }
    func(); // undefined
}
// 正確程式碼示例
document.getElementById('div1').onclick = function() {
    alert(this.id) // div1
    var func = function() {
        alert(this.id);
    }
    func.apply(this); // div1
}複製程式碼

當然,在外層儲存this變數,然後在內部函式定義中直接使用儲存的變數也可以達到相同的效果,但是使用apply的方法相對而言,保持了內部函式的獨立性。

6. 給既有方法打補丁

看下面一段程式碼:

// 儲存原函式
var originalfoo = someobject.foo;
someobject.foo = function() {
    // 在這裡新增需要在原函式呼叫前執行的操作
    console.log(arguments);
    // 呼叫原函式
    originalfoo.apply(this, arguments);
    // 在這裡新增需要在原函式呼叫後執行的操作
}複製程式碼

上面的例子,在呼叫原來的方法之前或者之後,執行了新的操作,補充增強了原來的方法,而且不改變原來的操作。這也是設計模式中裝飾者模式的實現思路

舉一個應用場景:假如你從上一位開發者手中接過了一個專案,你需要在不改動原來功能的基礎上開發一個新功能,你找到了這個功能的函式位置,但是因為程式碼組織很糟糕,你幾乎看不懂這段程式碼做了什麼,所以也不敢輕易改動。怎麼辦?也許上面利用apply打補丁的方法值得一試。只需在呼叫前後新增新操作,然後按照其呼叫方法呼叫,完全不用關心原函式的實現細節如何。

結語

善用applycall方法,可以大幅提高我們程式設計的效率,提升程式效能。這裡僅僅總結了一部分apply的用法,apply的強大之處肯定遠遠不止如此,更多的用法還有待我們進一步的發現和總結。

你有什麼好的用法,歡迎留言,我繼續補充。

相關文章