顯示具有 Udemy 標籤的文章。 顯示所有文章
顯示具有 Udemy 標籤的文章。 顯示所有文章

2017年9月21日

[演算法] Reverse Array in Place:暫存變數的使用

此系列筆記主要依照 [Udemy] Learning Algorithms in JavaScript from Scratch by Eric Traub 的課程脈絡加以整理,但部分程式碼是消化後以自己較易理解的方式重新撰寫,因此和原課程內容有些出入。

問題描述

在這次的練習中,我們要將輸入的陣列進行反轉,但有幾點需要注意的:
  1. 不能建立一個新的陣列,然後透過 push 的方式把新的元素內容推進去新陣列。
  2. 不能使用 Array.prototype.reverse() 這個方法。
function reverseArrayInPlace (arr) {...}

前置閱讀

演算法實做

第一個元素和倒數第一個元素對調
為了達到陣列反轉的效果,我們有一個很特別,而且實務上也經常用到的技巧,第一個元素和倒數第一個元素對調;第二個元素和倒數第二個元素對調,以此類推…。
這個作法的技巧在於,我們要建立一個 暫存變數 ,邏輯有點像這樣:
let tempVar = 第一個元素
第一個元素 = 倒數第一個元素
倒數第一個元素 = tempVar      // 因為這時候第一個元素的值已經改變了,所以不能再直接代入第一個元素
實做的方法會像這樣:
function reverseArrayInPlace (arr) {
  for (let i = 0; i < arr.length; i++) {
    let tempVar = arr[i]
    arr[i] = arr[arr.length - 1 - i]
    arr[arr.length - 1 - i] = tempVar
  }
  return arr
}
和上次演算法 Algorithm: reverse words 使用到相同的技巧 arr.length - 1 - i 來反轉陣列的元素;但是這麼做還有一個問題,就是一開始第一個元素會和最後一個元素交換位置,可是跑到最後一個元素的時候,又和第一個元素交換位置,最後使的陣列沒有達到預期反轉的效果,因此我們的迴圈只要跑前一半就好
for (let i = 0; i < (arr.length / 2); i++) {...}

完整程式碼

function reverseArrayInPlace (arr) {
  for (let i = 0; i < arr.length / 2; i++) {
    let tempVar = arr[i]
    arr[i] = arr[arr.length - 1 - i]
    arr[arr.length - 1 - i] = tempVar
  }
  return arr
}

let arr = ['a', 'b', 'c', 'd']
console.log(reverseArrayInPlace(arr))      // ['d', 'c', 'b', 'a']

資料來源

[演算法] Reverse Words: 把單字反過來寫

此系列筆記主要依照 [Udemy] Learning Algorithms in JavaScript from Scratch by Eric Traub 的課程脈絡加以整理,但部分程式碼是消化後以自己較易理解的方式重新撰寫,因此和原課程內容有些出入。

問題描述

在這次的練習中,我們要實做一個能夠將單字反轉過來的函式,但有兩點要注意的:
  1. 反轉的是單字,而不是整個句子,例如 This is a cat,應該要變成 sihT si a tac,而不是 tac a si sihT
  2. 不能使用 Array.prototype.reverse() 這個方法。
function reverseWords (str) {...}

演算法實做

字串反轉

比較重要的是如何不用 Array.prototype.reverse() 的方式來實做將字串反過來,我們可以觀察假設一個字串原本是 abcd,它的 index 會是 0123,如果反過來變成 dbca 的話,它的 index 會是 3210,寫成條列是我們就可以看出些有趣的規律:
abcd
0123
3210
dcba
可以發現 a+d=3, b+c = 3, c+b=3, d+a=3;利用這樣的規則,我們就可以把我們的字串反轉過來,例如:
let str = 'abcd'
let strReverse = ''
for (let i = str.length - 1; i >= 0 ; i--){
  strReverse += str[i]
}
console.log(strReverse) // dbca

不要整句反轉

為了不要整句反轉,所以我們要根據空行把句子拆開成陣列:
function reverseWords (str) {
  strArr = str.split(' ')
  /* ... */
}
接著要把陣列 strArr 中的每個元素進行字串反轉,這裡我們使用 Array.prototype.map() 這個方法,最後在透過 Array.prototype.join() 這個方法把它組回字串:
function reverseWords (str) {
  strArr = str.split(' ')

  strArrReverse = strArr.map(str => {
    let newStr = ''
    for (let i = str.length - 1; i >= 0; i--) {
      newStr += str[i]
    }
    return newStr
  })
  return strArrReverse.join(' ')
}

完整程式碼

function reverseWords (str) {
  strArr = str.split(' ')

  strArrReverse = strArr.map(str => {
    let newStr = ''
    for (let i = str.length - 1; i >= 0; i--) {
      newStr += str[i]
    }
    return newStr
  })
  return strArrReverse.join(' ')
}

reverseWords('This is a cat')                // sihT si a tac
reverseWords('This is a string of words')    // sihT si a gnirts fo sdrow
reverseWords('Coding JavaScript')            // gnidoC tpircSavaJ

資料來源

[演算法] Caesar Cipher: 往後或往前推移英文字母

此系列筆記主要依照 [Udemy] Learning Algorithms in JavaScript from Scratch by Eric Traub 的課程脈絡加以整理,但部分程式碼是消化後以自己較易理解的方式重新撰寫,因此和原課程內容有些出入。

問題描述

Caesar Cipher 要做的事,是把輸入的字串根據所給定的數值往前/後推幾位,例如輸入字串 a 和數字 2,則會把 a 往後推兩位,於是要回傳 c。
function caesarCipher (str, num) {...}

caesarCipher ('zoo keeper', 2)     // bqq mggrgt

前置知識

在實做這個演算法前,我們先來瞭解一下 ASCII Code。由於電腦實際上指認得數字,並不認得我們所輸入的字母,而 ASCII Code 簡單來說,就是英文字母和數字的轉換表。
舉例來說,英文字母 a 對應到的十進位代碼就是 97; A 對應到的十進位代碼則是 65;o 對應到的十進位代碼就是 111。
從下面的 ASCII Table 中我們可以看到大寫的英文字母分別對應到 65-90;小寫的英文字母分別對應到 97-122。
透過 JavaScript 函式,我們可以很容易地將字母與數值做轉換:
String.prototype.charCodeAt(index)
透過 String.prototype.charCodeAt(index) 這個函式,我們可以將英文字母轉為 ASCII Code:
'Aao'.charCodeAt(0)          // A 是 65
'Aao'.charCodeAt(1)          // a 是 97
'Aao'.charCodeAt(2)          // o 是 111
String.fromCharCode(num1[, …[, numN]])
透過 String.fromCharCode(num1[, ...[, numN]]) 我們則可以將數值轉換成回字串:
String.fromCharCode(65, 97, 111)      // Aao

演算法實做

這個演算法中比較麻煩的部分是函式後第二個數值沒有限制正負和數值大小,因此如果原本的字串是 a ,數值是 1 ,則回傳 b ;數值如果是 -1,則回傳 z。
因此我們必須先把輸入的數值限制在某一個範圍內,由於英文字母有 26 個,我們可以透過餘數的使用讓 num 的值限制在 -25 ~ 25 之間:
function caesarCipher (str, num) {
  num = num % 26        // num: -25 ~ 25
}
再來我們要分別去跑 str 裡面的每一個字母做轉換:
function caesarCipher (str, num) {
  /* ... */
  
  for (let i = 0; i < str.length; i++) {
    let currentCharCode = str.charCodeAt(i)
  }
}
大寫英文字母的 ASCII Code 65 ~ 90;小寫英文字母的 ASCII Code 97 ~ 122。
針對大寫英文字母,因為 num 會介於 -25 到 25 之間,而大寫英文字母會介於 65 到 90 之間,所以 newCharCode 將會介於 40 ~ 115 之間。
如果 newCharCode 小於 65 的話,那麼要加 26 讓它重新介於 65 以上;如果 newCharCode 大於 90 的話,那麼要減 26 讓它重新介於 90 以下:
if (currentCharCode >= 65 && currentCharCode <= 90) {
  // 大寫英文字母轉換
  newCharCode = currentCharCode + num  // newCharCode: 40 ~ 115
  if (newCharCode < 65) {
    newCharCode = newCharCode + 26
  } else if (newCharCode > 90) {
    newCharCode = newCharCode - 26
  }
}
同樣的道理,針對小寫英文字母的轉換:
else if (currentCharCode >= 97 && currentCharCode <= 122) {
  // 小寫英文字母轉換
  newCharCode = currentCharCode + num
  if (newCharCode < 97) {
    newCharCode = newCharCode + 26
  } else if (newCharCode > 122) {
    newCharCode = newCharCode - 26
  }
} 

完整程式碼

function caesarCipher (str, num) {
  let newString = []
  num = num % 26      // num: 0 ~ 25
  
  for (let i = 0; i < str.length; i++) {
    let currentCharCode = str.charCodeAt(i)
    let newCharCode
    
    /**
     * 大寫英文字母的 ASCII Code 65 ~ 90
     * 小寫英文字母的 ASCII Code 97 ~ 122
    **/
    
    if (currentCharCode >= 65 && currentCharCode <= 90) {
      // 大寫英文字母轉換
      newCharCode = currentCharCode + num
      if (newCharCode < 65) {
        newCharCode = newCharCode + 26
      } else if (newCharCode > 90) {
        newCharCode = newCharCode - 26
      }
    } else if (currentCharCode >= 97 && currentCharCode <= 122) {
      // 小寫英文字母轉換
      newCharCode = currentCharCode + num
      if (newCharCode < 97) {
        newCharCode = newCharCode + 26
      } else if (newCharCode > 122) {
        newCharCode = newCharCode - 26
      }
    } else {
      // 其餘保留原樣
       newCharCode = currentCharCode
    }
    
    newString.push(String.fromCharCode(newCharCode))
  }
  return newString.join('')
  
}

console.log(caesarCipher('Zoo Keeper', 2))    //  Bqq Mggrgt
console.log(caesarCipher('Big Car', -16))    //  Lsq Mkb
console.log(caesarCipher('JavaScript', -900))    //  TkfkCmbszd

資料來源

延伸閱讀

2017年9月19日

[演算法] Is Palindrome:判斷順寫逆寫是不是一樣

此系列筆記主要依照 [Udemy] Learning Algorithms in JavaScript from Scratch by Eric Traub 的課程脈絡加以整理,但部分程式碼是消化後以自己較易理解的方式重新撰寫,因此和原課程內容有些出入。

問題描述

在忽略單字大小寫和標點符號的情況下,判斷字串是不是迴文(palindrome),也就是順著寫和逆著寫都是一樣的,例如,Madam, I'm Adam, race car
function isPalindrome (str) {
  // return true or false
}
isPalindrome("Madam, I'm Adam")   // true

演算法實做

步驟一:將所有的單字轉成小寫,拆成陣列,並且排除非英文單字

我們先透過 String.toLowerCase() 將字串的內容全部轉成小寫,接著透過 String.split() 將字串拆成陣列,最後透過 Array.filter() 搭配一些正規表達式 /[a-z]/ 只保留小寫的英文字母,其他都過濾掉:
function isPalindrome (str) {
  // 將所有的單字轉成小寫,拆成陣列,並且排除非英文單字
  str = str.toLowerCase()
  charactersArr = str.split('').filter(character => {
    return /[a-z]/.test(character)
  })
  
  /* ... */
}
步驟二:如果正著寫和逆著寫都一樣,則回傳 true,否則 false
透過 Array.join() 將原本的陣列重新合併為字串。利用 Array.reverse() 將陣列反轉([a, b, c] --> [c, b, a]):
function isPalindrome (str) {
  // 將所有的單字轉成小寫,拆成陣列,並且排除非英文單字
  /* ... */
  
  // 如果正著寫和反轉過來寫的內容都一樣,則回傳 true,否則 false
  return charactersArr.join('') === charactersArr.reverse().join('')
}

完整程式碼

function isPalindrome (str) {
  // 將所有的單字轉成小寫,拆成陣列,並且排除非英文單字
  str = str.toLowerCase()
  charactersArr = str.split('').filter(character => {
    return /[a-z]/.test(character)
  })
  
  // 如果正著寫和反轉過來寫的內容都一樣,則回傳 true,否則 false
  return charactersArr.join('') === charactersArr.reverse().join('')
}

console.log(isPalindrome("Madam, I'm Adam"))    // true
console.log(isPalindrome("Hello, I'm Adam"))    // false

資料來源

[演算法] Big O Notation & Time Complexity

此系列筆記主要依照 [Udemy] Learning Algorithms in JavaScript from Scratch by Eric Traub 的課程脈絡加以整理,但部分程式碼是消化後以自己較易理解的方式重新撰寫,因此和原課程內容有些出入。

Big O Notation & Time Complexity

同樣的問題可以用許多種不同的方式加以解決,因此,我們需要一些指標來評量各種方式的好壞。在演算法中,常會使用 Big O NotationTime Complextiy 來衡量一個演算法(函式)的好壞。通常,會根據這個函式隨著輸入的資料量增加時,執行時間會拉長多少來作為衡量的標準之一,下面會說明其中四種類型:
補充:
Big O Notation 代表演算法時間函式的上限(Upper bound),表示在最壞的狀況下,演算法的執行時間不會超過Big-Ο。
[資料結構]演算法評估與資料型別

Constant Run Time (O(1))

第一個類型是屬於 constant run time(O(1)),這個演算法(函式)的執行時間不會隨著輸入資料量的增加而增加
以下面的函式為例,不論我們代入的資料量有多大,它都只是輸出陣列中第一和第二個元素的值,因此執行時間不會隨著輸入資料量的增加而增加。
let arr1 = [1,2,3,4,5]
let arr2 = [1,2,3,4,5,6,7,8,9,10]

/**
 * Constant Run Time:不會隨著輸入的資料量越大而使得執行時間變長
 * Big O Notation: "O(1)"
 **/
function log (arr) {
  console.log(arr[0])
  console.log(arr[1])
}
log(arr1)    // 1, 2
log(arr2)    // 1, 2

Linear Run Time (O(n))

下面的函式,當我們輸入的資料越多的時候,它就會需要等比例輸出越多的內容給我們,因此會需要消耗等比例越多的時間:
/**
 * Linear Run Time: 隨著資料量的增加,執行時間會等比增加
 * Big O Notation: "O(n)"
 **/
function logAll(arr) {
  for (let item of arr) {
    console.log(item)
  }
}

logAll(arr1)  // 1, 2, 3, 4, 5
logAll(arr2)  // 1, 2, 3, 4, 5, 6, 7, 8, 9, 10

Exponential Run Time (O(n^2))

隨著資料量的增加,執行時間會以指數成長。以下面的函式為例,當我們輸入的陣列包含 5 個元素的時候,它會輸出 25 (5^2) 筆資料;但是當我們數入的陣列包含 10 個元素的時候,它則會輸出 100 (10^2) 筆資料:
/**
 * Exponential Run Time:  隨著資料量的增加,執行時間會誇張的增長
 * Big O Notation: "O(n^2)"
 **/
function addAndLog (arr) {
  for (let item of arr) {
    for (let item2 of arr) {
      console.log ('First', item + item2)
    }
  }
}
addAndLog(arr1)  // 25 pairs logged out
addAndLog(arr2)  // 100 pairs logged out

Logarithmic Run Time (O(log n))

隨著資料量增加,執行時間雖然會增加,但增加率會趨緩。下面的程式碼類似 findIndex 的函式,當輸入的資料有 5 個元素時,它會先切對半後,再尋找,再切半再尋找,因此雖然隨著資料量增加,執行的時間會增加,但是當資料量越大時,執行速度增加的情況越不明顯:
/**
 * Logarithmic Run Time: 隨著資料量增加,執行時間雖然會增加,但增加率會趨緩
 * Big O Notation: "O (log n)"
 **/
function binarySearch (arr, key) {
  let low = 0
  let high = arr.length - 1
  let mid
  let element
  
  while (low <= high) {
    mid = Math.floor((low + high) / 2, 10)
    element = arr[mid]
    if (element < key) {
      low = mid + 1
    } else if (element > key) {
      high = mid - 1
    } else {
      return mid
    }
  }
  return -1
}

console.log(binarySearch(arr1, 3))
console.log(binarySearch(arr2, 3))

圖示

把上面這四種類型用圖線表示,縱軸是時間、橫軸是輸入資料量的多少,可以用來判斷這四種類型的演算法(函式)的好壞:

完整程式碼

/**
 * Demo Big O Notation
 **/

let arr1 = [1,2,3,4,5]
let arr2 = [1,2,3,4,5,6,7,8,9,10]


/**
 * Constant Run Time:不會隨著輸入的資料量越大而使得執行時間變長
 * Big O Notation: "O(1)"
 **/
