首先要理解調用位置: 調用位置就是函數在代碼中被調用的位置(而不是聲明的位置)。javascript
最重要的是要分析調用棧(就是爲了到達當前執行位置所調用的全部函數)。 咱們關心的調用位置就在當前正在執行的函數的前一個調用中。java
function baz() { // 當前調用棧是: baz // 所以, 當前調用位置是全局做用域 console.log( "baz" ); bar(); // <-- bar 的調用位置 } function bar() { // 當前調用棧是 baz -> bar // 所以, 當前調用位置在 baz 中 console.log( "bar" ); foo(); // <-- foo 的調用位置 } function foo() { // 當前調用棧是 baz -> bar -> foo // 所以, 當前調用位置在 bar 中 console.log( "foo" ); } baz(); // <-- baz 的調用位置
默認綁定。數組
最經常使用的函數調用類型:獨立函數調用。能夠把這條規則看做是沒法應用其餘規則時的默認規則。瀏覽器
function foo() { console.log( this.a ); } var a = 2; foo(); // 2
若是使用嚴格模式(strict mode), 那麼全局對象將沒法使用默認綁定, 所以 this 會綁定到 undefined:app
function foo() { "use strict"; console.log( this.a ); } var a = 2; foo(); // TypeError: this is undefined
隱式綁定函數
另外一條須要考慮的規則是調用位置是否有上下文對象, 或者說是否被某個對象擁有或者包含, 不過這種說法可能會形成一些誤導。oop
function foo() { console.log( this.a ); } var obj = { a: 2, foo: foo }; obj.foo(); // 2
首先須要注意的是 foo() 的聲明方式, 及其以後是如何被看成引用屬性添加到 obj 中的。可是不管是直接在 obj 中定義仍是先定義再添加爲引用屬性, 這個函數嚴格來講都不屬於obj 對象。
然而, 調用位置會使用 obj 上下文來引用函數, 所以你能夠說函數被調用時 obj 對象「擁有」 或者「包含」 它。this
當函數引用有上下文對象時, 隱式綁定規則會把函數調用中的 this 綁定到這個上下文對象。prototype
隱式丟失:一個最多見的 this 綁定問題就是被隱式綁定的函數會丟失綁定對象, 也就是說它會應用默認綁定, 從而把 this 綁定到全局對象或者 undefined 上, 取決因而否是嚴格模式。code
function foo() { console.log( this.a ); } var obj = { a: 2, foo: foo }; var bar = obj.foo; // 函數別名! var a = "oops, global"; // a 是全局對象的屬性 bar(); // "oops, global
雖然 bar 是 obj.foo 的一個引用, 可是實際上, 它引用的是 foo 函數自己, 所以此時的bar() 實際上是一個不帶任何修飾的函數調用, 所以應用了默認綁定。
一種更微妙、 更常見而且更出乎意料的狀況發生在傳入回調函數時:
function foo() { console.log( this.a ); } function doFoo(fn) { // fn 其實引用的是 foo fn(); // <-- 調用位置! } var obj = { a: 2, foo: foo }; var a = "oops, global"; // a 是全局對象的屬性 doFoo( obj.foo ); // "oops, global"
參數傳遞其實就是一種隱式賦值, 所以咱們傳入函數時也會被隱式賦值,回調函數丟失 this 綁定是很是常見的。
顯式綁定
像call, apply, bind這三種能夠直接指定 this 的綁定對象的方法,咱們稱之爲顯式綁定。
*若是你傳入了一個原始值(字符串類型、 布爾類型或者數字類型) 來看成 this 的綁定對象, 這個原始值會被轉換成它的對象形式(也就是 new String(..)、 new Boolean(..) 或者new Number(..))。 這一般被稱爲「裝箱」。
new綁定
JavaScript 中 new 的機制實際上和麪向類的語言徹底不一樣。
在 JavaScript 中, 構造函數只是一些使用 new 操做符時被調用的函數。 它們並不會屬於某個類, 也不會實例化一個類。 實際上,它們甚至都不能說是一種特殊的函數類型, 它們只是被 new 操做符調用的普通函數而已。
使用 new 來調用函數, 或者說發生構造函數調用時, 會自動執行下面的操做:
- 建立(或者說構造) 一個全新的對象。
- 這個新對象會被執行 [[ 原型 ]] 鏈接。
- 這個新對象會綁定到函數調用的 this。
- 若是函數沒有返回其餘對象, 那麼 new 表達式中的函數調用會自動返回這個新對象。
function foo(a) { this.a = a; } var bar = new foo(2); console.log( bar.a ); // 2
如今咱們能夠根據優先級來判斷函數在某個調用位置應用的是哪條規則。 能夠按照下面的順序來進行判斷:
在某些場景下 this 的綁定行爲會出乎意料, 你認爲應當應用其餘綁定規則時, 實際上應用的多是默認綁定規則。
function foo() { console.log( this.a ); } var a = 2; foo.call( null ); // 2
那麼什麼狀況下你會傳入null呢?
一種很是常見的作法是使用 apply(..) 來「展開」 一個數組, 並看成參數傳入一個函數。相似地, bind(..) 能夠對參數進行柯里化(預先設置一些參數), 這種方法有時很是有用:
function foo(a,b) { console.log( "a:" + a + ", b:" + b ); } // 把數組「展開」 成參數 foo.apply( null, [2, 3] ); // a:2, b:3 // 使用 bind(..) 進行柯里化 var bar = foo.bind( null, 2 ); bar( 3 ); // a:2, b:3
這兩種方法都須要傳入一個參數看成 this 的綁定對象。 若是函數並不關心 this 的話, 你仍然須要傳入一個佔位值, 這時 null 多是一個不錯的選擇, 就像代碼所示的那樣。
然而, 老是使用 null 來忽略 this 綁定可能產生一些反作用。 若是某個函數確實使用了this(好比第三方庫中的一個函數), 那默認綁定規則會把 this 綁定到全局對象(在瀏覽器中這個對象是 window), 這將致使不可預計的後果(好比修改全局對象)。
若是咱們在忽略 this 綁定時老是傳入一個 DMZ 對象, 那就什麼都不用擔憂了, 由於任何對於 this 的使用都會被限制在這個空對象中, 不會對全局對象產生任何影響。
在 JavaScript 中建立一個空對象最簡單的方法都是 Object.create(null),Object.create(null) 和 {} 很 像, 但 是 並 不 會 創 建 Object.prototype 這個委託, 因此它比 {}「更空」 :
function foo(a,b) { console.log( "a:" + a + ", b:" + b ); } // 咱們的 DMZ 空對象 var ø = Object.create( null ); // 把數組展開成參數 foo.apply( ø, [2, 3] ); // a:2, b:3 // 使用 bind(..) 進行柯里化 var bar = foo.bind( ø, 2 ); bar( 3 ); // a:2, b:3
另外一個須要注意的是, 你有可能(有意或者無心地) 建立一個函數的「間接引用」, 在這種狀況下, 調用這個函數會應用默認綁定規則。
function foo() { console.log( this.a ); } var a = 2; var o = { a: 3, foo: foo }; var p = { a: 4 }; o.foo(); // 3 (p.foo = o.foo)(); // 2
賦值表達式 p.foo = o.foo 的返回值是目標函數的引用, 所以調用位置是 foo() 而不是p.foo() 或者 o.foo()。 根據咱們以前說過的, 這裏會應用默認綁定。
注意: 對於默認綁定來講, 決定 this 綁定對象的並非調用位置是否處於嚴格模式, 而是函數體是否處於嚴格模式。 若是函數體處於嚴格模式, this 會被綁定到 undefined, 不然this 會被綁定到全局對象。
感慨一下這個究極藝術,原由硬綁定會大大下降函數的靈活性, 使用硬綁定以後就沒法使用隱式綁定或者顯式綁定來修改 this。
若是能夠給默認綁定指定一個全局對象和 undefined 之外的值, 那就能夠實現和硬綁定相同的效果, 同時保留隱式綁定或者顯式綁定修改 this 的能力。這個就是軟綁定。
實現以下:
if (!Function.prototype.softBind) { Function.prototype.softBind = function(obj) { var fn = this; // fn就是調用的函數 // 捕獲全部 curried 參數 var curried = [].slice.call( arguments, 1 ); var bound = function() { return fn.apply( (!this || this === (window || global)) ? obj : this curried.concat.apply( curried, arguments ) //這裏的argements是bound的arguments,也就是說在softBind的時候能夠傳參一次,後面能夠再傳一次,參數會在這裏合併起來 ); }; bound.prototype = Object.create( fn.prototype ); // 原型鏈繼承過來 return bound; }; }
首先檢查調用時的 this, 若是 this 綁定到全局對象或者 undefined, 那就把指定的默認對象 obj 綁定到 this, 不然不會修改 this。
應用場景:
function foo() { console.log("name: " + this.name); } var obj = { name: "obj" }, obj2 = { name: "obj2" }, obj3 = { name: "obj3" }; var fooOBJ = foo.softBind( obj ); fooOBJ(); // name: obj obj2.foo = foo.softBind(obj); obj2.foo(); // name: obj2 <---- 看! ! ! fooOBJ.call( obj3 ); // name: obj3 <---- 看! setTimeout( obj2.foo, 10 ); // name: obj <---- 應用了軟綁定
能夠看到, 軟綁定版本的 foo() 能夠手動將 this 綁定到 obj2 或者 obj3 上, 但若是應用默認綁定, 則會將 this 綁定到 obj。