JavaScript 系列六:函數

"Code tailor",爲前端開發者提供技術相關資訊以及系列基礎文章,微信關注「小和山的菜鳥們」公衆號,及時獲取最新文章。

前言

在開始學習以前,咱們想要告訴您的是,本文章是對JavaScript語言知識中 "函數" 部分的總結,若是您已掌握下面知識事項,則可跳過此環節直接進入題目練習javascript

  • 函數簡介
  • 函數名稱
  • 函數重載
  • 函數聲明與函數表達式
  • 函數做爲值
  • this
  • 函數的遞歸

若是您對某些部分有些遺忘,👇🏻 已經爲您準備好了!前端

彙總總結

函數簡介

函數是全部編程語言的核心部分,由於它們能夠封裝語句,且被定義後能夠在任何地方、任什麼時候間執行。ECMAScript 中的函數使用 function 關鍵字進行聲明,後跟一組參數,而後是函數體。java

如下是函數的基本語法:express

function functionName(arg0, arg1,...,argN) {
 //表達式
}

下面是一個例子:編程

function sayHi(name, message) {
  console.log('Hello ' + name + ', ' + message)
}

能夠經過函數名來調用函數,要傳給函數的參數放在括號裏(若是有多個參數,則用逗號隔開)。數組

下面是調用函數 sayHi() 的示例:微信

sayHi('xhs', 'do you study today?')

調用這個函數的輸出結果是 "Hello xhs, do you study today?"。 參數 namemessage 在函數內部做爲字符串被拼接在了一塊兒,最終經過 console.log() 輸出到控制檯。編程語言

ECMAScript 中的函數不須要指定是否返回值,也不限制返回值的類型。任何函數在任什麼時候間均可以使用 return 語句來返回函數的值,用法是後跟要返回的值,返回值能夠是任何類型。好比:函數

function sum(num1, num2) {
  return num1 + num2
}

函數 sum() 會將兩個值相加並返回結果。注意,除了 return 語句以外沒有任何特殊聲明代表該函數有返回值。而後就能夠這樣調用它:學習

const result = sum(5, 10)

值得注意的是,只要執行到 return 語句,函數就會當即中止執行並退出。所以,return 語句後面的代碼不會被執行。好比:

function sum(num1, num2) {
  return num1 + num2

  console.log('Hello world') //不會執行
}

在這個例子中,console.log() 不會執行,由於它在 return 語句後面,解釋器在執行到 return 語句後就中止對該函數的執行。

一個函數裏能夠有多個 return 語句,像這樣:

function subtract(num1, num2) {
  if (num1 < num2) {
    return num2 - num1
  } else {
    return num1 - num2
  }
}

這個 subtract() 函數用於計算兩個數值的差。若是第一個數值小於第二個,則用第二個減第一個;不然,就用第一個減第二個。代碼中每一個分支都有本身的 return 語句,返回正確的差值。return 語句也能夠不帶返回值。這時候,函數會當即中止執行並返回 undefined。這種用法最經常使用於提早終止函數執行,並非爲了返回值。好比在下面的例子中,console.log() 不會執行:

function sayHi(name, message) {
  return

  console.log('Hello ' + name + ', ' + message) // 不會執行
}
根據開發經驗而言,一個函數要麼返回值,要麼不返回值。只在某個條件下返回值的函數會帶來麻煩,尤爲是調試時。

函數名稱

函數名是一個指向函數的指針,因此它們跟其餘包含對象指針的變量具備相同的行爲。這意味着一個函數能夠有多個函數名,以下所示:

function sum(num1, num2) {
  return num1 + num2
}

console.log(sum(10, 10)) // 20

let xhsSum = sum

console.log(xhsSum(10, 10)) // 20

sum = null

console.log(xhsSum(10, 10)) // 20

