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(){
   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

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章