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

2021-02-21 前端大全

(點擊上方公眾號,可快速關注)

作者:冴羽 

github.com/mqyqingfeng/Blog/issues/11

如有好文章投稿,請點擊 → 這裡了解詳情

call

一句話介紹 call:

call() 方法在使用一個指定的 this 值和若干個指定的參數值的前提下調用某個函數或方法。

舉個例子:

var foo = {

    value: 1

};

 

function bar() {

    console.log(this.value);

}

 

bar.call(foo); // 1

注意兩點:

call 改變了 this 的指向,指向到 foo

bar 函數執行了

模擬實現第一步

那麼我們該怎麼模擬實現這兩個效果呢?

試想當調用 call 的時候,把 foo 對象改造成如下:

var foo = {

    value: 1,

    bar: function() {

        console.log(this.value)

    }

};

 

foo.bar(); // 1

這個時候 this 就指向了 foo,是不是很簡單呢?

但是這樣卻給 foo 對象本身添加了一個屬性,這可不行吶!

不過也不用擔心,我們用 delete 再刪除它不就好了~

所以我們模擬的步驟可以分為:

將函數設為對象的屬性

執行該函數

刪除該函數

以上個例子為例,就是:

// 第一步

foo.fn = bar

// 第二步

foo.fn()

// 第三步

delete foo.fn

fn 是對象的屬性名,反正最後也要刪除它,所以起成什麼都無所謂。

根據這個思路,我們可以嘗試著去寫第一版的 call2 函數:

// 第一版

Function.prototype.call2 = function(context) {

    // 首先要獲取調用call的函數,用this可以獲取

    context.fn = this;

    context.fn();

    delete context.fn;

}

 

// 測試一下

var foo = {

    value: 1

};

 

function bar() {

    console.log(this.value);

}

 

bar.call2(foo); // 1

正好可以列印 1 哎!是不是很開心!(~ ̄▽ ̄)~

模擬實現第二步

最一開始也講了,call 函數還能給定參數執行函數。舉個例子:

var foo = {

    value: 1

};

 

function bar(name, age) {

    console.log(name)

    console.log(age)

    console.log(this.value);

}

 

bar.call(foo, 'kevin', 18);

// kevin

// 18

// 1

注意:傳入的參數並不確定,這可咋辦?

不急,我們可以從 Arguments 對象中取值,取出第二個到最後一個參數,然後放到一個數組裡。

比如這樣:

// 以上個例子為例,此時的arguments為:

// arguments = {

//      0: foo,

//      1: 'kevin',

//      2: 18,

//      length: 3

// }

// 因為arguments是類數組對象,所以可以用for循環

var args = [];

for(var i = 1, len = arguments.length; i  len; i++) {

    args.push('arguments[' + i + ']');

}

 

// 執行後 args為 [foo, 'kevin', 18]

不定長的參數問題解決了,我們接著要把這個參數數組放到要執行的函數的參數裡面去。

// 將數組裡的元素作為多個參數放進函數的形參裡

context.fn(args.join(','))

// (O_o)??

// 這個方法肯定是不行的啦!!!

也許有人想到用 ES6 的方法,不過 call 是 ES3 的方法,我們為了模擬實現一個 ES3 的方法,要用到ES6的方法,好像……,嗯,也可以啦。但是我們這次用 eval 方法拼成一個函數,類似於這樣:

eval('context.fn(' + args +')')

這裡 args 會自動調用 Array.toString() 這個方法。

所以我們的第二版克服了兩個大問題,代碼如下:

// 第二版

Function.prototype.call2 = function(context) {

    context.fn = this;

    var args = [];

    for(var i = 1, len = arguments.length; i  len; i++) {

        args.push('arguments[' + i + ']');

    }

    eval('context.fn(' + args +')');

    delete context.fn;

}

 

// 測試一下

var foo = {

    value: 1

};

 

function bar(name, age) {

    console.log(name)

    console.log(age)

    console.log(this.value);

}

 

bar.call2(foo, 'kevin', 18);

// kevin

// 18

// 1

(๑•̀ㅂ•́)و✧

模擬實現第三步

模擬代碼已經完成 80%,還有兩個小點要注意:

1.this 參數可以傳 null,當為 null 的時候,視為指向 window

舉個例子:

var value = 1;

 

function bar() {

    console.log(this.value);

}

 

bar.call(null); // 1

雖然這個例子本身不使用 call,結果依然一樣。

2.函數是可以有返回值的!

舉個例子:

var obj = {

    value: 1

}

 

function bar(name, age) {

    return {

        value: this.value,

        name: name,

        age: age

    }

}

 

console.log(bar.call(obj, 'kevin', 18));

// Object {

//    value: 1,

//    name: 'kevin',

//    age: 18

// }

不過都很好解決,讓我們直接看第三版也就是最後一版的代碼:

// 第三版

