合理的使用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
方法兩個方面的特性而來的:
- 可以傳入
this
,改變函式執行時的執行環境。 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
型別的判斷,NaN
和Infinity
也會被識別為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]複製程式碼
unshift
和push
一樣,不過是將元素加在陣列前面:
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
打補丁的方法值得一試。只需在呼叫前後新增新操作,然後按照其呼叫方法呼叫,完全不用關心原函式的實現細節如何。
結語
善用apply
和call
方法,可以大幅提高我們程式設計的效率,提升程式效能。這裡僅僅總結了一部分apply
的用法,apply
的強大之處肯定遠遠不止如此,更多的用法還有待我們進一步的發現和總結。
你有什麼好的用法,歡迎留言,我繼續補充。