JS 關於this p9
關於this這個貨,常常讓我感到頭疼,也很難說清這貨到底是什麼機制,今天就詳細記錄一下this,瞭解他就跟理解閉包差不多,不理解的時候我們會感到很難受總想著避開他,當我們真正理解之後,會有種茅塞頓開的感覺,但是也不要掉以輕心,說不定哪天又給來一腳~
先看一個例子,之前的部落格中也提過到的this使用:
function fn(){ console.log(this.a) } var a = 2; var o = {a:7}; // 使用之前講到的apply fn.apply(o); // 7 fn();// 2
那麼this那麼簡單好用麼,當前不是,this是比較複雜的機制,有很多規則,不小心的話很會難受。
一、拋開上面的例子,對於this我們平時會有一些誤解:
1.指向自身:
function fn(){ console.log(this.a); this.a ++; } fn.a = 0; fn(1); console.log(fn.a);//0
我們預期是輸出1,因為fn被呼叫了1次,並且那麼a++ 會導致 變成1,但是最終卻是0,如果下面再來一句~
var a = 0; fn(1); console.log(a);//1
this.a 實際改變的是全域性作用域a,所以例子中的 this 並沒有指定為所包含的這個函式當中 = =。
如果非要呼叫自身,可以採用具名函式的方式(不要使用arguments.callee,在上一章提到了,被廢棄的方法我們還是不要接觸了):
function fn(){ fn.a ++; } fn.a = 0; fn(); fn.a;//1
或者,使用apply或者call
function fn(){ this.a ++; } fn.a = 0; fn.call(fn); fn.a;//1
說一下第一個例子為毛會是0,this 在當時指向的是全域性作用域,而不是函式本身,當呼叫fn()方法時,this.a 會在全域性作用域中宣告一個 a 並執行 ++,就像下面這樣
function fn(){ this.a ++; } fn(); console.log(a); // NaN (因為a只是宣告,當執行RHS的時候並沒有a,那麼undefined+1 會是啥? NaN)
所以:this 並不是指向自身。
2.作用域
function fn(){ var a = 0; this.fn2(); } function fn2(){ console.log(this.a) } fn();// undefined
首先this.fn2() 確實找到了fn2,在fn2中怎麼會找到a=0呢,按照之前說的,它也只是在全域性作用域中建立了一個a而已(實際上都沒有建立,因為此處只有LHS 沒有RHS,詳細瞭解的話到之前的文章中看一下作用域,在此呼叫成功就當成是個意外吧 = =)。
那麼var a 把它理解為私有的屬性,而在fn2中想要用fn的私有屬性怎麼可能呢? 這段程式碼實際上想通過詞法作用域的概念來用來fn中的a,但this並不會查到啥,除非這樣(平常使用的比較多的):
function fn(){ var a = 0; fn2(a); } function fn2(a){ console.log(a) } fn();
fn2中的a通過RHS 查詢在上一層找到了 0。
so、this和詞法作用的查詢是衝突的,不要再想著這樣用了,忘了它吧~~
那麼下一句你可能在很多地方都看到過:
this實際上是在函式被呼叫時發生的繫結,它的指向取決與函式在哪裡被呼叫。
二、呼叫位置:
如何尋找位置,先告訴你,上一層或者說最後一層~ 別急著想像,看程式碼:
function fn(){ fn2(); } function fn2(){ console.log('fn2'); } fn();// fn2
fn的呼叫位置在全域性作用域下,fn2的呼叫位置在fn下(fn2的呼叫棧就是 fn - > fn2)。
所以,明白了呼叫位置了? 如果是多層,還有一種辦法通過強大的瀏覽器開發者介面,如下圖:(多加了一個fn3,這樣可能更清晰一點)
三、繫結規則:
在呼叫棧中找了呼叫位置,接下來看看繫結規則:4種
優先順序為:new > 顯式 > 隱式 > 預設(為啥最後說)
1.預設規則
function fn(){ console.log(this.a); } var a =2; fn(); //2
這個例子上面基本上都用到了,它用到的時候預設規則,this指向的是全域性物件(並不是一定指向全域性物件的哦,後面會說到)
因為fn直接呼叫fn(),我們或許也可以這樣理解 window.fn() ,this.a 指向window.a ,輸出2~ 是不是很好理解(有點像隱式繫結這樣說~)
但是有一點是需要注意的是,嚴格模式下不適用
function fn(){ "use strict"; console.log(this.a); } var a = 2; fn(); // TypeError: Cannot read property 'a' of undefined
this不會預設繫結到window上,除非~:
window.fn();//2
真正意義上用到了隱式繫結~~(別急,馬上就說隱式繫結)
還有很 重要的一點 ,嚴格模式下預設繫結只會關注函式體內部,不會關注被誰呼叫,像下面這樣,是可以使用的:
function fn(){ console.log(this.a); } var a = 2; function fn2(){ "use strict"; fn(); } fn2();//2
2.隱式繫結:
是否呼叫位置有上下文物件或者是上下文物件,或者說被某個物件包含或擁有:
function fn(){ console.log(this.a); } var o = { a:2, fn: fn } // o.fn = fn; o.fn();//2
不管是先定義或者是後引用 ,在此隱式繫結的規則會把函式呼叫中的this 繫結到這個上下文物件。(回顧一下,在這裡o的fn擁有所在作用域o的閉包或者說行使權、使用權,把它看成 o = {a:2,fn:function(){ console.log(this.a) }})
在宣告一次:物件引用鏈只有 上一層或則最後一層 在呼叫位置起作用。
function fn(){ console.log(this.a) } var o = { a:0, fn:fn } var o2 = { a:2, o:o, fn:o.fn } o2.o.fn(); // 0 (繫結的是o) o.fn();//2(這裡o2的fn為函式本身,與o沒有直接關係,所以this繫結的是o2這個物件,或者說叫隱式丟失)
tip:隱式丟失
// 跟上面的例子一個意思 function fn(){ console.log(this.a) } var o = { a:2, fn : fn } var x = o.fn; x();//undefined
o.fn 引用的是函式本身,並沒有執行fn函式,所以x知識引用了fn函式,當執行x函式時,應用了到了上面說到的預設繫結,(在全域性作用域聲明瞭a,但是沒有賦值),好吧,怕忘記了,如果像下面這樣就更清晰了
// 在上面例子的基礎上加2句 var a =7; x();//7
再來一個,參考書《你不知道的javascript》:
function fn(){ console.log(this.a) } function fn2(f){ f(); } var o = { a:2, fn:fn } fn2(o.fn); // undefined
fn2執行的f 是fn函式本身,跟o沒有毛關係,所以最終也是使用了預設繫結。
還有一種就是window內建物件,跟上面結果一樣,在此就不寫例子了。
3.顯示繫結
這個可能最好理解,就是指定this要繫結的上下文物件,主要用到的就是 apply、call、bind,關於這3個貨,想看的可以看看之前的文章 JS 關於 bind ,call,apply 和arguments p8
這裡主要說一個概念:如果你傳入一個原始值比如:“”、1、true,當作this 的繫結物件,這個值會轉為它的物件形式(new String()、new Number()、new Boolean()),稱為裝箱。
function fn(){ console.log(this.a); } fn.call({a:2});//2 fn();// undefined
如上面看到的顯示繫結也不會解決丟失繫結的問題。
但是我們可以通過 硬繫結 來解決這個問題:
function fn(){ console.log(this.a) } var o = { a:2 } function fn2(){ fn.call(o); } fn2();//2
這樣呼叫fn2的時候都會預設顯示繫結。
它的典型行為是:建立一個包裹函式,負責接收引數並返回值。
看這個例子:
function fn(f){ console.log(this.a); console.log(f); } var o = { a:2 } function fn2(){ fn.apply(o,arguments) } fn2(6); // 2 // 6
跟上一個例子差不多,這裡利用了arguments內建物件來傳遞引數。
還有一種是建立輔助函式:
function fn(f){ console.log(this.a); console.log(f); } var o = { a:3} function fn2(){ return function (){ fn.apply(o,arguments); } } var fn3 = fn2(); fn3(8); // 3 // 8
對比上一個例子,一個是立即執行,另一個是返回繫結後的函式本身再進行呼叫。或者使用bind也行
function fn(f){ console.log(this.a); console.log(f); } var o = { a:3} function fn2(){ returnfn.bind(o); } var fn3 = fn2(); fn3(5); // 3 // 5
另外關於API 呼叫上下文在實際應用中有需要函式上就是通過call 與apply 實現了顯示繫結,比如[].forEach();
4.new 繫結
首先要說的是,new不會例項化某個類(和我之前的說法有些衝突,但是例項我們會比較好理解),因為他們是被new操作符呼叫的普通函式。
類似這種 new String() ,正確的說法叫做“ 函式呼叫 ”,因為實際上js中並不存在建構函式之說,只存在函式呼叫。
上面是官方一點的語言,其實我們只需要知道這幾點暫時:
new 會建立一個 全新的物件 ,並且這個物件會 繫結到函式呼叫的this ,如果這個函式 沒有return 那麼就 返回這個函式本身的 新物件 ~
function fn(){this.a = 3; } var fn2 = new fn(); fn2.a;//3
如上,new出來的新物件fn2 繫結到了fn的this上,是一個全新的物件(這跟之前的文自定義建立物件中說到的一樣,如果為私有變數,則不會擁有它,或者它看不到,但是可以使用它比如下面這種:)
function fn(){ this.a = 3; var b = 4; this.fn2 = function(){ console.log(b) ; } } var fn3 = fn(); console.log(fn3);// {a: 3, fn2: ƒ} fn3.fn2();//4
這裡除了this,還有閉包的相關概念,在此就不多說了。大家只要知道this繫結到了新物件上(全新的)。
好了,4種繫結說完了,接下來說下優先順序:
function fn(){ console.log(this.a) } var o = {a:2,fn:fn}; var o2 = {a:5,fn:fn} o.fn();//2 o.fn.call(o1);//2
那麼看顯示繫結應該是優先於隱式繫結的,通過最後一行可以看出來(這裡我感覺有點不好理解,或者我們可以這樣理解,o.fn() 是顯示繫結this所以從呼叫位置來看,上下文o的a為2所以this.a輸出的為2;o.fn 為函式本身,所以在對函式本身進行顯示繫結,所以this繫結到了o2上面)
並且顯示繫結和隱式繫結都會丟失this(上面提到的)。
看下一個new 繫結和隱式繫結:
function fn(f){ this.a = f } var o = { fn:fn } var o2 ={} o.fn(0); console.log(o.a);//0 o.fn.call(o2,3); console.log(o2.a);//3 var fn2 = new o.fn(5); console.log(o.a);//0 console.log(o2.a);//3 console.log(fn2.a);//5
o.fn(0) 為隱式繫結,this繫結到o 上,o.a 為0 這點不用多說。
o.fn.call(o2,3); fn函式本身中this被顯示繫結帶o2上,o2物件獲得a併為3;
最後fn2為一個new出來的新物件,this繫結到這個新物件上(上下文),它的a為5。(因為函式就是物件)
不是很明顯,下面來一個(比較new 和顯示繫結):
function fn(f){ this.a = f } var o = {} var x = fn.bind(o) x(1); o.a;//1 var y = new x(3) y.a;//3
個人感覺在判斷優先順序時,不能只是記住哪種規則優先順序高,而是需要仔細分析,還是理解最重要
比如:
o.fn.call() ,首先o是一個物件,o物件中包涵的函式fn 在這裡並沒有呼叫它,按照之前所說,它只是表示函式本身,那麼在呼叫call的顯示繫結並執行了該函式,那麼this肯定會繫結到顯示繫結的第一個引數上,所以是顯示優先
var fn2 = new o.fn(); 記住最關鍵的那句,new會 建立一個新物件 並繫結到this上,這樣就不會迷糊了,o.fn 是函式本身,並且建立一個新物件,那麼fn2 是一個全新的物件,所以這裡就是new 優先
(this與call無法同時使用但是可以用bind)
var fn2 = fn.bind(o);
var bar = new fn2();
縱使怎麼變,fn2是顯示繫結沒錯,如果像上面例子 fn是這樣的 function(f){ this.a = f },那麼fn2.a 肯定是o的a,
但是bar 宣告使用new 繫結,那麼會建立一個 新物件~新物件~新物件 ,所以,fn2裡如果加上一個引數比如 new fn2(3) ,那麼新宣告的bar.a 肯定就是3~
不要被所謂的優先順序弄暈了,記住這幾條重要的規則,管它怎麼變,相信都能找到最終的那個this。
這裡有一個概念:第一個引數用於繫結this,剩餘的引數用於傳遞給下層函式的這種行為被稱為“部分應用”、或者“ 柯里化 ”。(bind、apply、call)
那麼這裡對於判斷this繫結的是什麼就很好查了:
1.先看new
2.再看call、apply、bind
3.看隱式呼叫 o.fn()
4.啥都沒,那就是預設繫結(官方的語言是,如果在嚴格模式下,就繫結到undefined,否則繫結到 全域性物件 )
但是:
如果把null 或則undefined作為this的繫結物件傳給apply的話,嘿嘿~
呼叫的時候會被忽略~
function fn(){ console.log(this.a) } fn.apply(null);// undefined
其實就等於直接呼叫 fn()罷了。
當然我們可以另類的用這種機制:
function fn(){ for(let i = 0 ;i<arguments.length;i++){ console.log(arguments[i]); } } fn.apply(null,[1,2]); // 1 // 2
是的,可以用來做展開陣列(但是這裡看著沒必要)(在es6裡可以通過...來解決展開陣列的問題,像這樣fn(...[1,2]))
如果使用null 作為柯里化的這種操作很危險,為啥,看下面:
function fn(a){ this.a = a; } fn.call(null,2); console.log(a);//2
預設繫結使全域性作用域的a 賦值了2(成功進行了RHS 查詢)。
如果非要使用的話:可以使用空物件,如下
function fn(a){ this.a = a; console.log(this.a) } var n = Object.create(null); fn.call(n,2); console.log(a);// a is not defined // 或者使用嚴格模式也未嘗不可,但是程式碼中混用嚴格模式與懶惰模式真的會很不好維護~~ function fn(a){ 'use strict'; this.a = a; } fn.call(null,2) ; // Uncaught TypeError: Cannot set property 'a' of null
拓展一下Object.create()
Object.create()
方法建立一個新物件,使用現有的物件來提供新建立的物件的__proto__。 (請開啟瀏覽器控制檯以檢視執行結果。 )
另外:間接引用也會使用預設繫結
function fn(){ console.log(this.a) } var o = {a:2,fn:fn} var o2 = {} var x = o.fn; x();// undefined (o2.fn = o.fn)();//undefined
這裡其實很簡單,按照之前說的,o.fn 是函式本身,並沒有進行繫結~
還有一種繫結叫做“ 軟繫結 ”,可以給預設繫結指定一個全域性物件和undefined的值,同事保留隱式繫結或者顯示繫結this的能力~
說實話,平時我們不太會用到,瞭解一下就好,軟繫結在內建方法中並不存在,如果想要使用,必須自己實現,下面給出官方的例子:
if(!Function.prototype.softBind){ Function.prototype.softBind = function (obj){ var fn=this; var curried = [].slice.call(arguments,1); var bound = function(){ return fn.apply((!this||this===(window||global))?obj:this,curried.concat.apply(curried,arguments)); }; bound.prototype = Object.create(fn.prototype); return bound; }; }
檢查呼叫物件this繫結的物件到底是誰?如果是window或者undefined、null之類的那麼this繫結就交給引數物件obj去處理,相反
如果不是,則交給this本身去處理,方法的最後一步把fn 也就是this的原型保留並傳給新宣告的還說bound並返回~ :dizzy_face:
function fn(){console.log(this.a)} var o = {a:0}, o1 = {a:1}, o2 = {a:2}; var x = fn.softBind(o); x();// 0 o1.fn = fn.softBind(o); o1.fn();// 1~ 繫結的是o但是最終o1為1不是0 // 為了與bind區分下面來一個bind var y = {} y.fn = fn.bind(o); y.fn() // 0
bind 是顯示繫結,並返回一個顯示繫結後的函式(this已繫結,不是函式本身),所以y.fn() 中的this.a為顯示繫結物件o中的a 也就是0。
那麼軟繫結:
如果this繫結到全域性物件或者undefined,那麼把預設物件交給this (x=fn.softBind)這裡,因為x() 預設其實就是 window.x(), 呼叫物件是window所以,在執行x()沒有使用預設繫結,而是交給了obj也就是傳給softBind的o去處理。
因為o1.fn = fn.softBind(o),再看fn.softBind(o), 返回的方法交給了o去處理,但是呼叫o1.fn 時,呼叫物件o1 並不是window,所以交給了o1 去處理,也就是使用了隱式繫結。
那麼顯示繫結呢:
o2.fn = fn.softBind(o); o2.fn();//2
同上面一句話,我就不多打一遍了。
軟繫結我們平時用的很少~ 沒事就不要用了,省得跟bind 搞暈掉了 = =~~
五、胖函式(箭頭函式)對this的影響
箭頭函式跟let一樣會劫持所在的塊作用域{....},是隱式的或者說不會干擾父級,在這裡,它並不是使用以上4中繫結this的規則,而是根據外層作用域來決定this由誰繫結~!
function fn(){ console.log(this.a); return ()=>{ console.log(this.a); } } var o = {a:2} var x = fn.bind(o) var y = x();//2 一切正常fn的this繫結到o的a y(); // 2胖箭頭裡的this 也綁定了o?
按照之前所說,bind 只會繫結函式的作用域,而不會管子孫的死活,像是下面這樣
function fn(){ console.log(this.a); return function (){console.log(this.a)} } var o = {a:2} var x = fn.bind(o) var y = x();//2 y();// undefined
但是胖箭頭打破了這種規則,而且誰都不鳥~並且,箭頭函式在繫結後,無法被修改,及時new 也不行
function fn(){ console.log(this.a); return ()=>{ console.log(this.a); } } var o = {a:2} var x = fn.bind(o); var y = x(); //2 y();//2 // 下面開始裝了,根本不鳥你顯示繫結 y.call({a:5});//2
在這裡,箭頭函式更適合回撥函式~比如定時器等等,可以根據外層(詞法作用域)來繫結this。
另外:
其實之前降到的var self = this; 與之相似,道理都是一樣的。
如果在程式碼中你覺得用self = this 用的爽,那就不要考慮用箭頭函式,
如果你覺得直接用this顯得niuX,那麼如果遇到類似情況,可以使用胖箭頭 = =~
結束。(文章主要以書《你不知道的javascript為基礎》,加上大部分自己的理解,順便做個記錄,加深印象)