Javascript 函數(shù)對象,NFE

2023-02-17 10:50 更新

我們已經(jīng)知道,在 JavaScript 中,函數(shù)也是一個(gè)值。

而 JavaScript 中的每個(gè)值都有一種類型,那么函數(shù)是什么類型呢?

在 JavaScript 中,函數(shù)的類型是對象。

一個(gè)容易理解的方式是把函數(shù)想象成可被調(diào)用的“行為對象(action object)”。我們不僅可以調(diào)用它們,還能把它們當(dāng)作對象來處理:增/刪屬性,按引用傳遞等。

屬性 “name”

函數(shù)對象包含一些便于使用的屬性。

比如,一個(gè)函數(shù)的名字可以通過屬性 “name” 來訪問:

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

alert(sayHi.name); // sayHi

更有趣的是,名稱賦值的邏輯很智能。即使函數(shù)被創(chuàng)建時(shí)沒有名字,名稱賦值的邏輯也能給它賦予一個(gè)正確的名字,然后進(jìn)行賦值:

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

alert(sayHi.name); // sayHi(有名字?。?/code>

當(dāng)以默認(rèn)值的方式完成了賦值時(shí),它也有效:

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

f();

規(guī)范中把這種特性叫做「上下文命名」。如果函數(shù)自己沒有提供,那么在賦值中,會(huì)根據(jù)上下文來推測一個(gè)。

對象方法也有名字:

let user = {

  sayHi() {
    // ...
  },

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

}

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

這沒有什么神奇的。有時(shí)會(huì)出現(xiàn)無法推測名字的情況。此時(shí),屬性 name 會(huì)是空,像這樣:

// 函數(shù)是在數(shù)組中創(chuàng)建的
let arr = [function() {}];

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

而實(shí)際上,大多數(shù)函數(shù)都是有名字的。

屬性 “l(fā)ength”

還有另一個(gè)內(nèi)建屬性 “l(fā)ength”,它返回函數(shù)入?yún)⒌膫€(gè)數(shù),比如:

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 參數(shù)不參與計(jì)數(shù)。

屬性 length 有時(shí)在操作其它函數(shù)的函數(shù)中用于做 內(nèi)省/運(yùn)行時(shí)檢查(introspection)

比如,下面的代碼中函數(shù) ask 接受一個(gè)詢問答案的參數(shù) question 和可能包含任意數(shù)量 handler 的參數(shù) ...handlers。

當(dāng)用戶提供了自己的答案后,函數(shù)會(huì)調(diào)用那些 handlers。我們可以傳入兩種 handlers

  • 一種是無參函數(shù),它僅在用戶給出肯定回答時(shí)被調(diào)用。
  • 一種是有參函數(shù),它在兩種情況都會(huì)被調(diào)用,并且返回一個(gè)答案。

為了正確地調(diào)用 handler,我們需要檢查 handler.length 屬性。

我們的想法是,我們用一個(gè)簡單的無參數(shù)的 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);
    }
  }

}

// 對于肯定的回答,兩個(gè) handler 都會(huì)被調(diào)用
// 對于否定的回答,只有第二個(gè) handler 被調(diào)用
ask("Question?", () => alert('You said yes'), result => alert(result));

這就是所謂的 多態(tài)性 的一個(gè)例子 —— 根據(jù)參數(shù)的類型,或者根據(jù)在我們的具體情景下的 length 來做不同的處理。這種思想在 JavaScript 的庫里有應(yīng)用。

自定義屬性

我們也可以添加我們自己的屬性。

這里我們添加了 ?counter? 屬性,用來跟蹤總的調(diào)用次數(shù):

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

  // 計(jì)算調(diào)用次數(shù)
  sayHi.counter++;
}
sayHi.counter = 0; // 初始值

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

alert( `Called ${sayHi.counter} times` ); // Called 2 times

屬性不是變量

被賦值給函數(shù)的屬性,比如 sayHi.counter = 0,不會(huì) 在函數(shù)內(nèi)定義一個(gè)局部變量 counter。換句話說,屬性 counter 和變量 let counter 是毫不相關(guān)的兩個(gè)東西。

我們可以把函數(shù)當(dāng)作對象,在它里面存儲(chǔ)屬性,但是這對它的執(zhí)行沒有任何影響。變量不是函數(shù)屬性,反之亦然。它們之間是平行的。

函數(shù)屬性有時(shí)會(huì)用來替代閉包。例如,我們可以使用函數(shù)屬性將 變量作用域,閉包 章節(jié)中 counter 函數(shù)的例子進(jìn)行重寫:

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

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

  counter.count = 0;

  return counter;
}

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

現(xiàn)在 count 被直接存儲(chǔ)在函數(shù)里,而不是它外部的詞法環(huán)境。

那么它和閉包誰好誰賴?

兩者最大的不同就是如果 count 的值位于外層(函數(shù))變量中,那么外部的代碼無法訪問到它,只有嵌套的那些函數(shù)可以修改它。而如果它是綁定到函數(shù)的,那么就可以這樣:

function makeCounter() {

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

  counter.count = 0;

  return counter;
}

let counter = makeCounter();

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

所以,選擇哪種實(shí)現(xiàn)方式取決于我們的需求是什么。

命名函數(shù)表達(dá)式

命名函數(shù)表達(dá)式(NFE,Named Function Expression),指帶有名字的函數(shù)表達(dá)式的術(shù)語。

例如,讓我們寫一個(gè)普通的函數(shù)表達(dá)式:

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

然后給它加一個(gè)名字:

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

我們這里得到了什么嗎?為它添加一個(gè) "func" 名字的目的是什么?

首先請注意,它仍然是一個(gè)函數(shù)表達(dá)式。在 function 后面加一個(gè)名字 "func" 沒有使它成為一個(gè)函數(shù)聲明,因?yàn)樗匀皇亲鳛橘x值表達(dá)式中的一部分被創(chuàng)建的。

添加這個(gè)名字當(dāng)然也沒有打破任何東西。

函數(shù)依然可以通過? sayHi()? 來調(diào)用:

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

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

關(guān)于名字 func 有兩個(gè)特殊的地方,這就是添加它的原因:

  1. 它允許函數(shù)在內(nèi)部引用自己。
  2. 它在函數(shù)外是不可見的。

例如,下面的函數(shù) sayHi 會(huì)在沒有入?yún)?nbsp;who 時(shí),以 "Guest" 為入?yún)⒄{(diào)用自己:

let sayHi = function func(who) {
  if (who) {
    alert(`Hello, ${who}`);
  } else {
    func("Guest"); // 使用 func 再次調(diào)用函數(shù)自身
  }
};

sayHi(); // Hello, Guest

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

我們?yōu)槭裁词褂?nbsp;func 呢?為什么不直接使用 sayHi 進(jìn)行嵌套調(diào)用?

當(dāng)然,在大多數(shù)情況下我們可以這樣做:

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

上面這段代碼的問題在于 sayHi 的值可能會(huì)被函數(shù)外部的代碼改變。如果該函數(shù)被賦值給另外一個(gè)變量(譯注:也就是原變量被修改),那么函數(shù)就會(huì)開始報(bào)錯(cuò):

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,嵌套調(diào)用 sayHi 不再有效!

發(fā)生這種情況是因?yàn)樵摵瘮?shù)從它的外部詞法環(huán)境獲取 sayHi。沒有局部的 sayHi 了,所以使用外部變量。而當(dāng)調(diào)用時(shí),外部的 sayHi 是 null。

我們給函數(shù)表達(dá)式添加的可選的名字,正是用來解決這類問題的。

讓我們使用它來修復(fù)我們的代碼:

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

let welcome = sayHi;
sayHi = null;

welcome(); // Hello, Guest(嵌套調(diào)用有效)

現(xiàn)在它可以正常運(yùn)行了,因?yàn)槊?nbsp;func 是函數(shù)局部域的。它不是從外部獲取的(而且它對外部也是不可見的)。規(guī)范確保它只會(huì)引用當(dāng)前函數(shù)。

外部代碼仍然有該函數(shù)的 sayHi 或 welcome 變量。而且 func 是一個(gè)“內(nèi)部函數(shù)名”,是函數(shù)可以可靠地調(diào)用自身的方式。

函數(shù)聲明沒有這個(gè)東西

這里所講的“內(nèi)部名”特性只針對函數(shù)表達(dá)式,而不是函數(shù)聲明。對于函數(shù)聲明,沒有用來添加“內(nèi)部”名的語法。

有時(shí),當(dāng)我們需要一個(gè)可靠的內(nèi)部名時(shí),這就成為了你把函數(shù)聲明重寫成函數(shù)表達(dá)式的理由了。

總結(jié)

函數(shù)的類型是對象。

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

  • ?name? —— 函數(shù)的名字。通常取自函數(shù)定義,但如果函數(shù)定義時(shí)沒設(shè)定函數(shù)名,JavaScript 會(huì)嘗試通過函數(shù)的上下文猜一個(gè)函數(shù)名(例如把賦值的變量名取為函數(shù)名)。
  • ?length? —— 函數(shù)定義時(shí)的入?yún)⒌膫€(gè)數(shù)。Rest 參數(shù)不參與計(jì)數(shù)。

如果函數(shù)是通過函數(shù)表達(dá)式的形式被聲明的(不是在主代碼流里),并且附帶了名字,那么它被稱為命名函數(shù)表達(dá)式(Named Function Expression)。這個(gè)名字可以用于在該函數(shù)內(nèi)部進(jìn)行自調(diào)用,例如遞歸調(diào)用等。

此外,函數(shù)可以帶有額外的屬性。很多知名的 JavaScript 庫都充分利用了這個(gè)功能。

