JavaScript中的this詳解

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對象的。再來看個變化版本。bash

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

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

是的,雖然fn2拿到的是對象o裏面的一個方法,可是,萬變不離其宗,在執行fn2()的時候,仍然是沒有前綴的,那是誰在調用fn2的?固然是window對象。因此這裏的this也指向window。app

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在調用它。這種狀況,知道就好,無需太花力氣死磕。ui

2.2 動態綁定

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

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

這是經過動態綁定的方式,給元素添加了事件,這種狀況下,當回調函數執行的時候,是元素div1在調用它,因此此時函數內部的this,是指向元素div1的。this

2.3 事件監聽

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

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

一樣的,經過事件監聽器的方式綁定的回調函數,內部的this也是指向div1。因此咱們能夠總結一下得知:事件處理函數中的this,指向的是觸發這個事件的元素。spa

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。

相關文章
相關標籤/搜索