1. 程式人生 > >JavaScript——深入瞭解this

JavaScript——深入瞭解this

# 前言 我曾以為**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是這個呼叫的物件;靜態方法被類呼叫時,則沒