JS中的this是一個老生常談的問題了,因為它並不是一個確定的值,在不同情況下有不同的指向,所以也經常使人困惑。本篇文章會談談我自己對this的理解。
this到底是啥
其實this就是一個指標,它指示的就是當前的一個執行環境,可以用來對當前執行環境進行一些操作。因為它指示的是執行環境,所以在定義這個變數時,其實是不知道它真正的值的,只有執行時才能確定他的值。同樣一段程式碼,用不同的方式執行,他的this指向可能是不一樣的。我們來看看如下程式碼:
function func() {
this.name = "小小飛";
console.log(this); // 看一下this是啥
}
這個方法很簡單,只是給this新增了一個name屬性,我們把這個方法複製到Chrome除錯工具看下結果:
上圖中我們直接呼叫了func()
,發現this指向的是window,name
屬性新增到了window上。下面我們換一種呼叫方式,我們換成new func()
來呼叫:
我們看到輸出了兩個func {name: "小小飛"}
,一個是我們new返回的物件,另一個是方法裡面的console。這兩個值是一樣的,說明這時候方法裡面this就指向了new返回的物件,而不是前面例子的window了。這是因為當你使用new
去呼叫一個方法時,這個方法其實就作為建構函式使用了,這時候的this指向的是new出來的物件。
下面我們分別講解下幾種情況
使用new呼叫時,this指向new出來的物件
這個規則其實是JS物件導向的一部分,JS使用了一種很曲折的方式來支援物件導向。當你用new來執行一個函式時,這個函式就變成了一個類,new關鍵字會返回一個類的例項給你,這個函式會充當建構函式的角色。作為物件導向的建構函式,必須要有能夠給例項初始化屬性的能力,所以建構函式裡面必須要有某種機制來操作生成的例項,這種機制就是this。讓this指向生成的例項就可以通過this來操作例項了。關於JS的物件導向更詳細的解釋可以看這篇文章。
this的這種特性還有一些妙用。一個函式可以直接呼叫,也可以用new呼叫,那假如我只想使用者通過new呼叫有沒有辦法呢?下圖擷取自Vue原始碼:
Vue巧妙利用了this的特性,通過檢查this是不是Vue的一個例項來檢測使用者是通過new呼叫的還是直接呼叫的。
沒有明確呼叫者時,this指向window
這個其實在最開始的例子就講過了,那裡沒有明確呼叫者,this指向的是window。我們這裡講另外一個例子,函式裡面的函式,this指向誰?
function func() {
function func2() {
console.log('this:', this); // 這裡的this指向誰?
}
func2();
}
我們執行一下看看:
直接執行:
使用new執行:
我們發現無論是直接執行,還是使用new執行,this的值都指向的window。直接執行時很好理解,因為沒有明確呼叫者,那this自然就是window。需要注意的是使用new時,只有被new的func
才是建構函式,他的this指向new出來的物件,他裡面的函式的this還是指向window
。
有明確呼叫者時,this指向呼叫者
看這個例子:
var obj = {
myName: "小小飛",
func: function() {
console.log(this.myName);
}
}
obj.func(); // 小小飛
上述例子很好理解,因為呼叫者是obj,所以func裡面的this就指向obj,this.myName
就是obj.myName
。其實這一條和上一條可以合在一起,沒有明確呼叫者時其實隱含的呼叫者就是window,所以經常有人說this總是指向呼叫者。
下面我們將這個例子稍微改一下:
var myName = "大飛哥";
var obj = {
myName: "小小飛",
func: function() {
console.log(this.myName);
}
}
var anotherFunc = obj.func;
anotherFunc(); // 輸出是啥?
這裡的輸出應該是“大飛哥”,因為雖然anotherFunc
的函式體跟obj.func
一樣,但是他的執行環境不一樣,他其實沒有明確的呼叫者,或者說呼叫者是window。這裡的this.myName
其實是window.myName
,也就是“大飛哥”。
我們將這個例子再改一下:
let myName = "大飛哥";
var obj = {
myName: "小小飛",
func: function() {
console.log(this.myName);
}
}
var anotherFunc = obj.func;
anotherFunc(); // 注意這裡輸出是undefined
這次我們只是將第一個var
改成了let
,但是我們的輸出卻變成了undefined
。這是因為let,const定義變數,即使在最外層也不會變成window的屬性,只有var定義的變數才會成為window的屬性。
箭頭函式並不會繫結this
這句話的意思是箭頭函式本身並不具有this,箭頭函式在被申明確定this,這時候他會直接將當前作用域的this作為自己的this。還是之前的例子我們將函式改為箭頭函式:
var myName = "大飛哥";
var obj = {
myName: "小小飛",
func: () => {
console.log(this.myName);
}
}
var anotherFunc = obj.func;
obj.func(); // 大飛哥
anotherFunc(); // 大飛哥
上述程式碼裡面的obj.func()
輸出也是“大飛哥”,是因為obj
在建立時申明瞭箭頭函式,這時候箭頭函式會去尋找當前作用域,因為obj
是一個物件,並不是作用域,所以這裡的作用域是window,this也就是window了。
再來看一個例子:
var myName = "大飛哥";
var obj = {
myName: "小小飛",
func: function () {
return {
getName: () => {
console.log(this.myName);
}
}
}
}
var anotherFunc = obj.func().getName;
obj.func().getName(); // 小小飛
anotherFunc(); // 小小飛
兩個輸出都是“小小飛”,obj.func().getName()
輸出“小小飛”很好理解,這裡箭頭函式是在obj.func()
的返回值裡申明的,這時他的this其實就是func()
的this,因為他是被obj
呼叫的,所以this指向obj。
那為什麼anotherFunc()
輸出也是“小小飛”呢?這是因為anotherFunc()
輸出的this,其實在anotherFunc
賦值時就確定了:
-
var anotherFunc = obj.func().getName;
其實是先執行了obj.func()
- 執行
obj.func()
的時候getName
箭頭函式被申明 - 這時候箭頭函式的this應該是當前作用域的this,也就是
func()
裡面的this -
func()
因為是被obj
呼叫,所以this指向obj
- 呼叫
anotherFunc
時,其實this早就確定了,也就是obj
,最終輸出的是obj.myName
。
再來看一個建構函式裡面的箭頭函式,前面我們說了建構函式裡面的函式,直接呼叫時,他的this指向window,但是如果這個函式時箭頭函式呢:
var myName = "大飛哥";
function func() {
this.myName = "小小飛";
const getName = () => {
console.log(this.myName);
}
getName();
}
new func(); // 輸出啥?
這裡輸出的是“小小飛”,原理還是一樣的,箭頭函式在申明時this確定為當前作用域的this,在這裡就是func
的作用域,跟func
的this一樣指向new出來的例項。如果不用new,而是直接呼叫,這裡的this就指向window。
DOM事件回撥裡面,this指向繫結事件的物件
function func(e) {
console.log(this === e.currentTarget); // 總是true
console.log(this === e.target); // 如果target等於currentTarget,這個就為true
}
const ele = document.getElementById('test');
ele.addEventListener('click', func);
currentTarget
指的是繫結事件的DOM物件,target
指的是觸發事件的物件。DOM事件回撥裡面this總是指向currentTarget
,如果觸發事件的物件剛好是繫結事件的物件,即target === currentTarget
,this也會順便指向target
。如果回撥是箭頭函式,this是箭頭函式申明時作用域的this。
嚴格模式下this是undefined
function func() {
"use strict"
console.log(this);
}
func(); // 輸出是undefined
注意這裡說的嚴格模式下this是undefined是指在函式體內部,如果本身就在全域性作用域,this還是指向window。
<html>
...
<script>
"use strict"
console.log(this); // window
</script>
...
</html>
this能改嗎
this是能改的,call
和apply
都可以修改this,ES6裡面還新增了一個bind
函式。
使用call和apply修改this
const obj = {
myName: "大飛哥",
func: function(age, gender) {
console.log(`我的名字是${this.myName}, 我的年齡是${age},我是一個${gender}`);
}
}
const obj2 = {
myName: "小小飛"
}
obj.func.call(obj2, 18, "帥哥"); // 我的名字是小小飛, 我的年齡是18,我是一個帥哥
注意上面輸出的名字是"小小飛",也就是obj2.myName
。正常直接呼叫obj.func()
輸出的名字應該是obj.myName
,也就是"大飛哥"。但是如果你使用call
來呼叫,call的第一個引數就是手動指定的this
。我們將它指定為obj2
,那在函式裡面的this.myName
其實就是obj2.myName
了。
apply
方法跟call
方法作用差不多,只是後面的函式引數形式不同,使用apply呼叫應該這樣寫,函式引數應該放到一個陣列或者類陣列裡面:
obj.func.apply(obj2, [18, "帥哥"]); // 我的名字是小小飛, 我的年齡是18,我是一個帥哥
之所以有call和apply兩個方法實現了差不多的功能,是為了讓大家使用方便,如果你拿到的引數是一個一個的,那就使用call吧,但是有時候拿到的引數是arguments
,這是函式的一個內建變數,是一個類陣列結構,表示當前函式的所有引數,那就可以直接用apply,而不用將它展開了。
使用bind修改this
bind
是ES5引入的一個方法,也可以修改this,但是呼叫它並不會立即執行方法本身,而是會返回一個修改了this的新方法:
const obj = {
myName: "大飛哥",
func: function(age, gender) {
console.log(`我的名字是${this.myName}, 我的年齡是${age},我是一個${gender}`);
}
}
const obj2 = {
myName: "小小飛"
}
const func2 = obj.func.bind(obj2); // 返回一個this改為obj2的新方法
func2(18, "帥哥"); // 我的名字是小小飛, 我的年齡是18,我是一個帥哥
bind和call,apply最大的區別就是call,apply會立即執行方法,而bind並不會立即執行,而是會返回一個新方法供後面使用。
bind函式也可以接收多個引數,第二個及以後的引數會作為新函式的引數傳遞進去,比如前面的bind也可以這樣寫:
const func3 = obj.func.bind(obj2, 18); // 注意我們這裡已經傳了一個年齡引數
func3("帥哥"); //注意這裡只傳了性別引數,年齡引數已經在func3裡面了,輸出還是:我的名字是小小飛, 我的年齡是18,我是一個帥哥
自己寫一個call
知道了call的作用,我們自己來寫一個call:
Function.prototype.myCall = function(...args) {
// 引數檢查
if(typeof this !== "function") {
throw new Error('Must call with a function');
}
const realThis = args[0] || window;
const realArgs = args.slice(1);
const funcSymbol = Symbol('func');
realThis[funcSymbol] = this; // 這裡的this是原方法,儲存到傳入的第一個引數上
//用傳入的引數來調方法,方法裡面的this就是傳入的引數了
const res = realThis[funcSymbol](...realArgs);
delete realThis[funcSymbol]; // 最後刪掉臨時儲存的原方法
return res; // 將執行的返回值返回
}
自己寫一個apply
apply方法跟call方法很像,區別只是在取呼叫引數上:
Function.prototype.myApply = function(...args) {
if(typeof this !== "function") {
throw new Error('Must call with a function');
}
const realThis = args[0] || window;
// 直接取第二個引數,是一個陣列
const realArgs = args[1];
const funcSymbol = Symbol('func');
realThis[funcSymbol] = this;
const res = realThis[funcSymbol](...realArgs);
delete realThis[funcSymbol];
return res;
}
自己寫一個bind
自己寫一個bind需要用到前面的apply,注意他的返回值是一個方法
Function.prototype.myBind = function(...args) {
if(typeof this !== "function") {
throw new Error('Must call with a function');
}
const _func = this; // 原方法
const realThis = args[0] || window; // 繫結的this
const otherArgs = args.slice(1); // 取出後面的引數作為新函式的預設引數
return function(...args2) { // 返回一個方法
return _func.apply(realThis, [...otherArgs,...args2]); // 拼接儲存引數和新引數,然後用apply執行
}
}
總結
- 函式外面的this,即全域性作用域的this指向window。
- 函式裡面的this總是指向直接呼叫者。如果沒有直接呼叫者,隱含的呼叫者是window。
- 使用new呼叫一個函式,這個函式即為建構函式。建構函式裡面的this是和例項物件溝通的橋樑,他指向例項物件。
- 箭頭函式裡面的this在它申明時確定,跟他當前作用域的this一樣。
- DOM事件回撥裡面,this指向繫結事件的物件(currentTarget),而不是觸發事件的物件(target)。當然這兩個可以是一樣的。如果回撥是箭頭函式,請參考上一條,this是它申明時作用域的this。
- 嚴格模式下,函式裡面的this指向undefined,函式外面(全域性作用域)的this還是指向window。
- call和apply可以改變this,這兩個方法會立即執行原方法,他們的區別是引數形式不一樣。
- bind也可以修改this,但是他不會立即執行,而是返回一個修改了this的函式。
文章的最後,感謝你花費寶貴的時間閱讀本文,如果本文給了你一點點幫助或者啟發,請不要吝嗇你的贊和GitHub小星星,你的支援是作者持續創作的動力。
作者博文GitHub專案地址: https://github.com/dennis-jiang/Front-End-Knowledges