call、bind的模擬實現

一個 前端知識點彙總

綜合了學習過程當中的知識點,好比this、閉包、BFC、ES6等,若是大佬們以爲還能夠的話,求個star啦!html

call和apply

  • 每一個函數都包含兩個非繼承而來的方法:apply()和call()
  • 用途相同,都是在特定的做用域中調用函數
  • 接收參數方面不一樣,apply接收兩個參數,一個是函數運行時的做用域(this),另外一個是參數數組;call方法第一個參數與apply方法相同,但傳遞給函數的參數必須列舉出來。

call()方法調用一個函數,其具備一個指定的this值和分別提供的參數:前端

fun.call(thisArg, arg1, arg2, ...)

apply()方法調用一個函數,其具備一個指定的this值,以及做爲一個數組(或相似數組的對象)提供的參數:git

fun.apply(thisArg, [argsArray])

舉個栗子???:es6

var one = {
    name: 'one',
    sayName: function(age) {
        console.log(`Hello, I'm ${this.name}, and I'm ${age} years old`)
    }
}
var day = {
    name: 'day'
}
one.sayName.call(day, 20) // Hello, I'm day, and I'm 20 years old
one.sayName.apply(day, [20]) // Hello, I'm day, and I'm 20 years old

fn.call(o)的原理就是先經過o.m = fn將fn做爲o的某個臨時屬性m存儲,而後執行m,執行完畢後將m屬性刪除。數組

大體就是這樣:緩存

day.fn = one.sayName
day.fn()
delete day.fn

因此能夠模擬實現apply和call。閉包

首先來看apply的模擬:app

初版

Function.prototype.applyOne = function() {
    var context = arguments[0]
    var args = arguments[1]
    context.fn = this
    eval(context.fn(args.join(','))) // args.join(',')返回的是string,因此須要進行一下特殊處理)
    delete context.fn
}

one.sayName.applyOne(day, [20]) // Hello, I'm day, and I'm 20 years old

第二版

要注意到的是,若this傳入的是null,或者不傳入,則會默認是全局環境,而且apply是有返回值的。dom

Function.prototype.applyTwo = function() {
    var context = arguments[0] || window
    var args = arguments[1]
    context.fn = this

    if (args == void 0) {
        return context.fn()
    } 
    var result = eval(context.fn(args.join(','))) // args.join(',')返回的是string,因此須要進行一下特殊處理
    delete context.fn
    return result
}

var name = "oneday"

var one = {
    name: 'one',
    sayName: function(age) {
        console.log(`Hello, I'm ${this.name}, and I'm ${age} years old`)
    }
}
var day = {
    name: 'day'
}
one.sayName.applyTwo(null, [20]) // Hello, I'm oneday, and I'm 20 years old

emmmm...有一個問題就是萬一context裏面原本就有fn屬性怎麼辦呢...對於es6而言,能夠將fn設置爲一個獨特的Symbol值,以下:函數

var fn1 = Symbol('aaa')
var fn2 = Symbol('aaa')
fn1 == fn2 // false

可是畢竟symbol是es6的特性啊,因此在es5中可使用產生隨機數的方法,例如:

var fn1 = 'o' + Math.random()
var fn2 = 'o' + Math.random()

接下來就是apply:

Function.prototype.callOne = function() {
    var context = [].shift.applyTwo(arguments)
    var args = [].slice.applyTwo(arguments) // 將剩下的參數做爲數組傳入啊
    return this.applyTwo(context, args)
}

emmmm...第一個參數就是arguments的第一個(通常是this,或者沒有),後面的參數就做爲數組形式傳入。

bind方法

bind方法建立一個新的函數,當被調用時,this值是傳遞給bind的第一個參數,它的參數是bind其餘的參數和其本來的參數,返回的是由指定的this值和初始化參數改造的原函數拷貝。

fun.bind(thisArg[, arg1[, arg2[, ...]]])

實例:

var name = '2333'
function Person(name) {
    this.name = name
    this.sayName = function() {
        setTimeout(function() {
            console.log(`Hello, I'm ${this.name}`)
        }, 1000)
    }
}

var oneday = new Person('1111')
oneday.sayName() // Hello, I'm 2333

可是下面這樣就是1111~

this.sayName = function() {
    setTimeout(function() {
        console.log(`Hello, I'm ${this.name}`)
    }.bind(this), 1000)
}

var oneday = new Person('1111')
oneday.sayName() // Hello, I'm 1111

並且還有偏函數(Partial Functions),在mdn中是這麼說的:

bind()的另外一個最簡單的用法是使一個函數擁有預設的初始參數。這些參數做爲 bind()的第二個參數跟在this後面,以後它們會被插入到目標函數的參數列表的開始位置,傳遞給綁定函數的參數會跟在它們的後面。

emmmm...對呀沒看懂,因而就看例子啊...

function list() {
    return Array.prototype.slice.call(arguments)
}

var list1 = list(1, 2, 3) // [1, 2, 3]

// 因此listFun是擁有預設參數(5, 6)的,做爲預設參數跟在第一個參數this後面
var listFun = list.bind(undefined, 5, 6)

// 後面傳入的參數會跟在預設參數的後面
var list2 = listFun(7) // [5, 6, 7]
var list3 = listFun(8, 9) // [5, 6, 8, 9]

bind的模擬實現:

初版

Function.prototype.bindOne = function() {
    var me = this // 緩存this
    var argArray = Array.prototype.slice.call(arguments, 1)
    return function() {
        return me.apply(arguments[0], argArray)
    }
}

可是上述的沒有實現繼續傳參能夠添加到原參數後的功能...因此有了第二版

第二版

Function.prototype.bindTwo = function() {
    var context = arguments[0]
    var me = this
    var argArray = Array.prototype.slice.call(arguments, 1)
    return function() {
        var innerArgs = Array.prototype.slice.call(arguments)
        var finalArgs = argArray.concat(innerArgs)
        return me.apply(context, finalArgs)
    }
}

bind返回的函數若是做爲構造函數,這個構造函數搭配new關鍵字出現的話,bind綁定的this須要被忽略,可是參數還要繼續傳入。意思就是bind的綁定比new的優先級要低。並且要在函數體內判斷調用bind方法的必定要是一個函數。

複習一下new的做用:

  • 建立一個新對象
  • 新對象繼承了該函數的原型(所以this就指向了這個新對象)
  • 爲這個新對象添加屬性和方法並返回這個新對象
var obj = {}
obj.__proto__ = Base.prototype
Base.call(obj)

第三版

Function.prototype.bindThree = function() {
    if (typeof this !== 'function') {
        throw new TypeError('Function.prototype.bind - what is trying to be bound is not callable')
    }

    // context指要把this綁定到的目標函數
    var context = arguments[0]

    // 這裏的this指向調用bind的函數
    var me = this
    var argArray = Array.prototype.slice.call(arguments, 1)

    var F = function() {}
    F.prototype = this.prototype
    var bound = function() {
        var innerArgs = Array.prototype.slice.call(arguments)
        var finalArgs = argArray.concat(innerArgs)

        // 若是調用bind的函數是F的實例,那麼this就仍是指向調用bind的函數,若是不是F的實例,那麼this就進行改變
        return me.apply(this instanceof F ? this : context, finalArgs)
    }
    bound.prototype = new F()
    return bound
}

參考:

不用call和apply方法模擬實現ES5的bind方法

相關文章
相關標籤/搜索