js 實現call和apply方法,超詳細思路分析

壹 ❀ 引

我在 五種綁定策略完全弄懂this 一文中,咱們提到call,apply,bind屬於顯示綁定,這三個方法都能直接修改this指向。其中call與apply比較特殊,它們在修改this的同時還會直接執行方法,而bind只是返回一個修改完this的boundFunction並未執行,那麼今天咱們來說講若是經過JavaScript模擬實現call與apply方法。javascript

貳 ❀ 關於call與apply1

貳 ✿ 壹 call與apply區別

除了都能改變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方法

咱們從一個簡單的例子解析call方法函數

var name = '時間跳躍';
var obj = {
    name: '聽風是風'
};

function fn() {
    console.log(this.name);
};
fn(); //時間跳躍
fn.call(obj); //聽風是風

在這個例子中,call方法主要作了兩件事:this

  • 修改了this指向,好比fn()默認指向window,因此輸出時間跳躍
  • 執行了函數fn

叄 ✿ 壹 改變this並執行方法

先說第一步改變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, "個人", "名字", "是"); // 個人名字是聽風是風

叄 ✿ 叄 考慮特殊this指向

咱們知道,當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方法

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書寫的文章,爲了統同樣式,我也專門修改了博客樣式。

參考

JavaScript深刻之call和apply的模擬實現

深刻淺出 妙用Javascript中apply、call、bind

深度解析 call 和 apply 原理、使用場景及實現

相關文章
相關標籤/搜索