以上代碼定義了一個名爲 sum 的函數,用於求兩個數之和。而後又聲明瞭一個變量 xhsSum,並將它的值設置爲等於 sum 。注意,使用不帶括號的函數名會訪問函數指針名,而不會執行函數。此時,xhsSumsum 都指向同一個函數。調用 xhsSum() 也能夠返回結果。把 sum 設置爲 null 以後,就切斷了它與函數之間的關聯。而 xhsSum() 仍是能夠照常調用,沒有問題。

函數參數

ECMAScript 函數的參數跟大多數其餘語言不一樣。ECMAScript 函數既不關心傳入的參數個數,也不關心這些參數的數據類型。定義函數時要接收兩個參數,並不意味着調用時就傳兩個參數。你能夠傳一個、三個,也能夠一個也不傳,解釋器都不會報錯。

之因此會這樣,主要是由於 ECMAScript 函數的參數在內部表現爲一個數組。函數被調用時總會接收一個數組,但函數並不關心這個數組中包含什麼。若是數組中什麼也沒有,那沒問題;若是數組的元素超出了要求,那也沒問題。

事實上,在使用 function 關鍵字定義(非箭頭)函數時,能夠在函數內部訪問 arguments 對象,從中取得傳進來的每一個參數值。arguments 對象是一個類數組對象(但不是 Array 的實例),所以可使用中括號語法訪問其中的元素(第一個參數是 arguments[0],第二個參數是 arguments[1])。而要肯定傳進來多少個參數,能夠訪問 arguments.length 屬性。在下面的例子中,sayHi() 函數的第一個參數叫 name

function sayHi(name, message) {
  console.log('Hello ' + name + ', ' + message)
}

能夠經過 arguments[0] 取得相同的參數值。所以,把函數重寫成不聲明參數也能夠:

function sayHi() {
  console.log('Hello ' + arguments[0] + ', ' + arguments[1])
}

在重寫後的代碼中,沒有命名參數。namemessage 參數都不見了,但函數照樣能夠調用。這就代表,ECMAScript 函數的參數只是爲了方便才寫出來的,並非必須寫出來的。與其餘語言不一樣,在 ECMAScript 中的命名參數不會建立讓以後的調用必須匹配的函數簽名。這是由於根本不存在驗證命名參數的機制。也能夠經過 arguments 對象的 length 屬性檢查傳入的參數個數。下面的例子展現了在每調用一個函數時,都會打印出傳入的參數個數:

function getArgsNum() {
  console.log(arguments.length)
}

getArgsNum('string', 45) // 2

getArgsNum() // 0

getArgsNum(12) // 1

這個例子分別打印出 201 (按順序)。既然如此,那麼開發者能夠想傳多少參數就傳多少參數。

好比:

function getTotle() {
  if (arguments.length === 1) {
    console.log(arguments[0] + 10)
  } else if (arguments.length === 2) {
    console.log(arguments[0] + arguments[1])
  }
}

getTotle(10) // 20

getTotle(30, 20) // 50

這個函數 getTotle() 在只傳一個參數時會加 10 ,在傳兩個參數時會將它們相加,而後返回。所以 getTotle(10) 返回 20 ,而 getTotle(30,20) 返回 50 。雖然不像真正的函數重載那麼明確,但這已經足以彌補 ECMAScript 在這方面的缺失了。

還有一個必須理解的重要方面,那就是 arguments 對象能夠跟命名參數一塊兒使用,好比:

function getTotle(num1, num2) {
  if (arguments.length === 1) {
    console.log(num1 + 10)
  } else if (arguments.length === 2) {
    console.log(arguments[0] + num2)
  }
}

在這個 getTotle() 函數中,同時使用了兩個命名參數和 arguments 對象。命名參數 num1 保存着與 arugments[0] 同樣的值,所以使用誰都無所謂。(一樣,num2 也保存着跟 arguments[1] 同樣的值。)arguments 對象的另外一個有意思的地方就是,它的值始終會與對應的命名參數同步。來看下面的例子:

function getTotle(num1, num2) {
  arguments[1] = 10

  console.log(arguments[0] + num2)
}

這個 getTotle() 函數把第二個參數的值重寫爲 10。由於 arguments 對象的值會自動同步到對應的命名參數,因此修改 arguments[1] 也會修改 num2 的值,所以二者的值都是 10 。但這並不意味着它們都訪問同一個內存地址,它們在內存中仍是分開的,只不過會保持同步而已。

另外還要記住一點:若是隻傳了一個參數,而後把 arguments[1] 設置爲某個值,那麼這個值並不會反映到第二個命名參數。這是由於 arguments 對象的長度是根據傳入的參數個數,而非定義函數時給出的命名參數個數肯定的。對於命名參數而言,若是調用函數時沒有傳這個參數,那麼它的值就是 undefined。這就相似於定義了變量而沒有初始化。

好比,若是隻給 getTotle() 傳了一個參數,那麼 num2 的值就是 undefined。嚴格模式下,arguments 會有一些變化。首先,像前面那樣給 arguments[1] 賦值不會再影響 num2 的值。就算把 arguments[1] 設置爲 10num2 的值仍然仍是傳入的值。其次,在函數中嘗試重寫 arguments 對象會致使語法錯誤。(代碼也不會執行。)

函數重載

ECMAScript 函數不能像傳統編程那樣重載。在其餘語言好比 Java 中,一個函數能夠有兩個定義,只要簽名(接收參數的類型和數量)不一樣就行。如前所述,ECMAScript 函數沒有簽名,由於參數是由包含零個或多個值的數組表示的。沒有函數簽名,天然也就沒有重載。若是在ECMAScript 中定義了兩個同名函數,則後定義的會覆蓋先定義的。來看下面的例子:

function addSomeNumber(num) {
  return num + 100
}

function addSomeNumber(num) {
  return num + 200
}
let result = addSomeNumber(100) // 300

這裏,函數 addSomeNumber() 被定義了兩次。第一個版本給參數加 100,第二個版本加 200。最後一行調用這個函數時,返回了 300,由於第二個定義覆蓋了第一個定義。

函數聲明與函數表達式

咱們能夠經過 function 構造一個函數,也能夠將一個變量賦值成一個函數,這兩種方法在大多數狀況下均可以正常調用執行。但是在 JavaScript 引擎在加載數據時對函數聲明和函數表達式是區別對待的。JavaScript 引擎在任何代碼執行以前,會先讀取函數聲明,並在執行上下文中生成函數定義。而函數表達式必須等到代碼執行到它那一行,纔會在執行上下文中生成函數定義。來看下面的例子:

// 沒問題
console.log(sum(10, 10))

function sum(num1, num2) {
  return num1 + num2
}

以上代碼能夠正常運行,由於函數聲明會在任何代碼執行以前先被讀取並添加到執行上下文。這個過程叫做函數聲明提高(function declaration hoisting)。在執行代碼時,JavaScript 引擎會先執行一遍掃描,把發現的函數聲明提高到源代碼樹的頂部。所以即便函數定義出如今調用它們的代碼以後,引擎也會把函數聲明提高到頂部。若是把前面代碼中的函數聲明改成等價的函數表達式,那麼執行的時候就會出錯:

// 會出錯
console.log(sum(10, 10))

var sum = function (num1, num2) {
  return num1 + num2
}

函數做爲值

由於函數名在 ECMAScript 中就是變量,因此函數能夠用在任何可使用變量的地方。這意味着不只能夠把函數做爲參數傳給另外一個函數,並且還能夠在一個函數中返回另外一個函數。

function add10(num) {
  return num + 10
}
let result1 = callSomeFunction(add10, 10)
console.log(result1) // 20
function getGreeting(name) {
  return 'Hello, ' + name
}
let result2 = callSomeFunction(getGreeting, 'Nicholas')
console.log(result2) // "Hello, Nicholas"
function fun1(x) {
  return function (y) {
    return function (z) {
      console.log(x * y * z)
    }
  }
}
fun1(2)(3)(4) //24

this

this 是一個特殊的對象,它在標準函數和箭頭函數中有不一樣的行爲。

在標準函數中,this 引用的是把函數當成方法調用的上下文對象,這時候一般稱其爲 this 值(在網頁的全局上下文中調用函數時,this 指向 window 對象)。來看下面的例子:

window.color = 'red'

let o = {
  color: 'blue',
}

function sayColor() {
  console.log(this.color)
}

sayColor() // 'red'

o.sayColor = sayColor

o.sayColor() // 'blue'

定義在全局上下文中的函數 sayColor() 引用了 this 對象。這個 this 到底引用哪一個對象必須到函數被調用時才能肯定。所以這個值在代碼執行的過程當中可能會變。若是在全局上下文中調用 sayColor(),這結果會輸出 red ,由於 this 指向 window,而 this.color 至關於 window.color 。而在把 sayColor() 賦值給 o 以後再調用 o.sayColor()this 會指向 o ,即 this.color 至關於 o.color ,因此會顯示 blue

在箭頭函數中,this 引用的是定義箭頭函數的上下文。箭頭函數的內容咱們會在第二部分的函數擴展中詳細說起。

函數的遞歸

遞歸函數一般的形式是一個函數經過名稱調用本身,以下面的例子所示:

function factorial(num) {
  if (num <= 1) {
    return 1
  } else {
    return num * factorial(num - 1)
  }
}

這是經典的遞歸階乘函數。雖然這樣寫是能夠的,但若是把這個函數賦值給其餘變量,就會出問題:

let anotherFactorial = factorial

factorial = null

console.log(anotherFactorial(4)) // 報錯

這裏把 factorial()函數保存在了另外一個變量 anotherFactorial 中,而後將 factorial 設置爲 null,因而只保留了一個對原始函數的引用。而在調用 anotherFactorial() 時,要遞歸調用 factorial() ,但由於它已經不是函數了,因此會出錯。在寫遞歸函數時使用 arguments.callee 能夠避免這個問題。arguments.callee 就是一個指向正在執行的函數的指針,所以能夠在函數內部遞歸調用,以下所示:

function factorial(num) {
  if (num <= 1) {
    return 1
  } else {
    return num * arguments.callee(num - 1)
  }
}

把函數名稱替換成 arguments.callee ,能夠確保不管經過什麼變量調用這個函數都不會出問題。所以在編寫遞歸函數時 arguments.callee 是引用當前函數的首選。不過,在嚴格模式下運行的代碼是不能訪問 arguments.callee 的,由於訪問會出錯。此時,可使用命名函數表達式(named function expression)達到目的。好比:

const factorial = function f(num) {
  if (num <= 1) {
    return 1
  } else {
    return num * f(num - 1)
  }
}

題目自測

一:如下代碼輸出什麼

function callFunc(func, argument) {
  return func(argument)
}
function func1(argument) {
  console.log(argument + '.')
}
callFunc(func1, 'hello,world')

二:如下代碼輸出什麼

function recursion(num) {
  if (num === 0) return 1
  else if (num % 2 === 0) return recursion(num - 1) + 1
  else return recursion(num - 1)
}

console.log(recursion(6))

三:兩段代碼分別執行各會輸出什麼

func1(1, 2)
function func2(x, y) {
  console.log(x + y)
}
let func1 = func2
func2(1, 2)
function func2(x, y) {
  console.log(x + y)
}

題目解析

1、

Answer: 'hello,world.'

callFunc 函數的做用其實就是執行了第一個參數,並把第二個參數傳給第一個參數當作參數。


2、

Answer: 4

recursion 是一個遞歸函數,若是 num 等於0就返回1,若是 num 是偶數,就在下一個結果前 +1,若是是奇數就返回下一個結果。其做用就是統計從 0num 一共有多少個偶數。因此 recursion(6)4


3、

func1(1,2) 輸出 ReferenceError,由於在調用時 func1 還未被定義和賦值。 func2(1,2) 輸出 3,由於 function 的聲明會被提早,能夠在以前使用。

相關文章
相關標籤/搜索