你不知道的JavaScript--Item8 函式,方法,建構函式呼叫

itKingOne發表於2018-07-17

1、函式呼叫

Function絕對是JavaScript中的重中之重。在JavaScript中,Function承擔了procedures, methods, constructors甚至是classes以及modules的功能。

在物件導向程式設計中,functions,methods以及class constructor往往是三件不同的事情,由不同的語法來實現。但是在JavaScript中,這三個概念都由function來實現,通過三種不同的模式。

最簡單的使用模式就是function 呼叫:

function hello(username) {  
    return "hello, " + username;  
}  
hello("Keyser Söze"); // "hello, Keyser Söze"  

2、方法的呼叫

而methods這一概念在JavaScript中的表現就是,一個物件的屬性是一個function:同樣的是函式,將其賦值給一個物件的成員以後,就不一樣了。將函式賦值給物件的成員後,那麼這個就不在稱為函式,而應該叫做方法。

var obj = {  
    hello: function() {  
        return "hello, " + this.username;  
    },  
    username: "Hans Gruber"  
};  
obj.hello(); // "hello, Hans Gruber"  

真正的行為是,呼叫本身才會決定this會繫結到哪個物件,即: 
obj1.hello()會將this繫結到obj1,obj2.hello()則會將this繫結到obj2。記住一句話,誰呼叫,this就指向誰

正因為this繫結的這種規則,在下面的用法也是可行的:

function hello() {  
    return "hello, " + this.username;  
}  

var obj1 = {  
    hello: hello,  
    username: "Gordon Gekko"  
};  
obj1.hello(); // "hello, Gordon Gekko"  

var obj2 = {  
    hello: hello,  
    username: "Biff Tannen"  
};_  
obj2.hello(); // "hello, Biff Tannen" 

但是,在一個普通的函式中,如上面的hello函式,使用this關鍵字是不太好的方式,當它被直接呼叫的時候,this的指向就成了問題。在這種情況下,this往往被指向全域性物件(GlobalObject),在瀏覽器上一般就是window物件。 
而這種行為是不確定和沒有意義的。

所以在ES5標準中,如果使用了strict mode,那麼this會被設定為undefined:

function hello() {  
    "use strict";  
    return "hello, " + this.username;  
}  
hello(); // error: cannot read property "username" of undefined  

以上這種做法是為了讓潛在的錯誤更快的暴露出來,避免了誤操作和難以找到的bug。 
區別普通函式呼叫和方法呼叫,直接看這個例子就明確了。

var func = function() {
    alert(this);
};
var o = {};
o.fn = func;
// 比較
alert(o.fn === func);//true
// 呼叫
func();//[object Window]
o.fn();//[object Object]

這裡的執行結果是,兩個函式是相同的,因此列印結果是 true。但是由於兩個函式的呼叫是不一樣的,func 的呼叫,列印的是 [object Window],而 o.fn 的列印結果是 [object Object]。

這裡便是函式呼叫與方法呼叫的區別,函式呼叫中,this 專指全域性物件 window,而在方法中 this 專指當前物件,即 o.fn 中的 this 指的就是物件o。

3、建構函式的呼叫

function的第三種使用模式就是講它作為constructor:

構造器中的this

我們需要分析建立物件的過程,方能知道this的意義. 如下面程式碼:

  var Person = function() {
    this.name = "小平果";
  };
  var p = new Person();

這裡首先定義了函式Person,下面分析一下整個執行:

  1. 程式在執行到這一句的時候,不會執行函式體,因此 JavaScript的直譯器並不知道這個函式的內容.
  2. 接下來執行new關鍵字,建立物件,直譯器開闢記憶體,得到物件的引用,將新物件的引用交給函式.
  3. 緊接著執行函式,將傳過來的物件引用交給this. 也就是說,在構造方法中,this就是剛剛被new建立出來的物件.
  4. 然後為this 新增成員,也就是為物件新增成員.
  5. 最後函式結束,返回this,將this交給左邊的變數.

分析過建構函式的執行以後,可以得到,建構函式中的this就是當前物件.

構造器中的 return

在建構函式中return的意義發生了變化,首先如果在建構函式中,如果返回的是一個物件,那麼就保留原意. 如果返回的是非物件,比如數字、布林和字串,那麼就返回this,如果沒有return語句,那麼也返回this. 看下面程式碼:

 // 返回一個物件的 return
 var ctr = function() {
   this.name = "趙曉虎";
   return {
    name:"牛亮亮"
   };
 };
 // 建立物件
 var p = new ctr();
 // 訪問name屬性
 alert(p.name);

 //執行程式碼,這裡列印的結果是"牛亮亮". 因為構造方法中返回的是一個物件,那麼保留return的意義,返回內容為return後面的物件. 再看下面程式碼:

  // 定義返回非物件資料的構造器
  var ctr = function() {
    this.name = "趙曉虎";
    return "牛亮亮";
  };
  // 建立物件
  var p = new ctr();
  // 使用
  alert(p);
  alert(p.name);

程式碼執行結果是,先彈窗列印[object Object],然後列印”趙曉虎”. 因為這裡 return 的是一個字串,屬於基本型別,那麼這裡的return語句無效,返回的是this物件. 因此第一個列印的是[object Object]而第二個不會列印undefined.

function User(name, passwordHash) {  
    this.name = name;  
    this.passwordHash = passwordHash;  
}  
var u = new User("sfalken",  
    "0ef33ae791068ec64b502d6cb0191387");  
u.name; // "sfalken"  

使用new關鍵將function作為constructor進行呼叫。和function以及method呼叫不一樣的是,constructor會傳入一個新的物件並將它繫結到this,然後返回該物件作為constructor的返回值。而constructor function本身的作用就是為了初始化該物件。

建構函式呼叫常犯的一個錯誤

興致勃勃地定義了下面這麼個建構函式:

var Coder = function( nick ){ 
this.nick = nick; 
}; 

定義建構函式結束後呢?沒錯,趕緊例項化:

var coder = Coder( 'casper' ); 

這個coder兄弟叫什麼名字?趕緊列印下:

console.log( coder.nick ); //undefined 
= =b 竟然是undefined!!再回過頭看看例項化的那個語句,不難發現問題出在哪裡:少了個new 
var coder = Coder( 'casper' ); //當作普通的函式來呼叫,故內部的this指標其實指向window物件 
console.log( window.nick); //輸出:casper 
var coder = new Coder( 'casper' ); //加上new,一切皆不同,this正確地指向了當前建立的例項 
console.log( coder.nick ); //輸出:casper 

這樣的錯誤貌似挺低階的,但出現的概率挺高的,腫麼去避免或減少這種情況的發生呢? 
可以在內部實現裡面動下手腳:

var Coder = function( nick ){ 
    if( !(this instanceof Coder) ){ 
        return new Coder( nick ); 
    } 
        this.nick = nick; 
}; 

其實很簡單,例項化的時候,內部判斷下,當前this指向的物件的型別即可,如果非當前建構函式的型別,強制重新呼叫一遍建構函式。 
突然覺得Coder這名字不夠洋氣?想用Hacker,好吧,我改。。。數了下,一共有三處要改,這不科學,有沒有辦法只把建構函式的名字改了就行? 
當然有:

var Coder = function( nick ){ 
    if( !(this instanceof arguments.callee) ){ 
        return new arguments.callee( nick ); 
    } 
    this.nick = nick; 
}; 

tips:據說在ES 5的嚴格模式下面arguments.callee會被禁用,不過僅當ES 5普及同時你指定了要使用嚴格模式,否則還是可以用的發散下思維。

相關文章