1. 程式人生 > >徹底理解JavaScript函式的呼叫方式和傳參方式——結合經典面試題

徹底理解JavaScript函式的呼叫方式和傳參方式——結合經典面試題

JavaScript函式的呼叫方式和傳參方式

瞭解函式的呼叫過程有助於深入學習與分析JavaScript程式碼。

在JavaScript中,函式是一等公民,函式在JavaScript中是一個數據型別,而非像C#或其他描述性語言那樣僅僅作為一個模組來使用。函式有四種呼叫模式,分別是:函式呼叫形式方法呼叫形式構造器形式以及apply和call呼叫形式。這裡所有模式中,最主要的區別在於關鍵字this的意義,下面分別介紹這幾種呼叫形式。(注以下程式碼都是執行在瀏覽器環境中) 本文的主要內容:

  1. 分析函式的四種呼叫形式
  2. 弄清楚函式中this的意義
  3. 明確建構函式物件的過程
  4. 學會使用上下文呼叫函式

一、函式呼叫形式

函式呼叫形式是最常見的形式,也是最好理解的形式。所謂函式形式就是一般宣告函式後直接呼叫即時。例如:

function foo(){
    console.log(this);
}
foo();//Window
複製程式碼

單獨獨立呼叫的,就是函式呼叫模式,即函式名(引數),不能加任何其他東西,物件o.fun()就不是了。在函式呼叫模式中,this表示全域性物件window。 任何自呼叫函式都是函式模式。

二、方法呼叫模式

函式呼叫模式很簡單,是最基本的呼叫方式。但是同樣的是函式,將其賦值給一個物件的成員以後,就不一樣了。將函式賦值給物件的成員後,那麼這個就不再稱為函式,而應該稱為方法。 所謂方法呼叫,就是物件的方法呼叫。方法是什麼,方法本身就是函式,但是,方法不是單獨獨立

的,而是要通過一個物件引導來呼叫。就是說方法物件一定要有宿主物件。 即物件.方法(引數)。 this表示引導方法的物件,就是宿主物件。 對比函式呼叫模式:

  1. 方法呼叫模式不是獨立的,需要宿主,而函式呼叫模式是獨立的;
  2. 方法呼叫模式方式:obj.fun();函式呼叫模式方式:fun();
  3. 方法呼叫模式中,this指宿主;而函式呼叫模式中this指全域性物件window。
//定義一個函式
function foo(){
    console.log(this);
}
//將其賦值給一個物件
var o = {};
o.fn = foo;//注意這裡不要加括號
//呼叫
o.fn();//o
複製程式碼

函式呼叫中,this專指全域性物件window,而在方法中this專指當前物件,即o.fn中的this指的就是物件o。 美團的一道面試題:

var length = 10;
function fn(){
    console.log(this.length);
}
var obj = {
    length:5,
    method:function(fn){
        fn();//10 前面沒有引導物件,函式呼叫模式
        arguments[0]();//2 
        //arguments是一個偽陣列物件,這裡呼叫相當於通過陣列的索引來呼叫
        //這裡的this就是指的這個偽陣列,所以this.length為2
    }
};
obj.method(fn,1);//列印10和2
obj.method(fn,1,2,3);//列印10和4
複製程式碼

解析:

  1. fn()前面沒有引導物件,是函式呼叫模式, this是全域性物件,輸出 10;
  2. arguments0,arguments是一個偽陣列物件, 這裡呼叫相當於通過陣列的索引來呼叫。所以,執行時,this就是指arguments,由於傳了兩個引數,所以 輸出為arguments.length就是2。

這裡引導物件即宿主就是 arguments物件。

三、構造器呼叫模式

同樣是函式,在單純的函式模式下,this表示window;在物件方法模式下,this值的是當前物件。除了這兩種情況,JavaScript中函式還可以是構造器。將函式作為構造器來使用的語法就是在函式呼叫前面加上一個new關鍵字。例如:

//定義一個建構函式
var Person = function(){
    this.name = '程式設計師';
    this.sayHello = function(){
        console.log('Hello');
    };
};
//呼叫構造器,建立物件
var p = new Person();
//使用物件
p.sayHello();//Hello
複製程式碼

上面的案例首先建立一個建構函式Person,然後使用建構函式建立物件p。這裡使用new語法。然後再使用物件呼叫sayHello()方法。從案例可以看到,此時this指的是物件本身。此外,函式作為構造器還有幾個變化,分別為:

  1. 所有需要由物件使用的屬性,必須使用this引導;
  2. 函式的return語句意義被改寫,如果返回非物件,就返回this。

分享一道面試題 請問順序執行以下程式碼,會怎樣輸出

function Foo(){
    getName = function(){
        console.log(1);
    }
    return this;
}
Foo.getName = function(){
    console.log(2);
}
Foo.prototype.getName = function(){
    console.log(3);
}
var getName = function(){
    console.log(4);
}
function getName(){
    console.log(5);
}

Foo.getName();//輸出2.
//呼叫Foo函式作為物件動態新增的屬性方法 getName
//Foo.getName = function(){console.log(2);}

