了解 Functional Programming 的通用函式,能讓我們寫出更簡潔的程式碼,也能幫助我們學習 RxJS。

讀者可能會很好奇,我們的主題是 RxJS 為什麼要特別講 Functional Programming 的通用函式呢? 實際上,RxJS 核心的 Observable 操作觀念跟 FP 的陣列操作是極為相近的,只學會以下幾個基本的方法跟觀念後,會讓我們之後上手 Observable 簡單很多!

今天的程式碼比較多,大家可以直接看影片

ForEach

forEach 是 JavaScript 在 ES5 後,原生就有支援的方法。
原本我們可能要透過 for loop 取出陣列中的每一個元素

  1. var arr = ['Jerry', 'Anna'];
  2. for(var i = 0; i < arr.length; i++) {
  3. console.log(arr[i]);
  4. }

現在可以直接透過陣列的 forEach 取出每一個元素。

  1. var arr = ['Jerry', 'Anna'];
  2. arr.forEach(item => console.log(item));

forEach 是 FP 操作陣列的基本方法,我們可以用這個方法來實作下面三個我們今天要講的重點分別為 map, filter, concatAll。

Map

試著把 newCourseList 每個元素的 { id, title } 塞到新的陣列 idAndTitlePairs

  1. var newCourseList = [
  2. {
  3. "id": 511021,
  4. "title": "React for Beginners",
  5. "coverPng": "https://res.cloudinary.com/dohtkyi84/image/upload/v1481226146/react-cover.png",
  6. "rating": 5
  7. },
  8. {
  9. "id": 511022,
  10. "title": "Vue2 for Beginners",
  11. "coverPng": "https://res.cloudinary.com/dohtkyi84/image/upload/v1481226146/react-cover.png",
  12. "rating": 5
  13. },
  14. {
  15. "id": 511023,
  16. "title": "Angular2 for Beginners",
  17. "coverPng": "https://res.cloudinary.com/dohtkyi84/image/upload/v1481226146/react-cover.png",
  18. "rating": 5
  19. },
  20. {
  21. "id": 511024,
  22. "title": "Webpack for Beginners",
  23. "coverPng": "https://res.cloudinary.com/dohtkyi84/image/upload/v1481226146/react-cover.png",
  24. "rating": 4
  25. }
  26. ], idAndTitle = [];
  27. newCourseList.forEach((course) => {
  28. idAndTitle.push({ id: course.id, title: course.title });
  29. });

雖然我們成功的把 newCourseList 轉成 idAndTitlePairs,但這樣的寫法還是顯得有點太複雜了,我們可以用更抽象化的方式來完成。
上面我們練習到 newCourseList 轉換成一個新的陣列 idAndTitlePairs,這個轉換的過程其實就是兩件事

  • 遍歷 newCourseList 所有的元素
  • 把每個元素的預期值給到新的陣列

把這個過程抽象化成一個方法 map,以下是簡化的基本思路:

  1. 我們會讓每個 陣列 都有一個 map 方法
  2. 這個方法會讓使用者自訂傳入一個 callback function
  3. 這個 callback function 會回傳使用者預期的元素

    雖然 ES5 之後原生的 JavaScript 陣列有 map 方法了,但希望讀者自我實做一次,能幫助理解。

  1. // 我們希望每一個陣列都有 map 這個方法,所以我們在 Array.prototype 擴充 map function
  2. Array.prototype.map = function(callback) {
  3. var result = []; // map 最後一定會返回一個新陣列,所以我們先宣告一個新陣列
  4. this.forEach(function(element, index) {
  5. // this 就是呼叫 map 的陣列
  6. result.push(callback(element, index));
  7. // 執行使用者定義的 callback, callback 會回傳使用者預期的元素,所以我們把它 push 進新陣列
  8. })
  9. return result;
  10. }

這裡用到了 JavaScript 的 prototype chain 以及 this 等觀念,可以看此影片了解!

到這裡我們就實作完成 map 的方法了,讓我們來試試這個方法吧!

  1. var idAndTitle = newCourseList
  2. .map((course) => {
  3. return { id: course.id, title: course.title };
  4. });

