JavaScript 的函式(Function)


Posted by saffran on 2021-02-05

最基本的函式結構

y = f(x) 是數學上的「函數」,它跟程式裡面的「函式」是一樣的東西,都叫做「function」

  • f(x) 就是「函數」
  • x 就是「參數」
  • y 就是「回傳值」

return 也可以回傳一個 object

function twice(a){
  return {
    answer: a * 2
  }
}

console.log(twice(5))
// output: { answer: 10 }

錯誤寫法:

有些人為了排版,會把要回傳的東西按 Enter 到下一行去。
但這樣寫是錯的!
因為:
如果 return 後面沒有接東西的話,JavaScript 會把 return{answer: a * 2} 當成「兩句不相干的東西」,所以 return 就只會回傳 undefined

function twice(a){
  return 
    {
      answer: a * 2
    }
}

console.log(twice(5))
// output: undefined

參數的取名也很重要

好的做法:取個有意義的參數名稱,像是 minmax
不好的做法:無意義的參數名稱,像是 ab

function generateArray(min, max){
  var arr = []
  for(let i=min; i<=max; i++){
    arr.push(i)
  }
  return arr
}

console.log(generateArray(3, 10))

function 也可以傳變數進去

function generateArray(min, max){
  var arr = []
  for(let i=min; i<=max; i++){
    arr.push(i)
  }
  return arr
}

var a = 3
var b = 10
console.log(generateArray(a, b))

宣告函式的不同種方式

宣告方式一

宣告函式最常見的方式是這樣:

function greeting(){
  console.log('hello')
}

宣告方式二:讓變數等於一個 function

  1. 先宣告一個變數 greeting
  2. 讓變數 greeting 等於一個 function

注意!變數後面的 function 是沒有名字的,function 的名字就是由「前面那個變數名稱來決定」

  1. 同樣是使用 greeting() 來執行這個 function
var greeting = function(){
  console.log('hello')
}

greeting() // 執行這個 function
// output: hello

把 function 當作參數,傳進另一個 function

範例一:
hello 函式當作參數,傳進 print 函式,結果就會印出 [Function: hello] (也就是:hello 是一個函式),

但是,並不會去執行 hello 這個函式,因為我只是 console.log(anything)

function print(anything){
  console.log(anything)
}

function hello(){
  console.log('How are you')
}

print(hello)
// output: [Function: hello]

我現在把 print 函式改成:

執行 anything(),這樣就會去執行「傳進來的 hello 函式」了

function print(anything){
  anything()
}

function hello(){
  console.log('How are you')
}

print(hello)
// output: How are you
  • anything 參數,其實就是 hello 函式,所以我就可以執行 anything(),得到 hello 函式的結果

範例二:
test 函式當作參數,傳進 transform 函式

function transform(a, transformFunction){
  return transformFunction(a)
}

function test(a){
  return a * 2
}

console.log(
  transform(30, test)
)
// output: 60

範例三:
對每一個陣列元素 arr[i] 都使用「我傳進去的 transformFunction 函式」

function transform(arr, transformFunction){
  let newArr = []
  for(let i=0; i<arr.length; i++){
    newArr.push(transformFunction(arr[i]))
  }
  return newArr
}

function double(x){
  return x * 2
}

console.log(
  transform([3, 4, 5], double)
)
// output: [ 6, 8, 10 ]

也可以這樣寫:

直接傳入匿名函式(anonymous function)

「匿名函式」就是:沒有名字的 function,像是這裡的

function(x){
    return x * 2
  }

不需要幫函式取名字,可以把整個匿名函式直接傳進去 transform 函式

function transform(arr, transformFunction){
  let newArr = []
  for(let i=0; i<arr.length; i++){
    newArr.push(transformFunction(arr[i]))
  }
  return newArr
}

console.log(
  transform([3, 4, 5], function(x){
    return x * 2
  })
)
// output: [ 6, 8, 10 ]

參數(Parameter)與引數(Argument)

在使用 function 時,有兩個名詞常常會被混用:

  • 參數(Parameter)
  • 引數(Argument)

「參數」就是「在宣告 function 時,小括號裡面的東西」

