JavaScript——深入瞭解this

_Fatman 發表於 2021-01-26

前言

我曾以為func()其實就是window.func()

function func(){
	console.log('this : ' + this);
}

func();//this : [object Window]
window.func();//this : [object Window]

直到

'use strict'
function func(){
	console.log('this : ' + this);
}
func();//this : undefined
window.func();//this : [object Window]

也曾為輸出inside this : [object Window] 而困惑不已

function outside(){
	console.log('outside  this : ' + this);//outside  this : [object Object]
	function inside(){
		console.log('inside  this : ' + this);//inside  this : [object Window]
	}
	inside();
}
let obj = {
	outside : outside
}
obj.outside();

曾感慨Java之美好[1],唾棄JavaScript中this的‘靈活’。
...

一直到我嘗試總結出this的規律:
1.建構函式中的this關鍵字在任何模式下都指向new出來的物件;

2.嚴格模式下this關鍵字指向呼叫該函式的物件,如果該函式未被物件呼叫,則 this === 'undefined';

3.非嚴格模式下this關鍵字指向呼叫該函式的物件,如果該函式未被物件呼叫 this === window;

再到後來,拜讀了JavaScript語言精粹,知曉了4種呼叫模式,知曉了箭頭函式的一個我不曾知曉的作用,結合過往,我感覺自己已經摸清了this的規律,亦或者至少摸清了一部分的規律,特撰此文,作為總結;

首先,此文所有程式碼執行環境皆為瀏覽器,所以我不會強調global;
再者,function中this指向被確定於function被呼叫時(拋開箭頭函式和class不論),
類似像下面這種程式碼,我覺得沒有提的必要;

function func(){
	console.log('this : ' + this);
}
func;
setTimeout(func,100); 
//我想表達的意思是 func是被setTimeout呼叫的 同樣我也可以寫一個mySetTimeout
mySetTimeout = function(func,delay){
	setTimeout(func.bind({}),delay); //我們應該把目光放在function呼叫時
}
mySetTimeout(func,100); 
//在不執行程式碼的前提下,如果不看mySetTimeout程式碼,能準確判斷this是什麼嗎?

最後,我不想提with,因為with的使用往往會引起歧義,就如同下面的程式碼,明明呼叫時的程式碼一模一樣,但一個在全域性作用域window中呼叫func,而另一個在obj的作用域中呼叫,輸出的結果天差地別。

function func(){
	console.log('this : ' + this);
}
let obj = {};
with(obj){
	func();//this : [object Window]
}
obj.func = func;
with(obj){
	func();//this : [object Object]
}

接下來的內容我將以下圖中的思路展開:
在這裡插入圖片描述

ES6之前

這裡的主要思路還是沿用的JavaScript語言精粹。

函式呼叫模式

JavaScript中的function不同於Java,Java雖然說萬物皆物件,但是基礎型別和function就不是物件。Java中的function只是物件的行為,但是JavaScript不同,JavaScript雖然同時包含了一些像原型、函式柯里化等程式設計思想,但是在萬物皆物件這一方面,反而比Java更像是物件導向程式設計。JavaScript中的function是支援直接呼叫的。
在非嚴格模式

function func(){
	console.log('this : ' + this);
}
func();//this : [object Window]

在嚴格模式

'use strict'
function func(){
	console.log('this : ' + this);
}
func();//this : undefined

方法呼叫模式

方法呼叫模式就是把function當成物件的行為來呼叫,既然是物件的行為,那麼function中的this指向的當然是這個呼叫的物件了。
在非嚴格模式

let _self = null;
function func(){
	_self = this;
}
let obj = {
	 func : func
}
obj.func();
console.log('_self === obj : ' + (_self === obj));//_self === obj : true

在嚴格模式

'use strict'
let _self = null;
function func(){
	_self = this;
}
let obj = {
	 func : func
}
obj.func();
console.log('_self === obj : ' + (_self === obj));//_self === obj : true

構造呼叫模式

