JavaScript中的this詳解

Accumulate_HangZhou發表於2019-01-21

this是JavaScript這門語言中極其重要的一個知識點,特別是關於物件導向的相關的寫法,可以說掌握了this的特性,相當於掌握了一大半JavaScript物件導向的編寫能力。總的來說,JavaScript中的this大概有7種情況,理解到位了這些情況,基本上就掌握了這部分相關的內容,所有的高階寫法,都是基於這些情況的演變。這7種情況分別是:

  • 全域性環境呼叫下的this
  • 事件處理函式中的this
  • 物件方法內的this
  • 建構函式中的this
  • 原型鏈上函式中的this
  • getter和setter中個this
  • 箭頭函式中的this
    除了最後一個箭頭函式中的this指向,是基於函式書寫時候確定的,其它所有情況下,JavaScript中的this,都是由呼叫時決定的。很多時候,大家會很納悶,這個this,到底是什麼?this可以是全域性物件window,可以是一個具體的元素比如<div></div>,也可以是一個物件比如{},還可以是一個例項等等。至於this到底是什麼,就看函式執行的時候,到底是誰呼叫了它。

1.全域性環境呼叫

我們所說的全域性環境,其實指的就是window這個物件,也就是我們在瀏覽器中每開啟一個頁面,都會生成的一個window。先來看看最簡單的全域性呼叫。

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

fn1();  // window

// 相當於
window.fn1();
複製程式碼

我們都知道,全域性下使用var宣告的變數,都會隱式的被建立為window物件的屬性和方法。所以,當你看到一個函式被呼叫而沒有字首的時候(也就是說不是通過"."符號來呼叫),這其實就是全域性物件window在呼叫它。因此,此時函式內部的this是指向window物件的。再來看個變化版本。

let o = {
    name: 'abc',
    fn: function() {
        console.log( this.a );
    }
}

let fn2 = o.fn;
fn2();  // undefined
複製程式碼

是的,雖然fn2拿到的是物件o裡面的一個方法,但是,萬變不離其宗,在執行fn2()的時候,仍然是沒有字首的,那是誰在呼叫fn2的?當然是window物件。所以這裡的this也指向window。

1.1 嚴格模式和非嚴格模式的區別

我們現在知道,全域性物件window呼叫的函式,內部的this就是指向window。但是這裡有個問題需要注意一下。JavaScript有嚴格模式和非嚴格模式之分(嚴格模式就在程式碼的頂部加上一句"use strict")。在這兩種情況下,this的指向是有區別的。
非嚴格模式下this指向我們已經討論過了,指的是window物件,而嚴格模式下的全域性呼叫,this指向的是undefined。

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

fn1();  // undefined
複製程式碼

2.事件處理函式中的this

JavaScript中對於事件的處理是採用非同步回撥的方式,對一個元素繫結一個回撥函式,當事件觸發的時候去執行這個函式。而對於回撥函式的繫結,有下面幾種情況:

  • 元素標籤內繫結
  • 動態繫結
  • 事件監聽 這幾種情況下,回撥函式內的this分別又是什麼呢?分別來看看。

2.1元素標籤內繫結

<div id="div1" onclick="console.log( this )"></div>
複製程式碼

點選元素div1後,我們發現控制檯列印的是"<div id="div1" onclick="console.log( this )">",可以知道的是,元素內聯所執行的語句中的this,指向的是元素本身。但是,有一個特例,來改動一下方式。

<div id="div1" onclick="(function () {console.log( this )}()"></div>
複製程式碼

看明白了嗎,元素內聯的是一個匿名自執行函式,這個時候匿名自執行函式中的this,就不是指向元素本身了,而是window物件!雖然這種寫法很無聊,但這就是內聯寫法我們需要注意的一個點。我們可以這樣理解,匿名自執行函式有獨立的作用域,相當於是window在呼叫它。這種情況,知道就好,無需太花力氣死磕。

2.2 動態繫結

let div1 = document.getElementById("div1");

div1.onclick = function() {
    console.log( this );    // div1
}
複製程式碼

這是通過動態繫結的方式,給元素新增了事件,這種情況下,當回撥函式執行的時候,是元素div1在呼叫它,所以此時函式內部的this,是指向元素div1的。

2.3 事件監聽

let div1 = document.getElementById("div1");

div1.addEventListener("click", function() {
    console.log( this );    // div1
    }, false);