「引數」就是「在呼叫 function 時,我真正傳進去的東西」

範例一:

  • add 這個 function,接收了兩個參數:a 和 b
  • add 函式的引數:3 和 5
function add(a, b) {
  return a + b
}

add(3, 5)

範例二:

  • add 函式的參數:a 和 b
  • add 函式的引數:c 和 d
    • 也可以說:add 函式的引數是 10 和 20
      function add(a, b){
      return a + b
      }
      var c = 10
      var d = 20
      add(c, d)
      

取得引數

add 函式裡面,用 console.log(arguments) 就可以把「引數」給印出來

function add(a, b){
  console.log(arguments)
  return a + b
}

console.log(add(7, 9))

output:

[Arguments] { '0': 7, '1': 9 }
16

也可以個別取得引數:

  • 第一個引數 arguments[0]
  • 第二個引數 arguments[1]
function add(a, b){
  console.log(arguments[0])
  console.log(arguments[1])
  return a + b
}

console.log(add(7, 9))

output:

7
9
16

因此,也可以不寫參數

在 function 中,也可以不寫參數,直接用「取得引數」的方式做為回傳值 return arguments[0] + arguments[1]
(此方式會用到的機率很低,通常還是會直接把參數寫好)

function add(){
  return arguments[0] + arguments[1]
}

console.log(add(7, 9))
// output: 16

arguments 是一個「物件」

因為印出來的 arguments 是用「大括號」包起來的,所以 arguments 是一個「物件」

arguments 不是一個「陣列」,因此,用 Array.isArray(arguments) 來看 arguments 是不是一個陣列,就會回傳 false

function add(a, b){
  console.log(arguments)
  console.log(Array.isArray(arguments))
  return a + b
}

console.log(add(7, 9))

output:

[Arguments] { '0': 7, '1': 9 }
false
16

arguments 是一個類陣列(Array-like)物件

參考資料 Arguments 物件

但是,arguments 這種形式的物件(key 的值是 0, 1, 2...),就跟陣列沒兩樣,因為我同樣可以用 arguments[0] 讀取到第一個引數

其實正確是要用單引號把 key 包起來: arguments['0']
但是因為「number 的 0」和「string 的 '0'」是沒有區別的,所以也可以直接寫 arguments[0]

這就是讀取物件值的第二種方式:用中括號來讀取 key 的值(如果 key 是字串,就一定要用單引號把 key 包起來)

var a = {
  name: 'Harry'
}

console.log(a['name'])

arguments.length 共有幾個引數

正常的物件,是沒辦法使用 物件名稱.length 來知道物件長度的

但是,因為 arguments 是一個類陣列(Array-like)物件,所以才可以用 arguments.length 來知道總共傳進了幾個引數

function add(a, b){
  console.log(arguments.length)
  console.log(Array.isArray(arguments))
  return a + b
}

console.log(add(7, 9))

output:

2
false
16

為什麼 arguments 是一個物件,而不能是一個 array?

arguments 有一些屬性像是 arguments.calleearguments.caller,可以印出:是誰在 call 這個 function 的

要印出「是誰在 call 這個 function 的」,就需要帶有一些資訊,這時就需要用到「物件」的格式來儲存資料(用 value 來儲存每個 key 帶有的資訊)

如果是 array 的格式的話,根本沒辦法做到「儲存每個 key 帶有的資訊」這件事

使用 function 時的注意事項

關於這個主題,如果想要了解的更深入、更複雜,可以參考:深入探討 JavaScript 中的參數傳遞:call by value 還是 reference?

function 傳參數的運作機制

情況一:當變數的型別是 primitive 時(number, string, boolean),改 function 裡面的不會影響到 function 外的

用一個 function 交換兩個變數:
因為是要交換兩個值,所以一定要有第三個變數 temp,用來把 a 的值存起來

接著,宣告兩個變數 number1number2,先把兩個變數的值印出來

執行 swap 函式後,再一次把兩個變數的值印出來

function swap(a, b){
  var temp = a
  a = b
  b = temp
}

var number1 = 50
var number2 = 100
console.log(number1, number2)

