我在 五種綁定策略完全弄懂this 一文中,咱們提到call,apply,bind
屬於顯示綁定,這三個方法都能直接修改this指向。其中call與apply比較特殊,它們在修改this的同時還會直接執行方法,而bind只是返回一個修改完this的boundFunction
並未執行,那麼今天咱們來說講若是經過JavaScript模擬實現call與apply方法。javascript
除了都能改變this指向並執行函數,call與apply惟一區別在於參數不一樣,具體以下:html
var fn = function (arg1, arg2) { // do something }; fn.call(this, arg1, arg2); // 參數散列 fn.apply(this, [arg1, arg2]) // 參數使用數組包裹
call第一參數爲this指向,後續散列參數均爲函數調用所需形參,而在apply中這些參數被包裹在一個數組中。java
call與apply在平常開發中很是實用,咱們在此列舉幾個實用的例子。git
檢驗數據類型:github
function type(obj) { var regexp = /\s(\w+)\]/; var result = regexp.exec(Object.prototype.toString.call(obj))[1]; return result; }; console.log(type([123]));//Array console.log(type('123'));//String console.log(type(123));//Number console.log(type(null));//Null console.log(type(undefined));//Undefined
數組取最大/小值:數組
var arr = [11, 1, 0, 2, 3, 5]; // 取最大 var max1 = Math.max.call(null, ...arr); var max2 = Math.max.apply(null, arr); // 取最小 var min1 = Math.min.call(null, ...arr); var min2 = Math.min.apply(null, arr); console.log(max1); //11 console.log(max2); //11 console.log(min1); //0 console.log(min2); //0
函數arguments類數組操做:markdown
var fn = function () { var arr = Array.prototype.slice.call(arguments); console.log(arr); //[1, 2, 3, 4] }; fn(1, 2, 3, 4);
關於這兩個方法實用簡單說到這裏,畢竟本文的核心主旨是手動實現call與apply方法,咱們接着說。app
咱們從一個簡單的例子解析call方法函數
var name = '時間跳躍'; var obj = { name: '聽風是風' }; function fn() { console.log(this.name); }; fn(); //時間跳躍 fn.call(obj); //聽風是風
在這個例子中,call方法主要作了兩件事:this
this
指向,好比fn()
默認指向window
,因此輸出時間跳躍
fn
先說第一步改變this
怎麼實現,其實很簡單,只要將方法fn
添加成對象obj
的屬性不就行了。因此咱們能夠這樣:
//模擬call方法 Function.prototype.call_ = function (obj) { obj.fn = this; // 此時this就是函數fn obj.fn(); // 執行fn delete obj.fn; //刪除fn }; fn.call_(obj); // 聽風是風
注意,這裏的call_
是咱們模擬的call
方法,咱們來解釋模擬方法中作了什麼。
Function.prototype.call_
的形式綁定了call_
方法,因此全部函數均可以直接訪問call_
。fn.call_
屬於this隱式綁定,因此在執行時call_
時內部this
指向fn
,這裏的obj.fn = this
就是將方法fn
賦予成了obj
的一條屬性。obj
如今已經有了fn
方法,執行obj.fn
,由於隱式綁定的問題,fn
內部的this
指向obj
,因此輸出了聽風是風
。delete
刪除了obj
上的fn
方法,畢竟執行完不刪除會致使obj
上的屬性愈來愈多。咱們成功改變了this指向並執行了方法,但仍有一個問題待解決,call_
沒法接受參數。
其實也不難,咱們知道函數有一個arguments
屬性,代指函數接收的全部參數,它是一個類數組,好比下方例子:
Function.prototype.call_ = function (obj) { console.log(arguments); }; fn.call_(obj, 1, 2, 3);// [{name:'聽風是風'},1,2,3...]
很明顯arguments
第一位參數是咱們須要讓this指向的對象,因此從下標1開始纔是真正的函數參數,這裏就得對arguments
進行加工,將下標1以後的參數剪切出來。
有同窗確定就想到了arguments.splice
,前面說了arguments並不是數組,因此不支持Array
方法。不要緊,不是還有Array.prototype.slice.call(arguments)
嗎,轉一次數組再用。很遺憾,咱們如今是在模擬call
方法,也不行。那就用最保險的for
循環吧,以下:
Function.prototype.call_ = function (obj) { var args = []; // 注意i從1開始 for (var i = 1, len = arguments.length; i < len; i++) { args.push(arguments[i]); }; console.log(args);// [1, 2, 3] }; fn.call_(obj, 1, 2, 3);
數組也不能直接做爲參數傳遞給函數,有同窗可能想到array.join
字符拼接方法,這也存在一個問題,好比咱們是但願傳遞參數1 2 3
三個參數進去,但通過join
方法拼接,它會變成一個參數"1,2,3"
,函數此時接受的就只有一個參數了。
因此這裏咱們不得不借用惡魔方法eval,看個簡單的例子:
var fn = function (a, b, c) { console.log(a + b + c); }; var arr = [1, 2, 3]; fn(1, 2, 3);//6 eval("fn(" + arr + ")");//6
你必定有疑問,爲何這裏數組arr
都不分割一下,fn
在執行時又如何分割數組呢?其實eval
在執行時會將變量轉爲字符串,這裏隱性執行了arr.toString()
。來看個有趣的對比:
console.log([1, 2, 3].toString()); //"1,2,3" console.log([1, 2, 3].join(',')); //"1,2,3"
能夠看出`eval
幫咱們作了數組處理,這裏就不須要再使用join
方法了,所以eval("fn(" + arr + ")")
能夠當作eval("fn(1,2,3)")
。
咱們整理下上面的思路,改寫後的模擬方法就是這樣:
var name = '時間跳躍'; var obj = { name: '聽風是風' }; function fn(a, b, c) { console.log(a + b + c + this.name); }; //模擬call方法 Function.prototype.call_ = function (obj) { var args = []; // 注意i從1開始 for (var i = 1, len = arguments.length; i < len; i++) { args.push(arguments[i]); }; obj.fn = this; // 此時this就是函數fn eval("obj.fn(" + args + ")"); // 執行fn delete obj.fn; //刪除fn }; fn.call_(obj, "個人", "名字", "是");
能夠了嗎?很遺憾,這段代碼會報錯。由於咱們傳遞的後三個參數都是字符串。在args.push(arguments[i])
這一步咱們提早將字符串進行了解析,這就致使eval
在執行時,表達式變成了eval("obj.fn(個人,名字,是)");
設想一下咱們普通調用函數的形式是這樣obj.fn("個人","名字","是")
,因此對於eval而言就像傳遞了三個沒加引號的字符串,沒法進行解析。
不信咱們能夠傳遞三個數字,好比:
fn.call_(obj, 1,2,3); // 6聽風是風
由於數字無論加不加引號,做爲函數參數都是可解析的,而字符串不加引號,那就被認爲是一個變量,而不存在個人
這樣的變量,天然就報錯了。
怎麼辦呢?其實咱們能夠在args.push(arguments[i])
這裏先不急着解析,改寫成這樣:
args.push("arguments[" + i + "]");
遍歷完成的數組args最終就是這個樣子["arguments[1]","arguments[2]","arguments[3]"]
,當執行eval
時,arguments[1]
此時確實是做爲一個變量存在不會報錯,因而被eval
解析成了一個真正的字符傳遞給了函數。
因此改寫後的call_
應該是這樣:
var name = '時間跳躍'; var obj = { name: '聽風是風' }; function fn(a, b, c) { console.log(a + b + c + this.name); }; //模擬call方法 Function.prototype.call_ = function (obj) { var args = []; // 注意i從1開始 for (var i = 1, len = arguments.length; i < len; i++) { args.push("arguments[" + i + "]"); }; obj.fn = this; // 此時this就是函數fn eval("obj.fn(" + args + ")"); // 執行fn delete obj.fn; //刪除fn }; fn.call_(obj, "個人", "名字", "是"); // 個人名字是聽風是風
咱們知道,當call
第一個參數爲undefined
或者null
時,this默認指向window
,因此上面的方法還不夠完美,咱們進行最後一次改寫,考慮傳遞參數是不是有效對象:
var name = '時間跳躍'; var obj = { name: '聽風是風' }; function fn(a, b, c) { console.log(a + b + c + this.name); }; //模擬call方法 Function.prototype.call_ = function (obj) { //判斷是否爲null或者undefined,同時考慮傳遞參數不是對象狀況 obj = obj ? Object(obj) : window; var args = []; // 注意i從1開始 for (var i = 1, len = arguments.length; i < len; i++) { args.push("arguments[" + i + "]"); }; obj.fn = this; // 此時this就是函數fn eval("obj.fn(" + args + ")"); // 執行fn delete obj.fn; //刪除fn }; fn.call_(obj, "個人", "名字", "是"); // 個人名字是聽風是風 fn.call_(null, "個人", "名字", "是"); // 個人名字是時間跳躍 fn.call_(undefined, "個人", "名字", "是"); // 個人名字是時間跳躍
那麼到這裏,對於call
方法的模擬就完成了。
apply
方法由於接受的參數是一個數組,因此模擬起來就更簡單了,理解了call
實現,咱們就直接上代碼:
var name = '時間跳躍'; var obj = { name: '聽風是風' }; function fn(a, b, c) { console.log(a + b + c + this.name); }; //模擬call方法 Function.prototype.apply_ = function (obj, arr) { obj = obj ? Object(obj) : window; obj.fn = this; if (!arr) { obj.fn(); } else { var args = []; // 注意這裏的i從0開始 for (var i = 0, len = arr.length; i < len; i++) { args.push("arr[" + i + "]"); }; eval("obj.fn(" + args + ")"); // 執行fn }; delete obj.fn; //刪除fn }; fn.apply_(obj, ["個人", "名字", "是"]); // 個人名字是聽風是風 fn.apply_(null, ["個人", "名字", "是"]); // 個人名字是時間跳躍 fn.apply_(undefined, ["個人", "名字", "是"]); // 個人名字是時間跳躍
上述代碼總有些繁雜,咱們來總結下這兩個方法:
// call模擬 Function.prototype.call_ = function (obj) { //判斷是否爲null或者undefined,同時考慮傳遞參數不是對象狀況 obj = obj ? Object(obj) : window; var args = []; // 注意i從1開始 for (var i = 1, len = arguments.length; i < len; i++) { args.push("arguments[" + i + "]"); }; obj.fn = this; // 此時this就是函數fn var result = eval("obj.fn(" + args + ")"); // 執行fn delete obj.fn; //刪除fn return result; }; // apply模擬 Function.prototype.apply_ = function (obj, arr) { obj = obj ? Object(obj) : window; obj.fn = this; var result; if (!arr) { result = obj.fn(); } else { var args = []; // 注意這裏的i從0開始 for (var i = 0, len = arr.length; i < len; i++) { args.push("arr[" + i + "]"); }; result = eval("obj.fn(" + args + ")"); // 執行fn }; delete obj.fn; //刪除fn return result; };
若是容許使用ES6,使用拓展運算符會簡單不少,實現以下:
// ES6 call Function.prototype.call_ = function (obj) { obj = obj ? Object(obj) : window; obj.fn = this; // 利用拓展運算符直接將arguments轉爲數組 let args = [...arguments].slice(1); let result = obj.fn(...args); delete obj.fn return result; }; // ES6 apply Function.prototype.apply_ = function (obj, arr) { obj = obj ? Object(obj) : window; obj.fn = this; let result; if (!arr) { result = obj.fn(); } else { result = obj.fn(...arr); }; delete obj.fn return result; };
那麼到這裏,關於call與apply模擬實現所有結束。
這篇文章也是第一篇我使用markdown書寫的文章,爲了統同樣式,我也專門修改了博客樣式。