javascript中this、new、apply和call詳解

若是在javascript語言裏沒有經過new(包括對象字面量定義)、call和apply改變函數的this指針,函數的this指針都是指向window的,重要的話要說三遍。。。javascript

講解this指針的原理是個很複雜的問題,若是咱們從javascript裏this的實現機制來講明this,不少朋友可能會愈來愈糊塗,javascript裏的this指針邏輯上的概念也是實例化對象,這一點和c#語言裏的this指針是一致的,可是javascript裏的this指針卻比c#裏的this難以理解的多,究其根本我的以爲有三個緣由html

  緣由一:javascript是一個函數編程語言,怪就怪在它也有this指針,說明這個函數編程語言也是面向對象的語言,說的具體點,javascript裏的函數是一個高階函數,編程語言裏的高階函數是能夠做爲對象傳遞的,同時javascript裏的函數還有能夠做爲構造函數,這個構造函數能夠建立實例化對象,結果致使方法執行時候this指針的指向會不斷髮生變化,很難控制。java

  緣由二:javascript裏的全局做用域對this指針有很大的影響,由上面java的例子咱們看到,this指針只有在使用new操做符後纔會生效,可是javascript裏的this在沒有進行new操做也會生效,這時候this每每會指向全局對象window程序員

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

  上面的三個緣由都違反了傳統this指針使用的方法,它們都擁有有別於傳統this原理的理解思路,而在實際開發裏三個緣由又每每會交織在一塊兒,這就更加讓人疑惑不解了,今天我要爲你們理清這個思路,其實javascript裏的this指針有一套固有的邏輯,咱們理解好這套邏輯就能準確的掌握好this指針的使用。c#

  咱們先看看下面的代碼:數組

<script type="text/javascript">
    this.a = "aaa";
    console.log(a);//aaa
    console.log(this.a);//aaa
    console.log(window.a);//aaa
    console.log(this);// window
    console.log(window);// window
    console.log(this == window);// true
    console.log(this === window);// true
</script>

  

在script標籤裏咱們能夠直接使用this指針,this指針就是window對象,咱們看到即便使用三等號它們也是相等的。全局做用域經常會干擾咱們很好的理解javascript語言的特性,這種干擾的本質就是:app

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

  干擾的本質還和function的使用有關,咱們看看下面的代碼:函數

<script type="text/javascript">
    function ftn01(){
       console.log("I am ftn01!");
    }
    var ftn02 = function(){
        console.log("I am ftn02!");
    }
</script> 

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

<script type="text/javascript">
    console.log(ftn01);//ftn01()  注意:在firebug下這個打印結果是能夠點擊,點擊後會顯示函數的定義
    console.log(ftn02);// undefined
    function ftn01(){
       console.log("I am ftn01!");
    }
    var ftn02 = function(){
        console.log("I am ftn02!");
    }
</script> 

  

這又是一段沒有按順序執行的代碼,先看看ftn02,打印結果是undefined,undefined我在前文裏講到了,在內存的棧區已經有了變量的名稱,可是沒有棧區的變量值,同時堆區是沒有具體的對象,這是javascript引擎在預處理(羣裏東方說預處理比預加載更準確,我贊成他的說法,之後文章裏我都寫爲預處理)掃描變量定義所致,可是ftn01的打印結果很使人意外,既然打印出完成的函數定義了,並且代碼並無按順序執行,這隻能說明一個問題:

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

  關於函數表達式的寫法還有祕密能夠探尋,咱們看下面的代碼:

<script type="text/javascript">
    function ftn03(){
        var ftn04 = function(){
            console.log(this);// window
        };
        ftn04();
    }
    ftn03();
</script>

  

運行結果咱們發現ftn04雖然在ftn03做用域下,可是執行它裏面的this指針也是指向window,其實函數表達式的寫法咱們大多數更喜歡在函數內部寫,由於聲明函數裏的this指向window這已經不是祕密,可是函數表達式的this指針指向window倒是經常被咱們所忽視,特別是當它被寫在另外一個函數內部時候更加如此。

  其實在javascript語言裏任何匿名函數都是屬於window對象,它們也都是在全局做用域構造時候完成定義和賦值,可是匿名函數是沒有名字的函數變量,可是在定義匿名函數時候它會返回本身的內存地址,若是此時有個變量接收了這個內存地址,那麼匿名函數就能在程序裏被使用了,由於匿名函數也是在全局執行環境構造時候定義和賦值,因此匿名函數的this指向也是window對象,因此上面代碼執行時候ftn04的this也是指向window,由於javascript變量名稱無論在那個做用域有效,堆區的存儲的函數都是在全局執行環境時候就被固定下來了,變量的名字只是一個指代而已。

  這下子壞了,this都指向window,那咱們到底怎麼才能改變它了?

  在本文開頭我說出了this的祕密,this都是指向實例化對象,前面講到那麼多狀況this都指向window,就是由於這些時候只作了一次實例化操做,而這個實例化都是在實例化window對象,因此this都是指向window。咱們要把this從window變成別的對象,就得要讓function被實例化,那如何讓javascript的function實例化呢?答案就是使用new操做符。咱們看看下面的代碼:

<script type="text/javascript">
    var obj = {
        name:"sharpxiajun",
        job:"Software",
        show:function(){
            console.log("Name:" + this.name + ";Job:" + this.job);
            console.log(this);// Object { name="sharpxiajun", job="Software", show=function()}
        }
    };
    var otherObj = new Object();
    otherObj.name = "xtq";
    otherObj.job = "good";
    otherObj.show = function(){
        console.log("Name:" + this.name + ";Job:" + this.job);
        console.log(this);// Object { name="xtq", job="good", show=function()}
    };
    obj.show();//Name:sharpxiajun;Job:Software
    otherObj.show();//Name:xtq;Job:good
