簡單模擬實現javascript中的call、apply、bind方法


引子

讀完《你不知道的JavaScript--上卷》中關於this的介紹和深入的章節后,對於this的指向我用這篇文章簡單總結了一下。接着我就想着能不能利用this的相關知識,模擬實現一下javascript中比較常用到的call、apply、bind方法呢?

於是就有了本文,廢話不多說全文開始!

隱式丟失

由於模擬實現中有運用到隱式丟失, 所以在這還是先介紹一下。

隱式丟失是一種常見的this綁定問題, 是指: 被隱式綁定的函數會丟失掉綁定的對象, 而最終應用到默認綁定。說人話就是: 本來屬於隱式綁定(obj.xxx this指向obj)的情況最終卻應用默認綁定(this指向全局對象)。

常見的隱式丟失情況1: 引用傳遞

var a = 'window'
function foo() {
    console.log(this.a)
}
var obj = {
    a: 'obj',
    foo: foo
}

obj.foo() // 'obj' 此時 this => obj
var lose = obj.foo
lose()  // 'window' 此時 this => window

常見的隱式丟失情況2: 作為回調函數被傳入

var a = 'window'
function foo() {
    console.log(this.a)
}
var obj = {
    a: 'obj',
    foo: foo
}

function lose(callback) {
    callback()
}

lose(obj.foo)  // 'window' 此時 this => window


// ================   分割線  ===============
var t = 'window'
function bar() {
    console.log(this.t)
}

setTimeout(bar, 1000)   // 'window'

對於這個我總結的認為(不知對錯): 在排除顯式綁定后, 無論怎樣做值傳遞,只要最后是被不帶任何修飾的調用, 那么就會應用到默認綁定

進一步的得到整個實現的關鍵原理: 無論怎么做值傳遞, 最終調用的方式決定了this的指向

硬綁定

直觀的描述硬綁定就是: 一旦給一個函數顯式的指定完this之后無論以后怎么調用它, 它的this的指向將不會再被改變

硬綁定的實現解決了隱式丟失帶來的問題, bind函數的實現利用就是硬綁定的原理

// 解決隱式丟失
var a = 'window'
function foo() {
    console.log(this.a)
}
var obj = {
    a: 'obj',
    foo: foo
}

function lose(callback) {
    callback()
}

lose(obj.foo)   // 'window'

var fixTheProblem = obj.foo.bind(obj)
lose(fixTheProblem) // 'obj'

實現及原理分析

模擬實現call

// 模擬實現call
Function.prototype._call = function ($this, ...parms) {     // ...parms此時是rest運算符, 用於接收所有傳入的實參並返回一個含有這些實參的數組
    /* 
        this將會指向調用_call方法的那個函數對象   this一定會是個函數
        ** 這一步十分關鍵 **  => 然后臨時的將這個對象儲存到我們指定的$this(context)對象中
    */
    $this['caller'] = this
    //$this['caller'](...parms)

    // 這種寫法會比上面那種寫法清晰
    $this.caller(...parms) // ...parms此時是spread運算符, 用於將數組中的元素解構出來給caller函數傳入實參
    /* 
        為了更清楚, 采用下面更明確的寫法而不是注釋掉的
            1. $this.caller是我們要改變this指向的原函數
            2. 但是由於它現在是$this.caller調用, 應用的是隱式綁定的規則
            3. 所以this成功指向$this
    */
    delete $this['caller']  // 這是一個臨時屬性不能破壞人為綁定對象的原有結構, 所以用完之后需要刪掉
}

模擬實現apply

// 模擬實現apply  ** 與_call的實現幾乎一致, 主要差別只在傳參的方法/類型上 **
Function.prototype._apply = function ($this, parmsArr) {    // 根據原版apply  第二個參數傳入的是一個數組
    $this['caller'] = this
    $this['caller'](...parmsArr) // ...parmsArr此時是spread運算符, 用於將數組中的元素解構出來給caller函數傳入實參
    delete $this['caller']
}

既然_call與_apply之前的相似度(耦合度)這么高, 那我們可以進一步對它們(的相同代碼)進行抽離

function interface4CallAndApply(caller, $this, parmsOrParmArr) {
    $this['caller'] = caller
    $this['caller'](...parmsOrParmArr)
    delete $this['caller']
}


Function.prototype._call = function ($this, ...parms) {
    var funcCaller = this
    interface4CallAndApply(funcCaller, $this, parms)
}


Function.prototype._apply = function ($this, parmsArr) {
    var funcCaller = this
    interface4CallAndApply(funcCaller, $this, parmsArr)
}

一個我認為能夠較好展示_call 和 _apply實現原理的例子

var myName = 'window'
var obj = {
    myName: 'Fitz',
    sayName() {
        console.log(this.myName)
    }
}

var foo = obj.sayName

var bar = {
    myName: 'bar',
    foo
}

bar.foo()

模擬實現bind

// 使用硬綁定原理模擬實現bind
Function.prototype._bind = function ($this, ...parms) {
    $bindCaller = this  // 保存調用_bind函數的對象   注意: 該對象是個函數
    // 根據原生bind函數的返回值: 是一個函數
    return function () { // 用rest運算符替代arguments去收集傳入的實參
        return $bindCaller._apply($this, parms)
    }
}

一個能夠展現硬綁定原理的例子

function hardBind(fn) {
    var caller = this
    var parms = [].slice.call(arguments, 1)
    return function bound() {
        parms = [...parms, ...arguments]
        fn.apply(caller, parms) // apply可以接受一個偽數組而不必一定是數組
    }
}


var myName = 'window'
function foo() {
    console.log(this.myName)
}
var obj = {
    myName: 'obj',
    foo: foo,
    hardBind: hardBind
}

// 正常情況下
foo()   // 'window'
obj.foo()   // 'obj'

var hb = hardBind(foo)
// 可以看到一旦硬綁定后無論最終怎么調用都不能改變this指向
hb()    // 'window'
obj.hb = hb // 給obj添加該方法用於測試
obj.hb()    // 'window'

// 在加深一下印象
var hb2 = obj.hardBind(foo)
hb2()   // 'obj'    // 這里調用this本該指向window

總體實現(純凈版/沒有注釋)

function interface4CallAndApply(caller, $this, parmsOrParmArr) {
    $this['caller'] = caller
    $this['caller'](...parmsOrParmArr)
    delete $this['caller']
}


Function.prototype._call = function ($this, ...parms) {
    var funcCaller = this
    interface4CallAndApply(funcCaller, $this, parms)
}


Function.prototype._apply = function ($this, parmsArr) {
    var funcCaller = this
    interface4CallAndApply(funcCaller, $this, parmsArr)
}


Function.prototype._bind = function ($this, ...parms) {
    $bindCaller = this
    return function () {
        return $bindCaller._apply($this, parms)
    }
}



// ============ 測試 ===============
var foo = {
    name: 'foo',
    sayHello: function (a, b) {
        console.log(`hello, get the parms => ${a} and ${b}`)
    }
}

var bar = {
    name: 'bar'
}

foo.sayHello._call(bar, 'Fitz', 'smart')
foo.sayHello._apply(bar, ['Fitz', 'smart'])

var baz = foo.sayHello._bind(bar, 'Fitz', 'smart')
baz()

var testHardBind = foo.sayHello._bind(bar, 'hard', 'bind')
testHardBind._call(Object.create(null))   // hello, get the parms => hard and bind 測試_bind的硬綁定

寫在最后

我只是一個正在學習前端的小白,有不對的地方請各位多多指正

如果感覺對您有啟發或者幫助,也煩請您留言或給我個關注, 謝謝啦!


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM