ホームページ > ウェブフロントエンド > jsチュートリアル > ネイティブ JavaScript を使用してバインド分析を段階的に実装する

ネイティブ JavaScript を使用してバインド分析を段階的に実装する

高洛峰
リリース: 2016-11-03 09:14:47
オリジナル
1614 人が閲覧しました

bind

公式説明

bind() 関数は、呼び出された関数 (バインド関数のターゲット関数) と同じ関数本体を持つ新しい関数 (バインド関数と呼ばれる) を作成します (ECMAScript 5 仕様に組み込まれています)。呼び出し属性内)。ターゲット関数が呼び出されるとき、この値はbind()の最初のパラメータにバインドされ、オーバーライドできません。バインドされた関数が呼び出されるとき、bind() は元の関数に提供するプリセット パラメーターも受け入れます。バインドされた関数は、new 演算子を使用してオブジェクトを作成することもできます。これは、コンストラクターとして元の関数と同様に動作します。指定された this 値は無視され、呼び出しの引数がモック関数に指定されます。

使い方の紹介

JavaScriptのスコープは実行環境によって決まるため、関数の定義が実際に実行される環境と異なる場合が多く、それに伴ってスコープも変わります。

たとえば、次のような状況です。

var id = 'window'; 
//定义一个函数,但是不立即执行 
var test = function(){ 
    console.log(this.id) 
} 
test() // window 
//把test作为参数传递 
var obj = { 
    id:'obj', 
    hehe:test 
} 
//此时test函数运行环境发生了改变 
obj.hehe() // 'obj' 
//为了避免这种情况,javascript里面有一个bind方法可以在函数运行之前就绑定其作用域,修改如下 
 
var id = 'window'; 
var test = function(){ 
    console.log(this.id) 
}.bind(window) 
var obj = { 
    id:'obj', 
    hehe:test 
} 
test() // window 
obj.hehe() // window
ログイン後にコピー

上で紹介したように、bind メソッドの重要な役割はスコープを関数にバインドすることですが、bind メソッドは下位バージョンのブラウザと互換性がありません。ここでは手動で実装できます。 。

重要なアイデアを分割します

バインド メソッドは関数をすぐに実行しないため、実行される関数を返す必要があります (ここではクロージャが使用されており、関数を返すことができます) return function(){}

スコープ バインディング, ここでは、apply または call メソッドを使用して xx.call(yy)/xx.apply(yy)

パラメーターの転送を実装できます。パラメーターが不確実であるため、配列の転送には apply を使用する必要があります (例)。はより明確です) xx.apply(yy, [...Array...])、呼び出しの背後にあるパラメーターを 1 つずつリストする必要があるため、呼び出しを使用するのは不便です

実装

上記のアイデアでは、大まかなプロトタイプはすでに明確になっており、コードは実装も簡単です

バインディングスコープとバインディングパラメータ

Function.prototype.testBind = function(that){ 
    var _this = this, 
        /* 
        *由于参数的不确定性,统一用arguments来处理,这里的arguments只是一个类数组对象,有length属性 
        *可以用数组的slice方法转化成标准格式数组,除了作用域对象that以外, 
        *后面的所有参数都需要作为数组参数传递 
        *Array.prototype.slice.apply(arguments,[1])/Array.prototype.slice.call(arguments,1) 
        */ 
        slice = Array.prototype.slice, 
        args = slice.apply(arguments,[1]); 
    //返回函数     
    return function(){ 
        //apply绑定作用域,进行参数传递 
        return _this.apply(that,args) 
    }     
}
ログイン後にコピー

test

var test = function(a,b){ 
    console.log('作用域绑定 '+ this.value) 
    console.log('testBind参数传递 '+ a.value2) 
    console.log('调用参数传递 ' + b) 
} 
var obj = { 
    value:'ok' 
} 
var fun_new = test.testBind(obj,{value2:'also ok'}) 
 
fun_new ('hello bind') 
// 作用域绑定 ok 
// testBind参数传递 also ok 
// 调用参数传递  undefined
ログイン後にコピー

動的パラメータ

バインドメソッドのスコープバインディングは上記で実装されていますが、フライ軟膏は、 function を返すので、呼び出し時にパラメータの受け渡しをサポートする必要があるということです。明らかに、上記の fun_new は、testBind がバインドされている場合にのみパラメータを渡すことができます。これは、最終的に呼び出すのはこれであるためです。 return function

function(){ 
        return _this.apply(that,args) 
    }     
 
这里面的args在绑定的时候就已经确定了,调用的时候值已经固定, 
我们并没有处理这个function传递的参数。
ログイン後にコピー

変換します

return function(){ 
        return _this.apply(that, 
            args.concat(Array.prototype.slice.apply(arguments,[0])) 
        ) 
    }
ログイン後にコピー

ここでのArray.prototype.slice.apply(arguments,[0])はreturn関数の実行時に渡される一連のパラメータを指しますので、最初のパラメータ[0]から始まります。前の args = slide.apply(arguments,[1]) は、testBind メソッドの実行時に渡されるパラメーターを参照しているため、2 番目の [1] 以降では、2 つは本質的に異なり、混同することはできません。 2 つをマージする これは return 関数の完全なパラメーターです

したがって、次の実装があります

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])) 
                ) 
    }     
}
ログイン後にコピー

Test

var test = function(a,b){ 
    console.log('作用域绑定 '+ this.value) 
    console.log('testBind参数传递 '+ a.value2) 
    console.log('调用参数传递 ' + b) 
} 
var obj = { 
    value:'ok' 
} 
var fun_new = test.testBind(obj,{value2:'also ok'}) 
 
fun_new ('hello bind') 
// 作用域绑定 ok 
// testBind参数传递 also ok 
// 调用参数传递  hello bind
ログイン後にコピー

上記の 2 つのパラメーター受け渡しメソッドのうち、bind は args.concat(Array.prototype. lice.apply(arguments,[ 0])) バインドのパラメータが配列の前にあることもわかります。

プロトタイプチェーン

公式ドキュメントには次の文があります:

バインドされた関数は new 演算子を使用して構築することもできます: doing

so はあたかもターゲット関数が代わりに構築されているかのように動作します。

The provided this valueは無視されますが、先頭に追加された引数はエミュレートされた関数に

提供されます。

バインドされた関数が new によってインスタンス化された後、元の関数のプロトタイプ チェーン メソッドを継承する必要があることを示し、バインド プロセス中に提供されたこれは無視されます(このオブジェクトの元の関数を継承します)、ただしパラメーターは引き続き使用されます。

ここでは、プロトタイプチェーンを渡すために伝達関数が必要です

fNOP = function () {} //创建一个中转函数 
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 () {}, 
        //所以调用官方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メソッドの最初のパラメータthisを渡す必要がない場合は、2つの状況に分ける必要があります

bind後にメソッドを直接呼び出す

var f = function () { console.log('不传默认为'+this)  };f.bind()() 
// 不传默认为 Window
ログイン後にコピー

JavaScript ネイティブ ステップ バインド分析の段階的な実装

bind() 関数は、呼び出されたものと同じ関数本体 (ECMAScript 5 仕様に組み込まれている) を持つ新しい関数 (バインディング関数と呼ばれる) を作成します。関数 (バインディング関数のターゲット関数) の呼び出し属性)。ターゲット関数が呼び出されるとき、この値はbind()の最初のパラメータにバインドされ、オーバーライドできません。バインドされた関数が呼び出されるとき、bind() は元の関数に提供するプリセット パラメーターも受け入れます。ソース 著者: ソースは誰ですか: Segmentfault 2016-11-02 18:54

お気に入り


シェア


公式説明

バインド ( ) 関数は新しいものを作成します関数 (バインド関数と呼ばれる) の場合、新しい関数は呼び出し先関数 (バインド関数のターゲット関数) と同じ関数本体 (ECMAScript 5 仕様の組み込み呼び出し属性) を持ちます。ターゲット関数が呼び出されるとき、この値はbind()の最初のパラメータにバインドされ、オーバーライドできません。バインドされた関数が呼び出されるとき、bind() は元の関数に提供するプリセット パラメーターも受け入れます。バインドされた関数は、new 演算子を使用してオブジェクトを作成することもできます。これは、コンストラクターとして元の関数と同様に動作します。指定された this 値は無視され、呼び出しの引数がモック関数に指定されます。

使い方の紹介

JavaScriptのスコープは実行環境によって決まるため、関数の定義が実際に実行される環境と異なる場合が多く、それに伴ってスコープも変わります。

