JavaScript必須掌握的基礎 --- this

this

this是咱們在書寫代碼時最經常使用的關鍵詞之一,即便如此,它也是JavaScript最容易被最頭疼的關鍵詞。那麼this究竟是什麼呢?git

若是你瞭解執行上下文,那麼你就會知道,其實this是執行上下文對象的一個屬性:github

executionContext = {
    scopeChain:[ ... ],
    VO:{
        ...
    },
    this:  ? 
}

執行上下文中有三個重要的屬性,做用域鏈(scopeChain)、變量對象(VO)和this。瀏覽器

this是在進入執行上下文時肯定的,也就是在函數執行時才肯定,而且在運行期間不容許修改而且是永久不變的閉包

在全局代碼中的this

在全局代碼中this 是不變的,this始終是全局對象自己。app

var a = 10; 
this.b = 20;
window.c = 30;

console.log(this.a);
console.log(b);
console.log(this.c);

console.log(this === window) // true
// 因爲this就是全局對象window,因此上述 a ,b ,c 都至關於在全局對象上添加相應的屬性

若是咱們在代碼運行期嘗試修改this的值,就會拋出錯誤:ide

this = { a : 1 } ; // Uncaught SyntaxError: Invalid left-hand side in assignment
console.log(this === window) // true

函數代碼中的this

在函數代碼中使用this,纔是令咱們最容易困惑的,這裏咱們主要是對函數代碼中的this進行分析。函數

咱們在上面說過this的值是,進入當前執行上下文時肯定的,也就是在函數執行時而且是執行前肯定的。可是同一個函數,做用域中的this指向可能徹底不一樣,可是無論怎樣,函數在運行時的this的指向是不變的,並且不能被賦值。學習

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

foo();  // window
var obj={
    a: 1,
    bar: foo,
}
obj.bar(); // obj

函數中this的指向豐富的多,它能夠是全局對象、當前對象、或者是任意對象,固然這取決於函數的調用方式。在JavaScript中函數的調用方式有一下幾種方式:做爲函數調用、做爲對象屬性調用、做爲構造函數調用、使用apply或call調用。下面咱們將按照這幾種調用方式一一討論this的含義。this

做爲函數調用

什麼是做爲函數調用:就是獨立的函數調用,不加任何修飾符。prototype

function foo(){
    console.log(this === window); // true
    this.a = 1;
    console.log(b); // 2
}
var b = 2;
foo();
console.log(a); // 1

上述代碼中this綁定到了全局對象window。this.a至關於在全局對象上添加一個屬性 a 。

在嚴格模式下,獨立函數調用,this的綁定再也不是window,而是undefined

function foo() {
    "use strict";
    console.log(this===window); // false
    console.log(this===undefined); // true
}
foo();

這裏要注意,若是函數調用在嚴格模式下,而內部代碼執行在非嚴格模式下,this 仍是會默認綁定爲 window。

function foo() {
    console.log(this===window); // true
}


(function() {
    "use strict";
    foo();
})()

對於在函數內部的函數獨立調用 this 又指向了誰呢?

function foo() {
    function bar() {
        this.a=1;
        console.log(this===window); // true
    }
    bar()
}
foo();
console.log(a); // 1

上述代碼中,在函數內部的函數獨立調用,此時this仍是被綁定到了window。

總結:當函數做爲獨立函數被調用時,內部this被默認綁定爲(指向)全局對象window,可是在嚴格模式下會有區別,在嚴格模式下this被綁定爲undefined。

做爲對象屬性調用

var a=1;
var obj={
    a: 2,
    foo: function() {
        console.log(this===obj); // true
        console.log(this.a); // 2
    }
}
obj.foo();

上述代碼中 foo屬性的值爲一個函數。這裏稱 foo 爲 對象obj 的方法。foo的調用方式爲 對象 . 方法 調用。此時 this 被綁定到當前調用方法的對象。在這裏爲 obj 對象。

再看一個例子:

var a=1;
var obj={
    a: 2,
    bar: {
        a: 3,
        foo: function() {
            console.log(this===bar); // true
            console.log(this.a); // 3
        }
    }
}
obj.bar.foo();

遵循上面說的規則 對象 . 屬性 。這裏的對象爲 obj.bar 。此時 foo 內部this被綁定到了 obj.bar 。 所以 this.a 即爲 obj.bar.a 。

再來看一個例子:

var a=1;
var obj={
    a: 2,
    foo: function() {
        console.log(this===obj); // false
        console.log(this===window); // true
        console.log(this.a); // 1
    }
}

var baz=obj.foo;
baz();

這裏 foo 函數雖然做爲對象obj 的方法。可是它被賦值給變量 baz 。當baz調用時,至關於 foo 函數獨立調用,所以內部 this被綁定到 window。

使用apply或call調用

apply和call爲函數原型上的方法。它能夠更改函數內部this的指向。

var a=1;
function foo() {
    console.log(this.a);
}
var obj1={
    a: 2
}
var obj2={
    a: 3
}
var obj3={
    a: 4
}
var bar=foo.bind(obj1);
bar();// 2  this => obj1
foo(); // 1  this => window
foo.call(obj2); // 3  this => obj2
foo.call(obj3); // 4  this => obj3

