1. 程式人生 > 其它 >函式進階內容 函式物件,NFE

函式進階內容 函式物件,NFE

函式物件,NFE

我們已經知道,在 JavaScript 中,函式就是值。

JavaScript 中的每個值都有一種型別,那麼函式是什麼型別呢?

在 JavaScript 中,函式就是物件。

一個容易理解的方式是把函式想象成可被呼叫的“行為物件(action object)”。我們不僅可以呼叫它們,還能把它們當作物件來處理:增/刪屬性,按引用傳遞等。

屬性 “name”

函式物件包含一些便於使用的屬性。

比如,一個函式的名字可以通過屬性 “name” 來訪問:

function sayHi() {
  alert("Hi");
}

alert(sayHi.name); // sayHi

更有趣的是,名稱賦值的邏輯很智慧。即使函式被建立時沒有名字,名稱賦值的邏輯也能給它賦予一個正確的名字,然後進行賦值:

let sayHi = function() {
  alert("Hi");
};

alert(sayHi.name); // sayHi(有名字!)

當以預設值的方式完成了賦值時,它也有效:

function f(sayHi = function() {}) {
  alert(sayHi.name); // sayHi(生效了!)
}

f();

規範中把這種特性叫做「上下文命名」。如果函式自己沒有提供,那麼在賦值中,會根據上下文來推測一個。

物件方法也有名字:

let user = {

  sayHi() {
    // ...
  },

  sayBye: function() {
    // ...
  }

}

alert(user.sayHi.name); // sayHi
alert(user.sayBye.name); // sayBye

這沒有什麼神奇的。有時會出現無法推測名字的情況。此時,屬性name會是空,像這樣:

// 函式是在陣列中建立的
let arr = [function() {}];

alert( arr[0].name ); // <空字串>
// 引擎無法設定正確的名字,所以沒有值

而實際上,大多數函式都是有名字的。

屬性 “length”

還有另一個內建屬性 “length”,它返回函式入參的個數,比如:

function f1(a) {}
function f2(a, b) {}
function many(a, b, ...more) {}

alert(f1.length); // 1
alert(f2.length); // 2
alert(many.length); // 2

可以看到,rest 引數不參與計數。

屬性length有時在操作其它函式的函式中用於做內省/執行時檢查(introspection)

比如,下面的程式碼中函式ask接受一個詢問答案的引數question和可能包含任意數量handler的引數...handlers

當用戶提供了自己的答案後,函式會呼叫那些handlers。我們可以傳入兩種handlers

  • 一種是無參函式,它僅在使用者回答給出積極的答案時被呼叫。
  • 一種是有參函式,它在兩種情況都會被呼叫,並且返回一個答案。

為了正確地呼叫handler,我們需要檢查handler.length屬性。

我們的想法是,我們用一個簡單的無引數的handler語法來處理積極的回答(最常見的變體),但也要能夠提供通用的 handler:

function ask(question, ...handlers) {
  let isYes = confirm(question);

  for(let handler of handlers) {
    if (handler.length == 0) {
      if (isYes) handler();
    } else {
      handler(isYes);
    }
  }

}

// 對於積極的回答,兩個 handler 都會被呼叫
// 對於負面的回答,只有第二個 handler 被呼叫
ask("Question?", () => alert('You said yes'), result => alert(result));

這種特別的情況就是所謂的多型性—— 根據引數的型別,或者根據在我們的具體情景下的length來做不同的處理。這種思想在 JavaScript 的庫裡有應用。

自定義屬性

我們也可以新增我們自己的屬性。

這裡我們添加了counter屬性,用來跟蹤總的呼叫次數:

function sayHi() {
  alert("Hi");

  // 計算呼叫次數
  sayHi.counter++;
}
sayHi.counter = 0; // 初始值

sayHi(); // Hi
sayHi(); // Hi

alert( `Called ${sayHi.counter} times` ); // Called 2 times
屬性不是變數

被賦值給函式的屬性,比如sayHi.counter = 0不會在函式內定義一個區域性變數counter。換句話說,屬性counter和變數let counter是毫不相關的兩個東西。

我們可以把函式當作物件,在它裡面儲存屬性,但是這對它的執行沒有任何影響。變數不是函式屬性,反之亦然。它們之間是平行的。

函式屬性有時會用來替代閉包。例如,我們可以使用函式屬性將變數作用域,閉包章節中 counter 函式的例子進行重寫:

function makeCounter() {
  // 不需要這個了
  // let count = 0

  function counter() {
    return counter.count++;
  };

  counter.count = 0;

  return counter;
}

let counter = makeCounter();
alert( counter() ); // 0
alert( counter() ); // 1

現在count被直接儲存在函式裡,而不是它外部的詞法環境。

那麼它和閉包誰好誰賴?

兩者最大的不同就是如果count的值位於外層(函式)變數中,那麼外部的程式碼無法訪問到它,只有巢狀的函式可以修改它。而如果它是繫結到函式的,那麼就很容易:

