JavaScript——深入瞭解this
阿新 • • 發佈:2021-01-26
# 前言
我曾以為**func**()其實就是**window.func**()
```javascript
function func(){
console.log('this : ' + this);
}
func();//this : [object Window]
window.func();//this : [object Window]
```
直到
```javascript
'use strict'
function func(){
console.log('this : ' + this);
}
func();//this : undefined
window.func();//this : [object Window]
```
也曾為輸出**inside this : [object Window]** 而困惑不已
```javascript
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不論==),
類似像下面這種程式碼,我覺得沒有提的必要;
```javascript
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的作用域中呼叫,輸出的結果天差地別。
```javascript
function func(){
console.log('this : ' + this);
}
let obj = {};
with(obj){
func();//this : [object Window]
}
obj.func = func;
with(obj){
func();//this : [object Object]
}
```
接下來的內容我將以下圖中的思路展開:
![在這裡插入圖片描述](https://img-blog.csdnimg.cn/20200726161727907.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3FxXzM1NTA4ODM1,size_16,color_FFFFFF,t_70)
# ES6之前
這裡的主要思路還是沿用的JavaScript語言精粹。
## 函式呼叫模式
JavaScript中的function不同於Java,Java雖然說萬物皆物件,但是基礎型別和function就不是物件。Java中的function只是物件的行為,但是JavaScript不同,JavaScript雖然同時包含了一些像原型、函式柯里化等程式設計思想,但是在萬物皆物件這一方面,反而比Java更像是面向物件程式設計。JavaScript中的function是支援直接呼叫的。
==在非嚴格模式==:
```javascript
function func(){
console.log('this : ' + this);
}
func();//this : [object Window]
```
==在嚴格模式==:
```javascript
'use strict'
function func(){
console.log('this : ' + this);
}
func();//this : undefined
```
## 方法呼叫模式
方法呼叫模式就是把function當成物件的行為來呼叫,既然是物件的行為,那麼function中的this指向的當然是這個呼叫的物件了。
==在非嚴格模式==:
```javascript
let _self = null;
function func(){
_self = this;
}
let obj = {
func : func
}
obj.func();
console.log('_self === obj : ' + (_self === obj));//_self === obj : true
```
==在嚴格模式==:
```javascript
'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我將以大寫字母開頭。
==在非嚴格模式==:
```javascript
let _self = null;
function Person(){
_self = this;
}
let person = new Person();
console.log('_self === person : ' + (_self === person));//_self === person : true
```
==在嚴格模式==:
```javascript
'use strict'
let _self = null;
function Person(){
_self = this;
}
let person = new Person();
console.log('_self === person : ' + (_self === person));//_self === person : true
```
建構函式這裡我覺得有必要擴充套件一下:
==1.建構函式中返回物件(非基礎型別),會影響上面的結果;==
```javascript
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關鍵字,同樣會影響上面的結果;==
```javascript
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關鍵字還可能會==汙染全域性作用域==:
```javascript
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關鍵字的:
```javascript
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.
```
改進版
```javascript
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也一樣。
==在非嚴格模式==:
```javascript
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]
```
==在嚴格模式==:
```javascript
'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函式一致;
##### 程式碼
```javascript
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是一致的。
==在非嚴格模式==:
```javascript
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]
```
==在嚴格模式==:
```javascript
'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函式一致;
##### 程式碼
```javascript
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)一致。
==在非嚴格模式==:
```javascript
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
```
==在嚴格模式==:
```javascript
'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函式一致;
##### 程式碼
```javascript
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值==)。
```javascript
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也改成箭頭函式,結果又會大不一樣
```javascript
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的值。
==在非嚴格模式==:
```javascript
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
```
==在嚴格模式==:
```javascript
'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真的是太友好了。
==在非嚴格模式==:
```javascript
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...
```
==在嚴格模式==:
```javascript
'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是這個呼叫的物件;靜態方法被類呼叫時,則沒