iT邦幫忙

2019 iT 邦幫忙鐵人賽

DAY 6
3
Modern Web

從0.5開始的JavaScript系列 第 6

Day6 工具包: 函數&模組化(1)

前幾天的主題把自己認為寫 JS 一定要懂的前置觀念介紹了一下。那接下來的文章會盡量避免太過偏向討論特性的本質,而是會偏向實際應用的介紹,讓以後開發遇到問題時,能夠像工具書一樣查詢,畢竟這才符合我們第一天寫下的大綱與方向/images/emoticon/emoticon08.gif

函數

類型

函數的寫法有這幾種

  1. 命名函數
function sayHi(){
  console.log("Hi");
}
sayHi(); // Hi
  1. 匿名函數(想成把函數當作變數傳遞)
var sayHi = function(){
  console.log("Hi");
}
sayHi(); // Hi

But

這種寫法沒辦法透過 hoisting 來使用它,如果有看Day4的讀者應該就知道為什麼了。
Ans: 第一行 Hoisting 的變數 sayHi 此時的值是 undefined

  1. 自調用函數
(function(a, b){
  console.log(a + b);
})(10, 5); // 15

自調用函數不需要呼叫就可以自己立刻執行,方便建構自己的生存域。

  1. 箭頭函數(Since ES6)
    • (param1, param2, …, paramN) => { statements }
    • singleParam => { statements },只有一個參數時,括號才能不加。
    • (param1, param2, …, paramN) => expression,expression 等於 { return expression; },若函數內只有 return 值而已,則可省略大括號和 return。

所以這個

var sum = function(x, y){
  return x + y;
};
console.log(sum(1, 2));

可以改寫成

var sum = (x, y) => x + y;
console.log(sum(1, 2));

是不是很簡短呢~

預設參數值

函數可以接受參數值傳入,但是沒有參數值傳入時,也可以事先設定預設值喔。

function guessWhat(str = 'Hi'){
  console.log(str);
}
guessWhat(); // Hi

arguments

再來這邊要講一下,當傳入的參數大於函數事先定義的參數數量時,這些多出來的參數不會不見,還是會存在一個叫 arguments 的變數裡面(有定義的參數也會在裡面)。

function addNum(a, b){
    var arg = arguments;
    console.log(arg);
}
addNum(10, 20, 30); // [10, 20, 30]

中斷函數

大家都知道函數會有回傳值,也就是會有 return 值。
那這個特性也可以用來直接中斷函數,也就是直接 return 空值。

function sayHi(myStr = 'Hi'){
  if(myStr == 'bye'){
    return
  }
  console.log(myStr);
}

sayHi(); // Hi
sayHi('bye'); // show nothing

Closure

來人,還不先引用維基百科的介紹!

閉包(Closure)是參照了自由變數的函式。這個被參照的自由變數將和這個函式一同存在,即使已經離開了創造它的環境也不例外。所以,有另一種說法認為閉包是由函式和與其相關的參照環境組合而成的實體。

簡單來說就是:
閉包(Closure)是擁有自由變數的函數,只有當函數的自由變數與當時環境綁定,此函數才稱為閉包,也就是建立函數不等於建立閉包。

PS. 自由變數: 對於函數而言,既不是區域變數也不是參數的變數。

聽的霧煞煞嗎?

沒關係,我們直接來看例子,請問Q1會印出什麼?

function outer(){
  var myStr = "Hi";
  function sayHi(){
    console.log(myStr);
  }
  return sayHi;
}

var myFun = outer();
myFun(); // Q1=?

Ans:
Q1 = Hi

咦,可是 myFunouter return 出來的 function,也就是 sayHi。但是 sayHi 中並沒有 myStr 的變數阿,能夠使用的話,也應該是在 outer 中去呼叫 sayHi,然後由於 Scope 的特性,所以能夠使用 myStr

沒有道理 return 出來還能夠使用 outer 函數中的變數吧,這太扯了...?

別急阿,讓我們想想上面提到閉包的定義,所以可以理解成,

  1. 理論上變數 myStr 在函數執行完就消失,
  2. 但由於內部函數 sayHi 參考到變數 myStr
  3. 所以當 sayHi 被 return 給全域變數 myFun 時,
  4. 創建函數時的環境變數被記憶下來,所以變數 myStr 活下來了。

而此時的變數 myStr,對於 sayHi 來說就屬於自由變數,而 sayHi 也就建立了閉包,它把 myStr 關進了自己的世界。

瞧瞧這可怕的佔有欲(X

還是不太清楚嗎,沒關係我們可以再多看幾個例子~

function outer() {
  var a = 10;
  function addNum(b) {
    return a + b;
  }
  return addNum;
}

var myFun = outer();
console.log(myFun(10)); // Q2=?
console.log(myFun(20)); // Q3=?

沒錯,答案是
Ans:
Q2 = 20
Q3 = 30

Q: (舉手)請問這樣我可以說自己是閉包達人或是精通閉包了嗎?
A: Hmm...應該沒有人會這樣稱呼自己,讓我們再多看幾個範例好了...

上方的例子中,變數 a 被關入閉包,突破作用域活了下來,但是有一個觀念要牢記:

閉包關閉的是變數,而不是變數所參考的值。

所以把程式碼改成這樣,請問 Q4 是 666 還是 11 呢?

function outer() {
  var a = 10;
  function addNum(b) {
    return a + b;
  }
  a = 665;
  return addNum;
}

var myFun = outer();
console.log(myFun(1)); // Q4=?

沒錯,答案是
Ans:
Q4 = 666

因為我們已經有先做了小抄,了解到「閉包關閉的是變數,而不是變數所參考的值」,
所以在原環境中,return addNum 之前改變了變數 a 的值,也更新了關入閉包中的變數的值。

那接下來還有另外一個重點,

閉包關入的變數是獨立的,也就是每次呼叫建立閉包時,都是根據當時的環境來產生。

有了這個觀念我們來看看這個,請問 Q5 會印出 31 還是 32 ?
也就是 myFun1 執行之後,閉包中的變數 a 是 10 還是 11 呢?

function outer() {
  var a = 10;
  function addNum(b) {
    a = a + 1;
    return a + b;
  }
  return addNum;
}

var myFun1 = outer();
var myFun2 = outer();
console.log(myFun1(10)); // 21
console.log(myFun2(20)); // Q5=?

沒錯,答案是,
Ans:
Q5 = 31

因為 myFun1myFun2 中的閉包是各自獨立的,myFun1 執行之後,變數 a 變成 11,但是不關 myFun2 的事情。

小提醒,myFun1myFun2 執行後,各自關入的變數還是保存著,

...
console.log(myFun1(10)); // 21
console.log(myFun2(20)); // Q5=31

console.log(myFun1(10)); // Q6=?

所以 Q6 理所當然的也就是 22 啦。


謝謝樓主分享,我了解了!
Hmm...越想越不對,我知道這個幹嘛? 它能實際應用在哪裡?

別緊張還沒分享完嘛Q_Q,讓我們明天再來講講它的實際應用!


那今日的分享到這,我們明天見/images/emoticon/emoticon51.gif


上一篇
Day5 單執行緒&非同步發生的血案
下一篇
Day7 工具包: 函數&模組化(2)
系列文
從0.5開始的JavaScript30

尚未有邦友留言

立即登入留言