function makeCounter() {

  function counter() {
    return counter.count++;
  };

  counter.count = 0;

  return counter;
}

let counter = makeCounter();

counter.count = 10;
alert( counter() ); // 10

所以,選擇哪種實現方式取決於我們的需求是什麼。

命名函式表示式

命名函式表示式(NFE,Named Function Expression),指帶有名字的函式表示式的術語。

例如,讓我們寫一個普通的函式表示式:

let sayHi = function(who) {
  alert(`Hello, ${who}`);
};

然後給它加一個名字:

let sayHi = function func(who) {
  alert(`Hello, ${who}`);
};

我們這裡得到了什麼嗎?為它新增一個"func"名字的目的是什麼?

首先請注意,它仍然是一個函式表示式。在function後面加一個名字"func"沒有使它成為一個函式宣告,因為它仍然是作為賦值表示式中的一部分被建立的。

新增這個名字當然也沒有打破任何東西。

函式依然可以通過sayHi()來呼叫:

let sayHi = function func(who) {
  alert(`Hello, ${who}`);
};

sayHi("John"); // Hello, John

關於名字func有兩個特殊的地方,這就是新增它的原因:

  1. 它允許函式在內部引用自己。
  2. 它在函式外是不可見的。

例如,下面的函式sayHi會在沒有入參who時,以"Guest"為入參呼叫自己:

let sayHi = function func(who) {
  if (who) {
    alert(`Hello, ${who}`);
  } else {
    func("Guest"); // 使用 func 再次呼叫函式自身
  }
};

sayHi(); // Hello, Guest

// 但這不工作:
func(); // Error, func is not defined(在函式外不可見)

我們為什麼使用func呢?為什麼不直接使用sayHi進行巢狀呼叫?

當然,在大多數情況下我們可以這樣做:

let sayHi = function(who) {
  if (who) {
    alert(`Hello, ${who}`);
  } else {
    sayHi("Guest");
  }
};

上面這段程式碼的問題在於sayHi的值可能會被函式外部的程式碼改變。如果該函式被賦值給另外一個變數(譯註:也就是原變數被修改),那麼函式就會開始報錯:

let sayHi = function(who) {
  if (who) {
    alert(`Hello, ${who}`);
  } else {
    sayHi("Guest"); // Error: sayHi is not a function
  }
};

let welcome = sayHi;
sayHi = null;

welcome(); // Error,巢狀呼叫 sayHi 不再有效!

發生這種情況是因為該函式從它的外部詞法環境獲取sayHi。沒有區域性的sayHi了,所以使用外部變數。而當呼叫時,外部的sayHinull

我們給函式表示式新增的可選的名字,正是用來解決這類問題的。

讓我們使用它來修復我們的程式碼:

let sayHi = function func(who) {
  if (who) {
    alert(`Hello, ${who}`);
  } else {
    func("Guest"); // 現在一切正常
  }
};

let welcome = sayHi;
sayHi = null;

welcome(); // Hello, Guest(巢狀呼叫有效)

現在它可以正常運行了,因為名字func是函式區域性域的。它不是從外部獲取的(而且它對外部也是不可見的)。規範確保它只會引用當前函式。

外部程式碼仍然有該函式的sayHiwelcome變數。而且func是一個“內部函式名”,可用於函式在自身內部進行自呼叫。

函式宣告沒有這個東西

這裡所講的“內部名”特性只針對函式表示式,而不是函式宣告。對於函式宣告,沒有用來新增“內部”名的語法。

有時,當我們需要一個可靠的內部名時,這就成為了你把函式宣告重寫成函式表示式的理由了。

總結

函式就是物件。

我們介紹了它們的一些屬性:

  • name—— 函式的名字。通常取自函式定義,但如果函式定義時沒設定函式名,JavaScript 會嘗試通過函式的上下文猜一個函式名(例如把賦值的變數名取為函式名)。
  • length—— 函式定義時的入參的個數。Rest 引數不參與計數。

如果函式是通過函式表示式的形式被宣告的(不是在主程式碼流裡),並且附帶了名字,那麼它被稱為命名函式表示式(Named Function Expression)。這個名字可以用於在該函式內部進行自呼叫,例如遞迴呼叫等。

此外,函式可以帶有額外的屬性。很多知名的 JavaScript 庫都充分利用了這個功能。

它們建立一個“主”函式,然後給它附加很多其它“輔助”函式。例如,jQuery庫建立了一個名為$的函式。lodash庫建立一個_函式,然後為其添加了_.add_.keyBy以及其它屬性(想要了解更多內容,參查閱docs)。實際上,它們這麼做是為了減少對全域性空間的汙染,這樣一個庫就只會有一個全域性變數。這樣就降低了命名衝突的可能性。

所以,一個函式本身可以完成一項有用的工作,還可以在自身的屬性中附帶許多其他功能。