</script>    

  

這是我上篇講到的關於this使用的一個例子,寫法一是咱們大夥都愛寫的一種寫法,裏面的this指針不是指向window的,而是指向Object的實例,firebug的顯示讓不少人疑惑,其實Object就是面向對象的類,大括號裏就是實例對象了,即obj和otherObj。Javascript裏經過字面量方式定義對象的方式是new Object的簡寫,兩者是等價的,目的是爲了減小代碼的書寫量,可見即便不用new操做字面量定義法本質也是new操做符,因此經過new改變this指針的確是不過攻破的真理。

  下面我使用javascript來重寫本篇開頭用java定義的類,代碼以下:

<script type="text/javascript">
    function Person(name,sex,age,job){
        this.name = name;
        this.sex = sex;
        this.age = age;
        this.job = job;
        this.showPerson = function(){
            console.log("姓名:" + this.name);
            console.log("性別:" + this.sex);
            console.log("年齡:" + this.age);
            console.log("工做:" + this.job);
            console.log(this);// Person { name="馬雲", sex="男", age=46, 更多...}
        }
    }
    var person = new Person("馬雲", "男", 46, "董事長");
    person.showPerson();
</script>

  

  看this指針的打印,類變成了Person,這代表function Person就是至關於在定義一個類,在javascript裏function的意義實在太多,function既是函數又能夠表示對象,function是函數時候還能當作構造函數,javascript的構造函數我常認爲是把類和構造函數合二爲一,固然在javascript語言規範裏是沒有類的概念,可是我這種理解能夠做爲構造函數和普通函數的一個區別,這樣理解起來會更加容易些

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

  new操做符會讓構造函數產生以下變化:

  1.       建立一個新對象;

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

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

  4.       返回新對象

  關於第二點其實很容易讓人迷惑,例如前面例子裏的obj和otherObj,obj.show(),裏面this指向obj,我之前文章講到一個簡單識別this方式就是看方法調用前的對象是哪一個this就指向哪一個,其實這個過程還能夠這麼理解,在全局執行環境裏window就是上下文對象,那麼在obj裏局部做用域經過obj來表明了,這個window的理解是一致的。

  第四點也要着重講下,記住構造函數被new操做,要讓new正常做用最好不能在構造函數裏寫return,沒有return的構造函數都是按上面四點執行,有了return狀況就複雜了,這個知識我會在講prototype時候講到。

  Javascript還有一種方式能夠改變this指針,這就是call方法和apply方法,call和apply方法的做用相同,就是參數不一樣,call和apply的第一個參數都是同樣的,可是後面參數不一樣,apply第二個參數是個數組,call從第二個參數開始後面有許多參數。Call和apply的做用是什麼,這個很重要,重點描述以下:

  Call和apply是改變函數的做用域(有些書裏叫作改變函數的上下文)

  這個說明咱們參見上面new操做符第二條:

  將構造函數的做用域賦給新對象(所以this就指向了這個新對象);

  Call和apply是將this指針指向方法的第一個參數。

  咱們看看下面的代碼:

<script type="text/javascript">
    var name = "sharpxiajun";
    function ftn(name){
        console.log(name);
        console.log(this.name);
        console.log(this);
    }
    ftn("101");
    var obj = {
      name:"xtq"
    };
    ftn.call(obj,"102");
    /*
    * 結果以下所示:
    *101
     T002.html (第 73 行)
     sharpxiajun
     T002.html (第 74 行)
     Window T002.html
     T002.html (第 75 行)
     102
     T002.html (第 73 行)
     xtq
     T002.html (第 74 行)
     Object { name="xtq"}
    * */
</script>

  

  咱們看到apply和call改變的是this的指向,這點在開發裏很重要,開發裏咱們經常被this所迷惑,迷惑的根本緣由我在上文講到了,這裏我講講表面的緣由:

  表面緣由就是咱們定義對象使用對象的字面表示法,字面表示法在簡單的表示裏咱們很容易知道this指向對象自己,可是這個對象會有方法,方法的參數可能會是函數,而這個函數的定義裏也可能會使用this指針,若是傳入的函數沒有被實例化過和被實例化過,this的指向是不一樣,有時咱們還想在傳入函數裏經過this指向外部函數或者指向被定義對象自己,這些亂七八糟的狀況使用交織在一塊兒致使this變得很複雜,結果就變得糊里糊塗。

  其實理清上面狀況也是有跡可循的,就以定義對象裏的方法裏傳入函數爲例:

  情形一:傳入的參數是函數的別名,那麼函數的this就是指向window

  情形二:傳入的參數是被new過的構造函數,那麼this就是指向實例化的對象自己;

  情形三:若是咱們想把被傳入的函數對象裏this的指針指向外部字面量定義的對象,那麼咱們就是用apply和call

  咱們能夠經過代碼看出個人結論,代碼以下:

<script type="text/javascript">
var name = "I am window";
var obj = {
    name:"sharpxiajun",
    job:"Software",
    ftn01:function(obj){
        obj.show();
    },
    ftn02:function(ftn){
        ftn();
    },
    ftn03:function(ftn){
        ftn.call(this);
    }
};
function Person(name){
    this.name = name;
    this.show = function(){
        console.log("姓名:" + this.name);
        console.log(this);
    }
}
var p = new Person("Person");
obj.ftn01(p);
obj.ftn02(function(){
   console.log(this.name);
   console.log(this);
});
obj.ftn03(function(){
    console.log(this.name);
    console.log(this);
});
</script>

結果以下:

  

最後再總結一下:

  若是在javascript語言裏沒有經過new(包括對象字面量定義)、call和apply改變函數的this指針,函數的this指針都是指向window

相關文章
相關標籤/搜索