當函數foo 做爲獨立函數調用時,this被綁定到了全局對象window,當使用bind、call或者apply方法調用時,this 被分別綁定到了不一樣的對象。

做爲構造函數調用

var a=1;
function Person() {
    this.a=2;  // this => p;
}
var p=new Person();
console.log(p.a); // 2

上述代碼中,構造函數 Person 內部的 this 被綁定爲 Person的一個實例。

總結:

當咱們要判斷當前函數內部的this綁定,能夠依照下面的原則:

  • 函數是否在是經過 new 操做符調用?若是是,this 綁定爲新建立的對象
var bar = new foo();     // this => bar;
  • 函數是否經過call或者apply調用?若是是,this 綁定爲指定的對象
foo.call(obj1);  // this => obj1;
foo.apply(obj2);  // this => obj2;
  • 函數是否經過 對象 . 方法調用?若是是,this 綁定爲當前對象
obj.foo(); // this => obj;
  • 函數是否獨立調用?若是是,this 綁定爲全局對象。
foo(); // this => window

DOM事件處理函數中的this

1). 事件綁定

<button id="btn">點擊我</button>

// 事件綁定

function handleClick(e) {
    console.log(this); // <button id="btn">點擊我</button>
}
        document.getElementById('btn').addEventListener('click',handleClick,false);  //   <button id="btn">點擊我</button>
        
document.getElementById('btn').onclick= handleClick; //  <button id="btn">點擊我</button>

根據上述代碼咱們能夠得出:當經過事件綁定來給DOM元素添加事件,事件將被綁定爲當前DOM對象。

2).內聯事件

<button onclick="handleClick()" id="btn1">點擊我</button>
<button onclick="console.log(this)" id="btn2">點擊我</button>

function handleClick(e) {
    console.log(this); // window
}

//第二個 button 打印的是   <button id="btn">點擊我</button>

我認爲內聯事件能夠這樣理解:

//僞代碼

<button onclick=function(){  handleClick() } id="btn1">點擊我</button>
<button onclick=function() { console.log(this) } id="btn2">點擊我</button>

這樣咱們就能理解上述代碼中爲何內聯事件一個指向window,一個指向當前DOM元素。(固然瀏覽器處理內聯事件時並非這樣的)

定時器中的this

定時器中的 this 指向哪裏呢?

function foo() {
    setTimeout(function() {
        console.log(this); // window
    },1000)
}
foo();

再來看一個例子

var name="chen";
var obj={
    name: "erdong",
    foo: function() {
        console.log(this.name); // erdong
        setTimeout(function() {
            console.log(this.name); // chen
        },1000)
    }
}
obj.foo();

到這裏咱們能夠看到,函數 foo 內部this指向爲調用它的對象,即:obj 。定時器中的this指向爲 window。那麼有什麼辦法讓定時器中的this跟包裹它的函數綁定爲同一個對象呢?

1). 利用閉包:

var name="chen";
var obj={
    name: "erdong",
    foo: function() {
        console.log(this.name) // erdong
        var that=this;
        setTimeout(function() {
            // that => obj
            console.log(that.name); // erdong
        },1000)
    }
}
obj.foo();

利用閉包的特性,函數內部的函數能夠訪問含義訪問當前詞法做用域中的變量,此時定時器中的 that 即爲包裹它的函數中的 this 綁定的對象。在下面咱們會介紹利用 ES6的箭頭函數實現這一功能。

固然這裏也能夠適用bind來實現:

var name="chen";
var obj={
    name: "erdong",
    foo: function() {
        console.log(this.name); // erdong
        setTimeout(function() {
            // this => obj
            console.log(this.name); // erdong
        }.bind(this),1000)
    }
}
obj.foo();

被忽略的this

若是你把 null 或者 undefined 做爲 this 的綁定對象傳入 call 、apply或者bind,這些值在調用時會被忽略,實例 this 被綁定爲對應上述規則。

var a=1;
function foo() {
    console.log(this.a); // 1  this => window
}
var obj={
    a: 2
}
foo.call(null);
var a=1;
function foo() {
    console.log(this.a); // 1  this => window
}
var obj={
    a: 2
}
foo.apply(null);
var a=1;
function foo() {
    console.log(this.a); // 1  this => window
}
var obj={
    a: 2
}
var bar = foo.bind(null);
bar();

bind 也能夠實現函數柯里化:

function foo(a,b) {
    console.log(a,b); // 2  3
}
var bar=foo.bind(null,2);
bar(3);

更復雜的例子:

var foo={
    bar: function() {
        console.log(this);
    }
};

foo.bar(); // foo
(foo.bar)(); // foo

(foo.bar=foo.bar)(); // window
(false||foo.bar)();  // window
(foo.bar,foo.bar)();  // window

上述代碼中:

foo.bar()爲對象的方法調用,所以 this 綁定爲 foo 對象。

(foo.bar)() 前一個() 中的內容不計算,所以仍是 foo.bar()

