徹底搞明白this


this是我們在書寫代碼時最常用的關鍵詞之一,即使如此,它也是JavaScript最容易被最頭疼的關鍵詞。那么this到底是什么呢?

如果你了解執行上下文,那么你就會知道,其實this是執行上下文對象的一個屬性:

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

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

this是在進入執行上下文時確定的,也就是在函數執行時才確定,並且在運行期間不允許修改並且是永久不變的

在全局代碼中的this

在全局代碼中this 是不變的,this始終是全局對象本身。

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的值,就會拋出錯誤:

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的含義。

作為函數調用

什么是作為函數調用:就是獨立的函數調用,不加任何修飾符。

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)
        var that=this;
        setTimeout(function() {
            console.log(that.name); // window
        },1000)
    }
}
obj.foo();

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

當然這里也可以適用bind來實現:

var name="chen";
var obj={
    name: "erdong",
    foo: function() {
        console.log(this.name);
        setTimeout(function() {
            console.log(this.name); // window
        }.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() {
        console.log(this.name); // erdong
        setTimeout(() =>  {
            console.log(this.name); // erdong
        },1000)
    }
}
obj.foo();

  

如需轉載請注明出處。 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM