跳轉到內容

JavaScript/函式

來自華夏公益教科書,開放的書籍,開放的世界



函式是解決特定問題的程式碼塊,並將解決方案返回給呼叫語句。函式存在於它自己的上下文中。因此,函式將龐大的程式分解成更小的“磚塊”,這些磚塊構建了軟體及其開發過程。

// define a function
function <function_name> (<parameters>) {
  <function_body>
}

// call a function
<variable> = <function_name> (<arguments>);

JavaScript 支援軟體開發正規化 函數語言程式設計。函式是派生自物件的 資料型別;它們可以繫結到變數,作為引數傳遞,並從其他函式返回,就像任何其他資料型別一樣。

函式可以透過三種主要方式構造。第一種版本可以進一步簡化;見下文

傳統方式

"use strict";

// conventional declaration (or 'definition')
function duplication(p) {
  return p + "! " + p + "!";
}

// call the function
const ret = duplication("Go");
alert(ret);

透過變數和表示式進行構造

"use strict";

// assign the function to a variable
let duplication = function (p) {
  return p + "! " + p + "!";
};

const ret = duplication("Go");
alert(ret);

透過new運算子進行構造(此版本有點繁瑣)

"use strict";

// using the 'new' constructor
let duplication = new Function ("p",
  "return p + '! ' + p + '!'");

const ret = duplication("Go");
alert(ret);

對於函式的宣告,我們已經看到了 3 種變體。對於它們的呼叫,也有 3 種變體。宣告和呼叫是相互獨立的,您可以隨意組合它們。

傳統的呼叫變體使用函式名後跟圓括號( )。圓括號內是函式的引數(如果存在)。

"use strict";

function duplication(p) {
  return p + "! " + p + "!";
}

// the conventional invocation method
const ret = duplication("Go");
alert(ret);

如果指令碼在瀏覽器中執行,還有兩種可能性。它們使用瀏覽器提供的window物件。

"use strict";

function duplication(p) {
  return p + "! " + p + "!";
}

// via 'call'
let ret = duplication.call(window, "Go");
alert(ret);

// via 'apply'
ret = duplication.apply(window, ["Go"]);
alert(ret);

提示:如果您使用函式名而不帶圓括號(),您將收到函式本身(指令碼),而不是任何呼叫結果。

"use strict";

function duplication(p) {
  return p + "! " + p + "!";
}

alert(duplication); // 'function duplication (p) { ... }'

函式會受到“提升”的影響。此機制會自動將函式的宣告轉移到其作用域的頂部。因此,您可以從原始碼中比其宣告更上層的位置呼叫函式。

"use strict";

// use a function above (in source code) its declaration
const ret = duplication("Go");
alert(ret);

function duplication(p) {
  return p + "! " + p + "!";
}

立即呼叫函式

[編輯 | 編輯原始碼]

到目前為止,我們已經看到了宣告呼叫這兩個獨立的步驟。還有一種語法變體允許將兩者組合起來。它的特點是在函式宣告周圍使用圓括號,後面跟著()來呼叫該宣告。

"use strict";

alert(  // 'alert' to show the result
  // declaration plus invocation
  (function (p) {
    return p + "! " + p + "!";
  })("Go")   // ("Go"): invocation with the argument "Go"
);

alert(
  // the same with 'arrow' syntax
  ((p) => {
    return p + "! " + p + "!";
  })("Gooo")
);

此語法被稱為 立即呼叫函式表示式 (IIFE)

當函式被呼叫時,宣告階段的引數被呼叫時的引數替換。在上面的宣告中,我們使用變數名p作為引數名。當呼叫函式時,我們大多使用字面量“Go”作為引數。在執行時,它會替換函式中所有p的出現。

按值呼叫

[編輯 | 編輯原始碼]

此類替換是 '按值' 而不是'按引用'完成的。引數原始值的副本被傳遞給函式。如果此複製的值在函式內被改變,則函式外部的原始值不會改變。

"use strict";

// there is one parameter 'p'
function duplication(p) {
  // In this example, we change the parameter's value
  p = "NoGo";
  alert("In function: " + p);
  return p + "! " + p + "!";
};

let x = "Go";
const ret = duplication(x);

// is the modification of the argument done in the function visible here? No.
alert("Return value: " + ret + " Variable: " + x);

對於物件(所有非基本資料型別),這種“按值呼叫”可能會產生驚人的效果。如果函式修改了物件的屬性,則此更改在外部也能看到。

"use strict";

function duplication(p) {
  p.a = 2;       // change the property's value
  p.b = 'xyz';   // add a property
  alert("In function: " + JSON.stringify(p));
  return JSON.stringify(p) + "! " + JSON.stringify(p) + "!";
};

let x = {a: 1};
alert("Object: " + JSON.stringify(x));
const ret = duplication(x);

// is the modification of the argument done in the function visible here? Yes.
alert("Return value: " + ret + " Object: " + JSON.stringify(x));

當示例執行時,它顯示在呼叫duplication之後,函式所做的更改不僅在返回值中可見。此外,原始物件x的屬性也已改變。為什麼會這樣?它與基本資料型別的行為不同嗎?不。

函式接收對該物件的引用的副本。因此,在函式內部,引用的是同一個物件。該物件本身只存在一次,但有兩個(相同的)對該物件的引用。使用哪個引用修改物件的屬性並沒有區別。

另一個結果是(這可能與基本資料型別直觀地相同?)對引用本身的修改(例如,透過建立新的物件)在外部例程中不可見。對新物件的引用儲存在原始引用的副本中。現在我們不僅有兩個(值不同的)引用,而且還有兩個物件。

"use strict";

function duplication(p) {

  // modify the reference by creating a new object
  p = {};

  p.a = 2;       // change the property's value
  p.b = 'xyz';   // add a property
  alert("In function: " + JSON.stringify(p));
  return JSON.stringify(p) + "! " + JSON.stringify(p) + "!";
};

let x = {a: 1};
alert("Object: " + JSON.stringify(x));
const ret = duplication(x);

// is the modification of the argument done in the function visible here? No.
alert("Return value: " + ret + " Object: " + JSON.stringify(x));

註釋 1:這種引數傳遞技術的名字在不同語言中並不一致。有時它被稱為“按共享呼叫”。維基百科 對此進行了概述。

註釋 2:JavaScript 引數傳遞帶來的描述後果與使用關鍵字const帶來的後果類似,const宣告一個變數為常量。此類變數無法改變。然而,如果它們引用了一個物件,那麼該物件的屬性可以被改變。

預設值

[編輯 | 編輯原始碼]

如果函式被呼叫的引數少於它包含的引數,那麼多餘的引數將保持未定義。但是您可以透過在函式簽名中分配一個值來為這種情況定義預設值。缺少的引數將獲得這些值作為其預設值。

"use strict";

// two nearly identical functions; only the signature is slightly different
function f1(a, b) {
  alert("The second parameter is: " + b)
};

function f2(a, b = 10) {
  alert("The second parameter is: " + b)
};

// identical invocations; different results
f1(5);        //   undefined
f1(5, 100);   //   100

f2(5);        //   10
f2(5, 100);   //   100

可變數量的引數

[編輯 | 編輯原始碼]

對於一些函式來說,它們接收不同數量的引數是“正常的”。例如,考慮一個顯示姓名的函式。firstNamefamilyName必須始終提供,但也有可能需要顯示academicTitletitleOfNobility。JavaScript 提供了不同的方法來處理這種情況。

單個檢查

[編輯 | 編輯原始碼]

可以檢查“正常”引數和附加引數,以確定它們是否包含值。

"use strict";

function showName(firstName, familyName, academicTitle, titleOfNobility) {
  "use strict";

  // handle required parameters
  let ret = "";
  if (!firstName || !familyName) {
    return "first name and family name must be specified";
  }
  ret = firstName + ", " + familyName;

  // handle optional parameters
  if (academicTitle) {
    ret = ret + ", " + academicTitle;
  }
  if (titleOfNobility) {
    ret = ret + ", " + titleOfNobility;
  }

  return ret;
}

alert(showName("Mike", "Spencer", "Ph.D."));
alert(showName("Tom"));

每個可能沒有提供的引數都必須單獨檢查。

“剩餘”引數

[編輯 | 編輯原始碼]

如果可選引數的處理在結構上是相同的,則可以使用剩餘運算子語法來簡化程式碼——通常與迴圈結合使用。該功能的語法在函式簽名中由三個點組成——就像在展開語法中一樣。

它是如何運作的?作為函式呼叫的一部分,JavaScript 引擎會將給定的可選引數合併到一個數組中。(請注意,呼叫指令碼並不會使用陣列。)這個陣列作為最後一個引數傳遞給函式。

"use strict";

// the three dots (...) introduces the 'rest syntax'
function showName(firstName, familyName, ...titles) {

  // handle required parameters
  let ret = "";
  if (!firstName || !familyName) {
    return "first name and family name must be specified";
  }
  ret = firstName + ", " + familyName;

  // handle optional parameters
  for (const title of titles) {
    ret = ret + ", " + title;
  }

  return ret;
}

alert(showName("Mike", "Spencer", "Ph.D.", "Duke"));
alert(showName("Tom"));

呼叫中的第三個引數以及所有後續引數將被收集到一個數組中,該陣列可在函式中作為最後一個引數使用。這允許使用迴圈並簡化函式的原始碼。

'arguments' 關鍵字

[編輯 | 編輯原始碼]

與 C 語言家族的其他成員一致,JavaScript 在函式中提供了 arguments 關鍵字。它是一個類似陣列的物件,包含函式呼叫中給定的所有引數。您可以迴圈遍歷它或使用它的 length 屬性。

它的功能與上述的 *剩餘語法* 相當。主要區別在於 arguments 包含 **所有** 引數,而 *剩餘語法* 不一定影響所有引數。

"use strict";

function showName(firstName, familyName, academicTitles, titlesOfNobility) {

  // handle ALL parameters with a single keyword
  for (const arg of arguments) {
    alert(arg);
  }
}

showName("Mike", "Spencer", "Ph.D.", "Duke");

函式的目的是提供針對特定問題的解決方案。這個解決方案透過 return 語句返回給呼叫程式。

它的語法是 return <表示式>,其中 <表示式> 是可選的。

函式執行直到它遇到這樣的 return 語句(或發生未捕獲的異常,或在最後一個語句之後)。<表示式> 可以是一個簡單變數,可以是任何資料型別,例如 return 5,也可以是一個複雜表示式,例如 return myString.length,也可以完全省略:return

如果 return 語句中沒有 <表示式>,或者根本沒有遇到 return 語句,則返回 undefined

"use strict";

function duplication(p) {
  if (typeof p === 'object') {
    return;  // return value: 'undefined'
  }
  else if (typeof p === 'string') {
    return p + "! " + p + "!";
  }
  // implicit return with 'undefined'
}

let arg = ["Go", 4, {a: 1}];
for (let i = 0; i < arg.length; i++) {
  const ret = duplication(arg[i]);
  alert(ret);
}

箭頭函式 (=>)

[編輯 | 編輯原始碼]

箭頭函式是上面展示的傳統函式語法的緊湊替代方案。它們縮寫了一些語言元素,省略了其他元素,並且與原始語法相比,只有幾個 語義區別

它們始終是匿名函式,但可以被賦值給變數。

"use strict";

// original conventional syntax
function duplication(p) {
    return p + "! " + p + "!";
}

// 1. remove keyword 'function' and function name
// 2. introduce '=>' instead
// 3. remove 'return'; the last value is automatically returned
(p) => {
     p + "! " + p + "!"
}

// remove { }, if only one statement
(p) =>  p + "! " + p + "!"

// Remove parameter parentheses if it is only one parameter
// -----------------------------
      p => p + "! " + p + "!"     // that's all!
// -----------------------------

alert(
  (p => p + "! " + p + "!")("Go")
);

以下是一個使用陣列的例子。forEach 方法迴圈遍歷陣列,並依次生成一個數組元素。這被傳遞給箭頭函式的單個引數 e。箭頭函式會顯示 e 和簡短文字。

"use strict";

const myArray = ['a', 'b', 'c'];

myArray.forEach(e => alert("The element of the array is: " + e));

其他程式語言在匿名函式或 lambda 表示式等術語下提供了箭頭函式的概念。

遞迴呼叫

[編輯 | 編輯原始碼]

函式可以呼叫其他函式。在實際應用中,這種情況很常見。

當函式呼叫自身時,就會出現特殊情況。這被稱為 *遞迴呼叫*。當然,這意味著存在無限迴圈的風險。您必須更改引數以避免這種情況。

通常,當應用程式處理樹狀結構時,例如 物料清單DOM 樹家譜資訊 時,就會出現對這種遞迴呼叫的需求。這裡,我們介紹了易於實現的數學問題 階乘 計算。

階乘是小於或等於某個數字 的所有正整數的乘積,寫成 。例如, 。它可以透過從 的迴圈來解決,但它也存在遞迴解決方案。 的階乘是已經計算過的 的階乘乘以 ,或者用公式表示為: 。這種想法導致了相應的函式遞迴構造

"use strict";

function factorial(n) {
  if (n > 0) {
    const ret = n * factorial(n-1);
    return ret;
  } else {
    // n = 0;  0! is 1
    return 1;
  }
}

const n = 4;
alert(factorial(n));

只要 大於 ,指令碼會再次呼叫 factorial 函式,但這次傳入的引數是 。因此,引數會逐漸收斂到 。當達到 時,這是 factorial 函式第一次不再被呼叫。它返回 的值。這個數字乘以之前 factorial 函式呼叫時傳遞的下一個較大的數字。乘法的結果將返回給之前呼叫 factorial 函式的函式......

...可在另一個頁面找到(點選這裡)。

另請參閱

[編輯 | 編輯原始碼]
華夏公益教科書