文章出處

  

4)    this、new、call和apply的相關問題

  講解this指針的原理是個很復雜的問題,如果我們從javascript里this的實現機制來說明this,很多朋友可能會越來越糊涂,因此本篇打算換一個思路從應用的角度來講解this指針,從這個角度理解this指針更加有現實意義。

  下面我們看看在java語言里是如何使用this指針的,代碼如下:

public class Person {
    
    private String name;
    private String sex;
    private int age;
    private String job;

    public Person(String name, String sex, int age, String job) {
        super();
        this.name = name;
        this.sex = sex;
        this.age = age;
        this.job = job;
    }

    private void showPerson(){
        System.out.println("姓名:" + this.name);
        System.out.println("性別:" + this.sex);
        System.out.println("年齡:" + this.age);
        System.out.println("工作:" + this.job);
    }

    public void printInfo(){
        this.showPerson();
    }
    
    public static void main(String[] args) {
        Person person = new Person("馬云", "男", 46, "董事長");
        person.printInfo();
    }

}

//姓名:馬云
//性別:男
//年齡:46
//工作:董事長

  

  上面的代碼執行后沒有任何問題,下面我修改下這個代碼,加一個靜態的方法,靜態方法里使用this指針調用類里的屬性,如下圖所示:

 

  我們發現IDE會報出語法錯誤“Cannot use this in a static context”,this指針在java語言里是不能使用在靜態的上下文里的。

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

  有上面代碼我們可以看到,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原理的理解思路,而在實際開發里三個原因又往往會交織在一起,這就更加讓人迷惑不解了,今天我要為大家理清這個思路,其實javascript里的this指針有一套固有的邏輯,我們理解好這套邏輯就能準確的掌握好this指針的使用。

  我們先看看下面的代碼:

<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語言的特性,這種干擾的本質就是:

  在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

 


文章列表


不含病毒。www.avast.com
arrow
arrow
    全站熱搜
    創作者介紹
    創作者 大師兄 的頭像
    大師兄

    IT工程師數位筆記本

    大師兄 發表在 痞客邦 留言(0) 人氣()