(foo.bar=foo.bar)() 前一個 () 中的內容計算後爲 function() { console.log(this); } 因此這裏爲匿名函數自執行,所以 this 綁定爲 全局對象 window

後面兩個實例同上。

這樣理解會比較好:

(foo.bar=foo.bar)  括號中的表達式執行爲 先計算,再賦值,再返回值。
(false||foo.bar)()    括號中的表達式執行爲 判斷前者是否爲 true ,若爲true,不計算後者,若爲false,計算後者並返回後者的值。
(foo.bar,foo.bar)   括號中的表達式之行爲分別計算 「,」 操做符兩邊,而後返回  「,」 操做符後面的值。

箭頭函數中的this

箭頭函數時ES6新增的語法。

有兩個做用:

  1. 更簡潔的函數
  2. 自己不綁定this

代碼格式爲:

// 普通函數
function foo(a){
    // ......
}
//箭頭函數
var foo = a => {
    // ......
}

//若是沒有參數或者參數爲多個

var foo = (a,b,c,d) => {
    // ......
}

咱們在使用普通函數以前對於函數的this綁定,須要根據這個函數如何被調用來肯定其內部this的綁定對象。並且經常由於調用鏈的數量或者是找不到其真正的調用者對 this 的指向模糊不清。在箭頭函數出現後其內部的 this 指向不須要再依靠調用的方式來肯定。

箭頭函數有幾個特色(與普通函數的區別)

  1. 箭頭函數不綁定 this 。它只會從做用域鏈的上一層繼承 this。
  2. 箭頭函數不綁定arguments,使用reset參數來獲取實參的數量。
  3. 箭頭函數是匿名函數,不能做爲構造函數。
  4. 箭頭函數沒有prototype屬性。
  5. 不能使用 yield 關鍵字,所以箭頭函數不能做爲函數生成器。

這裏咱們只討論箭頭函數中的this綁定。

用一個例子來對比普通函數與箭頭函數中的this綁定:

var obj={
    foo: function() {
        console.log(this); // obj
    },
    bar: () => {
        console.log(this); // window
    }
}
obj.foo();
obj.bar();

上述代碼中,一樣是經過對象 . 方法調用一個函數,可是函數內部this綁定確是不一樣,只因一個數普通函數一個是箭頭函數。

用一句話來總結箭頭函數中的this綁定:

我的上面說的它會從做用域鏈的上一層繼承 this ,說法並非很正確。做用域中存放的是這個函數當前執行上下文與全部父級執行上下文的變量對象的集合。所以在做用域鏈中並不存在 this 。應該說是做用域鏈上一層對應的執行上下文中繼承 this 。

箭頭函數中的this繼承於做用域鏈上一層對應的執行上下文中的this

var obj={
    foo: function() {
        console.log(this); // obj
    },
    bar: () => {
        console.log(this); // window
    }
}
obj.bar();

上述代碼中obj.bar執行時的做用域鏈爲:

scopeChain = [
    obj.bar.AO,
    global.VO
]

根據上面的規則,此時bar函數中的this指向爲全局執行上下文中的this,即:window。

再來看一個例子:

var obj={
    foo: function() {
        console.log(this); // obj
        var bar=() => {
            console.log(this); // obj
        }
        bar();
    }
}
obj.foo();

在普通函數中,bar 執行時內部this被綁定爲全局對象,由於它是做爲獨立函數調用。可是在箭頭函數中呢,它卻綁定爲 obj 。跟父級函數中的 this 綁定爲同一對象。

此時它的做用域鏈爲:

scopeChain = [
     bar.AO,
     obj.foo.AO,
     global.VO
 ]

這個時候咱們就差很少知道了箭頭函數中的this綁定。

繼續看例子:

var obj={
    foo: () => {
        console.log(this); // window
        var bar=() => {
            console.log(this); // window
        }
        bar();
    }
}
obj.foo();

這個時候怎麼又指向了window了呢?

咱們還看當 bar 執行時的做用域鏈:

scopeChain = [
     bar.AO,
     obj.foo.AO,
     global.VO
 ]

當咱們找bar函數中的this綁定時,就會去找foo函數中的this綁定。由於它是繼承於它的。這時 foo 函數也是箭頭函數,此時foo中的this綁定爲window而不是調用它的obj對象。所以 bar函數中的this綁定也爲全局對象window。

咱們在回頭看上面關於定時器中的this的例子:

var name="chen";
var obj={
    name: "erdong",
    foo: function() {
        console.log(this.name); // erdong
        setTimeout(function() {
            console.log(this); // chen
        },1000)
    }
}
obj.foo();

這時咱們就能夠很簡單的讓定時器中的this與foo中的this綁定爲同一對象:

var name="chen";
var obj={
    name: "erdong",
    foo: function() {
        // this => obj
        console.log(this.name); // erdong
        setTimeout(() =>  {
            // this => foo中的this => obj
            console.log(this.name); // erdong
        },1000)
    }
}
obj.foo();

寫在最後

若是文中有錯誤,請務必留言指正,萬分感謝。

點個贊哦,讓咱們共同窗習,共同進步。

GitHub

相關文章
相關標籤/搜索