構造呼叫模式就是把function當做建構函式呼叫,在其左邊加上new關鍵字,為了迎合程式碼規範,這裡的function我將以大寫字母開頭。
在非嚴格模式

let _self = null;
function Person(){
	_self = this;
}
let person = new Person();
console.log('_self === person : ' + (_self === person));//_self === person : true

在嚴格模式

'use strict'
let _self = null;
function Person(){
	_self = this;
}
let person = new Person();
console.log('_self === person : ' + (_self === person));//_self === person : true

建構函式這裡我覺得有必要擴充套件一下:
1.建構函式中返回物件(非基礎型別),會影響上面的結果;

let _self = null;
function Person(){
	_self = this;
	return window;
}
let person = new Person();
console.log('_self === person : ' + (_self === person));//_self === person : false
console.log('window === person : ' + (window === person));//window === person : true

2.省略new關鍵字,同樣會影響上面的結果;

let _self = null;
function Person(){
	_self = this;
}
let person = Person();
console.log('_self === person : ' + (_self === person));//_self === person : false
console.log('window === person : ' + (window === person));//window === person : false
console.log('typeof person : ' + typeof person);//typeof person : undefined

在Person呼叫時省略new關鍵字還可能會汙染全域性作用域

function Person(){
	this.personName = 'person';
}
let person = Person();
console.log('person.personName : '+person.personName);//Cannot read property 'personName' of undefined
console.log('window.personName : '+window.personName);//window.personName : person

蠢辦法解決呼叫建構函式不用new關鍵字的:

function Person(){
	if(this === window){
        throw Error('You must use the new keyword.');
    }
	this.personName = 'person';
}
let person = Person();//You must use the new keyword.

改進版

function Person(){
	let context;
    (function(){
        context = this;
    }())
    if(this === context){
        throw Error('You must use the new keyword.');
    }
	this.personName = 'person';
}
let person = Person();//You must use the new keyword.

特指呼叫模式

bind雖然是es6的,但是我也放到這個模式一起講了,因為我覺得把bind和apply、call一起講可能會更容易理解一些。

apply

apply的第一個引數是繫結的物件,第二個引數是array。call和apply的不同之處在於call的第二個引數對於function中arguments的第一位,第三個引數對於function中的arguments的第二位,以此類推;而apply的第二個引數對應function中的arguments。由於這裡主要是講this,所以第二個引數的例子就不提了,後面的call也一樣。
在非嚴格模式

function func(){
	console.log('this : ' + this);
}
func.apply({});//this : [object Object]
func.apply(window);//this : [object Window]
func.apply(null);//this : [object Window]
func.apply();//this : [object Window]

在嚴格模式

'use strict'
function func(){
	console.log('this : ' + this);
}
func.apply({});//this : [object Object]
func.apply(window);//this : [object Window]
func.apply(null);//this : null
func.apply();//this : undefined

實現apply

滿足條件

1.把第一個引數繫結到呼叫myApply的function執行時的this;
2.第二個引數應與呼叫myApply的function的arguments內容一致;
3.嚴格模式和非嚴格模式第一個引數為null或undefined時情況要與apply函式一致;

程式碼
Function.prototype.myApply = function(){
	var context,arr;
	//誰呼叫的myApply this就指向誰
	if(typeof this !== 'function'){
		throw Error('typeof this !== "function"');
	}
	context = arguments[0];
	arr = arguments[1] ? arguments[1] : [];
	if(typeof context === 'undefined' || context === null){
		//滿足條件3
		context = (function(){
			return this;
		}());
	}
	if(typeof context === 'undefined'){
		this(...arr);
	}else{
		context.f = this;
		context.f(...arr);
	}
}

call

call如果只傳入第一個引數,結果和只傳入第一個引數的apply是一致的。
在非嚴格模式

function func(){
	console.log('this : ' + this);
}
func.call({});//this : [object Object]
func.call(window);//this : [object Window]
func.call(null);//this : [object Window]
func.call();//this : [object Window]

在嚴格模式

'use strict'
function func(){
	console.log('this : ' + this);
}
func.call({});//this : [object Object]
func.call(window);//this : [object Window]
func.call(null);//this : null
func.call();//this : undefined

實現call

滿足條件

1.把第一個引數繫結到呼叫myCall的function執行時的this;
2.除第一個引數外其餘引數組成的陣列應與呼叫myCall的function的arguments內容一致;
3.嚴格模式和非嚴格模式第一個引數為null或undefined時情況要與call函式一致;

程式碼
Function.prototype.myCall = function(){
	var context,arr;
	//誰呼叫的myCall this就指向誰
	if(typeof this !== 'function'){
		throw Error('typeof this !== "function"');
	}
	context = arguments[0];
	//差異點 call與apply的傳值方式所致
	arr = [...arguments].slice(1);//手動轉型
	if(typeof context === 'undefined' || context === null){
		//滿足條件3
		context = (function(){
			return this;
		}());
	}
	if(typeof context === 'undefined'){
		this(...arr);
	}else{
		context.f = this;
		context.f(...arr);
	}
}

bind

bind和call很相似,主要的不同點在於func.call(window)立馬就呼叫了,而func.bind(window)會返回一個繫結了window的function,但是這個function還沒有執行。可以這樣理解func.bind(window)()的效果與func.call(window)一致。
在非嚴格模式

function func(){
	console.log('this : ' + this);
}

func.bind({})();//this : [object Object]
func.bind(null)();//this : [object Window]
func.bind()();//this : [object Window]
let obj = {
	func : func.bind(window)
}
obj.func();//this : [object Window]
//建構函式
let _self = null;
function Person(){
	_self = this;
}
let P = Person.bind(window);
let person = new P();
console.log('_self === person : ' + (_self === person));//_self === person : true
console.log('window === person : ' + (window === person));//window === person : false

在嚴格模式

'use strict'
function func(){
	console.log('this : ' + this);
}

func.bind({})();//this : [object Object]
func.bind(null)();//this : null
func.bind()();//this : undefined
let obj = {
	func : func.bind(window)
}
obj.func();//this : [object Window]
//建構函式
let _self = null;
function Person(){
	_self = this;
}
let P = Person.bind(window);
let person = new P();
console.log('_self === person : ' + (_self === person));//_self === person : true
console.log('window === person : ' + (window === person));//window === person : false

從上面的例子,我們不單單可以發現bind在嚴格模式和非嚴格模式下的不同,還可以得出構造呼叫模式的優先順序最高,bind其次,方法呼叫模式和函式呼叫模式最低。

實現bind

滿足條件

1.把第一個引數繫結到呼叫myBind的function執行時的this;
2.將除第一個引數外其餘引數與function中引數合併;
3.嚴格模式和非嚴格模式第一個引數為null或undefined時情況要與bind函式一致;

程式碼
Function.prototype.myBind = function(){
	var context,arr,_self;
	//誰呼叫的myBind this就指向誰
	if(typeof this !== 'function'){
		throw Error('typeof this !== "function"');
	}
	context = arguments[0];
	arr = [...arguments].slice(1);//手動轉型
	if(typeof context === 'undefined' || context === null){
		//滿足條件3
		context = (function(){
			return this;
		}());
	}
	_self = this;
	return function(){
		if(typeof context === 'undefined'){//嚴格模式
			_self(arr.concat(...arguments));
		}else{
			context.f = _self;
			context.f(arr.concat(...arguments));
		}
		
	}
}

ES6

據我所知,有一部分人,他們奉行箭頭函式+class來解決一切問題。我對此觀點的正確性不表態,但是這樣做能減少很多判斷this的麻煩。

箭頭函式

箭頭函式沒有this,箭頭函式中的this來自於它處於的作用域鏈中的上一層。我在前言中說過,我曾為輸出inside this : [object Window] 而困惑不已,但是我現在把程式碼略微修改一下,輸出就將符合我的預期(inside繼承了outside的this值)。

function outside(){
	console.log('outside  this : ' + this);//outside  this : [object Object]
	let inside = ()=>{
		console.log('inside  this : ' + this);//inside  this : [object Object]
	}
	inside();
}
let obj = {
	outside : outside
}
obj.outside();