Function.prototype.call2 = function (context) {

    var context = context || window;

    context.fn = this;

 

    var args = [];

    for(var i = 1, len = arguments.length; i  len; i++) {

        args.push('arguments[' + i + ']');

    }

 

    var result = eval('context.fn(' + args +')');

 

    delete context.fn

    return result;

}

 

// 測試一下

var value = 2;

 

var obj = {

    value: 1

}

 

function bar(name, age) {

    console.log(this.value);

    return {

        value: this.value,

        name: name,

        age: age

    }

}

 

bar.call(null); // 2

 

console.log(bar.call2(obj, 'kevin', 18));

// 1

// Object {

//    value: 1,

//    name: 'kevin',

//    age: 18

// }

到此,我們完成了 call 的模擬實現,給自己一個贊 b( ̄▽ ̄)d

apply的模擬實現

apply 的實現跟 call 類似,在這裡直接給代碼,代碼來自於知乎 @鄭航的實現:

Function.prototype.apply = function (context, arr) {

    var context = Object(context) || window;

    context.fn = this;

 

    var result;

    if (!arr) {

        result = context.fn();

    }

    else {

        var args = [];

        for (var i = 0, len = arr.length; i  len; i++) {

            args.push('arr[' + i + ']');

        }

        result = eval('context.fn(' + args + ')')

    }

 

    delete context.fn

    return result;

}

重要參考

知乎問題 不能使用call、apply、bind,如何用 js 實現 call 或者 apply 的功能?

深入系列

JavaScript深入系列目錄地址:https://github.com/mqyqingfeng/Blog。

JavaScript深入系列預計寫十五篇左右,旨在幫大家捋順JavaScript底層知識,重點講解如原型、作用域、執行上下文、變量對象、this、閉包、按值傳遞、call、apply、bind、new、繼承等難點概念。

如果有錯誤或者不嚴謹的地方,請務必給予指正,十分感謝。如果喜歡或者有所啟發,歡迎star,對作者也是一種鼓勵。

本系列:

JavaScirpt 深入之從原型到原型鏈

JavaScript 深入之詞法作用域和動態作用域

JavaScript 深入之執行上下文棧

JavaScript 深入之變量對象

JavaScript 深入之作用域鏈

JavaScript 深入之從 ECMAScript 規範解讀 this

JavaScript 深入之執行上下文

JavaScript 深入之閉包

JavaScript 深入之參數按值傳遞

覺得本文對你有幫助?請分享給更多人

關注「前端大全」,提升前端技能

