[JavaScript總結]this綁定全面解析

this是什麼

當一個函數被調用時,會建立一個執行上下文。這個執行上下文會包含函數在哪裏被調用(執行棧)、函數的調用方式、傳入的參數等信息。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種:安全

  1. 默認綁定
  2. 隱式綁定
  3. 顯示綁定
  4. new 綁定
  5. 箭頭函數綁定

1.默認綁定

獨立函數調用 :能夠把這條規則看做是沒法應用其餘規則時的默認規則。閉包

function foo() { 
    console.log( this.a );
}

var a = 2;

foo(); // 2

嚴格模式(strict mode),不能將全局對象用於默認綁定,this會綁定到undefinedapp

function foo() { 
    "use strict";
    
    console.log( this.a );
}

var a = 2;

foo(); // TypeError: Cannot read property 'a' of undefined

2.隱式綁定

當函數引用有上下文對象時,隱式綁定規則會把函數中的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(); // <-- 調用位置!
}

3.顯式綁定

可使用函數的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),所以強制把foothis綁定到了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(...)實現了顯式綁定

4.new綁定

在Javascript中,構造函數只是一些使用new操做符時被調用的函數。它們並不屬於某個類,也不會實例化一個類。

包括內置對象函數(好比Number(...))在內的全部函數均可以用new來調用,這種函數調用被稱爲構造函數調用。

實際上並不存在所謂的「構造函數」,只有對於函數的「構造調用」。

使用new來調用函數,或者說發生構造函數調用時,會自動執行下面的操做。

  1. 建立(或者說構造)一個全新的對象。
  2. 這個新對象會被執行[[Prototype]]鏈接。
  3. 這個新對象會綁定到函數調用的this
  4. 若是函數沒有返回其餘對象,那麼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。

如今,咱們能夠根據優先級來判斷函數在某個調用位置應用的是哪條規則。

  1. 函數是否在new中調用(new綁定),若是是的話this綁定的是新建立的對象。
  2. 函數是否經過callapply(顯式綁定)或者硬綁定調用,若是是的話this綁定的是指定的對象。
  3. 函數是否在某個上下文對象中調用(隱式綁定),若是是的話this綁定的就是那個上下文對象。
  4. 若是都不是的話,使用默認綁定。

    • 若是在嚴格模式下,就綁定到undefined
    • 不然就綁定到全局對象。

綁定例外

被忽略的this

若是你把null或者undefined做爲this的綁定對象傳入callapply或者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綁定到obj1bar(引用箭頭函數)的this也會綁定到obj1,箭頭函數的綁定沒法被修該。(new也不行!)

箭頭函數經常使用於回調函數中,例如事件處理器或者定時器:

function foo(){
    setTimeout(()=>{
        //這裏的this在詞法上繼承自foo()
        console.log(this.a);
    },100);
}

var obj = {
    a:2
};

foo.call(obj) //2

箭頭函數中的this

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沒法經過bindcallapply直接修改。

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權威指南第七版]

從這兩套題,從新認識JS的this、做用域、閉包、對象

詳解箭頭函數和普通函數的區別以及箭頭函數的注意事項、不適用場景

相關文章
相關標籤/搜索