本文主要介紹了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中
我們可以看出在最後調用 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, '家里蹲大学')
這裡面有一個細節就是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時就是一個純數組最後也可以得到一個數組變數。
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'
上面這段模擬程式碼做了兩件重要的事。
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;
從上面可以看出,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; }; }
可以看到,其首先做了当前是否支持bind的判定,不支持再实行兼容。同时判断调用这个方法的对象是否是个函数,如果不是则报错。
同时这个模拟的方法也有一些缺陷,可关注MDN上的Polyfill部分
小结
模拟bind实现最大的一个缺陷是,模拟出来的函数中会一直存在prototype属性,但是原生的bind作为构造函数是没有prototype的,这点打印一下即可知。不过这样子new出来的实例没有原型链,那么它的意义是什么呢。
相关推荐:
Jquery中.bind()、.live()、.delegate()和.on()之间的区别实例分享
Js的this指向 apply().call(),bind()的问题
以上是Javascript中從學習bind到實現bind的過程詳解的詳細內容。更多資訊請關注PHP中文網其他相關文章!