理解並實現call、apply、bind

做用

改變函數執行時的上下文(改變函數運行時的this指向)javascript

引子

// 寫一個構造函數類
function Person(name){
  this.name = name;
}
Person.prototype = {
  constructor: Person,
  showName: function(){
    console.log(this.name);
  }
}
// 用該類new出的新對象擁有此類的屬性和方法
var person = new Person('wuqinhao');
person.showName();
複製代碼

而後咱們遇到個小需求,有一個對象不是基於此類new出,但想要showName方法,改如何處理呢java

// 這個對象只有一個name屬性,此時它想擁有一個showName方法
var animal = {
  name: 'cat'
}
// 雖然它能夠再寫一遍showName方法,可從代碼設計角度來講不太優雅,可重用的方法寫了兩遍。
// 因而創始者發明了call、apply、bind三個方法來解決此問題

// 1 call
person.showName.call(animal);
// 2 apply
person.showName.apply(animal);
// 3 bind
person.showName.bind(animal)();
複製代碼

call的定義

apply的定義

咱們發現apply和call差很少,差異是一個傳數組,一個傳多個參數。這是創始者爲了開發者在不一樣的語境方便調用不一樣的方法。(就好比你設計一個類庫時可能也會暴露兩個不一樣的API,方便開發者調用)git

bind的定義

call的應用

// 1.使用 call 方法調用父構造函數
// 通常用call,能夠明確的看到傳遞的參數
function Product(name, price) {
  this.name = name;
  this.price = price;
}

function Food(name, price) {
  Product.call(this, name, price);
  this.category = 'food';
}

function Toy(name, price) {
  Product.call(this, name, price);
  this.category = 'toy';
}

var cheese = new Food('feta', 5);
var fun = new Toy('robot', 40);
console.log(cheese);
console.log(fun);
複製代碼

apply的應用

// 1.數組合並
// 通常用apply,傳遞數組方便
var arr1 = [1, 2, 3];
var arr2 = [4, 5, 6];
[].push.apply(arr1, arr2);
console.log(arr1);
console.log(arr2);
// arr1 [1, 2, 3, 4, 5, 6]
// arr2 [4,5,6]
複製代碼
// 2.調用封裝好的內置函數
/* 找出數組中最大/小的數字 */
var numbers = [5, 6, 2, 3, 7];

/* 應用(apply) Math.min/Math.max 內置函數完成 */
var max = Math.max.apply(null, numbers); /* 基本等同於 Math.max(numbers[0], ...) 或 Math.max(5, 6, ..) */
var min = Math.min.apply(null, numbers);

// 若是咱們不用apply,那隻能用通常的for循環逐一查找
var max = -Infinity;
var min = +Infinity;
for (var i = 0; i < numbers.length; i++) {
    if (numbers[i] > max) { max = numbers[i]; }
    if (numbers[i] < min) { min = numbers[i]; }
}
複製代碼

bind的應用

// 1.配合 setTimeout 綁定this到當前實例,而不是window
function Person(name, age){
  this.name = name;
  this.age = age
}
Person.prototype = {
  constructor: Person,
  showName: function(){
    console.log(this.name);
  },
  showAge: function(){
    setTimeout(function () {
        console.log(this.age)
    }.bind(this), 1000)
    
    // setTimeout(function () {
    // console.log(this.age)
    // }, 1000)
    // 若是不bind,這裏的setTimeOut是window下面的方法,因此this指向會指到window,而window下沒有age,因此會輸出undefined
    
    // setTimeout( () => {
    // console.log(this.age)
    // }, 1000)
    // 因爲es6裏出現的箭頭函數,他能將this指向當前調用的實例上,因此用此方法也是可行的
  }
}
// 用該類new出的新對象擁有此類的屬性和方法
var person = new Person('wuqinhao', 26);
person.showAge();
// 一秒中後打印 26
複製代碼
// 2.偏函數(使一個函數擁有預設的初始參數)
function addArguments(arg1, arg2) {
    return arg1 + arg2
}
var result1 = addArguments(1, 2); // 3

// 建立一個函數,它擁有預設的第一個參數
var addThirtySeven = addArguments.bind(null, 37); 
var result2 = addThirtySeven(5); 
// 37 + 5 = 42 
var result3 = addThirtySeven(5, 10);
// 37 + 5 = 42 ,第二個參數被忽略

/* 只要將這些參數(若是有的話)做爲bind()的參數寫在this後面。 當綁定函數被調用時,這些參數會被插入到目標函數的參數列表的開始位置, 傳遞給綁定函數的參數會跟在它們後面。*/
複製代碼

call模擬實現

1.模擬實現基本語法

咱們看前面的引子示例,animal想執行showName方法,但又不想從新寫一遍就調用了call方法。若是咱們寫一遍showName方法,執行完,而後再刪除這個方法,而把這個過程封裝成一個函數,這個函數不就有點像call嘛。es6

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

  • 1.將函數設爲對象的屬性
  • 2.執行該函數
  • 3.刪除該函數
// 初版
Function.prototype.call2 = function(context) {
    // 首先要獲取調用call的函數,用this能夠獲取
    context.fn = this;
    context.fn();
    // fn這個名字能夠隨便取,由於後面會delete掉
    delete context.fn;
}

// 測試一下(此代碼的前提是要有引子裏的代碼)
person.showName.call2(animal); // cat
複製代碼

2.模擬實現傳遞參數

注意:傳入的參數並不肯定,咱們能夠從 Arguments 對象中取值,取出第二個到最後一個參數,而後放到一個數組裏。數組

// 第二版
Function.prototype.call2 = function(context) {
    context.fn = this;
    var args = [];
    for(var i = 1, len = arguments.length; i < len; i++) {
        args.push('arguments[' + i + ']');
    }
    console.log(args); // ["arguments[1]", "arguments[2]"]
    eval('context.fn(' + args +')'); // 至關於eval('context.fn(arguments[1],arguments[2])')
    // args會自動調用 Array.toString()
    delete context.fn;
}

// 測試一下
// 1.使用 call 方法調用父構造函數
// 通常用call,能夠明確的看到傳遞的參數
function Product(name, price) {
  this.name = name;
  this.price = price;
}

function Food(name, price) {
  Product.call2(this, name, price);
  this.category = 'food';
}

var cheese = new Food('feta', 5);
console.log(cheese);
複製代碼

eval語法請看 developer.mozilla.org/zh-CN/docs/… Arguments語法請看 developer.mozilla.org/zh-CN/docs/…app

3.模擬實現this綁定null和函數返回值

1.this 參數能夠傳 null,當爲 null 的時候,視爲指向 window(當不綁定this時,能夠指向window)函數

2.函數是能夠有返回值(retrun 出eval的結果便可)測試