複製程式碼

同樣的,通過事件監聽器的方式繫結的回撥函式,內部的this也是指向div1。所以我們可以總結一下得知:事件處理函式中的this,指向的是觸發這個事件的元素。

3.物件方法中的this

在JavaScript中,物件是可以有屬性和方法的,這個方法,其實就是函式。既然是函式,那麼內部肯定也會有this,作為物件方法中的this,到底是指的什麼呢?看個簡單的例子。

var name = 'aaa';
let obj = {
    name: 'jack',
    fn: function() {
        console.log( this.name );
    }
}

let f1 = obj.fn;

obj.fn();   // jack
f1();       // aaa
複製程式碼

作為物件的方法呼叫的函式,它內部的this,就指向這個物件。在這個例子中,當通過obj.fn()的形式呼叫fn函式的時候,它內部的this指的就是obj這個物件了。至於第二種情況,先把obj.fn賦值給f1,然後通過執行f1來執行函式的情況,我們在上面已經說過,這個時候,其實是window物件在呼叫f1,因此它內部的this就是指向window物件,因而列印的就是'aaa'。
如果是一個物件中巢狀著比較深的方法,它內部的this又是什麼呢?

let person = {
    name: 'jack',
    eat: {
        name: 'apple',
        fn1: function() {
            console.log( this.name );
        },
        obj: {
            name: 'grape',
            fn2: function() {
                console.log( this.name );
            }
        }
    }
}

person.eat.fn1();       // apple
person.eat.obj.fn2();   // grape
複製程式碼

這裡遵守一個就近原則:如果是通過物件方法的方式呼叫函式,則函式內部的this指向離它最近一級的那個物件。在這個例子中,person.eat.fn1()這種呼叫,fn1中的this指的就是eat這個物件;person.eat.obj.fn2()這種呼叫方式,fn2中的this,指的就是obj這個物件。

4.建構函式中的this

建構函式其實就是普通的函式,只是它內部一般都書寫了許多this,可以通過new的方式呼叫來生成例項,所以我們一般都用首字母大寫的方式,來區分建構函式和一般的函式。建構函式,是JavaScript中書寫物件導向的重要方式。

function Fn1(name) {
    this.name = name;
}

let n1 = new Fn1('abc');
n1.name; // abc
複製程式碼

這是一個非常簡單的建構函式書寫方式,以及對建構函式的呼叫。建構函式中的this,以及new呼叫的這種方式,其實都是為了能夠創造例項服務的,否則也就沒有意義了。那麼,建構函式中的this也就很清楚了:它指向建構函式所創造的例項。當通過new方法呼叫建構函式的時候,建構函式內部的this就指向這例項,並將相應的屬性和方法"生成"給這個例項。通過這個方法,生成的例項才能夠獲取屬性和方法。
凡事總有例外嘛,建構函式中有這樣一種例外,我們看看。

function Fn1(name) {
    this.name = name;
    return null;
}

function Fn2(name) {
    this.name = name;
    return {a: '123'};
}

let f1 = new Fn1("ttt");
console.log( f1 );  // {name: "ttt"}

let f2 = new Fn2("ggg");
console.log( f2 );  // {a: "123"}
複製程式碼

f1是通過new Fn1建立的一個例項,這沒有問題。但f2為什麼不是我們所想的結果呢? 當建構函式內部return的是一個物件型別的資料的時候,通過new所得到的,就是建構函式return出來的那個物件;當建構函式內部return的是基本型別資料(數字,字串,布林值,undefined,null),那麼對於建立例項沒有影響。

5.原型鏈函式中的this

原型鏈函式中個this,其實跟建構函式中的this一樣,也是指向建立的那個例項。

function Fn() {
    this.name = '878978'
}

Fn.prototype.sum = function() {
    console.log(this)
    return this;
}

let f5 = new Fn();
let f6 = new Fn();

console.log( f5 === f5.sum() );     // true
console.log( f6 === f6.sum() );     // true
複製程式碼

6.getter和setter中的this

我們知道,JavaScript中getter和setter是作為對物件屬性讀取和修改的一種劫持。可以分別在讀取和設定物件相應屬性的時候觸發。

let obj = {
    n: 1,
    m: 2,
    get sum() {
        console.log(this.n, this.m);
        return '正在嘗試訪問sum...';
    },
    set sum(k) {
        this.m = k;
        return '正在設定obj屬性sum...';
    }
}

