最近開始從新學習一波js,框架用久了有些時候以爲這樣子應該能夠實現發現就真的實現了,可是爲何這麼寫好像又說不太清楚,以前讀了LucasHC以及冴羽的兩篇關於bind的文章感受本身好像基礎知識都還給體育老師了哈哈哈,因此危機感爆棚,趕忙重頭複習一遍。本次主要圍繞bind是什麼;作了什麼;本身怎麼實現一個bind,這三個部分。其中會包含一些細節代碼的探究,往下看就知道。git
bind()方法建立一個新的函數, 當被調用時,將其this關鍵字設置爲提供的值,在調用新函數時,在任何提供以前提供一個給定的參數序列。
var result = fun.bind(thisArg[, arg1[, arg2[, ...]]]) result(newArg1, newArg2...)
沒看懂沒事接着往下看。github
從上面的介紹中能夠看出三點。首先調用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
第二個參數爲一個序列,你能夠傳遞任意數量的參數到其中。而且會預置到新函數參數以前。app
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中
咱們能夠看出在最後調用 result(22, '家裏蹲大學')
的時候,其內部已經包含了在調用bind的時候傳入的 'An'
。框架
一句話總結:調用bind,就會返回一個新的函數。這個函數裏面的this就指向bind的第一個參數,同時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, '家裏蹲大學')
這裏面有一個細節就是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; };
從源碼中能夠看到經過call將arguments下的length屬性賦給slice後,即可經過 start_i & end_i
來得到最後的數組,因此不須要傳遞進slice時就是一個純數組最後也能夠獲得一個數組變量。測試
被用做構造函數時,this應指向new出來的實例,同時有prototype屬性,其指向實例的原型。this
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'
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脫離關係。prototype
如何判斷當前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;
從上面能夠看出,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的指向便可。
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; }; }
能夠看到,其首先作了當前是否支持bind的斷定,不支持再實行兼容。同時判斷調用這個方法的對象是不是個函數,若是不是則報錯。
同時這個模擬的方法也有一些缺陷,可關注MDN上的Polyfill部分
模擬bind實現最大的一個缺陷是,模擬出來的函數中會一直存在prototype屬性,可是原生的bind做爲構造函數是沒有prototype的,這點打印一下便可知。不過這樣子new出來的實例沒有原型鏈,那麼它的意義是什麼呢。若是哪天做者知道了意義會更新在這裏的=。= 若是說錯的地方歡迎指正,一塊兒交流哈哈。