關於 JavaScript 的函式

(1) 函式的種類

傳統函式

  • 一段用於執行某個任務的程式碼
//語法
function name([param]) {
  statements
}

//example
//宣告了一個名為square的函式並執行
function square(number) {
  return number * number
}

console.log(square(10)) // 100

//以上例子中,number 是 parameter (參數)
//執行此函式時,10是argument (引數)

箭頭函式

  • ES6 語法,可以更簡潔的撰寫函式
//example
//原本
const sayHi = function () {
  console.log('hello')
}

//改寫
const sayHi = () => {
  console.log('hello')
}
  • 精簡的寫法
// 參數只有一個時,參數的小括號可以省略
const addOne = (n) => {
  return n + 1
}
// 只有一行時,省略大括號和 return
//ex1
const addOne = (n) => n + 1

//ex2
const form = document.querySelector('form')
form.addEventListener('click', (e) => e.preventDefault())

// 直接return一個物件時,用小括號包起來
const friend = (name) => ({ name })
friend('Nobody')
//{name: 'Nobody'}

(2) 傳統函式和箭頭函式的差別

1. This 關鍵字

  • 傳統函式的 this 依照呼叫的方法而定
  • 箭頭函式的 this 在定義時便被綁定
  • 箭頭函式沒有自己的 this,從自己的作用域上一層沿用 this
  • 箭頭函式的 this,是上一層作用域的 this 指向
//使用傳統函式時,this的指向是obj
const obj = {
  value: 1,
  hello: function () {
    console.log(this.value)
  }
}

obj.hello() // 1

//使用箭頭函式時,這裡的this是全域物件,回傳undefined
const obj = {
  value: 1,
  hello: () => {
    console.log(this.value)
  }
}

//obj的this => window.obj

obj.hello() // undefined

//傳統函式混合箭頭函式
const obj = {
  name: 'Andy',
  sayHi: function () {
    console.log(this)
    //walk裡沒有this,沿用sayHi的this (obj)
    const walk = () => {
      console.log(this)
    }
    walk()
  }
}

//傳統函式的this看怎麼被呼叫
obj.sayHi()

//{name:'Andy' , walk: f}
//{name:'Andy' , walk: f}
  • 事件監聽時的差別
const btn = document.querySelector('.btn')

//箭頭函式,this指向window
btn.addEventListener('click',() => {
    console.log(this)
})

//傳統函式,this指向DOM物件
btn.addEventListener('click',function () => {
    console.log(this)
})
  • 使用 setTimeout 時的差別
const normal = {
  msg: 'Hello',
  fn: function () {
    setTimeout(function () {
      console.log(this.msg)
    }, 10)
  }
}

const arrow = {
  msg: 'Bye',
  fn: function () {
    setTimeout(() => {
      console.log(this.msg)
    }, 10)
  }
}

normal.fn() // undefined 因this指向全域
arrow.fn() // Bye

2.arguments 物件不同

  • 傳統函式則會有自己的 arguments 物件
  • 箭頭函式沒有自己的 arguments 物件,而是會繼承其外層作用域的 arguments
  • arguments 物件是一個對應傳入函式之引數的類陣列(Array-like)物件。
// 傳統函式中,可以使用 arguments 對象來獲取傳遞給函式的所有參數,
function func() {
  console.log(arguments); /
}
func(1, 2, 3);
// output: [1, 2, 3, callee: ƒ, Symbol(Symbol.iterator): ƒ]

//箭頭函式
const func = () => {
  console.log(arguments);
};
func(1, 2, 3);
// output: ReferenceError: arguments is not defined

//使用剩餘參數...args來代替
const func = (...args) => {
  console.log(args);
};
func(1, 2, 3);
// output: [1, 2, 3]

//

3. 構造函式

  • 普通函式可以用作構造函式,被用於創建新的對象(使用 new 關鍵字)
  • 箭頭函式不能用作構造函式,不能使用 new 關鍵字
const Arrow = () => {}
const arrow = new Arrow() // TypeError: Arrow is not a constructor

4. prototype 屬性

  • 箭頭函式並沒有原型(prototype)屬性
function Arrow() {
  console.log('Hello, prototype')
}
console.log(Arrow.prototype) // {constructor: f}

const Arrow = () => {
  console.log('Hello, prototype')
}

console.log(Arrow.prototype) // undefined

(3) 具名函式和匿名函式

具名函式

  • 使用 function 關鍵字宣告函式,並命名
  • 函式陳述式 (Function Declaration) ⇒ 不會回傳值或結果
//example
//宣告一個function,名字是sayHi
function sayHi() {
  console.log('hello')
}

sayHi()

匿名函式

  • 沒有命名的函式,無法直接呼叫
  • 通常出現在函式表達式 (Function Expressions)
  • 函式表達式也有可能使用具名函式
//ex1
//把一個匿名函式賦值給一個變數 (expressions),然後再呼叫

const sayHi = function () {
  console.log('hello')
}

sayHi()

//ex2
const sayHi = function hello() {
  console.log('hello')
}

//要用被賦值的變數呼叫執行
sayHi()

//錯誤呼叫
hello() // hello is not defined

(4) 立即函式 IIFE

  • 定義完不用呼叫,便立即執行的函式表達式
  • 可用在只會執行一次的函式
  • 通常會是匿名函式,不需要取名
//語法1
;(function () {
  // 匿名函式的程式碼
})()

//語法2
;(function () {
  // 匿名函式的程式碼
})()

//ex1
;(function () {
  console.log('立刻執行') // 立刻執行
})()

//ex2
;(function () {
  console.log('立刻執行') // 立刻執行
})()

//ex3 傳入參數
;(function (x, y) {
  console.log(x + y)
})(1, 2)
  • 變數的作用域只存在函式內,防止污染全域作用域
  • 可以避免與全域變數重名或衝突
//ex1 - 使用var宣告變數也不能在函式外取得
;(function () {
  var hobbit = 'walking'
  console.log(hobbit) //'walking'
})()

console.log(hobbit) //is not defined
  • 可以是具名函式,但也無法在函式外再次被執行
;(function sayHi() {
  console.log('Hello') //Hello
})()

sayHi() //sayHi is not defined
  • 因為是表達式,可以賦值給變數
const dinner = (function (food) {
  return `晚餐吃${food}`
})('pizza')

//dinner用於接收立即函式return的值,不用加括號執行
dinner // '晚餐吃pizza'

//錯誤呼叫
dinner() //dinner is not a function
  • 利用物件的 call by reference 特性,在匿名函式間傳遞值
const obj = {}

;(function (input) {
  input.name = 'Mario'
})(obj)

console.log(obj) // {name: 'Mario'}

//取得obj.name做處理
;(function (input2) {
  console.log(`${input2.name} says Hi`) // 'Mario says Hi'
})(obj)
  • 立即函式之間必須用 ; 隔開
//放在後面
;(function () {})()
;(function () {})()

//或放在前面
;(function () {})()
;(function () {})()
  • 將變數和函式註冊到全域(window 物件)上
;(function (obj) {
  const sport = '跳繩'
  const exercise = function (todo) {
    console.log(`今天要${todo}`)
  }
  obj.sport = sport
  obj.exercise = exercise
})(window)

//在全域讀取
console.log(sport) // '跳繩'
exercise(sport) // '今天要跳繩'

函式作用域&閉包

  • 過程說明
    1. IIFE 在執行時創建了一個新的作用域,並且在這個作用域中定義了 count 變數
    2. 由於 JavaScript 的閉包(Closure)特性,返回的匿名函數可以訪問(並保留)這個作用域中的 count 變數,即使 IIFE 已經執行完畢。
    3. 每次調用 uniqueId() 時,都會執行這個匿名函數,而這個匿名函數會對 count 進行增加操作。
    4. 由於這個匿名函數保留了對 count 的引用(閉包), count 的值會在每次調用 uniqueId() 後被保留和累加。
const uniqueId = (function () {
  let count = 0
  return function () {
    ++count
    return `id_${count}`
  }
})()

console.log(uniqueId()) // "id_1"
console.log(uniqueId()) // "id_2"
console.log(uniqueId()) // "id_3"

(5) 函式的提升特性

提升(Hoisting)

  • JavaScript 引擎在執行代碼之前,將變數、函式陳述式及 class 移動到其範圍頂部的過程
  • 執行環境(execution context)是指 JavaScript 引擎模組化直譯程式碼時的區塊環境

執行環境在建立時會經歷兩個階段

  1. 創造階段 (Creation Phase)
    • 一行一行地執行程式碼。此時會把變數的記憶體空間準備好,但還不會給值。
  2. 執行階段 (Execution Phase)
    • 賦值給變數並執行被呼叫的函式

宣告變數時的提升

  • 使用 var 和 let / const 宣告時的差別
  • 只有變數的宣告會提升,賦值不會提升
//使用let / const 宣告
console.log(a);
let a = 3; //a is not defined
console.log(b);
let b = 5; //b is not defined

執行過程:
//創造階段
let a ; //暫時性死區TDZ,無法呼叫
const b; //暫時性死區TDZ,無法呼叫

//執行階段
a = 3;
b = 5;

//使用var宣告
console.log(c);
var c = 7; //b is undefined;

執行過程:
//創造階段
var c ; //被賦予值undefined

//執行階段
c = 7;

宣告函式時的提升

  • 函式陳述式:在創造階段優先載入,可以先執行後定義
sayHi() // '說你好'
function sayHi() {
  console.log('說你好')
}

//創造階段
執行過程: function sayHi() {
  console.log('說你好')
}
//執行階段
sayHi() // '說你好'
  • 函式表達式:無法先執行後定義
//使用var宣告
console.log(sayHi)
sayHi()
var sayHi = (function () {
  console.log('說你好')
})(
  //undefined
  //Uncaught TypeError: sayHi is not a function
  還沒有被賦予值
)

//使用let 宣告
walk()
let walk = function () {
  console.log('散步中')
}
//ReferenceError: walk is not defined

//創造階段:
執行過程: var sayHi //先賦予值undefined
let walk //暫時性死區

//執行階段:
sayHi = function () {
  console.log('說你好')
}

walk = function () {
  console.log('散步中')
}

參考文章

# JavaScript