可以看到我們的程式碼更加的簡潔!

Filter

如果我們希望過濾一個陣列,留下陣列中我們想要的元素,並產生一個新的陣列,要怎麼做呢? 先讓我們用 forEach 完成!
讓我們過濾出 rating 值是 5 的元素

  1. var ratingIsFive = [];
  2. newCourseList.forEach((course) => {
  3. if(course.rating === 5) {
  4. ratingIsFive.push(course);
  5. }
  6. });

同樣的我們試著來簡化這個過程,首先在這個轉換的過程中,我們做了兩件事:

  1. 遍歷 newCourseList 中的所有元素
  2. 判斷元素是否符合條件,符合則加到新的陣列中
  1. Array.prototype.filter = function(callback) {
  2. var result = [];
  3. this.forEach((item, index) => {
  4. if(callback(item, index))
  5. result.push(item);
  6. });
  7. return result;
  8. }

試試這個方法

  1. var ratingIsFive = newCourseList
  2. .filter((course) => course.rating === 5);

會發現我們的程式碼又變簡單了,接著我們試著把 filter, map 串起來。
如果我想要取出所有 rating 是 5 的所有 course title

  1. var ratingIsFive = newCourseList
  2. .filter((course) => course.rating === 5)
  3. .map(course => course.title);

ConcatAll

有時候我們會遇到組出一個二維陣列,但我們希望陣列是一維的,問題如下:
假如我們要取出 courseLists 中所有 rating 為 5 的課程,這時可能就會用到兩個 forEach

  1. var user = {
  2. id: 888,
  3. name: 'JerryHong',
  4. courseLists: [{
  5. "name": "My Courses",
  6. "courses": [{
  7. "id": 511019,
  8. "title": "React for Beginners",
  9. "coverPng": "https://res.cloudinary.com/dohtkyi84/image/upload/v1481226146/react-cover.png",
  10. "tags": [{ id: 1, name: "JavaScript" }],
  11. "rating": 5
  12. }, {
  13. "id": 511020,
  14. "title": "Front-End automat workflow",
  15. "coverPng": "https://res.cloudinary.com/dohtkyi84/image/upload/v1481226146/react-cover.png",
  16. "tags": [{ "id": 2, "name": "gulp" }, { "id": 3, "name": "webpack" }],
  17. "rating": 4
  18. }]
  19. }, {
  20. "name": "New Release",
  21. "courses": [{
  22. "id": 511022,
  23. "title": "Vue2 for Beginners",
  24. "coverPng": "https://res.cloudinary.com/dohtkyi84/image/upload/v1481226146/react-cover.png",
  25. "tags": [{ id: 1, name: "JavaScript" }],
  26. "rating": 5
  27. }, {
  28. "id": 511023,
  29. "title": "Angular2 for Beginners",
  30. "coverPng": "https://res.cloudinary.com/dohtkyi84/image/upload/v1481226146/react-cover.png",
  31. "tags": [{ id: 1, name: "JavaScript" }],
  32. "rating": 4
  33. }]
  34. }]
  35. };
  36. var allCourseIds = [];
  37. user.courseLists.forEach(list => {
  38. list.courses
  39. .filter(item => item.rating === 5)
  40. .forEach(item => {
  41. allCourseIds.push(item)
  42. })
  43. })

可以看到上面的程式碼,我們用了較為低階的操作來解決這個問題,我們剛剛已經試著用抽象化的方式實作了 map 跟 filter,那我們同樣也能夠定義一個方法用來 攤平二維陣列。
讓我們來加入一個 concatAll 方法來簡化這段程式碼吧! concatAll 要做的事情很簡單,就是把一個二維陣列轉成一維。

  1. Array.prototype.concatAll = function() {
  2. var result = [];
  3. // 用 apply 完成
  4. this.forEach((array) => {
  5. result.push.apply(result, array);
  6. });
  7. // 用兩個 forEach 完成
  8. // this.forEach((array) => {
  9. // array.forEach(item => {
  10. // result.push(item)
  11. // })
  12. // });
  13. // 用 ES6 spread 完成
  14. // this.forEach((array) => {
  15. // result.push(...array);
  16. // })
  17. return result;
  18. };

同樣的我們用前面定要好的 courseLists 來試試 concatAll 吧!

  1. var allCourseIds = user.courseLists.map(list => {
  2. return list.courses.filter(course => course.rating === 5)
  3. }).concatAll()

這邊出一個比較難的題目,大家可以想想看要怎麼解

  1. var courseLists = [{
  2. "name": "My Courses",
  3. "courses": [{
  4. "id": 511019,
  5. "title": "React for Beginners",
  6. "covers": [{
  7. width: 150,
  8. height: 200,
  9. url: "http://placeimg.com/150/200/tech"
  10. }, {
  11. width: 200,
  12. height: 200,
  13. url: "http://placeimg.com/200/200/tech"
  14. }, {
  15. width: 300,
  16. height: 200,
  17. url: "http://placeimg.com/300/200/tech"
  18. }],
  19. "tags": [{
  20. id: 1,
  21. name: "JavaScript"
  22. }],
  23. "rating": 5
  24. }, {
  25. "id": 511020,
  26. "title": "Front-End automat workflow",
  27. "covers": [{
  28. width: 150,
  29. height: 200,
  30. url: "http://placeimg.com/150/200/arch"
  31. }, {
  32. width: 200,
  33. height: 200,
  34. url: "http://placeimg.com/200/200/arch"
  35. }, {
  36. width: 300,
  37. height: 200,
  38. url: "http://placeimg.com/300/200/arch"
  39. }],
  40. "tags": [{
  41. "id": 2,
  42. "name": "gulp"
  43. }, {
  44. "id": 3,
  45. "name": "webpack"
  46. }],
  47. "rating": 5
  48. }]
  49. }, {
  50. "name": "New Release",
  51. "courses": [{
  52. "id": 511022,
  53. "title": "Vue2 for Beginners",
  54. "covers": [{
  55. width: 150,
  56. height: 200,
  57. url: "http://placeimg.com/150/200/nature"
  58. }, {
  59. width: 200,
  60. height: 200,
  61. url: "http://placeimg.com/200/200/nature"
  62. }, {
  63. width: 300,
  64. height: 200,
  65. url: "http://placeimg.com/300/200/nature"
  66. }],
  67. "tags": [{
  68. id: 1,
  69. name: "JavaScript"
  70. }],
  71. "rating": 5
  72. }, {
  73. "id": 511023,
  74. "title": "Angular2 for Beginners",
  75. "covers": [{
  76. width: 150,
  77. height: 200,
  78. url: "http://placeimg.com/150/200/people"
  79. }, {
  80. width: 200,
  81. height: 200,
  82. url: "http://placeimg.com/200/200/people"
  83. }, {
  84. width: 300,
  85. height: 200,
  86. url: "http://placeimg.com/300/200/people"
  87. }],
  88. "tags": [{
  89. id: 1,
  90. name: "JavaScript"
  91. }],
  92. "rating": 5
  93. }]
  94. }];
  95. /*
  96. var result = courseList
  97. 不得直接使用索引 covers[0],請用 concatAll, map, filter, forEach 完成
  98. result 結果為 [
  99. {
  100. id: 511019,
  101. title: "React for Beginners",
  102. cover: "http://placeimg.com/150/200/tech"
  103. }, {
  104. id: 511020,
  105. title: "Front-End automat workflow",
  106. cover: "http://placeimg.com/150/200/arch"
  107. }, {
  108. id: 511022,
  109. title: "Vue2 for Beginners",
  110. cover: "http://placeimg.com/150/200/nature"
  111. }, {
  112. id: 511023,
  113. title: "Angular2 for Beginners",
  114. cover: "http://placeimg.com/150/200/people"
  115. },
  116. ]
  117. */

這題有點難,大家可以想想看,我把答案寫在這裡了!
如果大家還想做更多的練習可以到這個連結:http://reactivex.io/learnrx/

這個連結是 Jafar 大神為他的 RxJS workshop 所做的練習網站!