相關焦點

  • JavaScript深入之bind的模擬實現
    JavaScript深入之bind的模擬實現JavaScript深入系列第十一篇,通過bind函數的模擬實現,帶大家真正了解bind的特性bind一句話介紹 bind:bind() 方法會創建一個新函數。當這個新函數被調用時,bind() 的第一個參數將作為它運行時的 this,之後的一序列參數將會在傳遞的實參前傳入作為它的參數。
  • 面試官問:能否模擬實現JS的call和apply方法
    1.面試官問:能否模擬實現JS的new操作符2.面試官問:能否模擬實現JS的bind方法3.面試官問:能否模擬實現JS的call和apply方法4.面試官問:JS的this指向5.面試官問:JS的繼承之前寫過兩篇《面試官問:能否模擬實現JS的new操作符》和《面試官問:能否模擬實現JS的bind
  • 深入淺出 妙用Javascript中apply、call、bind
    apply、call   在 javascript 中,call 和 apply 都是為了改變某個函數運行時的上下文(context)而存在的,換句話說,就是為了改變函數體內部 this 的指向。或apply 用 apple 的 say 方法:banana = { color: "yellow"}apple.say.call(banana); //My color is yellowapple.say.apply(banana); //My color is yellow  所以,可以看出 call 和 apply 是為了動態改變 this
  • JS中apply()和call()方法的區別詳解
    區分apply,call就一句話,  foo.call(this, arg1,arg2,arg3) == foo.apply(this, arguments)==this.foo(arg1, arg2, arg3) call, apply都屬於Function.prototype的一個方法,它是JavaScript
  • JavaScript中apply、call、bind的區別與用法
    語法:func.apply(thisArg, [argsArray])1.2 Function.prototype.call()call() 方法調用一個函數, 其具有一個指定的this值和分別地提供的參數(參數的列表)。語法:fun.call(thisArg, arg1, arg2, ...)
  • 深度解析 new 原理及模擬實現
    如果構造函數沒有顯式返回一個對象,則使用步驟1創建的對象。模擬實現第一步new 是關鍵詞,不可以直接覆蓋。這裡使用 create 來模擬實現 new 的效果。new 返回一個新對象,通過 obj.__proto__ = Con.prototype 繼承構造函數的原型,同時通過 Con.apply(obj, arguments)調用父構造函數實現繼承,獲取構造函數上的屬性(【進階3-3期】)。
  • 高級前端進階,為什麼要使用call、apply、bind
    通過本文可以了解:this是什麼call、apply、bind是如何實現的call、apply、bind的用處什麼是this、apply、bind原生實現解析1.callfunction.call(thisArg, arg1, arg2, ...)。
  • 文章 | JavaScript深入系列15篇正式完結!
    重點講解了如原型、作用域、執行上下文、變量對象、this、閉包、按值傳遞、call、apply、bind、new、繼承等 JS 語言中的比較難懂的概念。JavaScript 深入系列自 4 月 6 日發布第一篇文章,到 5 月 12 日發布最後一篇,感謝各位朋友的收藏、點讚,鼓勵、指正。
  • 最強大、最牛逼的javascript視頻免費發布啦
    本視頻教程相當於基本javascript的書籍的結晶《javascript高級程序設計》《javascript權威指南》《javascript徵途》《javascript王者歸來》、《javascript設計與模式》、《javascript編程精講》希望以此來提高你的javascript水平,以此來讓你更加精通javascript,體會javascript編程之美!!!
  • 5分鐘帶你搞懂 Javascript 中的this(包含apply、call、bind)
    或 apply 調用apply和call可以動態地改變傳入函數的 thisvar obj1 = { name: 'sven', getName: function(){ return this.name; }};var obj2 = { name: 'anne'};
  • 理解this及call,apply和bind的用法
    隱式綁定,顯式綁定,new綁定和window綁定。在介紹這些時,你還將學習一些其他令人困惑的JavaScript部分,例如.call,.apply,.bind和new關鍵字。」前言在深入研究JavaScript中this關鍵字的細節之前,我們先退一步想一想,為什麼this關鍵字存在於第一位。this關鍵字允許你重用具有不同上下文的函數。
  • call與apply方法的使用
    行內綁定中this指向了全局window對象動態綁定中this指向了當前正在操作的dom對象構造函數也存在一個this,誰調用了這個函數,函數內部的this就指向誰1)call([thisObj[,arg1[,arg2[,argN]]]]) :改變函數內部的this指向參數說明:thisObj:要指向的對象arg1,arg2,arg3…:參數列表,參數與參數之間通過逗號隔開2)apply([thisObj[,argArray
  • 12 個非常有用的 JavaScript 技巧
    作者: Caio Ribeiro Pereira轉載自:W3CPlus http://www.w3cplus.com/javascript/12-extremely-useful-hacks-for-javascript.html 譯者: 大漠在這篇文章中將給大家分享12個有關於JavaScript的小技巧。
  • JavaScript高級this指向和實現繼承的幾種方式
    () , call() 方法在新建的對象上執行構造函數,從而發揮二者之長 , 通過使用原型鏈實現對原型屬性和方法的繼承 ,通過構造函數實現實列數組 ,這樣既能通過在原型上定義方法實現函數復用 ,又能保證每一個實例都有他自己的數組 ,是JavaScript中常用的繼承模式
  • 面試官問:能否模擬實現JS的bind方法(高頻考點)
    1.面試官問:能否模擬實現JS的new操作符2.面試官問:能否模擬實現JS的bind方法(本文)3.面試官問:能否模擬實現JS的call和apply方法4.面試官問:JS的this指向5.面試官問:JS的繼承用過React的同學都知道,經常會使用bind來綁定this。
  • 笛卡爾乘積的javascript版實現和應用
    一般的實現中,c語言,python,java實現的方式比較多,但是對於前端而言,也是有其實現意義的,比如淘寶的sku商品訂單組合的實現就需要笛卡爾乘積,根據商品的子類型和不同尺寸生成n種可能的組合某些情況下用於尋找連續日期中殘缺的數據,可以先笛卡爾積做一個排列組合
  • 用javascript實現select的美化
    首頁 > 教程 > 關鍵詞 > 最新資訊 > 正文 用javascript實現select的美化
  • JavaScript支持計時事件,如何實現超時調用和間隔調用?
    JavaScript開發-計時事件#JavaScript#在瀏覽器中,我們能夠實現動畫效果,例如時鐘,可以實現秒數一直在走。這裡我們就這2個計時事件和相應的取消方法來講解。第2節. setTimeout()方法一、 基本概念和語法在JavaScript中,使用setTimeout()方法可以實現超時調用,其功能是在設定的一個毫秒數之後,去執行一個函數或一段JS代碼。例如,在Web頁面上經過20秒之後,顯示一張圖片。
  • Pandas三大利器-map、apply、applymap
    本文中介紹了 pandas中的三大利器: map、apply、applymap 來解決上述同樣的需求。—  01 —模擬數據通過一個模擬的數據來說明3個函數的使用,在這個例子中學會了如何生成各種模擬數據。
  • JavaScript中的「黑話」
    ())但是以上的實現並不是完全隨機的,究其原因,還是因為排序算法的不穩定性,導致一些元素沒有機會進行比較,具體請參考問題,在抽獎程序中若要實現完全隨機,請使用 Fisher–Yates shuffle 算法,以下是簡單實現:function shuffle(arrs){for(let i = arrs.length -1; i >0; i -=1){const random =Math.floor