學習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()函數最重要的兩個特性:

  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和傳遞參數兩個特性已經實現,如果截止到這就結束了,就不會單獨爲模擬實現bind()寫一篇博客了,bind還有一個特性,即當綁定函數作爲構造函數使用的時候裏面的this就會失效。例子:

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的表現:

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」,獲取一攬子前端技術書籍

前端進階學習

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章