關於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(){
return fn.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並返回~ 😵
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爲基礎》,加上大部分自己的理解,順便做個記錄,加深印象)
原文出處:https://www.cnblogs.com/jony-it/p/10344405.html