getName();//輸出4.
//這裡Foo函式還沒有執行,getName還沒有被覆蓋
//所以這裡還是最上面的getName=function(){console.log(4);}

Foo().getName();//輸出1
//Foo()執行,先覆蓋全域性的getName,再返回this
//this是window,Foo().getName()就是呼叫window.getName
//此時全域性的getName已被覆蓋成function(){console.log(1);}
//所以輸出為1
//從這裡開始window.getName已被覆蓋為function(){console.log(1);}

getName();//輸出1
//window.getName(),輸出1

new Foo.getName();//輸出2
//new 就是找建構函式(),由建構函式結合性,這裡即使Foo無參,也不能省略(),所以不是Foo().getName()
//所以Foo.getName為一個整體,等價於new (Foo.getName)();
//而 Foo.getName其實就是函式function(){console.log(2);}的引用
//那麼new (Foo.getName)(),就是在以Foo.getName為建構函式,例項化物件。
//就類似於 new Person();Person是一個建構函式


new Foo().getName();//輸出3
//new就是找建構函式(),等價於(new Foo() ).getName();
//執行new Foo() => 以Foo為建構函式,例項化一個物件
//(new Foo() ).getName;訪問這個例項化物件的getName屬性
//例項物件自己並沒有getName屬性,構造的時候也沒有新增,找不到,就到原型中找
//發現Foo.prototype.getName = fucntion(){console.log(3);}
//原型中有,找到了,所以執行(new Foo() ).getName()結果為3

new new Foo().getName();//輸出為3
//new就是找建構函式(),等價於new ( (new Foo() ).getName ) ()
//先看裡面的(new Foo() ).getName
//new Foo() 以Foo為建構函式,例項化物件
//new Foo().getName 找例項物件的 getName 屬性,自己沒有,就去原型中找
//發現 Foo.prototype.getName = function() {console.log(3);}
//所以裡層(new Foo() ).getName就是以Foo為建構函式例項出的物件的一個原型屬性
//屬性值為一個函式function(){console.log(3);}的引用
//所以外層new ( (new Foo() ).getName )()在以函式function(){console.log(3);}為建構函式,構造例項
//構造過程中執行了console.log(3),輸出3

複製程式碼

構造器中的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 foo = function(){
    this.name = '張三';
    return {
        name:'李四'
    };
};
//建立物件
var p = new foo();
//訪問name屬性
console.log(p.name);//李四
複製程式碼

執行程式碼,這裡輸出的結果是“李四”。因為構造方法中返回的是一個物件,那麼保留return的意義,返回內容為return後面的物件,再看如下程式碼:

//定義返回非物件資料的構造器
var foo = fucntion() {
    this.name = '張三';
    return '李四';
}
//建立物件
var p = new foo();
console.log(p.name);//張三
複製程式碼

執行程式碼,這裡輸出結果為“張三”,因為這裡return的是一個字串,屬於基本型別,那麼這裡的return語句無效,返回的是this物件。

四、上下文呼叫模式

就是環境呼叫模式 => 在不同環境下的不同調用模式 簡單說就是統一一種格式,可以實現函式模式與方法模式 語法

  • call形式,函式名.call(...)
  • apply形式,函式名.apply(...)

這兩種形式功能完全一樣,唯一不同的是引數的形式。先學習apply,再來看call形式

apply方法呼叫形式

存在上下文呼叫的目的就是為了實現方法借用,且不會汙染物件。

  • 如果需要讓函式以函式的形式呼叫,可以使用 foo.apply(null);//上下文為window

  • 如果希望它是方法呼叫模式,注意需要提供一個宿主物件 foo.apply(obj);//上下文為傳入的obj物件

function foo(num1,num2){
    console.log(this);
    return num1+num2;
}
//函式呼叫模式
var res1 = foo(123,567);
//方法呼叫
var o = { name: 'chenshsh' };
o.func = foo;
var res2 = o.func(123,567);
複製程式碼

使用apply進行呼叫,如果函式是帶有引數的。apply的第一個引數要麼是null要麼是物件。

  1. 如果是null,就是函式呼叫
  2. 如果是物件就是方法呼叫,該物件就是宿主物件,後面緊跟一個數組引數,將函式所有的引數依次放在陣列中
//函式模式
foo(123,567);
foo.apply(null,[123,567]);//以window為上下文執行apply

//方法模式
o.func(123,567);
var o = { name:'chenshsh' };
foo.apply(o,[123,567]);//以o為上下文執行apply
複製程式碼

call方法呼叫

在使用apply呼叫的時候,函式引數必須以陣列的形式存在。但是有些時候陣列封裝比較複雜,所以引入call呼叫,call呼叫與apply完全相同,唯一不同是引數不需要使用陣列。

foo(123,456);
foo.apply(null,[123,456]);
foo.call(null,123,456);
複製程式碼
  1. 函式呼叫:函式名.call(null,引數1,引數2,引數3...);
  2. 方法呼叫:函式名.call(obj,引數1,引數2,引數3...);

不傳遞引數時,apply和call完全一樣