要是把outside也改成箭頭函式,結果又會大不一樣

let outside = ()=>{
	console.log('outside  this : ' + this);//outside  this : [object Window]
	let inside = ()=>{
		console.log('inside  this : ' + this);//inside  this : [object Window]
	}
	inside();
}
let obj = {
	outside : outside
}
obj.outside();

因為箭頭函式的this值是繼承於它身處的作用域上一層的this,outside上一層是全域性作用域,不會再發生更改了,所以這裡就算用方法呼叫模式,也無法改變this的值。
在非嚴格模式

let func = ()=>{
	console.log('this : ' + this);
}
func();//this : [object Window]
let obj = {
	func : func
}
obj.func();//this : [object Window]
func.apply({});//this : [object Window]
func.call({});//this : [object Window]
func.bind({})();//this : [object Window]
func.apply(null);//this : [object Window]
func.apply();//this : [object Window]

let _self = null;
let Person = ()=>{
	_self = this;
}
let person = new Person();//Person is not a constructor

在嚴格模式

'use strict'
let func = ()=>{
	console.log('this : ' + this);
}
func();//this : [object Window]
let obj = {
	func : func
}
obj.func();//this : [object Window]
func.apply({});//this : [object Window]
func.call({});//this : [object Window]
func.bind({})();//this : [object Window]
func.apply(null);//this : [object Window]
func.apply();//this : [object Window]

let _self = null;
let Person = ()=>{
	_self = this;
}
let person = new Person();//Person is not a constructor

觀察上述程式碼執行結果可知:
1.嚴格模式和非嚴格模式對箭頭函式中的this無影響;
2.箭頭函式無法當作建構函式使用;
3.箭頭函式中的this只與自身處於的作用域鏈上一層有關;

class

第一次看到class的用法時,我就不禁感慨原型的強大,對於我這種以前使用Java的人來說,class真的是太友好了。
在非嚴格模式

let constructorThis = null;
let funcThis = null;
let staticFuncThis = null;
class Person{
	constructor(){
		constructorThis = this;
	}
	func(){
		funcThis = this;
	}
	static staticFunc(){
		staticFuncThis = this;
	}
}

let person = new Person();
person.func();
Person.staticFunc();

console.log('constructorThis === person : ' + (constructorThis === person));//constructorThis === person : true
console.log('funcThis === person : ' + (funcThis === person));//funcThis === person : true
console.log('staticFuncThis === person : ' + (staticFuncThis === person));//staticFuncThis === person : false
console.log('staticFuncThis : ' + staticFuncThis);//staticFuncThis : class Person...

在嚴格模式

'use strict'
let constructorThis = null;
let funcThis = null;
let staticFuncThis = null;
class Person{
	constructor(){
		constructorThis = this;
	}
	func(){
		funcThis = this;
	}
	static staticFunc(){
		staticFuncThis = this;
	}
}

let person = new Person();
person.func();
Person.staticFunc();

console.log('constructorThis === person : ' + (constructorThis === person));//constructorThis === person : true
console.log('funcThis === person : ' + (funcThis === person));//funcThis === person : true
console.log('staticFuncThis === person : ' + (staticFuncThis === person));//staticFuncThis === person : false
console.log('staticFuncThis : ' + staticFuncThis);//staticFuncThis : class Person...

觀察上述程式碼執行結果可知:
1.嚴格模式和非嚴格模式對class function中的this無影響;
2.建構函式和普通方法的this就是new出來的值(和方法呼叫模式、構造呼叫模式一致)
3.靜態方法的this就是這個class(還是和方法呼叫模式一致 畢竟是用class呼叫的靜態方法)

結尾

由於本人水平有限,如有缺失和錯誤,還望告知。


  1. Java中function只能是方法,被物件或者類呼叫。非靜態方法被物件呼叫時,this是這個呼叫的物件;靜態方法被類呼叫時,則沒有this; ↩︎