JavaScript基礎專題之手動實現call、apply、bind(六)

實現本身的call

MDN 定義:數組

call() 提供新的 this 值給當前調用的函數/方法。你可使用 call 來實現繼承:寫一個方法,而後讓另一個新的對象來繼承它(而不是在新對象中再寫一次這個方法)。瀏覽器

簡答的歸納就是:bash

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

舉個例子:app

var foo = {
    value: 1
};

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

bar.call(foo); // 1
複製代碼

簡單的解析一下call都作了什麼:函數

第一步:call 改變了 this 的指向,指向到 foopost

第二步:bar 函數執行測試

函數經過 call 調用後,結構就以下面代碼:ui

var foo = {
    value: 1,
    bar: function() {
        console.log(this.value)
    }
};

foo.bar(); // 1
複製代碼

這樣this 就指向了 foo,可是咱們給foo添加了一個屬性,這並不可取。因此咱們還要執行一步刪除的動做。this

因此咱們模擬的步驟能夠分爲:

第一步:將函數設爲傳入對象的屬性

第二步:執行該函數

第三部:刪除該函數

以上個例子爲例,就是:

// 第一步
foo.fn = bar
// 第二步
foo.fn()
// 第三步
delete foo.fn
複製代碼

注意:fn 是對象的臨時屬性,由於執行事後要刪除滴。

根據這個思路,咱們能夠嘗試着去寫一個call

Function.prototype._call = function(context) {
    // 首先要獲取調用call的函數,用this能夠獲取
    context.fn = this;
    context.fn();
    delete context.fn;
}

// 測試一下
var foo = {
    value: 1
};

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

bar._call(foo); // 1
複製代碼

OK,咱們能夠在控制檯看到結果了,和預想的同樣。

這樣只是將第一個參數做爲上下文進行執行,可是並沒用傳入參數,下面咱們嘗試傳入參數執行。

舉個例子:

var foo = {
    value: 1
};

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

bar.call(foo, 'chris', 10);
// chris
// 10
// 1
複製代碼

咱們會發現參數並不固定,因此要在 Arguments 對象的第二個參數截取,傳入到數組中。

好比這樣:

// 以上個例子爲例,此時的arguments爲:
// arguments = {
//      0: foo,
//      1: 'kevin',
//      2: 18,
//      length: 3
// }
// 由於arguments是類數組對象,因此能夠用for循環
var args = [];
vae len = arguments.length
for(var i = 1,  i < len; i++) {
    args.push('arguments[' + i + ']');
}

// 執行後 args爲 ["arguments[1]", "arguments[2]", "arguments[3]"]
複製代碼

OK,看到這樣操做第一反應會想到 ES6 的方法,不過 call 是 ES3 的方法,因此就麻煩一點吧。因此咱們此次用 eval 方法拼成一個函數,相似於這樣:

eval('context.fn(' + args +')')
複製代碼

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

代碼以下:

Function.prototype._call = 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._call(foo, 'chris', 10); 
// chris
// 10
// 1
複製代碼

OK,這樣咱們實現了 80% call的功能。

再看看定義:

根據 MDN 對 call 語法的定義:

第一個參數:

fun 函數運行時指定的 this 值*。*須要注意的是,指定的 this 值並不必定是該函數執行時真正的 this 值,若是這個函數在非嚴格模式下運行,則指定爲 nullundefinedthis 值會自動指向全局對象(瀏覽器中就是 window 對象),同時值爲原始值(數字,字符串,布爾值)的 this 會指向該原始值的自動包裝對象。

執行參數:

使用調用者提供的 this 值和參數調用該函數的返回值。若該方法沒有返回值,則返回 undefined

因此咱們還須要注意兩個點

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
    }
}

bar.call(obj, 'chris', 10)
// Object {
//    value: 1,
//    name: 'chris',
//    age: 10
// }
複製代碼

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

Function.prototype._call = function (context = window) {
    var context = context;
    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._call(obj, 'kevin', 18));
// 1
// Object {
//    value: 1,
//    name: 'kevin',
//    age: 18
// }
複製代碼

這樣咱們就成功的完成了一個call函數。

實現本身的apply

apply 的實現跟 call 相似,只是後面傳的參數是一個數組或者類數組對象。

Function.prototype.apply = function (context = window, arr) {
    var context = context;
    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;
}
複製代碼

實現本身的bind

根據 MDN 定義:

bind() 方法會建立一個新函數。當這個新函數被調用時,bind() 的第一個參數將做爲它運行時的 this,以後的一序列參數將會在傳遞的實參前傳入做爲它的參數。

由此咱們能夠首先得出 bind 函數的三個特色:

  1. 改變this指向
  2. 返回一個函數
  3. 能夠傳入參數
var foo = {
    value: 1
};

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

var bindFoo = bar.bind(foo); // 返回了一個函數

bindFoo(); // 1
複製代碼

關於指定 this 的指向,咱們可使用 call 或者 apply 實現。

Function.prototype._bind = function (context) {
    var self = this;
    return function () {
        return self.apply(context);
    }
}
複製代碼

之因此是 return self.apply(context) ,是考慮到綁定函數多是有返回值的,依然是這個例子:

var foo = {
    value: 1
};

function bar() {
	return this.value;
}

var bindFoo = bar.bind(foo);

console.log(bindFoo()); // 1
複製代碼

