call、apply和bind的實現

call方法

基礎版, 只能修改指向,不能傳參

Function.prototype.myCall = function(context) {
    // 獲取調用者,這裏爲bar
    context.fn = this;
    // 運行函數
    context.fn();
    // 刪除緩存
    delete context.fn;
}

let foo = {
    value: 1
}

function bar() {
    console.log(this.value);
}

bar.myCall(foo);
// 1

eval版本

Function.prototype.myCall = function(context) {
    // 獲取調用者,這裏爲sayHi
    // 將無調用者的狀況轉換爲有調用者的狀況
    // 有調用者那麼函數內部的this就指向調用者
    context.fn = this;
    var len = arguments.length;
    // 保存傳遞的參數
    var args = Array(len - 1);
    for (var i = 1; i < len; i++) {
        // 這裏只是把獲取參數的字符串拼接了,供eval調用
        args[i - 1] = 'arguments[' + i + ']';
    }
    // 不直接調用而用eval是由於參數個數不必定
    // 在這裏至關因而在執行context.fn(arguments[1],arguments[2]);
    eval('context.fn('+ args +')');
    delete context.fn;
}

let person = {
    name: 'Wango'
}

function sayHi(age, sex) {
    console.log(this.name, age, sex)
}

sayHi.myCall(person, 24, 'male');
// Wango 24 male

ES6 版本

Function.prototype.myCall = function(context) {
    context.fn = this;
    // 將arguments轉換爲數組並調用slice方法去除第一個參數
    // 再使用...將數組打散
    context.fn(...Array.from(arguments).slice(1));
    delete context.fn;
}

let person = {
    name: 'Wango'
}

function sayHi(age, sex) {
    console.log(this.name, age, sex);
}

sayHi.myCall(person, 24, 'male');
// Wango 24 male

ES6 升級版

Function.prototype.myCall = function(context, ...args) {
    context.fn = this;
    // 相比使用arguments,這裏只是用了ES6的剩餘參數
    context.fn(...args);
    delete context.fn;
}

let person = {
    name: 'Wango'
}

function sayHi(age, sex) {
    console.log(this.name, age, sex);
}

sayHi.myCall(person, 24, 'male');
// Wango 24 male

最終版

以前的版本若是傳入的對象本來的fn屬性或方法會被覆蓋,而後被刪除;並且傳入第一個參數若是不是對象,會報錯,因此還須要一些容錯處理javascript

// 保存一個全局變量做爲默認值
const root = this;

Function.prototype.myCall = function(context, ...args) {
    if (typeof context === 'object') {
        // 若是參數是null,使用全局變量
        context = context || root;
    } else {
        // 參數不是對象的建立一個空對象
        context = Object.create(null);
    }
    // 使用Symbol建立惟一值做爲函數名
    let fn = Symbol();
    context[fn] = this;
    context[fn](...args);
    delete context[fn];
}

let person = {
    name: 'Wango',
    fn: function() {
        console.log(this.name);
    }
}

function sayHi(age, sex) {
    console.log(this.name, age, sex);
}

sayHi.myCall(person, 24, 'male');
// Wango 24 male
sayHi.myCall(null, 24, 'male');
// undefined 24 male
sayHi.myCall(123, 24, 'male');
// undefined 24 male
// 原函數不受影響
person.fn();
// Wango

call的實現最核心的部分就是將沒有調用者的狀況轉換爲有調用者的狀況,函數內部的this天然就指向調用者java

apply方法

apply的實現思路和call方法是同樣的,只是只接收兩個參數,第二個參數爲類數組git

const root = this;

Function.prototype.myApply = function(context, arg) {
    if (typeof context === 'object') {
        context = context || root;
    } else {
        context = Object.create(null);
    }

    const fn = Symbol();
    context[fn] = this;
    context[fn](...arg);
    delete context[fn];
}

let person = {
    name: 'Wango',
    fn: function() {
        console.log(this.name);
    }
}

function sayHi(age, sex) {
    console.log(this.name, age, sex);
}

sayHi.myApply(person, [24, 'male']);
// Wango 24 male

bind方法

// bind 改變this指向可是不當即執行函數,而是返回一個綁定了this的函數

// bind方法在綁定this指向的同時也能夠傳遞參數
Function.prototype.myBind = function(context, ...innerArgs) {
    // 不須要對參數類型進行判斷,後邊調用call方法時會進行處理
    const fn = this;
    // 不執行函數,而是返回一個函數等待調用
    return function(...finalArgs) {
        // 經過已經實現的call方法實現對this指向的改變,並傳入參數執行
        return fn.call(context, ...innerArgs, ...finalArgs);
    }
}

const person = {
    name: 'Wango'
}

function sayHi(age, sex) {
    console.log(this.name, age, sex);
}

const personSayHi_1 = sayHi.myBind(person, 24, 'male');
personSayHi_1();
// Wango 24 male
const personSayHi_2 = sayHi.myBind(person);
personSayHi_2(24, 'male');
// Wango 24 male

bind方法的核心在於在返回的函數中調用call方法es6

參考網址:call、apply和bind的實現github

相關文章
相關標籤/搜索