學習Javascript之模擬實現bind

前言

本文1703字,閱讀大約須要5分鐘。

總括: 本文模擬實現了bind方法的更改this,傳參和綁定函數做爲構造函數調用時this失效的特性。前端

  • 參考文檔:Function.prototype.bind()
  • 公衆號:「前端進階學習」,回覆「666」,獲取一攬子前端技術書籍

願每次回憶,對生活都不感到負疚。數組

正文

bindcallapply的做用相似,都是用來更改函數的this值的,不一樣的是,callapply會直接把函數執行,但bind會返回一個函數,咱們稱之爲綁定函數:閉包

function foo(b = 0) {
    console.log(this.a + b);
}
var obj1  = {
  a: 1
};
foo.call(obj1, 1); // 2
foo.apply(obj1, [1]); // 2
var bar = foo.bind(obj1, 1);
bar(); // 2

看下bind()函數最重要的兩個特性:app

  1. 更改this;
  2. 傳參;

更改this&傳參

更改this咱們能夠藉助以前模擬實現過的call和apply的方式來實現,傳參就必要咱們藉助閉包來實現了,咱們看下咱們實現的初版代碼函數

Function.prototype.bind2 = function(context) {
  var _this = this;
  return function() {
        context.func = _this;
    context.func();
    delete context.func;
  }
}

傳參須要將外層函數(bind裏面的參數)和傳到綁定函數中的參數所有拼接到一塊兒,這就須要藉助閉包來實現,更改this咱們能夠直接使用apply來實現,將參數放到一個數組中傳到綁定函數中,咱們的第二版代碼學習

Function.prototype.bind2 = function(context) {
  // 保存上層函數this值
  var _this = this;
  // 保存上層函數的參數
  var args = [].slice.call(arguments, 1);
  return function() {
    // 將參數拼接
        var _args = args.concat([].slice.call(arguments));
    // 利用apply更改this,並把拼接的參數傳到函數中
    _this.apply(context, _args);
  }
}

如今咱們再來測試下:測試

function foo(b = 0) {
    console.log(this.a + b);
}
var obj1  = {
  a: 1
};
// 咱們成爲綁定函數
var bar1 = foo.bind2(obj1, 1);
bar1(); // 2
var bar2 = foo.bind2(obj1);
bar2(); // 1

兩個特性成功實現,完美。 而後重頭戲在下面:this

this失效

目前更改this和傳遞參數兩個特性已經實現,若是截止到這就結束了,就不會單獨爲模擬實現bind()寫一篇博客了,bind還有一個特性,即當綁定函數做爲構造函數使用的時候裏面的this就會失效。例子:spa

function Animal(name) {
  this.name = name;
}
var obj = {
    name: 'test'
};
var cat = new Animal('Tom');
var Animal2 = Animal.bind(obj);
var cat2 = new Animal2('Tom');
console.log(cat); // {name: "Tom"}
console.log(cat2); // {name: "Tom"}
console.log(obj); // {name: "test"}

咱們解釋下上面的代碼,咱們首先使用構造函數Animal實例化了一個cat對象,cat對象的內容如上打印,而後咱們聲明瞭一個Animal2來保存對象obj的綁定函數Animal.bind(obj)。實例化Animal2後發現cat2內容和cat是同樣的,此時咱們發現使用bind綁定的this失效了,由於咱們傳進去obj對象的內容並無發生改變。咱們再來看下咱們目前的bind2的表現:prototype

Function.prototype.bind2 = function(context) {
  // 保存上層函數this值
  var _this = this;
  // 保存上層函數的參數
  var args = [].slice.call(arguments, 1);
  return function() {
    // 將參數拼接
        var _args = args.concat([].slice.call(arguments));
    // 利用apply更改this,並把拼接的參數傳到函數中
    _this.apply(context, _args);
  }
}