function log (arr) {
  console.log(arr[0])
  console.log(arr[1])
}
log(arr1)    // 1, 2
log(arr2)    // 1, 2


/**
 * Linear Run Time: 隨著資料量的增加,執行時間會等比增加
 * Big O Notation: "O(n)"
 **/
function logAll (arr) {
  for (let item of arr) {
    console.log(item)
  }
}

logAll(arr1)  // 1, 2, 3, 4, 5
logAll(arr2)  // 1, 2, 3, 4, 5, 6, 7, 8, 9, 10

/**
 * Exponential Run Time:  隨著資料量的增加,執行時間會誇張的增加
 * Big O Notation: "O(n^2)"
 **/
function addAndLog (arr) {
  for (let item of arr) {
    for (let item2 of arr) {
      console.log (item + item2)
    }
  }
}
addAndLog(arr1)  // 25 pairs logged out
addAndLog(arr2)  // 100 pairs logged out

/**
 * Logarithmic Run Time: 隨著資料量增加,執行時間雖然會增加,但增加率會趨緩
 * Big O Notation: "O (log n)"
 **/
function binarySearch (arr, key) {
  let low = 0
  let high = arr.length - 1
  let mid
  let element
  
  while (low <= high) {
    mid = Math.floor((low + high) / 2, 10)
    element = arr[mid]
    console.log('ele', mid, element)
    if (element < key) {
      low = mid + 1
    } else if (element > key) {
      high = mid - 1
    } else {
      return mid
    }
  }
  return -1
}

console.log(binarySearch(arr1, 1))
console.log(binarySearch(arr2, 1))
    

資料來源

2017年6月28日

[學習筆記目錄] JavaScript: Understanding the Weird Part(JavaScript 全攻略:克服JS 的奇怪部分)



這是我在Udemy上修的一門課,當初想增進自己JavaScript的能力,這門課在特價,同時老師的評價也相當的好,所以就想說學學看。這門課和其他一般線上免費的JavaScript教程很不一樣,不只是教程式怎麼寫,而是進一步說明背後的原理和邏輯,學到的很多之前不清楚的概念,特別是在JavaScript中關於繼承、原型還有建構式的地方之前看了很多教學都還是不太瞭解,但這堂課說明的超級清楚,同時又有搭配英文字幕,歡迎大家參考,也很推薦大家上這門課!

JavaScript基本觀念:認識不同的資料型別、運算子和重要概念


JavaScript物件建立:瞭解基本物件建立的方法


JavaScript函式:為什麼說函式也是物件的一種呢?


JavaScript函式進階:瞭解什麼是IIFEs、什麼是closures?


JavaScript原型、繼承和建構式:這是我上過最清楚的說明


親手打造屬於你的 Framework/Library


其他資源


2017年1月25日

[筆記] JavaScript ES6 中的模版字符串(template literals)和標籤模版(tagged template)


在 ES6 中,我們多了一個非常好用的模版字符串(template literal),如果你會在 JS 中「放入 HTML 的內容」、或者有「很長的字串包含換行」、又或者會有「字串連結變數」這樣的需求,模版字符串會是非常方便的作法。

另外,在 ES6 中可以將模版字符串和函式結合使用,形成一個標籤模版(tagged template),可以以此過濾 HTML 字串,避免使用者輸入惡意內容。

模版字符串(template literal)的基本應用


模版字符串的使用非常簡單,就是使用反引號" ` "(鍵盤左上角的~),舉例來說,如果我們會在 JS 的字串中放入 HTML 內容,在過去我們可能需要這樣寫:


let component_es5 = '<header>\n'+
'<div class="banner">\n'+
'<img src="img1.jpg"\n'+
'</div>\n'+
'</header>'


這麼寫相當麻煩,而且不易閱讀。而在 ES6 中我們可以用反引號快速的解決這樣的狀況:


let component_es6 = `
<header>
    <div class='banner'>
        <img src="img1.jpg>
    </div>
</header>
`


這樣的作法,會直接輸出如下的結果:


也就是說,透過反引號包住的內容,會保留所有的換行和空行

在模版字符串中嵌入變數


另外,在模版字符串中,我們還可以透過 ${...} 這樣的方式,嵌入變量或任何的表達式:


let myName = "PJCHENder",
    numA = 4,
    numB = 7;

let content = `Hello, my name is ${myName}, my lucky number is ${2*(numA + numB)}`;

console.log(content);  // "Hello, my name is PJCHENder, my lucky number is 22"


我們可以看到透過 ${...},裡面我們不只可以放入變數,還可以放入表達式(例如${3+4})。

最後,如果在模版字符串中我們又會使用到反引號的話,這時候我們必須使用跳脫字元 \ 來處理,像是這樣:


var greeting = `\`Hello\` World!`;

console.log(greeting); // "`Hello` World!"


進階:標籤模版(tagged template)


接著我們可以來看一下模版字符串的進階用法,這樣的用法非常適合用在前端用來過濾使用者所輸入的訊息。讓我們先來看一下基本的用法。

標籤模版的使用就是直接在函式後面加上模版字符串,如果模版字符串中沒有使用 ${...} 代入任何的變數的話,那麼它其實就和一般將參數代入函式中差不多:


console.log `Hello EveryOne`;    //  Array  ["Hello EveryOne"]
console.log('Hello EveryOne');   //  String "Hello EveryOne"


但是如果在模版字符串中有 ${...} 的話,意義就相當不同:


let myName = "PJCHENder";
let myCountry = "Taiwan";

tag `<p> My name is ${myName} and my coutry is ${myCountry}</p>`;  //  使用標籤模版
tag(["<p> My name is ", " and my coutry is ", "</p>"], "PJCHENder", "Taiwan")  // 等同於上面這段


