從本文你可以得到:javascript
廢話很少說,咱們一塊兒來看看this的綁定機制。前端
開局上結論this有四種綁定模式分別是默認綁定、隱式綁定、顯式綁定、new綁定。java
他們之間的優先級關係爲:new綁定 > 顯式綁定 > 隱式綁定 > 默認綁定。面試
讓咱們來看一些例子分清這幾種綁定:app
例子1:默認綁定函數
// 默認綁定
var str = 'hello world'
function log() {
console.log(this.str)
}
// 此時this默認指向window
log() // hello world
// 在嚴格模式下this默認指向undefined的
'use strict'
var str2 = 'hello world'
function log() {
console.log(this.str2)
}
// 此時this指向undefined
log() // 報錯TypeError,由於程序從undefined中獲取str2
複製代碼
例子2:隱式綁定post
// 隱式綁定通常發生在函數做爲對象屬性調用時
var bar = 'hello'
function foo() {
console.log(this.bar)
}
var obj = {
bar: 'world',
foo: foo
}
foo() // hello,this指向window因此輸出hello
obj.foo() // world,this隱式綁定了obj,這時候this指向obj因此輸出world
複製代碼
例子3:顯式綁定ui
// 顯式綁定就是咱們常談的apply,call,bind
var bar = 'hello'
var context = {
bar: 'world'
}
function foo() {
console.log(this.bar);
}
foo() // hello
foo.call(context) // world 可見此時this的指向已經變成了context
複製代碼
例子4:new綁定this
new綁定比較特殊,new大部分狀況下是建立一個新的對象,並將this指向這個新對象,最後返回這個對象。spa
function Foo(bar) {
this.bar = bar
}
// 建立一個新的對象,並將this指向這個對象,將這個對象返回賦值給foo
var foo = new Foo(3);
foo.bar // 3
複製代碼
說完this的綁定類型,咱們考慮下下面的代碼的輸出
var context = {
bar: 2
}
function Foo() {
this.bar = 'new bar'
}
var FooWithContext = Foo.bind(context);
var foo = new FooWithContext();
// 考慮下面代碼的輸出
console.log(foo.bar)
console.log(context.bar)
// 結果是:new bar 2
/** * 咱們能夠發現雖然將使用bind函數將this綁定到context上, * 但被new調用的Foo,他的this並無綁定到context上。 */
複製代碼
從上述例子2能夠推斷隱式綁定優先級是高於默認綁定的,因此這裏咱們只推導後續三種的綁定的優先級關係。
例子5:
// 咱們先驗證隱式綁定和顯式綁定的優先級關係
var context = {
bar: 1
}
function foo() {
// 對bar進行賦值
this.bar = 3;
}
// 進行顯式綁定
var fooWithContext = foo.bind(context);
var instance = new fooWithContext();
console.log(context.bar); // 1
console.log(instance.bar); // 3
// 可見foo並無改變context.bar的值而是建立了一個新對象,符合咱們對new綁定的描述
複製代碼
根據上面的列子咱們能夠得出結論new綁定 > 顯式綁定
根據例子2和例子3,咱們能夠輕鬆推導出隱式綁定和顯式綁定要優先級高於默認綁定
咱們驗證一下隱式綁定和顯式綁定的優先級關係。
例子6:
var obj = {
bar: 2
}
var context = {
bar: 3
}
function foo () {
this.bar = 4
}
// 將foo的this綁定到context上
var fooWithContext = foo.bind(context);
// 將綁定後的函數,賦值給obj的屬性foo
obj.foo = fooWithContext;
obj.foo();
console.log(obj.bar); // 2 並無改變obj.bar的值
console.log(context.bar); // 4 context.bar的值發生了改變
複製代碼
可見顯式綁定的this優先級要高於隱式綁定
最後咱們即可以得出結論new綁定 > 顯式綁定 > 隱式綁定 > 默認綁定。
剛剛說了那麼多this的綁定問題,這到底和咱們實現bind有什麼關係?
咱們來看看一段簡單的bind的實現代碼:
Function.prototype.bind(context, ...args) {
const fn = this;
return (...innerArgs) => {
return fn.call(context, ...args, ...innerArgs)
}
}
複製代碼
這個bind函數,在大部分狀況都是能正常工做的,可是咱們考慮以下場景:
function foo() {
this.bar = 3
}
var context = {
bar: 4
}
var fooWithContext = foo.bind(context);
var fooInstance = new fooWithContext();
console.log(context.bar) // 3
複製代碼
能夠看到,被new調用後的foo,在運行時this依然指向context,這不符合咱們剛剛根據原生方法推斷的綁定優先級:new綁定 > 顯式綁定
因此咱們在實現bind的時候,須要考慮維護new調用的狀況
咱們來看看如何實現一個真正的bind:
Function.prototype.bind(context, ..args) {
var fToBind = this;
// 先聲明一個空函數,用途後面介紹
var fNop = function() {};
var fBound = function(...innerArgs) {
// 若是被new調用,this應該是fBound的實例
if(this instanceof fBound) {
/** * cover住new調用的狀況 * 因此其實咱們這裏要模擬fToBind被new調用的狀況,並返回 * 咱們使用new建立的對象替換掉bind傳進來的context */
return fToBind.call(this, ...args, ...innerArgs)
} else {
// 非new調用狀況下的正常返回
return fToBind.call(context, ...args, ...innerArgs)
}
}
// 除了維護new的this綁定,咱們還須要維護new致使的原型鏈變化
// 執行new後返回的對象的原型鏈會指向fToBind
// 可是咱們調用bind後實際返回的是fBound,因此咱們這裏須要替換掉fBound的原型
fNop.prototype = this.prototype;
// fBound.prototype.__proto__ = fNop.prototype
fBound.prototype = new fNop();
/** * 這樣當new調用fBound後,實例依然能訪問fToBind的原型方法 * 爲何不直接fBound.prototype = this.prototype呢 * 考慮下將fBound返回後,給fBound添加實例方法的狀況 * 即fBound.prototype.anotherMethod = function() {} * 若是將fToBind的原型直接賦值給fBound的原型,添加原型方法就會 * 污染源方法即fToBind的原型 */
return fBound
}
複製代碼
到這裏咱們就實現了一個符合原生表現的bind函數,可是有時候架不住有人問那不用apply和call如何實現bind呢?接下來咱們使用隱式綁定來實現一個bind
咱們剛剛分析完實現bind的實現須要注意的點,這裏就不重複說明了,咱們看看如何使用隱式綁定來模仿bind。
// 咱們把關注點放在如何替換call方法上
Function.prototype.bind(context, ...args) {
var fToBind = this;
var fNop = function() {};
var fBound = function(...innerArgs) {
// 咱們將fToBind賦值給context一個屬性上。
context.__fn = fToBind;
if(this instanceof fBound) {
// 模擬new調用,建立一個新對象,新對象的原型鏈指向fBound的原型
var instance = Object.create(fBound);
instance.__fn = fToBind;
var result = instance.__fn(...args, ...innerArgs);
delete instance.__fn;
// new調用時,若是構造函數返回了對象,使用返回的對象替換this
if(result) return result;
return instance;
} else {
// 在__fn沒有顯式綁定的狀況下,__fn運行時this指向context
var result = context.__fn(...args, ...innerArgs);
// 調用完後將context的__fn屬性刪除
delete context.__fn;
return result;
}
}
fNop.prototype = this.prototype;
fBound.prototype = new fNop();
return fBound;
}
複製代碼
到這裏不使用apply實現的bind就大功告成了
我來總結下一共有哪些點須要認清楚:
看了這麼多可能會有朋友問,箭頭函數呢?
歡迎閱讀我其餘文章:
參考資料: