99re热视频这里只精品,久久久天堂国产精品女人,国产av一区二区三区,久久久精品成人免费看片,99久久精品免费看国产一区二区三区

Javascript 函數對象,NFE

2023-02-17 10:50 更新

我們已經知道,在 JavaScript 中,函數也是一個值。

而 JavaScript 中的每個值都有一種類型,那么函數是什么類型呢?

在 JavaScript 中,函數的類型是對象。

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

屬性 “name”

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

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

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

alert(sayHi.name); // sayHi

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

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

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

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

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

f();

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

對象方法也有名字:

let user = {

  sayHi() {
    // ...
  },

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

}

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

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

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

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

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

屬性 “l(fā)ength”

還有另一個內建屬性 “l(fā)ength”,它返回函數入參的個數,比如:

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));

這就是所謂的 多態(tài)性 的一個例子 —— 根據參數的類型,或者根據在我們的具體情景下的 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 是毫不相關的兩個東西。

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

函數屬性有時會用來替代閉包。例如,我們可以使用函數屬性將 變量作用域,閉包 章節(jié)中 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

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

那么它和閉包誰好誰賴?

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

function makeCounter() {

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

  counter.count = 0;

  return counter;
}

let counter = makeCounter();

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

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

命名函數表達式

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

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

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

然后給它加一個名字:

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

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

首先請注意,它仍然是一個函數表達式。在 function 后面加一個名字 "func" 沒有使它成為一個函數聲明,因為它仍然是作為賦值表達式中的一部分被創(chuàng)建的。

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

函數依然可以通過? 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(在函數外不可見)

我們?yōu)槭裁词褂?nbsp;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 不再有效!

發(fā)生這種情況是因為該函數從它的外部詞法環(huán)境獲取 sayHi。沒有局部的 sayHi 了,所以使用外部變量。而當調用時,外部的 sayHi 是 null。

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

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

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

let welcome = sayHi;
sayHi = null;

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

現(xiàn)在它可以正常運行了,因為名字 func 是函數局部域的。它不是從外部獲取的(而且它對外部也是不可見的)。規(guī)范確保它只會引用當前函數。

外部代碼仍然有該函數的 sayHi 或 welcome 變量。而且 func 是一個“內部函數名”,是函數可以可靠地調用自身的方式。

函數聲明沒有這個東西

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

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

總結

函數的類型是對象。

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

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

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

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

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

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

任務


為 counter 添加 set 和 decrease 方法

重要程度: 5

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

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

P.S. 你可以使用閉包或者函數屬性來保持當前的計數,或者兩種都寫。

  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,而進行加法操作的方法是直接寫在 counter 中的。它們共享同一個外部詞法環(huán)境,并且可以訪問當前的 count

function makeCounter() {
  let count = 0;

  function counter() {
    return count++;
  }

  counter.set = value => count = value;

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

  return counter;
}

任意數量的括號求和

重要程度: 2

寫一個函數 ?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. 提示:你可能需要創(chuàng)建自定義對象來為你的函數提供基本類型轉換。


解決方案

  1. 為了使整個程序無論如何都能正常工作,?sum? 的結果必須是函數。
  2. 這個函數必須將兩次調用之間的當前值保存在內存中。
  3. 根據這個題目,當函數被用于 ?==? 比較時必須轉換成數字。函數是對象,所以轉換規(guī)則會按照 對象 —— 原始值轉換 章節(jié)所講的進行,我們可以提供自己的方法來返回數字。
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 函數只工作一次,它返回了函數 f。

然后,接下來的每一次子調用,f 都會把自己的參數加到求和 currentSum 上,然后 f 自身。

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

遞歸是這樣子的:

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

在我們的例子中,只是返回了函數,并沒有調用它:

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

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


以上內容是否對您有幫助:
在線筆記
App下載
App下載

掃描二維碼

下載編程獅App

公眾號
微信公眾號

編程獅公眾號