🎜たとえば、次のような状況です: 🎜
var id = 'window'; 
//定义一个函数,但是不立即执行 
var test = function(){     
console.log(this.id) } 
test() 
// window 
//把test作为参数传递 
var obj = {     
id:'obj',     
hehe:test } 
//此时test函数运行环境发生了改变 obj.hehe() 
// 'obj' 
//为了避免这种情况,javascript里面有一个bind方法可以在函数运行之前就绑定其作用域,修改如下  
var id = 'window'; 
var test = function(){     
console.log(this.id) }.bind(window) 
var obj = {     
id:'obj',     
hehe:test } 
test() 
// window obj.hehe() 
// window
ログイン後にコピー

上面介绍了bind方法的一个重要作用就是为一个函数绑定作用域,但是bind方法在低版本浏览器不兼容,这里我们可以手动实现一下。

拆分一下关键思路

因为bind方法不会立即执行函数,需要返回一个待执行的函数(这里用到闭包,可以返回一个函数)return function(){}

作用域绑定,这里可以使用apply或者call方法来实现 xx.call(yy)/xx.apply(yy)

参数传递,由于参数的不确定性,需要用apply传递数组(实例更明了)xx.apply(yy,[...Array...]),如果用call就不太方便了,因为call后面的参数需要一个个列出来

实现

有了上述的思路,大致的雏形已经明了了,代码应该也很容易实现

绑定作用域,绑定传参

Function.
prototype.
testBind = function(that){     
var _this = this,         
/*         
*由于参数的不确定性,统一用arguments来处理,这里的arguments只是一个类数组对象,有length属性         
*可以用数组的slice方法转化成标准格式数组,除了作用域对象that以外,         
*后面的所有参数都需要作为数组参数传递         
*Array.prototype.slice.apply(arguments,[1])/Array.prototype.slice.call(arguments,1)         
*/         
slice = Array.prototype.slice,         
args = slice.apply(arguments,[1]);     
//返回函数         
return function(){         
//apply绑定作用域,进行参数传递         
return _this.apply(that,args)     
}     
}
ログイン後にコピー

测试

var test = function(a,b){     
console.log('作用域绑定 '+ this.value)     
console.log('testBind参数传递 '+ a.value2)     
console.log('调用参数传递 ' + b) 
} 
var obj = {     
value:'ok' } 
var fun_new = test.testBind(obj,{value2:'also ok'})  
fun_new ('hello bind') 
// 作用域绑定 ok 
// testBind参数传递 also ok 
// 调用参数传递  undefined
ログイン後にコピー

动态参数

上面已经实现了bind方法的作用域绑定,但是美中不足的是,既然我们返回的是一个函数,调用的时候应该支持传递参数,很显然,上面的 fun_new 调用的时候并不支持传参,只能在 testBind 绑定的时候传递参数,因为我们最终调用的是这个返回函数

function(){         
return _this.apply(that,args)     
}      
这里面的args在绑定的时候就已经确定了,调用的时候值已经固定, 我们并没有处理这个function传递的参数。
ログイン後にコピー

我们对其进行改造

return function(){         
return _this.apply(that,             
args.concat(Array.prototype.slice.apply(arguments,[0]))         
)    
}
ログイン後にコピー


这里的 Array.prototype.slice.apply(arguments,[0]) 指的是这个返回函数执行的时候传递的一系列参数,所以是从第一个参数开始 [0] ,之前的args = slice.apply(arguments,[1])指的是 testBind方法执行时候传递的参数,所以从第二个开始 [1],两则有本质区别,不能搞混,只有两者合并了之后才是返回函数的完整参数

所以有如下实现

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参数传递 '+ a.value2)     
console.log('调用参数传递 ' + b) } 
var obj = {     value:'ok' } 
var fun_new = test.testBind(obj,{value2:'also ok'})  
fun_new ('hello bind') 
// 作用域绑定 ok 
// testBind参数传递 also ok 
// 调用参数传递  hello bind
ログイン後にコピー

在以上2种传参方式中,bind的优先级高,从 args.concat(Array.prototype.slice.apply(arguments,[0])) 也可以看出来,bind的参数在数组前面。

原型链

官方文档上有一句话:

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.

