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。