1. 程式人生 > >圖說js中的this——深入理解javascript中this指針

圖說js中的this——深入理解javascript中this指針

前端 javascript this

沒搞錯吧!js寫了那麽多年,this還是會搞錯!沒搞錯,javascript就是回搞錯!

…………

在寫java的時候,this用錯了,idea都會直接報錯!

比如……

技術分享圖片

但是,js,……idea,愛莫能助了……

在面向對象編程裏有兩個重要的概念:一個是類,一個是實例化的對象,類是一個抽象的概念,用個形象的比喻表述的話,類就像一個模具,而實例化對象就是通過這個模具制造出來的產品,實例化對象才是我們需要的實實在在的東西,類和實例化對象有著很密切的關系,但是在使用上類的功能是絕對不能取代實例化對象,就像模具和模具制造的產品的關系,二者的用途是不相同的。

  有上面代碼我們可以看到,this指針在java語言裏只能在實例化對象裏使用,this指針等於這個被實例化好的對象,而this後面加上點操作符,點操作符後面的東西就是this所擁有的東西,例如:姓名,工作,手,腳等等。


  其實javascript裏的this指針邏輯上的概念也是實例化對象,這一點和java語言裏的this指針是一致的,但是javascript裏的this指針卻比java裏的this難以理解的多,究其根本原因我個人覺得有三個原因:


  原因一:javascript是一個函數編程語言,怪就怪在它也有this指針,說明這個函數編程語言也是面向對象的語言,說的具體點,javascript裏的函數是一個高階函數,編程語言裏的高階函數是可以作為對象傳遞的,同時javascript裏的函數還有可以作為構造函數,這個構造函數可以創建實例化對象,結果導致方法執行時候this指針的指向會不斷發生變化,很難控制。


  原因二:javascript裏的全局作用域對this指針有很大的影響,由上面java的例子我們看到,this指針只有在使用new操作符後才會生效,但是javascript裏的this在沒有進行new操作也會生效,這時候this往往會指向全局對象window。


  原因三:javascript裏call和apply操作符可以隨意改變this指向,這看起來很靈活,但是這種不合常理的做法破壞了我們理解this指針的本意,同時也讓寫代碼時候很難理解this的真正指向

技術分享圖片

上面的三個原因都違反了傳統this指針使用的方法,它們都擁有有別於傳統this原理的理解思路,而在實際開發裏三個原因又往往會交織在一起,so,this,雲裏霧裏了……

技術分享圖片

入門書:professionnal Javascript for web devolopers,——高級的說法是這樣的:

this總是指向調用該方法的對象!

技術分享圖片


 var name="zhoulujun";
    function say(){
        console.log(this.name)
    }
    say(); //zhoulujun

技術分享圖片

在script標簽裏我們可以直接使用this指針,this指針(指向window對象,結果)就是window對象,即使使用三等號它們也是相等的。全局作用域常常會幹擾我們很好的理解javascript語言的特性,這種幹擾的本質就是:

在javascript語言裏全局作用域可以理解為window對象,記住window是對象而不是類,也就是說window是被實例化的對象,這個實例化的過程是在頁面加載時候由javascript引擎完成的,整個頁面裏的要素都被濃縮到這個window對象,因為程序員無法通過編程語言來控制和操作這個實例化過程,所以開發時候我們就沒有構建這個this指針的感覺,常常會忽視它,這就是幹擾我們在代碼裏理解this指針指向window的情形。

這裏this指向window對象,所以this.name->zhoulujun!

技術分享圖片


當執行 say函數的時候, JavaScript 會創建一個 Execute context (執行上下文),執行上下文中就包含了 say函數運行期所需要的所有信息。 Execute context 也有自己的 Scope chain, 當函數運行時, JavaScript 引擎會首先從用 say函數的作用域鏈來初始化執行上下文的作用域鏈。

這方面的知識,建議參考:

http://blog.csdn.net/wangxiaohu__/article/details/7260668

http://www.jb51.net/article/30706.htm

這裏可以大致記一下:


var myObj={
    name:"zhoulujun",
    fn:function(){
        console.log(this.name)
    }

};
 myObj.fn();

技術分享圖片


這裏的this指向obj,因為fn()運行在obj裏面……

然後再來看……

var name="zhoulujun";
function say(){
    console.log(this.name)
    console.log(this)
}
say();

function say2(){
    var site="zhoulujun.cn";
    console.log(this.site);
}
say2();

技術分享圖片


myObj2={
    site:"zhoulujun.cn",
    fn:function(){
        console.log(this.site)
    }
}


技術分享圖片

這裏的this指向的是對象myObj2,因為你調用這個fn是通過myObj2.fn()執行的,那自然指向就是對象myObj2,這裏再次強調一點,this的指向在函數創建的時候是決定不了的,在調用的時候才能決定,誰調用的就指向誰,一定要搞清楚這個


