當一個函數被調用時,會建立一個執行上下文。這個執行上下文會包含函數在哪裏被調用(執行棧)、函數的調用方式、傳入的參數等信息。this就是這個執行上下文的一個屬性,會在函數執行的過程當中用到。javascript
在理解this
的綁定過程以前,首先要理解調用位置:調用位置就是函數在代碼中被調用的位置(而不是聲明的位置)。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的調用位置
this
的綁定規則總共有5種:安全
new
綁定獨立函數調用 :能夠把這條規則看做是沒法應用其餘規則時的默認規則。閉包
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: Cannot read property 'a' of undefined
當函數引用有上下文對象時,隱式綁定規則會把函數中的this
綁定到這個上下文對象。函數
function foo() { console.log( this.a ); } var obj = { a: 2, foo: foo }; obj.foo(); // 2
對象屬性引用鏈中只有上一層或者說最後一層在調用位置中起做用。oop
function foo() { console.log( this.a ); } var obj2 = { a: 42, foo: foo }; var obj1={ a:2, obj2:obj2 } obj1.obj2.foo() // 42
隱式丟失post
被隱式綁定的函數特定狀況下會丟失綁定對象,它會應用默認綁定,把this
綁定到全局對象或者undefined
上(取決因而否是嚴格模式)。this
// 雖然bar是obj.foo的一個引用,可是實際上,它引用的是foo函數自己。 // 所以此時的bar()是一個不帶任何修飾的函數調用,所以應用了默認綁定。 function foo() { console.log( this.a ); } var obj = { a: 2, foo: foo }; var bar = obj.foo; // 函數別名! var a = "oops, global"; // a是全局對象的屬性 bar(); // "oops, global"
一種更微妙、更常見而且更出乎意料的狀況發生在傳入回調函數時:
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"
參數傳遞其實就是一種隱式賦值,咱們傳入函數時也會被隱式賦值。
若是把函數傳入語言內置的函數而不是傳入本身聲明的函數,結果是同樣的。
function foo() { console.log( this.a ); } var obj = { a: 2, foo: foo }; var a = "oops, global"; // a是全局對象的屬性 setTimeout(obj.foo,100); // "oops, global" // JS環境中內置的setTimeout()函數實現和下面的僞代碼相似: function setTimeout(fn, delay) { // 等待delay毫秒 fn(); // <-- 調用位置! }
可使用函數的call(...)
和apply(...)
方法,它們的第一個參數是一個對象,是給this
準備的,接着在調用函數時將其綁定到this
。由於你能夠直接指定this
的綁定對象,所以咱們稱之爲顯式綁定。
function foo() { console.log( this.a ); } var obj = { a: 2 }; foo.call( obj ); // 2
經過foo.call(...)
咱們能夠在調用foo
時強制把它的this
綁定到obj
上。
惋惜,顯式綁定仍然沒法解決咱們以前提出的丟失綁定的問題。
硬綁定
可是顯式綁定的一個變種能夠解決這個問題。
function foo() { console.log( this.a ); } var obj = { a: 2 }; var bar = function() { foo.call( obj ); }; bar(); // 2 setTimeout( bar, 100 ); // 2 // 硬綁定的bar不可能再修改它的this bar.call( window ); // 2
咱們建立了函數bar()
,並在它的內部手動調用了foo.call(obj)
,所以強制把foo
的this
綁定到了obj
。
不管以後如何調用函數bar
,它總會手動在obj
上調用foo
。咱們稱之爲硬綁定。
典型應用場景是建立一個包裹函數,負責接收參數並返回值:
function foo(something) { console.log( this.a, something ); return this.a + something; } var obj = { a: 2 }; var bar = function() { return foo.apply( obj, arguments ); }; var b = bar( 3 ); // 2 3 console.log( b ); // 5
另外一種使用方法是建立一個能夠重複使用的輔助函數:
function foo(something) { console.log( this.a, something ); return this.a + something; } // 簡單的輔助綁定函數 function bind(fn, obj) { return function() { return fn.apply( obj, arguments ); } } var obj = { a: 2 }; var bar = bind( foo, obj ); var b = bar( 3 ); // 2 3 console.log( b ); // 5
ES5提供了內置的方法Function.prototype.bind
:
function foo(something) { console.log( this.a, something ); return this.a + something; } var obj = { a: 2 }; var bar = foo.bind( obj ); var b = bar( 3 ); // 2 3 console.log( b ); // 5
bind(...)
會返回一個硬編碼的新函數,它會把你指定的參數設置爲this
的上下文並調用原始函數。
API調用的「上下文」
第三方庫以及JavaScript語言和宿主環境中許多新的內置函數,都提供了一個可選的參數,一般被稱爲「上下文」(context),其做用和bind(...)
同樣,確保你的回調函數使用指定的this
。
function foo(el) { console.log( el, this.id ); } var obj = { id: "awesome" } let myArr = [1,2,3] // 調用foo(..)時把this綁定到obj myArr.forEach( foo, obj ); // 1 awesome 2 awesome 3 awesome
這些函數實際上就是經過call(...)
或者apply(...)
實現了顯式綁定。
在Javascript中,構造函數只是一些使用new
操做符時被調用的函數。它們並不屬於某個類,也不會實例化一個類。
包括內置對象函數(好比Number(...)
)在內的全部函數均可以用new
來調用,這種函數調用被稱爲構造函數調用。
實際上並不存在所謂的「構造函數」,只有對於函數的「構造調用」。
使用new
來調用函數,或者說發生構造函數調用時,會自動執行下面的操做。
this
。new
表達式中的函數調用會自動返回這個新對象。function foo(a) { this.a = a; } var bar = new foo(2); console.log( bar.a ); // 2
使用new
來調用foo(...)
時,咱們會構造一個新對象並把它綁定到foo(...)
調用中的this
上。
new
是又一種能夠影響函數調用時this
綁定行爲的方法,咱們稱之爲new
綁定。
function foo1() { console.log(this.a) } function foo2(something) { this.a = something } var obj1 = { a: 2, foo: foo1 } var obj2 = { a: 3, foo: foo1 } var obj3 = { foo: foo2 } var obj4 = {} obj1.foo(); //2 obj2.foo(); //3 obj1.foo.call(obj2); //3 obj2.foo.call(obj1); //2 //可見,顯式綁定比隱式綁定優先級高 obj3.foo(4); console.log(obj3.a); //4 obj3.foo.call(obj4, 5); console.log(obj4.a); //5 var bar = new obj3.foo(6); console.log(obj3.a); //4 console.log(bar.a); //6 //可見,new綁定比隱式綁定優先級高 var qux = foo2.bind(obj4); qux(7); console.log(obj4.a); //7 var quux = new qux(8); console.log(obj4.a); //7 console.log(quux.a); //8 //new綁定修改了硬綁定(到obj4的)調用qux(...)中的this。
如今,咱們能夠根據優先級來判斷函數在某個調用位置應用的是哪條規則。
new
中調用(new
綁定),若是是的話this
綁定的是新建立的對象。call
、apply
(顯式綁定)或者硬綁定調用,若是是的話this
綁定的是指定的對象。this
綁定的就是那個上下文對象。若是都不是的話,使用默認綁定。
undefined
。被忽略的this
若是你把null
或者undefined
做爲this
的綁定對象傳入call
、apply
或者bind
,這些值在調用時會被忽略,實際應用的是默認規則。
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
老是使用null
來忽略this
綁定可能會產生一些反作用。
若是某個函數確實使用了this
(比方說第三方庫中的一個函數),那默認綁定規則會把this
綁定到全局對象,這將致使不可預計的後果(好比修改全局對象)。
更安全的this
一種「更安全」得作法是傳入一個特殊的對象,把this
綁定到這個對象不會對你的程序產生任何反作用。
在Javascript中建立一個空對象最簡單的方法是Object.create(null)。它和{}
很想,可是並不會建立Object.prototype
這個委託。
function foo(a, b) { console.log( "a:" + a + ",b:" + b ); } // 咱們的空對象 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
強制綁定到指定的對象(除了使用new
時),防止函數調用應用默認綁定規則。
缺點是硬綁定會大大下降函數的靈活性,使用硬綁定以後就沒法使用隱式綁定或者顯式綁定來修改this
。
若是能夠給默認綁定指定一個全局對象和undefined之外的值,那就能夠實現和硬綁定相同的效果,同時保留隱式綁定或者顯式綁定來修改this
。
if(!Function.prototype.softBind) { Function.prototype.softBind = function(obj) { var fn = this; // 捕獲全部curried參數 var curried = [].slice.call( arguments, 1 ); var bound = function() { return fn.apply( (!this || this === (window || global)) ? obj : this, curried.concat.apply( curried, arguments ) ); }; bound.prototype = Object.create( fn.prototype ); return bound; }; }
除了軟綁定以外,softBind(...)
的其它原理和ES5內置的bind(...)
相似。
它會對指定的函數進行封裝,首先檢查調用時的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
。
咱們以前介紹的四條規則能夠包含全部正常的函數。可是ES6
中介紹了一種沒法使用這些規則的特殊函數類型:箭頭函數。
箭頭函數不使用this
的四種標準規則,而是根據外層(函數或者全局)做用域來決定this
。
function foo() { //返回一個箭頭函數 return (a) => { //this繼承自foo() console.log(this.a); }; } var obj1 = { a:2 }; var obj2 = { a:3 }; var bar =foo.call(obj1); bar.call(obj2); //2,不是3!
foo()
內部建立的箭頭函數會捕獲調用時的foo()
的this
。因爲foo()
的this
綁定到obj1
,bar
(引用箭頭函數)的this
也會綁定到obj1
,箭頭函數的綁定沒法被修該。(new
也不行!)
箭頭函數經常使用於回調函數中,例如事件處理器或者定時器:
function foo(){ setTimeout(()=>{ //這裏的this在詞法上繼承自foo() console.log(this.a); },100); } var obj = { a:2 }; foo.call(obj) //2
1.箭頭函數沒有prototype
(原型),因此箭頭函數自己沒有this
。
let a = () => {} console.log(a.prototype) //undefined
2.箭頭函數中的this
是從定義它們的上下文繼承的(Javascript權威指南第7版P206),繼承自外層第一個普通函數的this
。
let foo let barObj = { msg: 'bar的this指向' } let bazObj = { msg: 'baz的this指向' } bar.call(barObj) //bar的this指向barObj baz.call(bazObj) //baz的this指向bazObj function bar() { foo = () => { console.log(this, 'this指向定義它們的上下文,外層的第一個普通函數') } } function baz() { foo() } //msg: "bar的this指向" "this指向定義它們的上下文,外層的第一個普通函數"
3.箭頭函數的this
沒法經過bind
,call
,apply
來直接修改。
let quxObj = { msg: '嘗試直接修改箭頭函數的this指向' } function baz() { foo.call(quxObj) } //{msg: "bar的this指向"} "this指向定義它們的上下文,外層的第一個普通函數"
間接修改箭頭函數的指向:
bar.call(bazObj) //普通函數bar的this指向bazObj,內部的箭頭函數也會指向bazObj
被繼承的普通函數的this
指向改變,箭頭函數的this
指向也會跟着改變。
4.若是箭頭函數沒有外層函數,this
指向window
var obj = { i: 10, b: () => console.log(this.i, this), c: function() { console.log( this.i, this) } } obj.b()//undefined, window obj.c()//10, {i: 10, b: ƒ, c: ƒ}
/** * Question 1 * 非嚴格模式下 */ var name = 'window' var person1 = { name: 'person1', show1: function () { console.log(this.name) }, show2: () => console.log(this.name), show3: function () { return function () { console.log(this.name) } }, show4: function () { return () => console.log(this.name) } } var person2 = { name: 'person2' } person1.show1() person1.show1.call(person2) person1.show2() person1.show2.call(person2) person1.show3()() person1.show3().call(person2) person1.show3.call(person2)() person1.show4()() person1.show4().call(person2) person1.show4.call(person2)()
正確答案以下:
person1.show1() // person1,隱式綁定 person1.show1.call(person2) // person2,顯式綁定 person1.show2() // window,箭頭函數綁定,沒有外層函數,指向window person1.show2.call(person2) // window,箭頭函數綁定,不能直接修改,仍是指向window person1.show3()() // window,高階函數,person1.show3()返回一個函數ƒ(){ console.log(this.name)}到全局 //從而致使最終函數執行環境是window,因此此時this 指向 var name = 'window' person1.show3().call(person2) // person2 ,返回函數之後,顯式綁定person2,this指向person2對象 person1.show3.call(person2)() // window,高階函數ƒ(){return function(){console.log(this.name)}} 顯式綁定person2, //也就是高階函數this指向person2,它的返回值ƒ(){console.log(this.name)}執行環境是window,同上 person1.show4()() // person1,箭頭函數綁定,this是從定義函數的上下文繼承的,也就是外層函數所在的上下文,外層函數的this指向person1 person1.show4().call(person2) // person1,沒法經過call直接修改箭頭函數綁定 person1.show4.call(person2)() // person2,高階函數,外層函數this顯式綁定person2,修改箭頭函數的外層函數this指向,能夠改變箭頭函數this指向
/** * Question 2 */ var name = 'window' function Person (name) { this.name = name; this.show1 = function () { console.log(this.name) } this.show2 = () => console.log(this.name) this.show3 = function () { return function () { console.log(this.name) } } this.show4 = function () { return () => console.log(this.name) } } var personA = new Person('personA') var personB = new Person('personB') personA.show1() personA.show1.call(personB) personA.show2() personA.show2.call(personB) personA.show3()() personA.show3().call(personB) personA.show3.call(personB)() personA.show4()() personA.show4().call(personB) personA.show4.call(personB)()
正確答案以下:
personA.show1() // personA,new綁定之後,構造函數Person中的this綁定到personA,Person傳入的參數personA,因此結果是personA personA.show1.call(personB) // personB,new綁定之後,構造函數Person中的this綁定到personA,personA.show1就是ƒ(){console.log(this.name)} // 再顯式綁定personB,personA.show1的this指向personB實例對象,因此結果是personB personA.show2() // personA,new綁定之後,構造函數Person中的this綁定到personA,personA.show2就是()=>console.log(this.name) //而後箭頭函數綁定,調用箭頭函數,this指向外層函數的this.name,也就是personA personA.show2.call(personB) // personA,new綁定之後,構造函數Person中的this綁定到personA,personA.show2就是()=>console.log(this.name) //箭頭函數不能直接修改,因此仍是personA personA.show3()() // window,new綁定之後,構造函數Person中的this綁定到personA,personA.show3()返回一個函數ƒ(){console.log(this.name)}到全局 // 執行環境是window,因此執行之後的結果是var name = 'window',也就是window personA.show3().call(personB) // personB,new綁定之後,構造函數Person中的this綁定到personA, // personA.show3()返回一個函數ƒ(){console.log(this.name)}到全局, // 再顯式綁定personB,因此最終結果是personB personA.show3.call(personB)() // window,new綁定之後,構造函數Person中的this綁定到personA, //高階函數ƒ(){return function(){console.log(this.name)}}顯式綁定personB, //返回一個函數ƒ(){console.log(this.name)}到全局 //執行環境是window,因此執行之後的結果是var name = 'window',也就是window personA.show4()() // personA,new綁定之後,構造函數Person中的this綁定到personA, // 高階函數ƒ(){return ()=>console.log(this.name)}執行後返回箭頭函數()=>console.log(this.name),執行箭頭函數 // 箭頭函數綁定,繼承外層普通函數this,因此結果是personA personA.show4().call(personB) // personA,new綁定之後,構造函數Person中的this綁定到personA, // 高階函數ƒ(){return ()=>console.log(this.name)}執行後返回箭頭函數()=>console.log(this.name), // 箭頭函數不能直接修改,因此結果仍是personA personA.show4.call(personB)() // personB,new綁定之後,構造函數Person中的this綁定到personA, // 顯式綁定 外層函數 ,因此箭頭函數也被修改成 personB
[你不知道的JavaScript 上卷]
[Javascript權威指南第七版]