前言
我曾以為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呼叫的靜態方法)
結尾
由於本人水平有限,如有缺失和錯誤,還望告知。
Java中function只能是方法,被物件或者類呼叫。非靜態方法被物件呼叫時,this是這個呼叫的物件;靜態方法被類呼叫時,則沒有this; ↩︎