談談javascript的Function中那些隱藏的屬性/方法:caller/callee/apply/call/bind
javascript的Function中有不少不那么常用,又或者用了也是 知其然而不知其所以然 的屬性/方法,本文就來談談這一系列屬性/方法: caller / callee / apply / call / bind 。
caller 屬性
直接上DEMO比較好理解:
// caller demo { function callerDemo() { if (callerDemo.caller) { var a = callerDemo.caller.toString(); console.log(a); } else { console.log("this is a top function"); } } function handleCaller() { callerDemo(); } handleCaller(); //"function handleCaller() { callerDemo(); }" callerDemo(); //"this is a top function"
我們先來按照思路一步一步來看這段代碼:
-
首先我們看到定義了倆function:handleCaller和callerDemo,并且還可以看出handleCaller函數里是調用了callerDemo函數的。
-
在callerDemo函數里,我們看到了本文介紹的主角之一: caller 屬性,并且可以看出這 caller 屬性是函數對象本身的一個成員屬性。
-
在callerDemo函數里,有一段判斷 caller 屬性是否存在的代碼,這段代碼有什么意義呢?這就要看最后的結果了:直接調用callerDemo()發現此時 callerDemo.caller 是為空的,而反觀通過調用handleCaller()并在其內部調用callerDemo()則 callerDemo.caller 不為空。這說明只有在函數里調用函數,才會生成 caller 屬性,而直接在全局環境里調用函數則不會生成。
-
繼續看 var a = callerDemo.caller.toString();console.log(a); ,這里打印出來的居然是handleCaller整個函數體,這說明,此時的 callerDemo.caller 實際上就是對于handleCaller這個函數對象的一個引用。
這么分析下來, caller 屬性就很容易明白了:
-
caller 屬性是幫助我們在當前函數里獲取調用當前函數的某個未知函數,之所以稱 未知函數 ,是因為我們在寫一個函數時,很可能根本不知道哪個函數會調用到我們的這個函數。
-
在全局環境中調用函數是不會生成此 caller 屬性,因為不符合此屬性的存在意義/價值(見上條)。
-
只有在當前函數的內部(上下文環境)才能調用當前函數的 caller 屬性,不能從外部調用。
callee 屬性
還是先放代碼:
function calleeDemo() { console.log(arguments.callee); }
有了上文對 caller 屬性的認知, callee 屬性就很好理解了,它實際上就是對當前函數對象的一個引用。有以下的點需要注意:
-
callee 屬性隸屬于Function的一個隱藏對象—— arguments 中,這個 arguments 對象大家應該不陌生,表示的就是當前函數傳入的參數,一般用于函數不限制參數數量的傳參。
-
與 caller 屬性一樣,也是要在當前函數的內部(上下文環境)才有效。
-
可配合 caller 屬性一起使用: arguments.callee.caller ,這樣就可以完全忽略到具體的函數名了。
-
函數遞歸時用起來比用函數名調用函數更帶感!
apply / call 方法
這倆方法性質一樣,只是用法稍有不同,因此放在一起來介紹。還記得我上一篇文章《 javascript如何判斷變量的數據類型 》中介紹的利用 Object.prototype.toString.call 來判斷數據類型的方法么:
function type(obj) { return Object.prototype.toString.call(obj).slice(8, -1); //換成用apply方法亦可 }
-
apply / call 方法的意義在于 借用 其它對象的成員方法來對目標對象執行操作。
-
在 借用 的過程中, apply / call 方法會改變被借用的成員方法的上下文環境:把 this 這一與上下文環境高度相關的變量指向目標對象,而非原來的對象。看下面的這段代碼:
function Point(x, y){ this.x = x; this.y = y; } Point.prototype.move = function(x, y) { this.x += x; this.y += y; } var p = new Point(0,0); var circle = {x:1, y:1, r:1}; //只是一個普通的Object對象 p.move.call(circle, 2, 1); //借用了Point類對象中的move方法 //p.move.apply(circle, [2, 1]); //等價于p.move.call(circle, 2, 1);
這里的circle只是一個普通的Object對象,不含任何自定義的成員方法,但通過 apply / call 方法,可以借用Point類對象定義的move方法來幫助circle達到目的(本例其實是圓心在坐標軸上的移動)。在 借用 Point類對象的move方法時,move方法中的this就不再指向p,而是指向circle了,達到了上下文環境改變的效果。
另外,從代碼里也可以看出, call 方法與 apply 方法的區別僅在于: call 方法直接把需要傳入的參數列在目標對象其后,而 apply 方法則以數組的形式整體傳入。
bind 方法
bind 方法與 apply / call 方法也非常類似,相當于稍微再封裝了一下,仍以上述DEMO作為案例:
function Point(x, y){ this.x = x; this.y = y; } Point.prototype.move = function(x, y) { this.x += x; this.y += y; } var p = new Point(0,0); var circle = {x:1, y:1, r:1}; // p.move.call(circle, 2, 1); // p.move.apply(circle, [2, 1]); var circleMove = p.move.bind(circle, 2, 1); //此時并不執行move方法 circleMove(); //此時才執行
從上面這段DEMO可以看出, bind 方法其實是給 apply / call 方法緩了一下,也可以說是封裝了一下方便后續調用,其實質上相當于下面的這段代碼:
function circleMove() { p.move.call(circle, 2, 1); } circleMove();
bind 方法兼容性適應
bind 方法,即 Function.prototype.bind ,屬于 ECMAScript 5 ,IE從 IE 10 版本才開始支持,那怎么做兼容性適應呢?
if(typeof Function.prototype.bind !== 'function') { Function.prototype.bind = function() { var thatFunc = this; var args = []; for(var i = 0; i < arguments.length; i++) { args[i] = arguments[i]; }return function() { var context = args.shift(); thatFunc.apply(context, args); }
} }</pre>
其思路是利用 apply 方法來封裝成 bind 方法。
</div>