1. 程式人生 > >解析Javascript中從學習bind到實現bind的過程

解析Javascript中從學習bind到實現bind的過程

這篇文章主要介紹了Javascript中從學習bind到實現bind的過程,寫的十分的全面細緻,具有一定的參考價值,對此有需要的朋友可以參考學習下。如有不足之處,歡迎批評指正。

bind是什麼

bind()方法建立一個新的函式, 當被呼叫時,將其this關鍵字設定為提供的值,在呼叫新函式時,在任何提供之前提供一個給定的引數序列。

var result = fun.bind(thisArg[, arg1[, arg2[, ...]]]) 
result(newArg1, newArg2...)

沒看懂沒事接著往下看。

bind到底做了什麼

從上面的介紹中可以看出三點。首先呼叫bind方法會返回一個新的函式(這個新的函式的函式體應該和fun是一樣的)。同時bind中傳遞兩個引數,第一個是this指向,即傳入了什麼this就等於什麼。如下程式碼所示:\

this.value = 2
var foo = {
  value: 1
}
var bar = function() {
 console.log(this.value)
}
var result = bar.bind(foo)
bar() // 2
result() // 1,即this === foo

第二個引數為一個序列,你可以傳遞任意數量的引數到其中。並且會預置到新函式引數之前。

this.value = 2
var foo = {
  value: 1
};
var bar = function(name, age, school) {
 console.log(name) // 'An'
 console.log(age) // 22
 console.log(school) // '家裡蹲大學'
}
var result = bar.bind(foo, 'An') //預置了部分引數'An'
result(22, '家裡蹲大學') //這個引數會和預置的引數合併到一起放入bar中
歡迎加入前端全棧開發交流圈一起學習交流:619586920

我們可以看出在最後呼叫 result(22, ‘家裡蹲大學’) 的時候,其內部已經包含了在呼叫bind的時候傳入的 ‘An’。
一句話總結:呼叫bind,就會返回一個新的函式。這個函式裡面的this就指向bind的第一個引數,同時this後面的引數會提前傳給這個新的函式。呼叫該新的函式時,再傳遞的引數會放到預置的引數後一起傳遞進新函式。

自己實現一個bind

實現一個bind需要實現以下兩個功能
返回一個函式,繫結this,傳遞預置引數
bind返回的函式可以作為建構函式使用。故作為建構函式時應使得this失效,但是傳入的引數依然有效
1、返回一個函式,繫結this,傳遞預置引數

this.value = 2
var foo = {
  value: 1
};
var bar = function(name, age, school) {
  console.log(name) // 'An'
  console.log(age) // 22
  console.log(school) // '家裡蹲大學'
  console.log(this.value) // 1
}
Function.prototype.bind = function(newThis) {
  var aArgs  = Array.prototype.slice.call(arguments, 1) //拿到除了newThis之外的預置引數序列
  var that = this
  return function() {
    return that.apply(newThis, aArgs.concat(Array.prototype.slice.call(arguments)))
    //繫結this同時將呼叫時傳遞的序列和預置序列進行合併
  }
}
var result = bar.bind(foo, 'An')
result(22, '家裡蹲大學')
歡迎加入前端全棧開發交流圈一起學習交流:619586920

這裡面有一個細節就是Array.prototype.slice.call(arguments, 1) 這句話,我們知道arguments這個變數可以拿到函式呼叫時傳遞的引數,但不是一個數組,但是其具有一個length屬性。為什麼如此呼叫就可以將其變為純陣列了呢。那麼我們就需要回到V8的原始碼來進行分析。#這個版本的原始碼為早期版本,內容相對少一些。

function ArraySlice(start, end) {
 var len = ToUint32(this.length); 
 //需要傳遞this指向物件,那麼call(arguments),
 //便可將this繫結到arguments,拿到其length屬性。
 var start_i = TO_INTEGER(start);
 var end_i = len;
 if (end !== void 0) end_i = TO_INTEGER(end);
 if (start_i < 0) {
  start_i += len;
  if (start_i < 0) start_i = 0;
 } else {
  if (start_i > len) start_i = len;
 }
 if (end_i < 0) {
  end_i += len;
  if (end_i < 0) end_i = 0;
 } else {
  if (end_i > len) end_i = len;
 }
 var result = [];
 if (end_i < start_i)
  return result;
 if (IS_ARRAY(this))
  SmartSlice(this, start_i, end_i - start_i, len, result);
 else 
  SimpleSlice(this, start_i, end_i - start_i, len, result);
 result.length = end_i - start_i;
 return result;
};歡迎加入前端全棧開發交流圈一起學習交流:619586920

從原始碼中可以看到通過call將arguments下的length屬性賦給slice後,便可通過 start_i & end_i來獲得最後的陣列,所以不需要傳遞進slice時就是一個純陣列最後也可以得到一個數組變數。
2、bind返回的函式可以作為建構函式使用
被用作建構函式時,this應指向new出來的例項,同時有prototype屬性,其指向例項的原型。