obj.sum;   // 1,2
obj.sum = 5;  // 正在設定obj屬性sum..
複製程式碼

getter和setter中的this,規則跟作為物件方法呼叫時候函式內部的this指向是一樣的,它指的就是這個物件本身。

7.箭頭函式中的this

箭頭函式是ES6中新推出的一種函式簡寫方法,跟ES5函式最大的區別,就要數它的this規則了。在ES5的函式中,this都是在函式呼叫的時候,才能確定具體的this指向。而箭頭函式,其實是沒有this的,但是它內部的這個所謂this,在箭頭函式書寫的時候,就已經繫結了(繫結父級的this),並且無法改變。看個例子。

let div1 = document.getElementById("div");

div1.onclick = function() {
    setTimeout(() => {
        console.log( this );    // div1
    }, 500);
}
複製程式碼

我們知道,setTimeout中所繫結的回撥函式,其實是window在呼叫它,所以它內部的this指向的是window。但是,當回撥函式是箭頭函式的寫法的時候,內部的this竟然是div1!這在箭頭函式書寫的時候,就已經決定了它內部的this指向,就是它父級的this。而它父級函式作用域中的this,其實就是元素div1。作為物件方法的箭頭函式,其實也是類似的道理。

var name = 'aaa';
let obj = {
    name: 'jack',
    fn1: () => {
        console.log( this.name );
    }
}

obj.fn1();  // aaa
複製程式碼

沒錯,還是那句話,當我們寫下箭頭函式的時候,它內部的this就已經確定了,並且無法修改(call, apply, bind)。這個例子中,箭頭函式最近的父級作用域顯然是全域性環境window,因此它的this就指向window。

8.call, apply, bind的用法

說到JavaScript中的this,就沒法不說call, apply, bind這三個方法。在所有JavaScript函式的高階用法,或者是JavaScript框架中,都會有這三個方法的蹤影。這三個方法都是Function.prototype上的方法,所以所有的函式都預設繼承了這三個方法。現在具體說說這三個方法的分別用途。

8.1 call

call方法可以實現對函式的立即呼叫,並且顯示的指定函式內部的this以及傳參。

let obj = {
    color: 'green'
}

function Fn() {
    console.log( this.color );
}

Fn();   // undefined

Fn.call(obj);   // green
複製程式碼

call可以實現對函式的立即呼叫,並且改變函式內部的this指向。上面的例子中,直接呼叫函式Fn的時候,它內部的this指向window物件,因此列印的是undefined;當通過call指定函式內部的this指向obj的時候,它就能獲取到obj上的屬性和方法了。call呼叫還能實現呼叫時候的傳參,請看。

let obj = {
    color: 'blue'
}

function Fn(height, width) {
    console.log(`the tree is ${this.color}, and the tall is ${height}, width is ${width}`);
}

Fn.call(obj, 20, 3);   // the tree is blue, and the tall is 20, width is 3
複製程式碼

8.2 apply

apply的作用和call是一模一樣的,都是實現對函式內部this的改變,唯一的區別就是傳參的方式不一樣:call是通過一個一個引數的方式傳遞引數,而apply是通過陣列的形式傳遞多個引數。

let obj = {
    color: 'orange'
}

function Fn(height, width) {
    console.log(`the tree is ${this.color}, and the tall is ${height}, width is ${width}`);
}

Fn.apply(obj, [16, 7]);   // the tree is orange, and the tall is 16, width is 7
複製程式碼

8.3 bind

call和apply都是實現對函式的立即呼叫,並且改變函式內部this的指向,如果說我只想改變函式內部的this,而不執行函式,該怎麼辦?這個時候,就需要用到bind。

let person = {
    name: 'jack'
}

function Person() {
    console.log(this.name);
}

let p1 = Person.bind(person);
p1();   // 'jack'
複製程式碼

當一個函式執行完bind方法後,會返回一個新的函式,而這個新的函式跟原函式相比,內部的this指向被顯示的改變了。但是不會立即執行新的函式,而是在你需要的時候才去呼叫。 但是有一點需要注意,返回的新函式p1,它內部的this就無法再改變了。接著上面的例子。

let animal = {
    name: 'animal'
}

let p2 = p1.bind();
p2();   // 'jack'
複製程式碼

p2的this依然是指向obj,而非animal。

相關文章