// 第三版
Function.prototype.call2 = function(context) {
    context = context || window;
    context.fn = this;
    var args = [];
    for(var i = 1, len = arguments.length; i < len; i++) {
        args.push('arguments[' + i + ']');
    }
    console.log(args); // ["arguments[1]", "arguments[2]"]
    var result = eval('context.fn(' + args +')'); // 至關於eval('context.fn(arguments[1],arguments[2])')
    // args會自動調用 Array.toString()
    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.call2(null); // 2

console.log(bar.call2(obj, 'kevin', 18));
// 1
// Object {
// value: 1,
// name: 'kevin',
// age: 18
// }
複製代碼

apply模擬實現

apply的實現與call類型,區別在與apply傳入數組。因此arguments獲取改爲arr數組。優化

Function.prototype.apply2 = function(context, arr) {
    context = context || window;
    context.fn = this;
    
    var result;
    if (!arr) {
        result = context.fn()
    } else {
        var args = [];
        for(var i = 1, len = arr.length; i < len; i++) {
            args.push('arr[' + i + ']');
        }
        console.log(args); // ["arr[1]", "arr[2]"]
        result = eval('context.fn(' + args +')'); // 至關於eval('context.fn(arr[1],arr[2])')
        // args會自動調用 Array.toString()
    }
    delete context.fn;
    return result;
}
複製代碼

bind模擬實現

1.返回一個函數。2.能夠傳入參數。3.一個綁定函數也能使用new操做符建立對象。

1.模擬返回函數

Function.prototype.bind2 = function (context) {
    var self = this;
    return function () {
    // 應用apply來指定this的指向
        self.apply(context);
    }
}
複製代碼

有時綁定的函數可能也會有返回值,因此綁定的函數return了一個內容,若是再也不加個return,內容是返回不出來的。

Function.prototype.bind2 = function (context) {
    var self = this;
    return function () {
    // 應用apply來指定this的指向
        return self.apply(context); // 再加一個return,將綁定函數的返回值return出來。
    }
}
複製代碼

2.模擬傳參

先看一個例子(傳參還能在返回的函數裏繼續傳)

var foo = {
    value: 1
};

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

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

解決辦法:仍是使用arguments,只不過在最後將參數合併到一塊兒再使用

Function.prototype.bind2 = function (context) {
    var self = this;
    
    // 獲取bind2函數從第二個參數到最後一個參數
    var args = Array.prototype.slice.call(arguments, 1);
    
    return function () {
        // 這個時候的arguments是指bind返回的函數傳入的參數
        var bindArgs = Array.prototype.slice.call(arguments);
        
        // 應用apply來指定this的指向
        return self.apply(context, args.concat(bindArgs)); // 再加一個return,將綁定函數的返回值return出來。
    }
}
複製代碼

3.模擬bind返回的函數做爲構造函數調用

看個例子

var value = 2;

var foo = {
    value: 1
};

function bar (name, age) {
    this.a = 'aaa';
    console.log(this.value);
    console.log(name);
    console.log(age);
}
bar.prototype.b = 'bbb';

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

var obj = new bindFoo('18');
// undefined
// daisy
// 18
console.log(obj.a);
console.log(obj.b);
// shopping
// kevin
複製代碼

咱們看到現象:當 bind 返回的函數做爲構造函數的時候,bind 時指定的 this 值會失效,但傳入的參數依然生效。this指向了obj,然而obj上沒有value屬性,因此是undefined

實現

Function.prototype.bind2 = function (context) {
    var self = this;
    
    // 獲取bind2函數從第二個參數到最後一個參數
    var args = Array.prototype.slice.call(arguments, 1);
    
    var fBound = function () {
        var bindArgs = Array.prototype.slice.call(arguments);
        
        // 看成爲構造函數時,this指向實例,此時結果爲true,將綁定函數的this指向該實例,可讓實例得到來自綁定函數的值
        return self.apply(this instanceof fBound ? this : context, args.concat(bindArgs));
        // 看成爲普通函數時,this指向window,此時結果未false,將綁定函數的this指向context
        // 以上代碼若是改爲`this instanceof fBound ? null : context`實例只是一個空對象,將null改成this,實例會具備綁定函數的屬性
    }
    fBound.prototype = this.prototype;
    
    return fBound;
}
複製代碼

優化一波

1.在這個寫法中,咱們直接將 fBound.prototype = this.prototype,咱們直接修改 fBound.prototype 的時候,也會直接修改綁定函數的 prototype。這個時候,咱們能夠經過一個空函數來進行中轉 2.當調用bind的不是函數時,咱們要拋出異常

Function.prototype.bind2 = function (context) {
    if (typeof this !== 'function') {
        throw new Error('Function.prototype.bind - 試圖綁定的內容不可調用');
    }
    
    var self = this;
    
    // 獲取bind2函數從第二個參數到最後一個參數
    var args = Array.prototype.slice.call(arguments, 1);
    
    var f = function () {};
    
    var fBound = function () {
        var bindArgs = Array.prototype.slice.call(arguments);
        
        // 看成爲構造函數時,this指向實例,此時結果爲true,將綁定函數的this指向該實例,可讓實例得到來自綁定函數的值
        return self.apply(this instanceof f ? this : context, args.concat(bindArgs));
        // 看成爲普通函數時,this指向window,此時結果未false,將綁定函數的this指向context
        // 以上代碼若是改爲`this instanceof f ? null : context`實例只是一個空對象,將null改成this,實例會具備綁定函數的屬性
    }
    f.prototype = this.prototype;
    fBound.prototype = new f();
    
    return fBound;
}
複製代碼

參考至 github.com/mqyqingfeng…

相關文章
相關標籤/搜索