第三點,能夠傳入參數。這個很困惑是在 bind 時傳參仍是在 bind 以後傳參。

var foo = {
    value: 1
};

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

var bindFoo = bar.bind(foo, 'chris');
bindFoo('18');
// 1
// chris
// 18
複製代碼

經過實例,咱們發現二者參數是能夠累加的,就是第一次 bind 時傳的參數和能夠在調用的時候傳入。

因此咱們仍是用 arguments 進行處理:

Function.prototype._bind = function (context) {
    var self = this;
    // 獲取_bind函數從第二個參數到最後一個參數
    var args = Array.prototype.slice.call(arguments, 1);

    return function () {
        // 這個時候的arguments是指bind返回的函數傳入的參數
        var bindArgs = Array.prototype.slice.call(arguments);
        return self.apply(context, args.concat(bindArgs));
    }
}
複製代碼

完成了上面三步,其實咱們還有一個問題沒有解決。

根據 MDN 定義:

一個綁定函數也能使用new操做符建立對象:這種行爲就像把原函數當成構造器。提供的 this 值被忽略,同時調用時的參數被提供給模擬函數。

舉個例子:

var value = 2;

var foo = {
    value: 1
};

function bar(name, age) {
    this.habit = 'shopping';
    console.log(this.value);
    console.log(name);
    console.log(age);
}

bar.prototype.friend = 'james';

var bindFoo = bar.bind(foo, 'chris');

var obj = new bindFoo('18');
// undefined
// chris
// 18
console.log(obj.habit);
console.log(obj.friend);
// shopping
// james
複製代碼

儘管在全局和 foo 中都聲明瞭 value 值,仍是返回了 undefind,說明this已經失效了,若是你們瞭解 new 的實現,就會知道this是指向 obj 的。

因此咱們能夠經過修改返回的函數的原型來實現,讓咱們寫一下:

Function.prototype.bind2 = function (context) {
    var self = this;
    var args = Array.prototype.slice.call(arguments, 1);

    var fBound = function () {
        var bindArgs = Array.prototype.slice.call(arguments);
        // 看成爲構造函數時,this 指向實例,此時結果爲 true,將綁定函數的 this 指向該實例,可讓實例得到來自綁定函數的值
        // 以上面的是 demo 爲例,若是改爲 `this instanceof fBound ? null : context`,實例只是一個空對象,將 null 改爲 this ,實例會具備 habit 屬性
        // 看成爲普通函數時,this 指向 window,此時結果爲 false,將綁定函數的 this 指向 context
        return self.apply(this instanceof fBound ? this : context, args.concat(bindArgs));
    }
    // 修改返回函數的 prototype 爲綁定函數的 prototype,實例就能夠繼承綁定函數的原型中的值
    fBound.prototype = this.prototype;
    return fBound;
}
複製代碼

可是在這個寫法中,咱們直接將 fBound.prototype = this.prototype,咱們直接修改 fBound.prototype 的時候,也會直接修改綁定函數的 prototype。這個時候,咱們能夠須要一個空函數來進行中轉:

Function.prototype._bind = function (context) {

    var self = this;
    var args = Array.prototype.slice.call(arguments, 1);

    var fNOP = function () {};

    var fBound = function () {
        var bindArgs = Array.prototype.slice.call(arguments);
        return self.apply(this instanceof fNOP ? this : context, args.concat(bindArgs));
    }

    fNOP.prototype = this.prototype;
    fBound.prototype = new fNOP();
    return fBound;
}
複製代碼

還存在一些問題:

1.調用 bind 的不是函數咋辦?

作一個類型判斷唄

if (typeof this !== "function") {
  throw new Error("Function.prototype.bind - what is trying to be bound is not callable");
}
複製代碼

2.我要在線上用

作一下兼容性測試

Function.prototype.bind = Function.prototype.bind || function () {
    ……
};
複製代碼

好了,這樣就咱們就完成了一個 bind

Function.prototype._bind = function (context) {

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

    var self = this;
    var args = Array.prototype.slice.call(arguments, 1);

    var fNOP = function () {};

    var fBound = function () {
        var bindArgs = Array.prototype.slice.call(arguments);
        return self.apply(this instanceof fNOP ? this : context, args.concat(bindArgs));
    }

    fNOP.prototype = this.prototype;
    fBound.prototype = new fNOP();
    return fBound;
}
複製代碼

補充

eval根據 MDN 定義:表示JavaScript表達式,語句或一系列語句的字符串。表達式能夠包含變量以及已存在對象的屬性。

一個簡單的例子:

var x = 2;
var y = 39;
function add(x,y){
	return x + y
}
eval('add('+ ['x','y'] + ')')//等於add(x,y)
複製代碼

也就說eavl調用函數後,字符串會被解析出變量,達到去掉字符串調用變量的目的。

JavaScript基礎系列目錄

JavaScript基礎專題之原型與原型鏈(一)

JavaScript基礎專題之執行上下文和執行棧(二)

JavaScript基礎專題之深刻執行上下文(三)

JavaScript基礎專題之閉包(四)

JavaScript基礎專題之參數傳遞(五)

新手寫做,若是有錯誤或者不嚴謹的地方,請大夥給予指正。若是這片文章對你有所幫助或者有所啓發,還請給一個贊,鼓勵一下做者,在此謝過。

相關文章
相關標籤/搜索