function Animal(name) {
  this.name = name;
}
var obj = {
    name: 'test'
};
var mouse = new Animal('jerry');
var Animal3 = Animal.bind2(obj);
var mouse2 = new Animal3('jerry');
console.log(mouse); // {name: "jerry"}
console.log(mouse2); // {}
console.log(obj); // {name: 'jerry'}

咱們先看下這裏的Animal3實際的返回函數,它是bind2方法的這一部分:

function() {
    // 將參數拼接
        args.concat([].slice.call(arguments));
    // 利用apply更改this,並把拼接的參數傳到函數中
    _this.apply(context, args);
 }

如上,代碼中咱們new Animal3('jerry')實際上就是對上面的這個函數的實例化,這就是爲何mouse2是個空對象的緣由。而後因爲前面bind2綁定的是obj,_this.apply(context, args)這行代碼就把obj對象的name屬性給更改了,context指向obj,_this指向Animal函數。而咱們的目標是但願當綁定函數被當作構造函數使用的時候,context不會指向被傳進來的上下文對象(好比這裏的obj)而是指向綁定函數的this。咱們的問題轉移到這上面上了:如何在一個函數中去判斷這個函數是被正常調用仍是被當作構造函數調用的。答案是經過原型。不熟悉原型的同窗能夠移步:理解Javascript的原型和原型鏈。例子:

function Animal() {
  console.log(this.__proto__ === Animal.prototype);
}
new Animal(); // true
Animal(); // false

所以能夠把咱們能夠在咱們返回的函數裏面進行這樣的判斷,這是咱們第三版代碼

Function.prototype.bind2 = function(context) {
  // 保存上層函數this值
  var _this = this;
  // 保存上層函數的參數
  var args = [].slice.call(arguments, 1);
  function Func() {
    // 將參數拼接
        var _args = args.concat([].slice.call(arguments));
    _this.apply(this.__proto__ === Func.prototype ? this : context, _args);
  }
  return Func;
}

// 測試代碼
function Animal(name) {
  this.name = name;
}
var obj = {
    name: 'test'
};
var mouse = new Animal('jerry');
var Animal3 = Animal.bind2(obj);
var mouse2 = new Animal3('jerry');
console.log(mouse); // {name: "jerry"}
console.log(mouse2); //{name: "jerry"}
console.log(obj); // {name: 'test'}

如上例子,咱們的mouse2和obj都是正常的返回了。但這樣的實現有一個問題,就是咱們無法拿到Animal的原型,此時mouse2.__proto__ === Func.prototype

所以須要再改寫下,當實例對象可以連接到構造函數的原型,第四版代碼以下

Function.prototype.bind2 = function(context) {
  // 保存上層函數this值
  var _this = this;
  // 保存上層函數的參數
  var args = [].slice.call(arguments, 1);
  function Func() {
    // 將參數拼接
        var _args = args.concat([].slice.call(arguments));
    _this.apply(this.__proto__ === Func.prototype ? this : context, _args);
  }
  Func.prototype = this.prototype;
  return Func;
}

這個時候咱們再去實例化mouse2,就能夠作到mouse2.__proto__ === Animal.prototype了。

還有一個問題,由於咱們是直接Func.prototype = this.prototype, 因此咱們在修改Func.prototype的時候,也會直接修改函數的prototype,咱們看下咱們的最終代碼

Function.prototype.bind2 = function(context) {
  // 保存上層函數this值
  var _this = this;
  // 保存上層函數的參數
  var args = [].slice.call(arguments, 1);
  function Transfer() {}
  function Func() {
    // 將參數拼接
        var _args = args.concat([].slice.call(arguments));
    _this.apply(this.__proto__ === Func.prototype ? this : context, _args);
  }
  Transfer.prototype = this.prototype;
  Func.prototype = new Transfer();
  return Func;
}

以上。


能力有限,水平通常,歡迎勘誤,不勝感激。

訂閱更多文章可關注公衆號「前端進階學習」,回覆「666」,獲取一攬子前端技術書籍

前端進階學習

相關文章
相關標籤/搜索