javascript原生一步步實現(xiàn)bind分析
bind
官方描述
bind() 函數(shù)會創(chuàng)建一個新函數(shù)(稱為綁定函數(shù)),新函數(shù)與被調(diào)函數(shù)(綁定函數(shù)的目標函數(shù))具有相同的函數(shù)體(在 ECMAScript 5 規(guī)范中內(nèi)置的call屬性)。當目標函數(shù)被調(diào)用時 this 值綁定到 bind() 的***個參數(shù),該參數(shù)不能被重寫。綁定函數(shù)被調(diào)用時,bind() 也接受預設的參數(shù)提供給原函數(shù)。一個綁定函數(shù)也能使用new操作符創(chuàng)建對象:這種行為就像把原函數(shù)當成構(gòu)造器。提供的 this 值被忽略,同時調(diào)用時的參數(shù)被提供給模擬函數(shù)。
使用介紹
由于javascript中作用域是由其運行時候所處的環(huán)境決定的,所以往往函數(shù)定義和實際運行的時候所處環(huán)境不一樣,那么作用域也會發(fā)生相應的變化。
例如下面這個情況:
- var id = 'window';
- //定義一個函數(shù),但是不立即執(zhí)行
- var test = function(){
- console.log(this.id)
- }
- test() // window
- //把test作為參數(shù)傳遞
- var obj = {
- id:'obj',
- hehe:test
- }
- //此時test函數(shù)運行環(huán)境發(fā)生了改變
- obj.hehe() // 'obj'
- //為了避免這種情況,javascript里面有一個bind方法可以在函數(shù)運行之前就綁定其作用域,修改如下
- var id = 'window';
- var test = function(){
- console.log(this.id)
- }.bind(window)
- var obj = {
- id:'obj',
- hehe:test
- }
- test() // window
- obj.hehe() // window
上面介紹了bind方法的一個重要作用就是為一個函數(shù)綁定作用域,但是bind方法在低版本瀏覽器不兼容,這里我們可以手動實現(xiàn)一下。
拆分一下關(guān)鍵思路
- 因為bind方法不會立即執(zhí)行函數(shù),需要返回一個待執(zhí)行的函數(shù)(這里用到閉包,可以返回一個函數(shù))return function(){}
- 作用域綁定,這里可以使用apply或者call方法來實現(xiàn) xx.call(yy)/xx.apply(yy)
- 參數(shù)傳遞,由于參數(shù)的不確定性,需要用apply傳遞數(shù)組(實例更明了)xx.apply(yy,[...Array...]),如果用call就不太方便了,因為call后面的參數(shù)需要一個個列出來
實現(xiàn)
有了上述的思路,大致的雛形已經(jīng)明了了,代碼應該也很容易實現(xiàn)
綁定作用域,綁定傳參
- Function.prototype.testBind = function(that){
- var _this = this,
- /*
- *由于參數(shù)的不確定性,統(tǒng)一用arguments來處理,這里的arguments只是一個類數(shù)組對象,有l(wèi)ength屬性
- *可以用數(shù)組的slice方法轉(zhuǎn)化成標準格式數(shù)組,除了作用域?qū)ο髏hat以外,
- *后面的所有參數(shù)都需要作為數(shù)組參數(shù)傳遞
- *Array.prototype.slice.apply(arguments,[1])/Array.prototype.slice.call(arguments,1)
- */
- slice = Array.prototype.slice,
- args = slice.apply(arguments,[1]);
- //返回函數(shù)
- return function(){
- //apply綁定作用域,進行參數(shù)傳遞
- return _this.apply(that,args)
- }
- }
測試
- var test = function(a,b){
- console.log('作用域綁定 '+ this.value)
- console.log('testBind參數(shù)傳遞 '+ a.value2)
- console.log('調(diào)用參數(shù)傳遞 ' + b)
- }
- var obj = {
- value:'ok'
- }
- var fun_new = test.testBind(obj,{value2:'also ok'})
- fun_new ('hello bind')
- // 作用域綁定 ok
- // testBind參數(shù)傳遞 also ok
- // 調(diào)用參數(shù)傳遞 undefined
動態(tài)參數(shù)
上面已經(jīng)實現(xiàn)了bind方法的作用域綁定,但是美中不足的是,既然我們返回的是一個函數(shù),調(diào)用的時候應該支持傳遞參數(shù),很顯然,上面的 fun_new 調(diào)用的時候并不支持傳參,只能在 testBind 綁定的時候傳遞參數(shù),因為我們最終調(diào)用的是這個返回函數(shù)
- function(){
- return _this.apply(that,args)
- }
- 這里面的args在綁定的時候就已經(jīng)確定了,調(diào)用的時候值已經(jīng)固定,
- 我們并沒有處理這個function傳遞的參數(shù)。
我們對其進行改造
- return function(){
- return _this.apply(that,
- args.concat(Array.prototype.slice.apply(arguments,[0]))
- )
- }
這里的 Array.prototype.slice.apply(arguments,[0]) 指的是這個返回函數(shù)執(zhí)行的時候傳遞的一系列參數(shù),所以是從***個參數(shù)開始 [0] ,之前的args = slice.apply(arguments,[1])指的是 testBind方法執(zhí)行時候傳遞的參數(shù),所以從第二個開始 [1],兩則有本質(zhì)區(qū)別,不能搞混,只有兩者合并了之后才是返回函數(shù)的完整參數(shù)
所以有如下實現(xiàn)
- Function.prototype.testBind = function(that){
- var _this = this,
- slice = Array.prototype.slice,
- args = slice.apply(arguments,[1]);
- return function(){
- return _this.apply(that,
- args.concat(Array.prototype.slice.apply(arguments,[0]))
- )
- }
- }
測試
- var test = function(a,b){
- console.log('作用域綁定 '+ this.value)
- console.log('testBind參數(shù)傳遞 '+ a.value2)
- console.log('調(diào)用參數(shù)傳遞 ' + b)
- }
- var obj = {
- value:'ok'
- }
- var fun_new = test.testBind(obj,{value2:'also ok'})
- fun_new ('hello bind')
- // 作用域綁定 ok
- // testBind參數(shù)傳遞 also ok
- // 調(diào)用參數(shù)傳遞 hello bind
在以上2種傳參方式中,bind的優(yōu)先級高,從 args.concat(Array.prototype.slice.apply(arguments,[0])) 也可以看出來,bind的參數(shù)在數(shù)組前面。
原型鏈
官方文檔上有一句話:
A bound function may also be constructed using the new operator: doing
so acts as though the target function had instead been constructed.
The provided this value is ignored, while prepended arguments are
provided to the emulated function.
說明綁定過后的函數(shù)被new實例化之后,需要繼承原函數(shù)的原型鏈方法,且綁定過程中提供的this被忽略(繼承原函數(shù)的this對象),但是參數(shù)還是會使用。
這里就需要一個中轉(zhuǎn)函數(shù)把原型鏈傳遞下去
- fNOP = function () {} //創(chuàng)建一個中轉(zhuǎn)函數(shù)
- fNOP.prototype = this.prototype;
- xx.prototype = new fNOP()
- 修改如下
- Function.prototype.testBind = function(that){
- var _this = this,
- slice = Array.prototype.slice,
- args = slice.apply(arguments,[1]),
- fNOP = function () {},
- //所以調(diào)用官方bind方法之后 有一個name屬性值為 'bound '
- bound = function(){
- return _this.apply(that,
- args.concat(Array.prototype.slice.apply(arguments,[0]))
- )
- }
- fNOP.prototype = _this.prototype;
- bound.prototype = new fNOP();
- return bound;
- }
而且bind方法的***個參數(shù)this是可以不傳的,需要分2種情況
- 直接調(diào)用bind之后的方法
- var f = function () { console.log('不傳默認為'+this) };f.bind()()
- // 不傳默認為 Window
所以直接調(diào)用綁定方法時候 apply(that, 建議改為 apply(that||window,,其實不改也可以,因為不傳默認指向window
- 使用new實例化被綁定的方法
容易糊涂,重點在于弄清楚標準的bind方法在new的時候做的事情,然后就可以清晰的實現(xiàn)
這里我們需要看看 new 這個方法做了哪些操作 比如說 var a = new b()
- 創(chuàng)建一個空對象 a = {},并且this變量引用指向到這個空對象a
- 繼承被實例化函數(shù)的原型 :a.__proto__ = b.prototype
- 被實例化方法b的this對象的屬性和方法將被加入到這個新的 this 引用的對象中: b的屬性和方法被加入的 a里面
- 新創(chuàng)建的對象由 this 所引用 :b.call(a)
通過以上可以得知,如果是var after_new = new bindFun(); 由于這種行為是把原函數(shù)當成構(gòu)造器,那么那么最終實例化之后的對象this需要繼承自原函數(shù), 而這里的 bindFun 目前是
- function(){
- return _this.apply(that || window,
- args.concat(Array.prototype.slice.apply(arguments,[0]))
- )
- }
這里apply的作用域是綁定的that || window,在執(zhí)行 testBind()的時候就已經(jīng)固定,并沒有把原函數(shù)的this對象繼承過來,不符合我們的要求,我們需要根據(jù)apply的特性解決這個問題:
在一個子構(gòu)造函數(shù)中,你可以通過調(diào)用父構(gòu)造函數(shù)的 `apply/call` 方法來實現(xiàn)繼承
例如
- function Product(name, price) {
- this.name = name;
- this.price = price;
- if (price < 0) {
- throw RangeError('Cannot create product ' +
- this.name + ' with a negative price');
- }
- }
- function Food(name, price) {
- Product.call(this, name, price);
- this.category = 'food';
- }
- //等同于(其實就是把Product放在Food內(nèi)部執(zhí)行了一次)
- function Food(name, price) {
- this.name = name;
- this.price = price;
- if (price < 0) {
- throw RangeError('Cannot create product ' +
- this.name + ' with a negative price');
- }
- this.category = 'food';
- }
所以在new新的實例的時候?qū)崟r將這個新的this對象 進行 apply 繼承原函數(shù)的 this 對象,就可以達到 new 方法里面的第 3 步的結(jié)果
- apply(that||window,
- //修改為 如果是new的情況,需要綁定new之后的作用域,this指向新的實例對象
- apply(isNew ? this : that||window, ==>
- Function.prototype.testBind = function(that){
- var _this = this,
- slice = Array.prototype.slice,
- args = slice.apply(arguments,[1]),
- fNOP = function () {},
- //所以調(diào)用官方bind方法之后 有一個name屬性值為 'bound '
- bound = function(){
- return _this.apply(isNew ? this : that||window,
- args.concat(Array.prototype.slice.apply(arguments,[0]))
- )
- }
- fNOP.prototype = _this.prototype;
- bound.prototype = new fNOP();
- return bound;
- }
這里的 isNew 是區(qū)分 bindFun 是直接調(diào)用還是被 new 之后再調(diào)用,通過原型鏈的繼承關(guān)系可以知道,
bindFun 屬于 after_new的父類,所以 after_new instanceof bindFun 為 true,同時
bindFun.prototype = new fNOP() 原型繼承; 所以 fNOP 也是 after_new的父類, after_new instanceof fNOP 為 true
最終結(jié)果
- Function.prototype.testBind = function(that){
- var _this = this,
- slice = Array.prototype.slice,
- args = slice.apply(arguments,[1]),
- fNOP = function () {},
- bound = function(){
- //這里的this指的是調(diào)用時候的環(huán)境
- return _this.apply(this instanceof fNOP ? this : that||window,
- args.concat(Array.prototype.slice.apply(arguments,[0]))
- )
- }
- fNOP.prototype = _this.prototype;
- bound.prototype = new fNOP();
- return bound;
- }
我看到有些地方寫的是
- this instanceof fNOP && that ? this : that || window,
我個人覺得這里有點不正確,如果綁定時候不傳參數(shù),那么that就為空,那無論怎樣就只能綁定 window作用域了。
以上是個人見解,不對的地方望指導,謝謝!