this.value = 2
var foo = {
 value: 1
};
var bar = function(name, age, school) {
 ...
 console.log('this.value', this.value)
}
Function.prototype.bind = function(newThis) {
 var aArgs  = Array.prototype.slice.call(arguments, 1)
 var that = this //that始終指向bar
 var NoFunc = function() {}
 var resultFunc = function() {
  return that.apply(this instanceof that ? this : newThis, aArgs.concat(Array.prototype.slice.call(arguments)))
 } 
 NoFunc.prototype = that.prototype //that指向bar
 resultFunc.prototype = new NoFunc()
 return resultFunc
}
var result = bar.bind(foo, 'An')
result.prototype.name = 'Lsc' // 有prototype屬性
var person = new result(22, '家裡蹲大學')
console.log('person', person.name) //'Lsc'
歡迎加入前端全棧開發交流圈一起學習交流:619586920

上面這段模擬程式碼做了兩件重要的事。
1.給返回的函式模擬一個prototype屬性。,因為通過建構函式new出來的例項可以查詢到原型上定義的屬性和方法

var NoFunc = function() {}
...
NoFunc.prototype = that.prototype //that指向bar
resultFunc.prototype = new NoFunc()
return resultFunc

歡迎加入我們

通過上面程式碼可以看出,that始終指向bar。同時返回的函式已經繼承了that.prototype即bar.prototype。為什麼不直接讓返回的函式的prototype屬性resultFunc.prototype 等於為bar(that).prototype呢,這是因為任何new出來的例項都可以訪問原型鏈。如果直接賦值那麼new出來的物件可以直接修改bar函式的原型鏈,這也就是是原型鏈汙染。所以我們採用繼承的方式(將建構函式的原型鏈賦值為父級建構函式的例項),讓new出來的物件的原型鏈與bar脫離關係。
2.判斷當前被呼叫時,this是用於普通的bind還是用於建構函式從而更改this指向。
如何判斷當前this指向了哪裡呢,通過第一點我們已經知道,通過bind方法返回的新函式已經有了原型鏈,剩下需要我們做的就是改變this的指向就可以模擬完成了。通過什麼來判斷當前被呼叫是以何種姿勢呢。答案是instanceof 。
instanceof 運算子用來測試一個物件在其原型鏈中是否存在一個建構函式的 prototype 屬性。

// 定義建構函式
function C(){} 
function D(){} 
var o = new C();
// true,因為 Object.getPrototypeOf(o) === C.prototype
o instanceof C; 
// false,因為 D.prototype不在o的原型鏈上
o instanceof D;歡迎加入前端全棧開發交流圈一起學習交流:619586920

從上面可以看出,instanceof可以判斷出一個物件是否是由這個函式new出來的,如果是new出來的,那麼這個物件的原型鏈應為該函式的prototype.
所以我們來看這段關鍵的返回的函式結構:

var resultFunc = function() {
  return that.apply(this instanceof that ? 
    this : 
    newThis, 
    aArgs.concat(Array.prototype.slice.call(arguments)))
 }

在這其中我們要先認清this instanceof that 中的this是bind函式被呼叫後,返回的新函式中的this。所以這個this可能執行在普通的作用域環境,同時也可能被new一下從而改變自己的指向。再看that,that始終指向了bar,同時其原型鏈that.prototype是一直存在的。所以如果現在這個新函式要做new操作,那麼this指向了新函式,那麼 this instanceof that === true, 所以在apply中傳入this為指向,即指向新函式。如果是普通呼叫,那麼this不是被new出來的,即新函式不是作為建構函式,this instanceof that === false就很顯而易見了。這個時候是正常的bind呼叫。將呼叫的第一個引數作為this的指向即可。
完整程式碼(MDN下的實現)

if (!Function.prototype.bind) {
 Function.prototype.bind = function(oThis) {
  if (typeof this !== 'function') {
   // closest thing possible to the ECMAScript 5
   // internal IsCallable function
   throw new TypeError('Function.prototype.bind - what is trying to be bound is not callable');
  }
 
  var aArgs  = Array.prototype.slice.call(arguments, 1),
    fToBind = this,
    fNOP  = function() {},
    fBound = function() {
     return fToBind.apply(this instanceof fNOP
         ? this
         : oThis,
         aArgs.concat(Array.prototype.slice.call(arguments)));
    };
 
  if (this.prototype) {
   // Function.prototype doesn't have a prototype property
   fNOP.prototype = this.prototype; 
  }
  fBound.prototype = new fNOP();
  return fBound;
 };歡迎加入前端全棧開發交流圈一起學習交流:619586920
}

可以看到,其首先做了當前是否支援bind的判定,不支援再實行相容。同時判斷呼叫這個方法的物件是否是個函式,如果不是則報錯。
同時這個模擬的方法也有一些缺陷,可關注MDN上的Polyfill部分

模擬bind實現最大的一個缺陷是,模擬出來的函式中會一直存在prototype屬性,但是原生的bind作為建構函式是沒有prototype的,這點列印一下即可知。不過這樣子new出來的例項沒有原型鏈,那麼它的意義是什麼呢。
歡迎加入我們

結語

感謝您的觀看,如有不足之處,歡迎批評指正。

本次給大家推薦一個免費的學習群,裡面概括移動應用網站開發,css,html,webpack,vue node angular以及面試資源等。
對web開發技術感興趣的同學,歡迎加入Q群:864305860,不管你是小白還是大牛我都歡迎,還有大牛整理的一套高效率學習路線和教程與您免費分享,同時每天更新視訊資料。
最後,祝大家早日學有所成,拿到滿意offer,快速升職加薪,走上人生巔峰。