Javascript中從學(xué)習(xí)bind到實現(xiàn)bind的過程
bind是什么
bind()方法創(chuàng)建一個新的函數(shù), 當被調(diào)用時,將其this關(guān)鍵字設(shè)置為提供的值,在調(diào)用新函數(shù)時,在任何提供之前提供一個給定的參數(shù)序列。
var result = fun.bind(thisArg[, arg1[, arg2[, ...]]]) result(newArg1, newArg2...)
沒看懂沒事接著往下看。
bind到底做了什么
從上面的介紹中可以看出三點。首先調(diào)用bind方法會返回一個新的函數(shù)(這個新的函數(shù)的函數(shù)體應(yīng)該和fun是一樣的)。同時bind中傳遞兩個參數(shù),第一個是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
第二個參數(shù)為一個序列,你可以傳遞任意數(shù)量的參數(shù)到其中。并且會預(yù)置到新函數(shù)參數(shù)之前。
this.value = 2 var foo = { value: 1 }; var bar = function(name, age, school) { console.log(name) // 'An' console.log(age) // 22 console.log(school) // '家里蹲大學(xué)' } var result = bar.bind(foo, 'An') //預(yù)置了部分參數(shù)'An' result(22, '家里蹲大學(xué)') //這個參數(shù)會和預(yù)置的參數(shù)合并到一起放入bar中
我們可以看出在最后調(diào)用 result(22, '家里蹲大學(xué)') 的時候,其內(nèi)部已經(jīng)包含了在調(diào)用bind的時候傳入的 'An'。
一句話總結(jié):調(diào)用bind,就會返回一個新的函數(shù)。這個函數(shù)里面的this就指向bind的第一個參數(shù),同時this后面的參數(shù)會提前傳給這個新的函數(shù)。調(diào)用該新的函數(shù)時,再傳遞的參數(shù)會放到預(yù)置的參數(shù)后一起傳遞進新函數(shù)。
自己實現(xiàn)一個bind
實現(xiàn)一個bind需要實現(xiàn)以下兩個功能
返回一個函數(shù),綁定this,傳遞預(yù)置參數(shù)
bind返回的函數(shù)可以作為構(gòu)造函數(shù)使用。故作為構(gòu)造函數(shù)時應(yīng)使得this失效,但是傳入的參數(shù)依然有效
1、返回一個函數(shù),綁定this,傳遞預(yù)置參數(shù)
this.value = 2 var foo = { value: 1 }; var bar = function(name, age, school) { console.log(name) // 'An' console.log(age) // 22 console.log(school) // '家里蹲大學(xué)' console.log(this.value) // 1 } Function.prototype.bind = function(newThis) { var aArgs = Array.prototype.slice.call(arguments, 1) //拿到除了newThis之外的預(yù)置參數(shù)序列 var that = this return function() { return that.apply(newThis, aArgs.concat(Array.prototype.slice.call(arguments))) //綁定this同時將調(diào)用時傳遞的序列和預(yù)置序列進行合并 } } var result = bar.bind(foo, 'An') result(22, '家里蹲大學(xué)')
這里面有一個細節(jié)就是Array.prototype.slice.call(arguments, 1) 這句話,我們知道arguments這個變量可以拿到函數(shù)調(diào)用時傳遞的參數(shù),但不是一個數(shù)組,但是其具有一個length屬性。為什么如此調(diào)用就可以將其變?yōu)榧償?shù)組了呢。那么我們就需要回到V8的源碼來進行分析。#這個版本的源碼為早期版本,內(nèi)容相對少一些。
function ArraySlice(start, end) { var len = ToUint32(this.length); //需要傳遞this指向?qū)ο?,那么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來獲得最后的數(shù)組,所以不需要傳遞進slice時就是一個純數(shù)組最后也可以得到一個數(shù)組變量。
2、bind返回的函數(shù)可以作為構(gòu)造函數(shù)使用
被用作構(gòu)造函數(shù)時,this應(yīng)指向new出來的實例,同時有prototype屬性,其指向?qū)嵗脑汀?/p>
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, '家里蹲大學(xué)') console.log('person', person.name) //'Lsc'
上面這段模擬代碼做了兩件重要的事。
1.給返回的函數(shù)模擬一個prototype屬性。,因為通過構(gòu)造函數(shù)new出來的實例可以查詢到原型上定義的屬性和方法
var NoFunc = function() {} ... NoFunc.prototype = that.prototype //that指向bar resultFunc.prototype = new NoFunc() return resultFunc
通過上面代碼可以看出,that始終指向bar。同時返回的函數(shù)已經(jīng)繼承了that.prototype即bar.prototype。為什么不直接讓返回的函數(shù)的prototype屬性resultFunc.prototype 等于為bar(that).prototype呢,這是因為任何new出來的實例都可以訪問原型鏈。如果直接賦值那么new出來的對象可以直接修改bar函數(shù)的原型鏈,這也就是是原型鏈污染。所以我們采用繼承的方式(將構(gòu)造函數(shù)的原型鏈賦值為父級構(gòu)造函數(shù)的實例),讓new出來的對象的原型鏈與bar脫離關(guān)系。
2.判斷當前被調(diào)用時,this是用于普通的bind還是用于構(gòu)造函數(shù)從而更改this指向。
如何判斷當前this指向了哪里呢,通過第一點我們已經(jīng)知道,通過bind方法返回的新函數(shù)已經(jīng)有了原型鏈,剩下需要我們做的就是改變this的指向就可以模擬完成了。通過什么來判斷當前被調(diào)用是以何種姿勢呢。答案是instanceof 。
instanceof 運算符用來測試一個對象在其原型鏈中是否存在一個構(gòu)造函數(shù)的 prototype 屬性。
// 定義構(gòu)造函數(shù) 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可以判斷出一個對象是否是由這個函數(shù)new出來的,如果是new出來的,那么這個對象的原型鏈應(yīng)為該函數(shù)的prototype.
所以我們來看這段關(guān)鍵的返回的函數(shù)結(jié)構(gòu):
var resultFunc = function() { return that.apply(this instanceof that ? this : newThis, aArgs.concat(Array.prototype.slice.call(arguments))) }
在這其中我們要先認清this instanceof that 中的this是bind函數(shù)被調(diào)用后,返回的新函數(shù)中的this。所以這個this可能執(zhí)行在普通的作用域環(huán)境,同時也可能被new一下從而改變自己的指向。再看that,that始終指向了bar,同時其原型鏈that.prototype是一直存在的。所以如果現(xiàn)在這個新函數(shù)要做new操作,那么this指向了新函數(shù),那么 this instanceof that === true, 所以在apply中傳入this為指向,即指向新函數(shù)。如果是普通調(diào)用,那么this不是被new出來的,即新函數(shù)不是作為構(gòu)造函數(shù),this instanceof that === false就很顯而易見了。這個時候是正常的bind調(diào)用。將調(diào)用的第一個參數(shù)作為this的指向即可。
完整代碼(MDN下的實現(xiàn))
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的判定,不支持再實行兼容。同時判斷調(diào)用這個方法的對象是否是個函數(shù),如果不是則報錯。
同時這個模擬的方法也有一些缺陷,可關(guān)注MDN上的Polyfill部分
小結(jié)
模擬bind實現(xiàn)最大的一個缺陷是,模擬出來的函數(shù)中會一直存在prototype屬性,但是原生的bind作為構(gòu)造函數(shù)是沒有prototype的,這點打印一下即可知。不過這樣子new出來的實例沒有原型鏈,那么它的意義是什么呢。
大家在學(xué)習(xí)的時候如果還有任何問題,可以在下面的留言區(qū)討論,感謝你的支持。
相關(guān)文章
javascript操作向表格中動態(tài)加載數(shù)據(jù)
這篇文章主要為大家詳細介紹了javascript操作向表格中動態(tài)加載數(shù)據(jù),文中示例代碼介紹的非常詳細,具有一定的參考價值,感興趣的小伙伴們可以參考一下2020-08-08相關(guān)JavaScript在覽器中實現(xiàn)可視化的四種方式
這篇文章主要介紹了相關(guān)JavaScript在覽器中實現(xiàn)可視化的四種方式,文章圍繞主題展開詳細的內(nèi)容介紹,具有一定的參考價值,感興趣的小伙伴可以參考一下2022-09-09JavaScript:new 一個函數(shù)和直接調(diào)用函數(shù)的區(qū)別分析
或許許多人對此不以為然,在函數(shù)前加 new 關(guān)鍵字,不就是實例化一個對象嗎?但事情顯然沒那么簡單2013-07-07深入聊聊Array的sort方法的使用技巧.詳細點評protype.js中的sortBy方法
深入聊聊Array的sort方法的使用技巧.詳細點評protype.js中的sortBy方法...2007-04-04