然後,我們更深入(受不了 …………

myObj3={
    site:"zhoulujun.cn",
    andy:{
        site:"www.zhoulujun.cn",
        fn:function(){
            console.log(this.site)
        }
    }
};
myObj3.andy.fn();

技術分享圖片

這裏同樣也是對象Object點出來的,但是同樣this並沒有執行它,那你肯定會說我一開始說的那些不就都是錯誤的嗎?其實也不是,只是一開始說的不準確,接下來我將補充一句話,我相信你就可以徹底的理解this的指向的問題。

如果,你實在理解不了,就這麽樣背下來吧!


情況1:如果一個函數中有this,但是它沒有被上一級的對象所調用,那麽this指向的就是window,這裏需要說明的是在js的嚴格版中this指向的不是window,但是我們這裏不探討嚴格版的問題,你想了解可以自行上網查找。


情況2:如果一個函數中有this,這個函數有被上一級的對象所調用,那麽this指向的就是上一級的對象。


情況3:如果一個函數中有this,這個函數中包含多個對象,盡管這個函數是被最外層的對象所調用,this指向的也只是它上一級的對象,如果不相信,那麽接下來我們繼續看幾個例子。

這樣既對了嗎??深入點(就受不了了……討厭……

    myObj3={
        site:"zhoulujun.cn",
        andy:{
            site:"www.zhoulujun.cn",
            fn:function(){
                console.log(this)
                console.log(this.site)
            }
        }
    };
//    myObj3.andy.fn();
    var fn=myObj3.andy.fn;
    fn();

技術分享圖片

其實,這裏的 fn等價於

fn:function(age){

console.log(this.name+age);

}

下面我們來聊聊函數的定義方式:聲明函數和函數表達式

我們最上面第一個案例定義函數的方式在javascript語言稱作聲明函數,第二種定義函數的方式叫做函數表達式,這兩種方式我們通常認為是等價的,但是它們其實是有區別的,而這個區別常常會讓我們混淆this指針的使用,我們再看看下面的代碼:

技術分享圖片

為什麽say可以執行,say3不可以?那是因為:

技術分享圖片

為什麽say3打印結果是undefined,我在前文裏講到了undefined是在內存的棧區已經有了變量的名稱,但是沒有棧區的變量值,同時堆區是沒有具體的對象,這是javascript引擎在預加載掃描變量定義所致,但是ftn01的打印結果很令人意外,既然打印出完成的函數定義了,而且代碼並沒有按順序執行,這只能說明一個問題:


在javascript語言通過聲明函數方式定義函數,javascript引擎在預處理過程裏就把函數定義和賦值操作都完成了,在這裏我補充下javascript裏預處理的特性,其實預處理是和執行環境相關,在上篇文章裏我講到執行環境有兩大類:全局執行環境和局部執行環境,執行環境是通過上下文變量體現的,其實這個過程都是在函數執行前完成,預處理就是構造執行環境的另一個說法,總而言之預處理和構造執行環境的主要目的就是明確變量定義,分清變量的邊界,但是在全局作用域構造或者說全局變量預處理時候對於聲明函數有些不同,聲明函數會將變量定義和賦值操作同時完成,因此我們看到上面代碼的運行結果。由於聲明函數都會在全局作用域構造時候完成,因此聲明函數都是window對象的屬性,這就說明為什麽我們不管在哪裏聲明函數,聲明函數最終都是屬於window對象的原因了。

這裏推薦看下——java一個類的執行順序:

http://www.zhoulujun.cn/zhoulujun/html/java/javaBase/7704.html

技術分享圖片

其實在javascript語言裏任何匿名函數都是屬於window對象,它們也都是在全局作用域構造時候完成定義和賦值,但是匿名函數是沒有名字的函數變量,但是在定義匿名函數時候它會返回自己的內存地址,如果此時有個變量接收了這個內存地址,那麽匿名函數就能在程序裏被使用了,因為匿名函數也是在全局執行環境構造時候定義和賦值,所以匿名函數的this指向也是window對象,所以上面代碼執行時候fn的this都是指向window,因為javascript變量名稱不管在那個作用域有效,堆區的存儲的函數都是在全局執行環境時候就被固定下來了,變量的名字只是一個指代而已。


類似的情況(面試題喜歡這麽考!)……比如:

技術分享圖片

this都是指向實例化對象,前面講到那麽多情況this都指向window,就是因為這些時候只做了一次實例化操作,而這個實例化都是在實例化window對象,所以this都是指向window。我們要把this從window變成別的對象,就得要讓function被實例化,那如何讓javascript的function實例化呢?答案就是使用new操作符。

再來看 構造函數:

function  User(){
    this.name="zhoulujun";
    console.log(this);
}
var andy=new User();
console.log(andy.name)

技術分享圖片


why andy 的name 是 zhoulujun,那是:因為:

new關鍵字可以改變this的指向,將這個this指向對象andy,

那andy什麽時候又成了思密達,oh,no,is Object?

因為用了new關鍵字就是創建一個對象實例(重要的事情默讀三遍)

我們這裏用變量andy創建了一個User用戶實例(相當於復制了一份User到對象andy裏面),此時僅僅只是創建,並沒有執行,而調用這個函數User的是對象andy,那麽this指向的自然是對象andy,那麽為什麽對象User中會有name,因為你已經復制了一份User函數到對象andy中,用了new關鍵字就等同於復制了一份。

java 程序猿: Class user=new User();似曾相識木有……

技術分享圖片

function既是函數又可以表示對象,function是函數時候還能當做構造函數,javascript的構造函數我常認為是把類和構造函數合二為一,當然在javascript語言規範裏是沒有類的概念,但是我這種理解可以作為構造函數和普通函數的一個區別,這樣理解起來會更加容易些

下面我貼出在《javascript高級編程》裏對new操作符的解釋:

new操作符會讓構造函數產生如下變化:

1. 創建一個新對象;

2. 將構造函數的作用域賦給新對象(因此this就指向了這個新對象);

3. 執行構造函數中的代碼(為這個新對象添加屬性);

4. 返回新對象




……

媽的:讀的那麽拗口,不明覺厲…………看圖……還不

不明白……

var myObj5={
    name:"andy"
};
var myObj6=new Object();
myObj6.name="andy";

 function  say5(name){
     console.log(name)
 }
 var say6=new Function("name","console.log(name)");
console.log(myObj5)
console.log(myObj6)
 say5("andy");
 say6("andy");

技術分享圖片


還不明白,就請奶奶買塊豆腐,撞死算了……

第四點也要著重講下,記住構造函數被new操作,要讓new正常作用最好不能在構造函數裏寫return,沒有return的構造函數都是按上面四點執行,有了return情況就復雜了

return這王八蛋……

技術分享圖片

那麽我這樣呢……

技術分享圖片


does it have to be like this?Tell me why(why),is there something I have missed?

Tell me why(why),cos I don't understand…………

技術分享圖片

那是因為……because of u?no return……技術分享圖片


所以:如果返回的是基本類型,就會丟掉…只能返回Object類型……typeof xx ===“object”

看到called 沒有?什麽鬼!!

其實new關鍵字會創建一個空的對象,然後會自動調用一個函數apply方法,將this指向這個空對象,這樣的話函數內部的this就會被這個空的對象替代。

 var a={
     name:"andy",
     site:"zhoulujun.cn",
     fn:function(age){
         console.log(this.name+age);
     }
 };
var b={
    name:"zhoulujun",
    site:"www.zhoulujun.cn",
    fn:function(age){
        console.log(this.name+age);
    }
};
a.fn(2); //andy2
a.fn.call(b,2) //zhoulujun2
a.fn.apply(b,[2])//zhoulujun2

當然,還有bind……

var arr = [1, 2];
var add = Array.prototype.push.bind(arr, 3);
 
// effectively the same as arr.push(3)
add();
 
// effectively the same as arr.push(3, 4)
add(4);
 
console.log(arr);
// <- [1, 2, 3, 3, 4]

在下面的例子,this將無法在作用域鏈中保持不變。這是規則的缺陷,並且常常會給業余開發者帶來困惑。

function scoping () {
  console.log(this);
 
  return function () {
    console.log(this);
  };
}
 
scoping()();
// <- Window
// <- Window

有一個常見的方法,創建一個局部變量保持對this的引用,並且在子作用域中不能有同命變量。子作用域中的同名變量將覆蓋父作用域中對this的引用。

function retaining () {
  var self = this;
 
  return function () {
    console.log(self);
  };
}
 
retaining()();
// <- Window

除非你真的想同時使用父作用域的this,以及當前this值,由於某些莫名其妙的原因,我更喜歡是使用的方法.bind函數。這可以用來將父作用域的this指定給子作用域。

function bound () {
  return function () {
    console.log(this);
  }.bind(this);
}
 
bound()();
// <- Window

寫到這裏,都看不下去,邏輯有點混亂,有的是從前輩哪裏引用的。

改天有時間整理下,然後,在去講下閉包(……closer

技術分享圖片

技術分享圖片




參考文章:

http://blog.jobbole.com/81018/

http://blog.jobbole.com/74110/

http://blog.jobbole.com/54267/

http://www.cnblogs.com/aaronjs/archive/2011/09/02/2164009.html#commentform

http://www.codeceo.com/article/javascript-this-pointer.html


轉載請註明文章來源:圖說js中的this--深入理解javascript中this指針 - js - 周陸軍的個人網站

圖說js中的this——深入理解javascript中this指針