很大的差別在於若我們使用標籤模版(tagged template),也就是在函式後面直接代入模版字符串(`...`),它等於會先將模版字符串的內容切成多個參數,在放入函式中,切法就是把所有沒有被放在 ${...} 中的內容都組成一個陣列(就像上例中["<p> My name is ", " and my coutry is ", "</p>"]),當作這個函式的第一個參數;接著將 ${...}  裡面的內容,依次當做後面的第二個、第三個、...參數(看你使用了幾次${...} )。

因此,如果我們將 tag 這個 function 寫成這樣:

let myName = "PJCHENder";
let myCountry = "Taiwan";

tag `<p> My name is ${myName} and my coutry is ${myCountry}</p>`;
// tag(["<p> My name is ", " and my coutry is ", "</p>"], "PJCHENder", "Taiwan")  // 等同於上面這一段

function tag(template){
  console.log(template);
  console.log(arguments);
}


這個 function 做的事情很簡單,就是把 template 呼叫出來,還有把 arguments 呼叫出來(arguments 是 JS 中內建的關鍵字,若不清楚可參考:[筆記] 談談JavaScript中函式的參數(parameter),arguments和展開運算子(spread))。輸出的結果如下,其中上面是 template,下面是 arguments:


在下圖中下面的部分,我們可以看到它實際代入了三個參數,第一個參數是一個陣列,裡面包含所有除了${...} 內的內容,後面的兩個參數,則分別放入 ${...} 的內容作為參數。

結合其餘運算子(rest operator)做使用:因為 ${...}  的數量可能是不固定的,因此我們可以搭配其餘運算子(...),將這些參數組成陣列來使用(若不清楚其餘運算子,可參考:[筆記] JavaScript ES6 中的展開運算子(spread operator)和其餘運算子(rest operator)),像是這樣:


let myName = "PJCHENder";
let myCountry = "Taiwan";

tag `<p> My name is ${myName} and my coutry is ${myCountry}</p>`;
// tag(["<p> My name is ", " and my coutry is ", "</p>"], "PJCHENder", "Taiwan")  // 等同於上面這一段

function tag(template, ...values){
  console.log(template);  //  ["<p> My name is ", " and my coutry is ", "</p>"]
  console.log(values);  //  ["PJCHENder", "Taiwan"]
}


等於把不在${...}內的,和在${...},拆成兩個陣列來做使用。

這樣的標籤模版有一個好處,就是我們可以把使用者輸入的內容${...},和我們網頁原本的內容分開做處理,以達到過濾 HTML 字符串,防止用戶輸入惡意內容的效果:

截圖自:阮一峰-ECMAScript 6 入門

2017年1月24日

[筆記] JavaScript ES6 中的 for ... of(處理陣列的好幫手)


過去我們可以使用 for, while, do while, for...in 等內在的函式來處理資料,而在 ES6 中我們多了 for...of 這個簡易的用法來處理這些疊代型的資料(iterable objects),包含陣列、字串、map、set、等等...。

陣列中 for...of 的基本用法


for...of 的使用非常簡單,以陣列為例:


let arr = [10, 20, 30]

for(let value of arr){
  console.log(value);  // 10, 20, 30
}


只要用這樣的方式,就可以把陣列的值一個個取出,不用像過去寫一大串像是 for(let i = 0; i < arr.length; i++){...} 是不是方便許多呢

for...of 的其它用法


for...of 除了用在陣列之外,也可以用在其他的資料型態,像是字串、map、set 等等...,舉字串為例:


let string = "ES6";

for(let value of string){
  console.log(value);  // "E", "S", "6"
}


是不是非常方便呢?


2017年1月19日

[筆記] JavaScript ES6 中的展開運算子(spread operator)和其餘運算子(rest operator)


在 ES6 中,新增了一個 "..." 的關鍵字,這個關鍵字在不同時間點有不同的效果,有些時候它會被當作展開運算子(spread operator)使用,有些時候則是被當作其餘運算子(rest operator)使用。

其餘運算子(rest operator)


假設現在我想要寫一個函式,它可以把所有陣列的值相加後取平均,在過去如果我們要在函式中放入陣列的資料,一般我們會這樣寫:


let arr = [1,2,3,4,5];

let avg = function(arr){
  let sum = 0;
  for(let i = 0; i < arr.length; i++){
    sum += arr[i];
  }
  return sum / arr.length;
}

console.log(avg(arr));  //  3


但是在使用函式的時候,有些時候我們並不清楚我們會放入多少參數數目,假設我們把輸入的參數改成許多數值的話,最後會回傳 NaN 這樣的結果:


let avg = function(arr){
  let sum = 0;
  for(let i = 0; i < arr.length; i++){
    sum += arr[i];
  }
  return sum / arr.length;
}

console.log(avg(1,3,5,7,9));  // NaN


這時候就可以用到其餘運算字的概念(...),其餘運算字會幫助我們把輸入函式中的參數值變成陣列的形式,這時候我們就可以像這樣子撰寫程式:


let avg = function(...arr){
  console.log(arr)  // [1,3,5,7,9]
  let sum = 0;
  for(let i = 0; i < arr.length; i++){
    sum += arr[i];
  }
  return sum / arr.length;
}

console.log(avg(1,3,5,7,9));  // 5


我們可以看到在上面程式碼第二行的地方,從原本的 function(arr) 改成 function(...arr),透過在參數的前面加上 "..." 便可以把所輸入多個參數轉成陣列(我們可以從上面程式碼中把參數 arr 呼叫出來看)。

展開運算子(spread operator)


展開運算子和其餘運算子一樣都是 "..." ,但是在應用的效果上是完全相反的,其餘運算子是把許多的參數轉換成一個陣列,而展開運算子則是可以把陣列中的元素取出

假設我們想用 Math.max( ) 這個函式來找出最大值,但是我們輸入的參數卻是陣列:


let number = [1,2,3,4,5];

console.log(Math.max(number));  //  NaN


這時候因為帶入的參數是陣列的關係,所以我們會得到 NaN 的結果。但如果我們能夠適時的應用展開運算子,我們就可以把這個陣列展開成許多數值:


let number = [1,2,3,4,5];

console.log(Math.max(...number));  //  5

console.log(...number);  // 1,2,3,4,5


這時候我們在陣列的前面加上 "..." ,它就會把陣列從 [1,2,3,4,5] 轉換成 1,2,3,4,5,如此就可以正確的取得最大值。



2017年1月17日

[筆記] JavaScript ES6 中的物件的擴展(object literal extension)


在 ES6 中,對於物件的使用有了更彈性的應用,除了撰寫物件的方式變得更為精簡之外,更允許將屬性名稱指定為變數,以達到動態賦予屬性名稱的效果,讓我們來看看在 ES6 中物件的擴展吧。

物件的擴展(object literal extension)


在 ES6 中允許在物件中直接給變量,這時候物件的屬性名稱為變數的名稱,物件的屬性值為變數的值,讓我們來看一下這個例子:


let website = "pjchender";
let obj = {website};
console.log(obj);    //[Object]{website: "pjchender"}


這時候變數的名稱(website)會變成物件的屬性名稱,變數的值("pjchener")會變成物件的屬性值。實際上,let obj = {website} 也是一種縮寫,完整的寫法會是 let obj = {website:website} 它實際上對應的關係如下圖所示:


也因此,如果我們輸入如下的程式碼:


let website = "pjchender";
let obj = {abc:website};
console.log(obj); // [object]{abc: "pjchender}


建立出來的物件,他的屬性名稱就會是 "abc" 而不是 "website"。

我們可以用同樣的方式為多個物件的屬性賦值:


let website = "pjchender";
let country = "Taiwan";

let obj = {
  website,
  country
};
console.log(obj); // [object]{country: "Taiwan", website: "pjchender"}


在 object literal 中所賦予的值,會覆蓋掉在更上 let 的宣告:


let name = "PJCHEN";
let country = "Taiwan";

let obj_es6 = {
  name: "Aaron",
  country,
}

console.log(obj_es6); // [Object]{name: "Aaron", country: "Taiwan"}


從這裡我們可以看到最後 name 這個屬性值會是 Aaron 而不是 PJCHEN。

物件中的方法也可以簡寫


除了物件中的屬性可以簡寫外,物件中的方法也可以透過這樣的方式簡寫(第8-10行),例如:


let name = "PJCHEN";
let country = "Taiwan";

let obj = {
  name,
  country,
  location(){
    console.log(this.name + ' lives in ' + this.country);
  }
}

obj.location();  // PJCHEN lives in Taiwan


這樣方法的簡寫,原本的寫法如下:

let obj = {
  name: name,
  country: country,
  location: function(){
    console.log(this.name + ' lives in ' + this.country);
  }
}


另外,還有一點需要注意的是,這樣簡寫的方法,預設的屬性名稱會是字串的型態,也因此原本的簡寫 location( ){...} 等同於 'location'( ){...}

在 ES6 中允許將表達式作為屬性名稱,以達到動態賦值的效果


在 ES6 中,允許將表達式作為屬性的名稱,只需要使用 [ ] 就可以了,方法如下:


let obj_es = {
  ["web"+"site"]: "pjchender"
}
console.log(obj_es); // [Object]{website: "pjchender"}


透過這樣的方式,我們更可以去動態賦予屬性名稱:



let websiteName = "pjchender";
let a = 2;
let b = 3;

let obj_es = {
  [websiteName]: "welcome",
  [a+b]: "sumNumber"
}
console.log(obj_es);  // [Object]{5: "sumNumber", pjchender: "welcome"}



透過 [ ] 的方式,我們的屬性名稱就可以放入變數,以達到動態給予屬性名稱的效果。

2017年1月16日

[筆記] JavaScript ES6 中的物件解構賦值(object destructuring)


在上一篇文章中我們說明了 ES6 當中如何使用陣列解構賦值([筆記] ES6 中的陣列解構賦值(array destructuring)),這篇我們一樣著重在解構賦值的部分,進一步說明如何以物件解構賦值(object destructuring)。

物件解構賦值的基本使用


物件解構賦值的基本使用方法如下:


let obj = {
  website: "pjchender",
  country: "Taiwan"
}

let {website, country} = obj;
console.log(website);  // pjchender
console.log(country);  // Taiwan


在這當中有一個很重要的點,陣列的解構賦值強調的順序,而物件的解構賦值強調的則是屬性名稱,屬性名稱必須相互對應才能夠取得到值。

物件解構賦值的寫法看似簡單,但實際上,在上面這段程式碼中 let {website, country} = obj ,實際上完整的寫法應該是像這樣子(也就是說上面那段程式碼是簡寫):


let {website:website, country:country} = obj;


它會根據前面的屬性名稱來對應要給的值,但值其實是給冒號(:)後面的變數,用圖來看像是這樣子:


所以真正被建立和賦值的 let{ } 當中,冒號(:)後的變數。

我們可以透過另一個例子更容易了解這個概念:


let obj = {
  website: "pjchender",
  country: "Taiwan"
}

let {website:wb, country:ct} = obj;

console.log(website, country);   //  Error:website in not defined
console.log(wb, ct)  // "pjchender", "Taiwan"



我們把冒號後的內容改寫成 wb 和 ct,接著我們會發現,當我們呼叫原本的變數 website 和 country 時,會回報 error;但是當我們呼叫 wb 和 ct 這兩個變數時,則能夠正確回傳變數值,也就是說,在物件解構賦值中,冒號前是用來對應物件的屬性名稱,冒號後才是真正建立的變數名稱和被賦值的對象

相對的,當冒號前的屬性名稱對應不到物件中的屬性名稱時,則會出現 undefined:


// website 和 wb 並沒有相對應
let{website} = {wb: "pjchender"};
console.log(website);    // undefined


物件解構賦值是以屬性名稱做對應


和陣列解構賦值不同的是,在陣列解構賦值中,我們可以接受下面這樣的寫法:


let [a, , c] = [1, 2, 3];
console.log(a, c); // 1, 3


但是在物件解構賦值中,我們不能像上面這樣寫:


let{a, ,c} = {a:1, b:2, c:3};
console.log(a,c); // Error


之所以會回報錯誤,主要的原因是在陣列的解構賦值強調的是順序,而物件的解構賦值強調的則是屬性名稱,屬性名稱必須相互對應才能夠取得到值

物件解構賦值同樣能給予預設值


和陣列的解構賦值一樣,物件的解構賦值也能賦予預設值,方法如下:

此範例擷取自阮一峰-ECMAScript 6 入門

物件解構賦值的用途


物件解構賦值的用途相當多(可參考阮一峰-ECMAScript 6 入門),其中在提取 JSON 數據時相當方便:


let data_JSON = {
  id: 74,
  website: "pjchender",
  country: "Taiwan",
  detail:{
    add: "Tainan",
    phone: "0933333333"
  }
}

let {id, website, country, detail} = data_JSON;
console.log(id, website, country, detail);


如此就能夠快速提領出 JSON 物件的屬性名和屬性值。

2017年1月15日

[筆記] JavaScript ES6 中的陣列解構賦值(array destructuring)

在 ES6 中過去的陣列和物件可以透過解構(destructuring)的方式來賦值。這篇文章中,我們會先說明如何透過陣列的方式來賦值。

陣列解構賦值的方法(array destructuring)

過去陣列內的元素在賦值的時候,只能透過直接給值的方式,像是下面這樣:
let numbers = [1, 2, 3];
let a = numbers[0];
let b = numbers[1];
let c = numbers[2];
console.log(a,b,c);   // 1, 2, 3

一般用法

然而在 ES6 中可以直接透過解構的方式賦值,像是下面這樣子:
let [a, b, c] = [1, 2, 3];
console.log(a, b, c); // 1, 2, 3
如此變數 a = 1, b = 2, c = 3,這是最基本的陣列解構賦值方式。

當輸入的變數多於所給的值

當變數的數量多於賦予的值時,多出來的那個變數會被賦予 undefined 的值(d = undefined):
// 當變數多於所給的值
let [a, b, c, d] = [1, 2, 3];
console.log(a, b, c, d); // 1, 2, 3, undefined

當輸入的變數少於所給的值

當輸入的變數少於所給的值的時候,只有被指定到的變數會有值,少掉的變數可以直接空過去,這時候 g = 1, i = 3:
// 當變數少於給的值
let [g, , i] = [1,2,3];
console.log(g, i);  // 1, 3

在陣列解構中賦予預設值

我們也可以在陣列解構中賦予預設值,像是下面這樣:
let [a, b, c = 4, d = 'Hello'] = [1, 2, 3];
console.log(a, b, c, d); // 1, 2, 3, "Hello"
如此就可以將變數 c 賦予 4,d 賦予 Hello 的預設值,輸出的結果 c 因為後面有給值,所以依然是 3 ,而 d 在後面沒有給值,就直接帶入了預設值,得到 “Hello"。

防雷須知

如果你使用的是 standardJS 當作你的 code style,那麼你應該很習慣在結尾不加分號,但是在使用陣列的結構賦值時,這麼做 有可能會噴錯誤。
例如這樣:
let x = 0
let y = 0
console.log(x, y)
[x, y] = [1, 2]
console.log(x, y)
這時候會得到 "TypeError: Cannot set property '0' of undefined 的錯誤。首先簡單說明一下之所以可以不用在結尾加分號是因為在 多數情況下,
在語句或一段代碼敘述後,加了Enter鍵(\n)後,JS剖析器會在執行期間自動幫你插入分號 @ eddyChang
上面提到是多數情況,但是在 某些情況下 JS 引擎是不會幫你加上分號的,其中像是這裡的開頭以 [ 開頭的語句。因此在這裡,請記得在 [ 的前面加上分號,寫起來會像是這樣 ;[x, y] = [1, 2]
更多關於分號的使用請參考 JavaScript裡的語句用分號結尾是個選項嗎 @ eddyChang

資料來源

[筆記] JavaScript ES6 中的函數預設值(default value)


在過去如過要在函式中建立預設值常常要利用到 JS 中強制轉換型別(coercion)的這種特性,但在 ES6 中則可以用相當簡易的方式就可以設定函式的預設值(default value),寫起來更簡潔方便,讓我們來看一下可以怎麼樣使用。

函式預設值的使用


函式預設值的使用非常簡單,只需要在函式中給予參數的地方用等號賦值就可以了,方式如下:

function add(x = 3, y = 5){
  console.log(x+y);
}

add();  // 8

也就是說,只要在 add 這個函式的參數位置寫 add(x =3, y=5) 就可以直接帶入預設值,因此雖然我在執行 add( ) 的時候沒有帶入任何的參數值,但它不會報錯,而是回傳 8 。

使用預設值的基本觀念


丟入的參數值會由前往後代入

假設我只給予後面的 y 預設值,如下:

function add(x, y = 5){
  console.log(x+y);
}

add(2);   // 7
add(2,8); // 10


這時候我可以只輸入一個參數是沒有問題的,這個值會先被丟到 x 這個參數。然而,如果我只給予前面的 x 預設值,如下:

function add(x = 3, y){
  console.log(x+y);
}

add(2);   // NaN
add(2,8); // 10


這時候當我輸入 add(2) 來執行這個函式時,因為 2 會先被帶入 x ,所以 x = 2;而 y 沒有被賦值,因此 y = undefined,這也使得最後回傳的結果會是 NaN。


2017年1月14日

[筆記] JavaScript ES6 中的箭頭函數(arrow function)及對 this 的影響


在這篇文章中要來說明一下在 ES6 中相當常見的箭頭函數(arrow function),讓我們來看一下可以怎麼樣使用。

什麼是箭頭函數(arrow function)


首先,我們來看一下過去我們撰寫函數的方法:



在 ES6 中,我們可以把它改成箭頭函數的寫法,它會變成下面這樣:


沒有參數的時候要記得加上空括號

要特別留意的地方是,在箭頭函數中如果沒有帶入參數時,一樣要加上空括號。



如果只是要回傳某個值,可以省略 return

如果我們的函式本身只是要回傳某個值的話,可以把 return 這個字省略掉:


箭頭函數帶入參數值


兩個以上的參數,需要使用括號

當我們的函式擁有兩個以上的參數時,我們一樣要使用括號來帶入參數,寫法像是下面這樣子:


當函數只有一個參數時,不需要使用括號

從上面的例子我們可以知道,當函數沒有參數或有兩個以上的參數時,我們都要加上括號( ),但是當函數只有一個參數時,可以省略括號不寫,因此,當我們的函數只有一個參數時,我們的函數長得像這樣:



箭頭函數當中的 this 是定義時的對象,而不是使用時的對象


在使用箭頭函數時,有一點要注意的是,在箭頭函數中,this 指稱的對象在所定義時就固定了,而不會隨著使用時的脈絡而改變。

讓我們來看一下這個例子:

在這個範例中,不論我們使用的是原本 function 的寫法或 ES6中的箭頭函式,都會回傳得到最外層的 window 物件(如果不清楚 this 的話可以先參考 [筆記] 談談JavaScript中的"this"和它的bug),這樣看起來似乎兩者沒有太大的差別。



然而,換個例子的情況就不一樣,讓我們來看看下面兩個不同的例子:

例子一(參考自 阮一峰 - ECMAScript 6 入門

我們分別用原本的寫法和箭頭函示的寫法建立了兩個 function:

// 原本的 function
let fn = function(){
  console.log(this.constructor.name);  // Object(data)
  setTimeout(function(){
    console.log(this.constructor.name) // Window
  },100);
}

// 箭頭函式 Arrow function
let fn_arr = function(){
  console.log(this.constructor.name);  // Object(data)
  setTimeout(() => {
    console.log(this.constructor.name) // Object(data)
  },100);
}

let id = 21;
let data = {
  id: 21
}

fn.call(data);     
fn_arr.call(data); 


setTimeout:裡面都分別帶入 setTimeout 的函式,我們知道 setTimeout 執行的時間會在整個 JS execution context 都被執行完後才會執行(如果對這概念不清楚的話可參考:[筆記] 談談JavaScript中的asynchronous和event queue),因此函式建立的時間和實際執行的時間是不同的,因此這也創造了兩個不同時間點的 this 所指稱的對象。

call:另一個要了解的是 call 的用法,在 call 當中,我們會帶入後面所指定的物件當作所指稱的 this 對象(對這個概念不清楚的話可參考:[筆記] 了解function borrowing和function currying ─ bind(), call(), apply() 的應用)。

this.constructor.name:這樣子的寫法只是避免在回傳出物件的時候把整個物件內容給傳出來,而是指示傳出該物件的名稱(參考自:stackoverflow)。

綜合上述,我們可以知道,因為有用 call(data) 的緣故,因此不論是使用傳統函式寫法(fn)或箭頭函式(fn_arr)時,在一開始函式裡面的 this 都指稱的是 data 這個物件。

然而不同的地方會在執行 setTimeout 中的函式,在使用傳統函式的寫法時,使用 fn.call(data) 時,因為它執行的時間點是在整個 JS execution context 執行完才執行,而當時的環境會變成是 global environment,因此使用傳統函式時,這個 this 指稱的對象會轉變成 window object 。但是,如果是使用新的箭頭函式(arrow function),這個 this 所指稱的對象則不會改變,依然是 data 這個 object

範例:https://jsbin.com/wodegu/edit?js,console

例子二(參考自 Accelerated ES6 JavaScript Training

第二個例子是使用 addEventListener 來達到示範,首先我們在 HTML 中建立一個 button element,然後利用 JS 來抓取這個 button,接著 JS 部分則如下所示:

var button = document.querySelector('button');
var fn_arr = () => {
  // 建立 function 時 this 指 Window
  console.log(this.constructor.name)  // 執行function時 this 指 Window
};
var fn = function(){
  // 建立 function 時 this 指 Window
  console.log(this.constructor.name)  // 執行function時 this 指 HTMLButtonElement

button.addEventListener('click', fn_arr);


和例子一中的 setTimeout 類似,我們使用的 addEventListener ,也會在整個 execution context 執行結束後,在網頁觸發事件時才執行。

因此不論在傳統的函式寫法(fn)或箭頭函式(fn_arr)的寫法,一開始建立 function 的時候 this 所指稱的都是  window 這個物件,然而,如果是使用傳統的寫法,在觸發這個事件時所指稱的對象會從原本的 window 變成 HTMLButtonElement;若使用的是箭頭函式,則會固定所指稱的對象,因此 this 依然指稱的是 window 這個物件。

範例:https://jsbin.com/kotetu/edit?js,output