说明绑定过后的函数被new实例化之后,需要继承原函数的原型链方法,且绑定过程中提供的this被忽略(继承原函数的this对象),但是参数还是会使用。

这里就需要一个中转函数把原型链传递下去

fNOP = function () {} 
//创建一个中转函数 
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 () {},         
//所以调用官方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方法的第一个参数this是可以不传的,需要分2种情况

直接调用bind之后的方法

var f = function () { console.log('不传默认为'+this)  };f.bind()() // 不传默认为 Window
ログイン後にコピー

所以直接调用绑定方法时候 apply(that, 建议改为 apply(that||window,,其实不改也可以,因为不传默认指向window

使用new实例化被绑定的方法

容易糊涂,重点在于弄清楚标准的bind方法在new的时候做的事情,然后就可以清晰的实现

这里我们需要看看 new 这个方法做了哪些操作 比如说 var a = new b()

创建一个空对象 a = {},并且this变量引用指向到这个空对象a

继承被实例化函数的原型 :a.__proto__ = b.prototype

被实例化方法b的this对象的属性和方法将被加入到这个新的 this 引用的对象中: b的属性和方法被加入的 a里面

新创建的对象由 this 所引用 :b.call(a)

通过以上可以得知,如果是var after_new = new bindFun(); 由于这种行为是把原函数当成构造器,那么那么最终实例化之后的对象this需要继承自原函数, 而这里的 bindFun 目前是

function(){ 
            return _this.apply(that || window, 
                args.concat(Array.prototype.slice.apply(arguments,[0])) 
            ) 
        }
ログイン後にコピー

这里apply的作用域是绑定的that || window,在执行 testBind()的时候就已经固定,并没有把原函数的this对象继承过来,不符合我们的要求,我们需要根据apply的特性解决这个问题:

在一个子构造函数中,你可以通过调用父构造函数的 `apply/call` 方法来实现继承

例如

function Product(name, price) { 
  this.name = name; 
  this.price = price; 
 
  if (price < 0) { 
    throw RangeError(&#39;Cannot create product &#39; + 
                      this.name + &#39; with a negative price&#39;); 
  } 
} 
 
function Food(name, price) { 
  Product.call(this, name, price);  
  this.category = &#39;food&#39;; 
} 
 
//等同于(其实就是把Product放在Food内部执行了一次) 
function Food(name, price) {  
    this.name = name; 
    this.price = price; 
    if (price < 0) { 
        throw RangeError(&#39;Cannot create product &#39; + 
                this.name + &#39; with a negative price&#39;); 
    } 
 
    this.category = &#39;food&#39;;  
}
ログイン後にコピー

所以在new新的实例的时候实时将这个新的this对象 进行 apply 继承原函数的 this 对象,就可以达到 new 方法里面的第 3 步的结果

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 () {}, 
        //所以调用官方bind方法之后 有一个name属性值为 &#39;bound &#39; 
        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 是区分 bindFun 是直接调用还是被 new 之后再调用,通过原型链的继承关系可以知道,

bindFun 属于 after_new的父类,所以 after_new instanceof bindFun 为 true,同时

bindFun.prototype = new fNOP() 原型继承; 所以 fNOP 也是 after_new的父类, after_new instanceof fNOP 为 true

最终结果

Function.prototype.testBind = function(that){ 
        var _this = this, 
            slice = Array.prototype.slice, 
            args = slice.apply(arguments,[1]), 
            fNOP = function () {}, 
            bound = function(){ 
                //这里的this指的是调用时候的环境 
                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,
ログイン後にコピー

我个人觉得这里有点不正确,如果绑定时候不传参数,那么that就为空,那无论怎样就只能绑定 window作用域了。

以上就是javascript原生一步步实现bind分析的内容,更多相关内容请关注PHP中文网(www.php.cn)!


関連ラベル:
ソース:php.cn
このウェブサイトの声明
この記事の内容はネチズンが自主的に寄稿したものであり、著作権は原著者に帰属します。このサイトは、それに相当する法的責任を負いません。盗作または侵害の疑いのあるコンテンツを見つけた場合は、admin@php.cn までご連絡ください。
最新の問題
人気のチュートリアル
詳細>
最新のダウンロード
詳細>
ウェブエフェクト
公式サイト
サイト素材
フロントエンドテンプレート