swap(number1, number2) // 執行 swap 函式
console.log(number1, number2)

output:

50 100
50 100

結果會發現,兩個變數(number1, number2)的值並沒有交換!

swap 函式裡面,再把 a, b 印出來看看 console.log('a, b:', a, b)
會發現:

  • 一開始的 number1, number2 是 50, 100
  • 經過 swap 函式的 a, b 確實有交換:是 100, 50
  • 但是 number1, number2 的值並沒有改變:依然還是 50, 100
function swap(a, b){
  var temp = a
  a = b
  b = temp
  console.log('a, b:', a, b)
}

var number1 = 50
var number2 = 100
console.log(number1, number2)

swap(number1, number2) // 執行 swap 函式
console.log(number1, number2)

output:

50 100
a, b: 100 50
50 100

原因

在傳入引數時:
把 number1, number2 傳進去 swap 函式時,其實是「先複製一份」

function swap(a, b) {
  /*
  會先複製一份(可以想成是這樣,但實際上不是這樣的)
    var a = number1
    var b = number2
  */
  var temp = a
  a = b
  b = temp
  console.log('a, b:', a, b)
}

var number1 = 50
var number2 = 100
console.log(number1, number2)

swap(number1, number2) // 執行 swap 函式
console.log(number1, number2)

因為變數 number1, number2 的型別是 number(屬於 primitive 型別),因此變數存的是「值」,每個變數都會是獨立的,都會在不同的記憶體位置上

既然 a, b 是由 number1, number2 複製來的,因此型別也都會是 number,那麼「a, b, number1, number2」就會在四個完全不一樣的記憶體位置上

「a 和 number1」、「b 和 number2」只是「值一樣」而已,但是卻是「完全不同的變數」(都在不同的記憶體位置上)

所以,在 function 裡面的 a, b 的確是改變了(值交換了),但是這並不會影響到「分別在別的記憶體位置的 number1, number2」的值

情況二:當變數的型別是 object 時(改 function 裡面的也會改到 function 外的)

修改屬性,改 function 裡面的也會改到 function 外的

有一個 addValue 函式,會傳一個 object 進去

  • addValue function 裡面,會讓 obj.number +1
  • 在 function 外面,有一個 object 叫做 a = {number: 5}
  • 接著,我執行 addValue 函式,把 a 傳進去
  • 最後,把 a 印出來

思考一下,a.number 會跟著 +1 嗎?

function addValue(obj){
  obj.number++
  return 1
}

var a = {
  number: 5
}

addValue(a)
console.log(a)

output:

{ number: 6 }

結果會發現,物件 a 也被改到了!

原因是:

詳細說明可參考 讓兩個物件相等:指向同一個記憶體位置(改一個,也同時改到另一個)

在把 a 傳進 addValue 函式時,也會先複製一份,可以想成是:
addValue 函式裡面加上這句 var obj = a

因為物件存的是「記憶體位置」,所以複製一份之後,兩個物件(obja)還是會指向「同一個記憶體位置」
所以,當我修改物件 obj時,也會同時改到物件 a

function addValue(obj){
  /*
  先把「物件 a」複製一份
    var obj = a
  */
  obj.number++
  return 1
}

var a = {
  number: 5
}

addValue(a)
console.log(a)

新增屬性,改 function 裡面的也會改到 function 外的

function addValue(obj) {
  /*
  先把「物件 a」複製一份
    var obj = a
  */
  obj.test = 30
  return 1
}

var a = {
  number: 5
}

addValue(a)
console.log(a)

output:

{ number: 5, test: 30 }

結果是:物件 a 也跟著新增 test 屬性了!

讓變數 obj 等於一個新的物件(改 function 裡面的,不會影響到 function 外的)

這時,我讓變數 obj 等於一個新的物件: obj = {number: 100}
思考一下,最後印出來的物件 a,會是 {number: 5} 還是 {number: 100} 呢?

function addValue(obj) {
  /*
  先把「物件 a」複製一份
    var obj = a
  */
  obj = {
    number: 100
  }
  return 1
}

var a = {
  number: 5
}

addValue(a)
console.log(a)

