目錄
手寫實作call
手寫實作 apply
手写实现 bind
首頁 web前端 js教程 一文帶你深入了解實現call、apply和bind方法

一文帶你深入了解實現call、apply和bind方法

Jul 12, 2021 pm 06:03 PM
apply bind javascript

這篇文章透過程式碼範例,給大家深入解析一下如何實現 call、apply 和 bind,至於這幾個方法的具體用法,MDN 或者站內的文章已經描述得很清楚,這裡不再贅述。

一文帶你深入了解實現call、apply和bind方法

手寫實作call

#ES3 版本

Function.prototype.myCall = function(thisArg){
    if(typeof this != 'function'){
        throw new Error('The caller must be a function')
    }
     if(thisArg === undefined || thisArg === null){
        thisArg = globalThis
    } else {
        thisArg = Object(thisArg)
    }   
    var args = []
    for(var i = 1;i < arguments.length;i ++){
        args.push(&#39;arguments[&#39; + i + &#39;]&#39;)
    }
    thisArg.fn = this
    var res = eval(&#39;thisArg.fn(&#39; + args + &#39;)&#39;)
    delete thisArg.fn
    return res
}
登入後複製

ES6 版本

Function.prototype.myCall = function(thisArg,...args){
    if(typeof this != &#39;function&#39;){
        throw new Error(&#39;The caller must be a function&#39;)
    }
    if(thisArg === undefined || thisArg === null){
        thisArg = globalThis
    } else {
        thisArg = Object(thisArg)
    }
    thisArg.fn = this
    const res = thisArg.fn(...args)
    delete thisArg.fn
    return res
}
登入後複製

透過call 呼叫函數的時候,可以透過傳給call  的thisArg 指定函數中的this。而只要使得函數是透過 thisArg 呼叫的,就能實現這一點,這就是我們的主要目標。

實作要點

  • #最終是透過函數去呼叫myCall 的,所以myCallcall 一樣掛載在函式原型上。同時,也因為是透過函數去呼叫myCall 的,所以在myCall 內部我們可以透過this 拿到myCall的呼叫者,也就是實際執行的那個函數。

  • 照理說,myCall 是掛載在函數原型上,當我們透過一個非函數去呼叫myCall 的時候,肯定會拋出錯誤,那麼為什麼還要在myCall 中檢查呼叫者的類型,並自訂錯誤呢?這是因為,當一個呼叫者obj = {} 是一個對象,但是繼承自Function 的時候(obj.__proto__ = Function.prototype),它作為一個非函數實際上也是可以調用myCall 方法的,這時候如果不進行類型檢查以確保它是個函數,那麼後面直接將它當作函數調用的時候,就會拋出錯誤了

  • 傳給call 的thisArg 如果是null 或undefined,那麼thisArg 實際上會指向全域物件;如果thisArg 是基本型,那麼可以使用Object() 做一個裝箱操作,將其轉換為一個物件- 主要是為了確保後續可以以方法呼叫的方式去執行函數。那麼可不可以寫成 thisArg = thisArg ? Object(thisArg) : globalThis  呢?其實是不可以的,如果 thisArg 是布林值 false,那麼會導致 thisArg 最終等於 globalThis,但實際上它應該等於 Boolean {false}

  • 前面說過,可以在myCall 裡透過this 拿到實際執行的那個函數,所以thisArg.fn = this 相當於將這個函數當作thisArg 的一個方法,後面我們就可以透過thisArg 物件去呼叫這個函數了。

  • thisArg.fn = this 相當於是為 thisArg 增加了一個 fn 屬性,所以在返回執行結果之前要 delete 這個屬性。此外,為了避免覆蓋thisArg 上可能存在的同名屬性fn,這裡也可以使用const fn = Symbol('fn') 建構一個唯一屬性,然後thisArg[fn] = this

  • ES3 版本和ES6 版本主要的區別在於參數的傳遞以及函數的執行上:

    • ES6 因為引入了剩餘參數,所以不管實際執行函數的時候傳入了多少個參數,都可以透過args 數組拿到這些參數,同時因為引入了展開運算符,所以可以展開args 參數數組,把參數一個個傳遞給函數執行

    • 但在ES3 中沒有剩餘參數這個東西,所以在定義myCall 的時候只接收一個thisArg 參數,然後在函數體中透過arguments 類別陣列拿到所有參數。我們需要的是 arguments 中除第一個元素(thisArg)之外的所有元素,怎麼做呢?如果是ES6,直接[...arguments].slice(1)就可以了,但這是ES3,於是我們只能從索引1 開始遍歷arguments,然後push 到一個args 數組中了。而且也要注意的是,這裡 push 進去的是字串形式的參數,這主要是為了方便後續透過 eval 執行函數的時候,將參數一個一個傳遞給函數。

    • 為什麼必須透過 eval 才能執行函數呢?因為我們不知道函數實際上要接收多少個參數,況且也用不了展開運算符,所以只能建構一個可執行的字串表達式,明確地傳入函數的所有參數。

手寫實作 apply

apply 的用法和 call 很類似,因此實作也很類似。要注意的差異是,call 在接受一個thisArg 參數之後還可以接收多個參數(即接受的是參數列表),而apply 在接收一個thisArg 參數之後,通常第二個參數是一個數組或類別數組物件:

fn.call(thisArg,arg1,arg2,...)
fn.apply(thisArg,[arg1,arg2,...])
登入後複製

如果第二个参数传的是 null 或者 undefined,那么相当于是整体只传了 thisArg 参数。

ES3 版本

Function.prototype.myApply = function(thisArg,args){
    if(typeof this != &#39;function&#39;){
        throw new Error(&#39;the caller must be a function&#39;)
    } 
    if(thisArg === null || thisArg === undefined){
        thisArg = globalThis
    } else {
        thisArg = Object(thisArg)
    }
    if(args === null || args === undefined){
        args = []
    } else if(!Array.isArray(args)){
        throw new Error(&#39;CreateListFromArrayLike called on non-object&#39;)
    }
    var _args = []
    for(var i = 0;i < args.length;i ++){
        _args.push(&#39;args[&#39; + i + &#39;]&#39;)
    }
    thisArg.fn = this
    var res = _args.length ? eval(&#39;thisArg.fn(&#39; + _args + &#39;)&#39;):thisArg.fn()
    delete thisArg.fn
    return res
}
登入後複製

ES6 版本

Function.prototype.myApply = function(thisArg,args){
    if(typeof thisArg != &#39;function&#39;){
        throw new Error(&#39;the caller must be a function&#39;)
    } 
    if(thisArg === null || thisArg === undefined){
        thisArg = globalThis
    } else {
        thisArg = Object(thisArg)
    }
    if(args === null || args === undefined){
        args = []
    } 
    // 如果传入的不是数组,仿照 apply 抛出错误
    else if(!Array.isArray(args)){
        throw new Error(&#39;CreateListFromArrayLike called on non-object&#39;)
    }
    thisArg.fn = this
    const res = thisArg.fn(...args)
    delete thisArg.fn
    return res
}
登入後複製

实现要点

基本上和 call 的实现是差不多的,只是我们需要检查第二个参数的类型。

手写实现 bind

bind 也可以像 callapply 那样给函数绑定一个 this,但是有一些不同的要点需要注意:

  • bind 不是指定完 this 之后直接调用原函数,而是基于原函数返回一个内部完成了 this 绑定的新函数
  • 原函数的参数可以分批次传递,第一批可以在调用 bind 的时候作为第二个参数传入,第二批可以在调用新函数的时候传入,这两批参数最终会合并在一起,一次传递给新函数去执行
  • 新函数如果是通过 new 方式调用的,那么函数内部的 this 会指向实例,而不是当初调用 bind 的时候传入的 thisArg。换句话说,这种情况下的 bind 相当于是无效的

ES3 版本

这个版本更接近 MDN 上的 polyfill 版本。

Function.prototype.myBind = function(thisArg){
    if(typeof this != &#39;function&#39;){
        throw new Error(&#39;the caller must be a function&#39;)
    }
    var fnToBind = this
    var args1 = Array.prototype.slice.call(arguments,1)
    var fnBound = function(){
        // 如果是通过 new 调用
        return fnToBind.apply(this instanceof fnBound ? this:thisArg,args1.concat(args2))     
    }
    // 实例继承
    var Fn = function(){}
    Fn.prototype = this.prototype
    fnBound.prototype = new Fn()
    return fnBound
}
登入後複製

ES6 版本

Function.prototype.myBind = function(thisArg,...args1){
    if(typeof this != &#39;function&#39;){
        throw new Error(&#39;the caller must be a function&#39;)
    }
    const fnToBind = this
    return function fnBound(...args2){
        // 如果是通过 new 调用的
        if(this instanceof fnBound){
            return new fnToBind(...args1,...args2)
        } else {
            return fnToBind.apply(thisArg,[...args1,...args2])
        }
    }
}
登入後複製

实现要点

1.bind 实现内部 this 绑定,需要借助于 apply,这里假设我们可以直接使用 apply 方法

2.先看比较简单的 ES6 版本:

1). 参数获取:因为 ES6 可以使用剩余参数,所以很容易就可以获取执行原函数所需要的参数,而且也可以用展开运算符轻松合并数组。

2). 调用方式:前面说过,如果返回的新函数 fnBound 是通过 new 调用的,那么其内部的 this 会是 fnBound 构造函数的实例,而不是当初我们指定的 thisArg,因此 this instanceof fnBound会返回 true,这种情况下,相当于我们指定的 thisArg 是无效的,new 返回的新函数等价于 new 原来的旧函数,即 new fnBound 等价于 new fnToBind,所以我们返回一个 new fnToBind 即可;反之,如果 fnBound 是普通调用,则通过 apply 完成 thisArg 的绑定,再返回最终结果。从这里可以看出,bind 的 this 绑定,本质上是通过 apply 完成的。

3.再来看比较麻烦一点的 ES3 版本:

1). 参数获取:现在我们用不了剩余参数了,所以只能在函数体内部通过 arguments 获取所有参数。对于 myBind,我们实际上需要的是除开第一个传入的 thisArg 参数之外的剩余所有参数构成的数组,所以这里可以通过 Array.prototype.slice.call 借用数组的 slice 方法(arguments 是类数组,无法直接调用 slice),这里的借用有两个目的:一是除去 arguments 中的第一个参数,二是将除去第一个参数之后的 arguments 转化为数组(slice 本身的返回值就是一个数组,这也是类数组转化为数组的一种常用方法)。同样地,返回的新函数 fnBound 后面调用的时候也可能传入参数,再次借用 slice 将 arguments 转化为数组

2). 调用方式:同样,这里也要判断 fnBound 是 new 调用还是普通调用。在 ES6 版本的实现中,如果是 new 调用 fnBound,那么直接返回 new fnToBind(),这实际上是最简单也最容易理解的方式,我们在访问实例属性的时候,天然就是按照 实例 => 实例.__proto__ = fnToBind.prototype 这样的原型链来寻找的,可以确保实例成功访问其构造函数 fnToBInd 的原型上面的属性;但在 ES3 的实现中(或者在网上部分 bind 方法的实现中),我们的做法是返回一个 fnToBind.apply(this),实际上相当于返回一个 undefined 的函数执行结果,根据 new 的原理,我们没有在构造函数中自定义一个返回对象,因此 new 的结果就是返回实例本身,这点是不受影响的。这个返回语句的问题在于,它的作用仅仅只是确保 fnToBind 中的 this 指向 new fnBound 之后返回的实例,而并没有确保这个实例可以访问 fnToBind 的原型上面的属性。实际上,它确实不能访问,因为它的构造函数是 fnBound 而不是 fnToBind,所以我们要想办法在 fnBound 和 fnToBind 之间建立一个原型链关系。这里有几种我们可能会使用的方法:

 // 这里的 this 指的是 fnToBind
 fnBound.prototype = this.prototype
登入後複製

这样只是拷贝了原型引用,如果修改 fnBound.prototype,则会影响到 fnToBind.prototype,所以不能用这种方法

// this 指的是 fnToBind
fnBound.prototype = Object.create(this.prototype)
登入後複製

通过 Object.create 可以创建一个 __proto__ 指向 this.prototype 的实例对象,之后再让 fnBound.prototype 指向这个对象,则可以在 fnToBind 和 fnBound 之间建立原型关系。但由于 Object.create 是 ES6 的方法,所以无法在我们的 ES3 代码中使用。

// this 指的是 fnToBind
const Fn = function(){}
Fn.prototype = this.prototype
fnBound.prototype = new Fn()
登入後複製

这是上面代码采用的方法:通过空构造函数 Fn 在 fnToBind 和 fnBound 之间建立了一个联系。如果要通过实例去访问 fnToBind 的原型上面的属性,可以沿着如下原型链查找:

实例 => 实例.__proto__ = fnBound.prototype = new Fn() => new Fn().__proto__ = Fn.prototype = fnToBind.prototype

更多编程相关知识,请访问:编程教学!!

以上是一文帶你深入了解實現call、apply和bind方法的詳細內容。更多資訊請關注PHP中文網其他相關文章!

本網站聲明
本文內容由網友自願投稿,版權歸原作者所有。本站不承擔相應的法律責任。如發現涉嫌抄襲或侵權的內容,請聯絡admin@php.cn

熱AI工具

Undresser.AI Undress

Undresser.AI Undress

人工智慧驅動的應用程序,用於創建逼真的裸體照片

AI Clothes Remover

AI Clothes Remover

用於從照片中去除衣服的線上人工智慧工具。

Undress AI Tool

Undress AI Tool

免費脫衣圖片

Clothoff.io

Clothoff.io

AI脫衣器

Video Face Swap

Video Face Swap

使用我們完全免費的人工智慧換臉工具,輕鬆在任何影片中換臉!

熱門文章

<🎜>:泡泡膠模擬器無窮大 - 如何獲取和使用皇家鑰匙
3 週前 By 尊渡假赌尊渡假赌尊渡假赌
北端:融合系統,解釋
3 週前 By 尊渡假赌尊渡假赌尊渡假赌
Mandragora:巫婆樹的耳語 - 如何解鎖抓鉤
3 週前 By 尊渡假赌尊渡假赌尊渡假赌

熱工具

記事本++7.3.1

記事本++7.3.1

好用且免費的程式碼編輯器

SublimeText3漢化版

SublimeText3漢化版

中文版,非常好用

禪工作室 13.0.1

禪工作室 13.0.1

強大的PHP整合開發環境

Dreamweaver CS6

Dreamweaver CS6

視覺化網頁開發工具

SublimeText3 Mac版

SublimeText3 Mac版

神級程式碼編輯軟體(SublimeText3)

熱門話題

Java教學
1664
14
CakePHP 教程
1423
52
Laravel 教程
1321
25
PHP教程
1269
29
C# 教程
1249
24
如何使用WebSocket和JavaScript實現線上語音辨識系統 如何使用WebSocket和JavaScript實現線上語音辨識系統 Dec 17, 2023 pm 02:54 PM

如何使用WebSocket和JavaScript實現線上語音辨識系統引言:隨著科技的不斷發展,語音辨識技術已成為了人工智慧領域的重要組成部分。而基於WebSocket和JavaScript實現的線上語音辨識系統,具備了低延遲、即時性和跨平台的特點,成為了廣泛應用的解決方案。本文將介紹如何使用WebSocket和JavaScript來實現線上語音辨識系

WebSocket與JavaScript:實現即時監控系統的關鍵技術 WebSocket與JavaScript:實現即時監控系統的關鍵技術 Dec 17, 2023 pm 05:30 PM

WebSocket與JavaScript:實現即時監控系統的關鍵技術引言:隨著互聯網技術的快速發展,即時監控系統在各個領域中得到了廣泛的應用。而實現即時監控的關鍵技術之一就是WebSocket與JavaScript的結合使用。本文將介紹WebSocket與JavaScript在即時監控系統中的應用,並給出程式碼範例,詳細解釋其實作原理。一、WebSocket技

如何利用JavaScript和WebSocket實現即時線上點餐系統 如何利用JavaScript和WebSocket實現即時線上點餐系統 Dec 17, 2023 pm 12:09 PM

如何利用JavaScript和WebSocket實現即時線上點餐系統介紹:隨著網路的普及和技術的進步,越來越多的餐廳開始提供線上點餐服務。為了實現即時線上點餐系統,我們可以利用JavaScript和WebSocket技術。 WebSocket是一種基於TCP協定的全雙工通訊協議,可實現客戶端與伺服器的即時雙向通訊。在即時線上點餐系統中,當使用者選擇菜餚並下訂單

如何使用WebSocket和JavaScript實現線上預約系統 如何使用WebSocket和JavaScript實現線上預約系統 Dec 17, 2023 am 09:39 AM

如何使用WebSocket和JavaScript實現線上預約系統在當今數位化的時代,越來越多的業務和服務都需要提供線上預約功能。而實現一個高效、即時的線上預約系統是至關重要的。本文將介紹如何使用WebSocket和JavaScript來實作一個線上預約系統,並提供具體的程式碼範例。一、什麼是WebSocketWebSocket是一種在單一TCP連線上進行全雙工

JavaScript與WebSocket:打造高效率的即時天氣預報系統 JavaScript與WebSocket:打造高效率的即時天氣預報系統 Dec 17, 2023 pm 05:13 PM

JavaScript和WebSocket:打造高效的即時天氣預報系統引言:如今,天氣預報的準確性對於日常生活以及決策制定具有重要意義。隨著技術的發展,我們可以透過即時獲取天氣數據來提供更準確可靠的天氣預報。在本文中,我們將學習如何使用JavaScript和WebSocket技術,來建立一個高效的即時天氣預報系統。本文將透過具體的程式碼範例來展示實現的過程。 We

簡易JavaScript教學:取得HTTP狀態碼的方法 簡易JavaScript教學:取得HTTP狀態碼的方法 Jan 05, 2024 pm 06:08 PM

JavaScript教學:如何取得HTTP狀態碼,需要具體程式碼範例前言:在Web開發中,經常會涉及到與伺服器進行資料互動的場景。在與伺服器進行通訊時,我們經常需要取得傳回的HTTP狀態碼來判斷操作是否成功,並根據不同的狀態碼來進行對應的處理。本篇文章將教你如何使用JavaScript來取得HTTP狀態碼,並提供一些實用的程式碼範例。使用XMLHttpRequest

javascript如何使用insertBefore javascript如何使用insertBefore Nov 24, 2023 am 11:56 AM

用法:在JavaScript中,insertBefore()方法用於在DOM樹中插入一個新的節點。這個方法需要兩個參數:要插入的新節點和參考節點(即新節點將要插入的位置的節點)。

JavaScript與WebSocket:打造高效率的即時影像處理系統 JavaScript與WebSocket:打造高效率的即時影像處理系統 Dec 17, 2023 am 08:41 AM

JavaScript是一種廣泛應用於Web開發的程式語言,而WebSocket則是一種用於即時通訊的網路協定。結合二者的強大功能,我們可以打造一個高效率的即時影像處理系統。本文將介紹如何利用JavaScript和WebSocket來實作這個系統,並提供具體的程式碼範例。首先,我們需要明確指出即時影像處理系統的需求和目標。假設我們有一個攝影機設備,可以擷取即時的影像數

See all articles