它們創(chuàng)建一個(gè)“主”函數(shù),然后給它附加很多其它“輔助”函數(shù)。例如,jQuery 庫創(chuàng)建了一個(gè)名為 $ 的函數(shù)。lodash 庫創(chuàng)建一個(gè) _ 函數(shù),然后為其添加了 _.add、_.keyBy 以及其它屬性(想要了解更多內(nèi)容,參查閱  docs)。實(shí)際上,它們這么做是為了減少對全局空間的污染,這樣一個(gè)庫就只會(huì)有一個(gè)全局變量。這樣就降低了命名沖突的可能性。

所以,一個(gè)函數(shù)本身可以完成一項(xiàng)有用的工作,還可以在自身的屬性中附帶許多其他功能。

任務(wù)


為 counter 添加 set 和 decrease 方法

重要程度: 5

修改 makeCounter() 代碼,使得 counter 可以進(jìn)行減一和設(shè)置值的操作:

  • ?counter()? 應(yīng)該返回下一個(gè)數(shù)字(與之前的邏輯相同)。
  • ?counter.set(value)? 應(yīng)該將 ?count? 設(shè)置為 ?value?。
  • ?counter.decrease()? 應(yīng)該把 ?count? 減 1。

P.S. 你可以使用閉包或者函數(shù)屬性來保持當(dāng)前的計(jì)數(shù),或者兩種都寫。

  function makeCounter() {
    let count = 0;
  
    // ... your code ...
  }
  
  let counter = makeCounter();
  
  alert( counter() ); // 0
  alert( counter() ); // 1
  
  counter.set(10); // set the new count
  
  alert( counter() ); // 10
  
  counter.decrease(); // decrease the count by 1
  
  alert( counter() ); // 10 (instead of 11)

解決方案

該解決方案在局部變量中使用 count,而進(jìn)行加法操作的方法是直接寫在 counter 中的。它們共享同一個(gè)外部詞法環(huán)境,并且可以訪問當(dāng)前的 count

function makeCounter() {
  let count = 0;

  function counter() {
    return count++;
  }

  counter.set = value => count = value;

  counter.decrease = () => count--;

  return counter;
}

任意數(shù)量的括號求和

重要程度: 2

寫一個(gè)函數(shù) ?sum?,它有這樣的功能:

sum(1)(2) == 3; // 1 + 2
sum(1)(2)(3) == 6; // 1 + 2 + 3
sum(5)(-1)(2) == 6
sum(6)(-1)(-2)(-3) == 0
sum(0)(1)(2)(3)(4)(5) == 15

P.S. 提示:你可能需要?jiǎng)?chuàng)建自定義對象來為你的函數(shù)提供基本類型轉(zhuǎn)換。


解決方案

  1. 為了使整個(gè)程序無論如何都能正常工作,?sum? 的結(jié)果必須是函數(shù)。
  2. 這個(gè)函數(shù)必須將兩次調(diào)用之間的當(dāng)前值保存在內(nèi)存中。
  3. 根據(jù)這個(gè)題目,當(dāng)函數(shù)被用于 ?==? 比較時(shí)必須轉(zhuǎn)換成數(shù)字。函數(shù)是對象,所以轉(zhuǎn)換規(guī)則會(huì)按照 對象 —— 原始值轉(zhuǎn)換 章節(jié)所講的進(jìn)行,我們可以提供自己的方法來返回?cái)?shù)字。
function sum(a) {

  let currentSum = a;

  function f(b) {
    currentSum += b;
    return f;
  }

  f.toString = function() {
    return currentSum;
  };

  return f;
}

alert( sum(1)(2) ); // 3
alert( sum(5)(-1)(2) ); // 6
alert( sum(6)(-1)(-2)(-3) ); // 0
alert( sum(0)(1)(2)(3)(4)(5) ); // 15

請注意 sum 函數(shù)只工作一次,它返回了函數(shù) f

然后,接下來的每一次子調(diào)用,f 都會(huì)把自己的參數(shù)加到求和 currentSum 上,然后 f 自身。

在 f 的最后一行沒有遞歸。

遞歸是這樣子的:

function f(b) {
  currentSum += b;
  return f(); // <-- 遞歸調(diào)用
}

在我們的例子中,只是返回了函數(shù),并沒有調(diào)用它:

function f(b) {
  currentSum += b;
  return f; // <-- 沒有調(diào)用自己,只是返回了自己
}

這個(gè) f 會(huì)被用于下一次調(diào)用,然后再次返回自己,按照需要重復(fù)。然后,當(dāng)它被用做數(shù)字或字符串時(shí) —— toString 返回 currentSum。我們也可以使用 Symbol.toPrimitive 或者 valueOf 來實(shí)現(xiàn)轉(zhuǎn)換。


以上內(nèi)容是否對您有幫助:
在線筆記
App下載
App下載

掃描二維碼

下載編程獅App

公眾號
微信公眾號

編程獅公眾號