關於this,你真的了解嗎?


前言

我曾以為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; ↩︎


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM