合理的使用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),有兩種直觀的解決方案:bash
第一,給cat直接賦予一個makeSound方法併發
var cat = {
sound: 'miao',
makeSound: function() {
console.log(this.sound);
}
}複製代碼
第二,利用原型繼承app
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
方法兩個方面的特性而來的:ui
this
,改變函數運行時的執行環境。apply
的第二個參數是函數參數組成的數組或者類數組,且被借用函數是以散列形式傳參。類數組雖然能和數組同樣使用下標索引,可是它不具備數組擁有的內置方法,沒法直接使用這些方法,幸運的是,類數組的特性決定了數組的方法也能在其對象上使用,這就給apply
方法發揮的空間:this
// 錯誤例子
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
等方法拿到的對象都是類數組,通常來說,只要轉化成數組類型就會極大地方便咱們的操做。
求數組中的最大最小值操做是很常見的,可是,數組並無爲咱們實現這樣的操做,最直觀的方法就是遍歷數組,查找最大最小值,無疑,這種方法不只笨拙低效,並且性能極差。可是,若是給你的不是一個數組,而就是一些數字呢?你可能會當即想到Math.max
方法。這兩種形式參數的關係剛好就符合咱們以前提到的兩個特性之二:函數要求以散列形式傳參,apply
又要求以數組或類數組傳參。
Math.max.apply(null, [1,6,5,3,5]) // 6
Math.min.apply(null, [1,6,5,3,5] ) // 1複製代碼
判斷對象類型,咱們有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也徹底是能夠的。
先來看看數組的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]複製代碼
在函數內部定義的函數,若是直接調用,則該內部函數的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
的方法相對而言,保持了內部函數的獨立性。
看下面一段代碼:
// 保存原函數
var originalfoo = someobject.foo;
someobject.foo = function() {
// 在這裏添加須要在原函數調用前執行的操做
console.log(arguments);
// 調用原函數
originalfoo.apply(this, arguments);
// 在這裏添加須要在原函數調用後執行的操做
}複製代碼
上面的例子,在調用原來的方法以前或者以後,執行了新的操做,補充加強了原來的方法,並且不改變原來的操做。這也是設計模式中裝飾者模式的實現思路
舉一個應用場景:假如你從上一位開發者手中接過了一個項目,你須要在不改動原來功能的基礎上開發一個新功能,你找到了這個功能的函數位置,可是由於代碼組織很糟糕,你幾乎看不懂這段代碼作了什麼,因此也不敢輕易改動。怎麼辦?也許上面利用apply
打補丁的方法值得一試。只需在調用先後添加新操做,而後按照其調用方法調用,徹底不用關心原函數的實現細節如何。
善用apply
和call
方法,能夠大幅提升咱們編程的效率,提高程序性能。這裏僅僅總結了一部分apply
的用法,apply
的強大之處確定遠遠不止如此,更多的用法還有待咱們進一步的發現和總結。
你有什麼好的用法,歡迎留言,我繼續補充。