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物件的。再來看個變化版本。
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。