output:

{ number: 5 }

結果是:物件 a 依然還是原本的 { number: 5 }
原因是:

詳細可參考 讓兩個物件不相等:指向不同的記憶體位置(改一個,並不會影響到另一個)

因為我「用 =」讓變數 obj 等於一個新的物件,變數 obj 就會指向另一個新的記憶體位置(斷開原本跟物件 a 的連結)

兩個物件(obja)不再是同一個記憶體位置了,所以修改 obj 根本不會去改到 a

重點總結

以上這些就是在使用 function 時,要注意的事情:
在 function 裡面修改 object(物件、陣列)時,有可能也會改到 function 外的物件、陣列

但如果是在 function 裡面修改 primitive 型別的變數,就絕對不會影響到外面的

return 不 return,有差嗎?

在 function 裡面,
return 的作用是什麼?什麼時候要用?什麼時候不用?

可以把 function 簡單分成兩類:

1. 只是呼叫這個 function 而已,它不會回傳任何值(我不需要知道它的結果)--> 有沒有 return 都沒差

例如:

function greeting(name){
  console.log('hello', name)
}

greeting('Harry')
// output: hello Harry

當然,如果我想要回傳,也是可以加上一個 return,例如下面的 return 1,但是這不會有任何影響,output 還是一樣是 hello Harry

function greeting(name){
  console.log('hello', name)
  return 1
}

greeting('Harry')

如果我在 function 裡面沒有特別寫 return 的話,預設會是 return undefined

像是這樣:

function greeting(name){
  console.log('hello', name)
  return undefined
}

greeting('Harry')

要怎麼看到這個 return undefined 的結果呢?

  • 在 function 裡面不執行任何事情
  • 宣告一個變數 var a = greeting('Harry'),a 會去接收「greeting 這個 function 的回傳值」,也就是 return 的值
  • 最後把 a 印出來

結果就是:a 會是 undefined

function greeting(name){
  // console.log('hello', name)
}

var a = greeting('Harry')
console.log(a)
// output: undefined

自己 return 其他東西

function greeting(name){
  console.log('hello', name)
  return 'I am a'
}

var a = greeting('Harry')
console.log(a)

output:

hello Harry
I am a

執行順序是:

  1. 執行函式 greeting('Harry')
  • 先執行 console.log('hello', name),印出 hello Harry
  • 再執行 return 'I am a',這時,a 就會等於我要 return 的值 'I am a'
  1. 印出 a

2. 需要 function 做運算後,回傳運算完的值(我需要知道它的結果)--> 就要在 function 裡面加上 return

例如:
這個 double 函式,我想要知道一個數乘以 2 的結果是多少
如果我只有寫 double(8),是不會印出任何東西的

function double(x){
  return x * 2
}

double(8)

宣告一個變數 var result = double(8),這個 result 就會是「double 函式 return 的值」
再把 result 印出來

function double(x){
  return x * 2
}

var result = double(8)
console.log(result)
// output: 16

在 function 執行完 return 後,就不會繼續往下執行 return 後面的程式碼了

例如:
我把 console.log('hello') 放在 return 前面,就會印出 hello

function double(x){
  console.log('hello')
  return x * 2
}

var result = double(8)
console.log(result)

output:

hello
16

但是,如果把 console.log('hello') 放在 return 後面,執行完 return 後,就會跳出 function(不會執行到 console.log('hello') 這句),然後接著執行第六行

function double(x){
  return x * 2
  console.log('hello')
}

var result = double(8)
console.log(result)

output:

16

return 多個值,就用 + 連接

例如: return str[i] + ' ' + i

function position(str){
  for(let i=0; i<str.length; i++){
    if(str[i] >= 'A' && str[i] <= 'Z'){
      return str[i] + ' ' +  i
    }
  }
  return -1
}

console.log(position("abCD"))
// output: C 2

#javascript







Related Posts

有趣的 WEB 特效與技巧分享

有趣的 WEB 特效與技巧分享

3. 優美地定義 React 型別

3. 優美地定義 React 型別

如何成為專家 - 體力篇

如何成為專家 - 體力篇


Comments