常用技巧與函數

這個部分, 統整了一些常用的 JavaScript 函式及語法

解構賦值

解構賦值的概念是, 如果被引用的值屬於物件或是陣列, 可以用同樣結構的宣告, 把值取出來:


const point = {
  x: 10,
  y: 20,
  z: 30
}

const { x, y, z } = point;
// x = 10 , y = 20 , z = 30

也可以使用別名的方式, 比方說

const point = {
  x: 10,
  y: 20,
  z: 30
}

const { x: a, y: b, z: c } = point;
// a = 10 , b = 20 , c = 30

// 稍微排序一下
// 對照 point 的宣告, a b c 剛好在 x y z 的值上
// 因此會宣告a, b, c 三個變數分別對應到x, y ,z
const { 
  x: a,
  y: b,
  z: c
} = point;

陣列也可以:

const arr = [1,2,3,4,5,6,7,8];
const [first] = arr;
// first = 1

// 可以解構更多的值:
const [a,b,c,d,e,f,g,h] = arr;
// a = 1, b = 2, c = 3, d = 4, e = 5, f = 6, g = 7, h = 8

引數也可以解構:

const point = {
  x: 10,
  y: 20,
  z: 30
}


function getPoint({x, y, z}) {
  console.log(x, y, z);
}

getPoint(point) // print 10 20 30
getPoint({x: 100, y: 200}) // print 100 200 undefined

即使物件多麼複雜, 只要有辦法宣告成同樣的形式, 都可以解構

const complexObj = {
  a: {
    b: {
      c: {
        d: 100
      }
    }
  }
};

const { a: { b: { c: { d }}}} = complexObj
console.log(d) // print 100

// alias
const complexObj = {
  a: {
    b: {
      c: {
        d: 400
      }
    }
  }
};

const { a: { b: { c: { d: myVar }}}} = complexObj
console.log(myVar) // print 400

參考 Destructuring assignment


展開運算子

可以使用 ... 運算子, 對物件或是迭代器進行展開:

陣列展開

let arr = [10, 20, 30, 40, 50];
let [first, ...data] = arr;
// first = 10
// data 則是把剩下的展開,  data = [20, 30, 40, 50]

let [first, ...data, last] = arr //Error, 展開運算子必須得要放在最後

物件展開

const point = {
  x: 10, y: 20, z: 30
}

const { x, ...other } = point
// x = 10
// other = { y: 20, z : 30 }

也可以在引數展開

function sum(a, b, c) {
  return a + b + c;
}

const arr = [1, 2, 3];
sum(...arr); //會展開成 sum(1, 2, 3)

反過來, 也可以在參數展開

function sum(...numbers) {
  let res = 0;
  for(let i = 0; i < numbers.length; ++i)
    res += numbers[i];
  return res;
}

sum(1,2,3,4,5,6); // return 21

參考 Spread syntax

進階用法

可以用來拷貝一層 array (請注意, 該方法不是深拷貝, 僅是拷貝一層陣列而已)

let arr = [1,2,3,4,5,6,7,8];
let arr2 = arr;
let arr3 = [...arr]; // 展開 arr

console.log(arr === arr2); // true
console.log(arr2 === arr3); // false

也可以用來合併物件 or 陣列:

let obj1 = {
  x: 10,
  y: 20
}

let obj2 = {
  a: true,
  b: false
}

let obj3 = { ...obj1, ...obj2 };
console.log(obj3) 
/* {
  x: 10,
  y: 20,
  a: true,
  b: false
} */

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

let arr3 = [...arr, ...arr2] // [1,2,3,2,3,4]
let arr4 = [...arr,...arr,...arr2,...arr]// [1,2,3,1,2,3,2,3,4,1,2,3]

/* 初始化物件的技巧 */

const defaultValue = {
  x: 100,
  y: 200,
  z: 300,
}

const obj = {
  ...defaultValue,
  z: 400
}
// obj = { x: 100, y: 200, z: 400 }

/* 要注意展開的順序 */
const obj = {
  z: 400,
  ...defaultValue,
}
// obj = { x: 100, y: 200, z: 300 }
// 因為 defaultValue 後展開, 所以把原本的值又蓋掉了

物件迭代器

Object.prototype 中, 提供了幾種實用的方法

const obj = {
  x: 10,
  y: 20,
  a: true,
  b: false
};

let objKeyIter = Object.keys(obj); //回傳迭代器
for(const key of objKeyIter) {
  console.log(key) // 依序印出 x, y, a, b
}

for(const value of Object.values(obj)) {
  console.log(value) // 依序印出 10, 20, true, false
}

for(const pair of Object.entries(obj)) {
  console.log(pair[0], pair[1]) 
  // 依序印出:
  // x 10
  // y 20
  // a true
  // b false
}

/* 當然, 上面的用法可以合併解構附值 */
for(const [key, value] of Object.entries(obj)) {
  console.log(key, value) // 結果同上
}

/* 前面有提到, 迭代器可以被展開 */
let keys = [...Object.keys(obj)]  // ['x', 'y', 'a', 'b']
let value = [...Object.values(obj)] // [10, 20, true, false]
let entries = [...Object.entries(obj)] 
/*
[
  ['x', 10]
  ['y', 20]
  ['a', true]
  ['b', false]
]
*/

/* 使用 fromEntries 還原物件 */
const o = Object.fromEntries(entries) // o = obj

此外, 諸如string 等也有實現迭代器, 因此


console.log(...'hello,world') //輸出 h e l l o , w o r l d 共11個字元


短路求值以及 ?? 運算子

在 JavaScript 中, boolean 判斷主要分為 falsytruthy

falsy 的意義是, 在判斷式中會被當成 false 的值, truthy 則反之

只需要記得 falsy 就好, falsy的值有:

  • false
  • 0
  • -0
  • 0n (bigint)
  • "", '', `` (空字串)
  • null
  • undefined
  • NaN
  • document.all

其餘的都是 Truthy 值

而看到的 ||&& 運算子, 其真正的定義如下:

let res = A || B //若 A 為false, 才執行 B 敘述

/* Example */
let res1 = 100 || 'string' || console.log(100) // 100 是 truthy, 後面不必判斷了, 短路求值
let res2 = 0 || 'string' || console.log(100) // 0 是 falsy, 'string' 是 truthy, 短路求值
let res3 = A() || B() || C() // 若 A() 回傳 truthy 值, 則B、C 不執行

/* 以下與 res3 等價 */
if(!A()) {
  if(!B()) {
    C()
  }
}
let res = A && B //若 A 為truthy, 才執行 B 敘述

/* Example */
let res1 = false && 0 && console.log(100) // false 是 falsy, 短路求值
let res2 = true && 0 && console.log(100) // true 是 truthy, 0 是 falsy, 短路求值
let res3 = true && 100 && console.log(100) // 前面兩個都是 truthy, 執行 console.log
let res4 = A() && B() && C();

/* 以下與 res4 等價 */
if(A()) {
  if(B()) {
    C();
  }
}

?? 空值合併運算符比較特殊, 他會排除nullish(null 或是 undefined)來判斷求值

let value1 = 0 || 100; // value = 100
let value2 = 0 ?? 100; // value = 0, 因為 0 不是 nullish
let value3 = null ?? 100; // value = 100
let value4 = undefined ?? 100; // value = 100
let value5 = '' || 'default string' // value = 'default string'
let value6 = '' ?? 'default string' // value = '', 因為 '' 不為 nullish

?. 可選鏈式運算子

假設有個函式:

async function findOrEmpty() {
  const res = await fetch(urlWithId) // 回傳的結果有可能為 null, 找不到 Id
  // 1. { id, userData: ... }
  // 2. 沒找到, null

  // userData 的格式可能為 { name: string, age: number } 或是忘記填為 null
  // 需要回傳 user 的名字
  return res.userData.name;
}

上面的函式有個重大錯誤, 那就是 res 有可能是 null, 那執行 res.userData 相當於存取 nulluserData 屬性

res.userData 也有可能是 null, 導致相當於存取 nullname 屬性

這會導致 Error 被擲出, 因此需要進行檢查, 比方說:

async function findOrEmpty() {
  const res = await fetch(urlWithId)
  let name = 'DEFAULT';
  if ( res ) {
    if ( res.userData ) {
      name = res.userData.name
    }
  }
  return name;
}

學會了短路求值, 可以使用:

async function findOrEmpty() {
  const res = await fetch(urlWithId)
  let name = 'DEFAULT';
  if( res && res.userData )
    name = res.userData.name;

  return name;
}

?. 運算子則允許存取一個可能為nullish的數值, 並當遇上nullish時, 回傳undefined 並且中止求值

async function findOrEmpty() {
  const res = await fetch(urlWithId)

  // 如果 res = null, 直接回傳 undefine
  // 並中止對 res.userData、res.userData.name 求值
  return res?.userData.name; 
}

已經知道遇上 nullish 會回傳 undefined, 上面使用預設值的寫法可以改成:

async function findOrEmpty() {
  const res = await fetch(urlWithId)
  return res?.userData.name ?? 'DEFAULT'; // 使用空值合併運算子, 給予初始值
}

參考 ?? operatorOptional chaining

陣列

在 JavaScript 中, 陣列的方法算是極度常用的, 這裡只說明幾個常用的 Function:

先假定原始資料:

const users = [
  {
    name: 'Peter',
    age: 20,
    salary: 3800
  },
  {
    name: 'Mary',
    age: 22,
    salary: 4000
  },
  {
    name: 'Jason',
    age: 30,
    salary: 8900
  },
  {
    name: 'Benson',
    age: 45,
    salary: 7000
  },
  {
    name: 'Neil',
    age: 35,
    salary: 5200
  },
]

陣列大部分的參數, 都是接受一個簽章為:

function callback(element, index, arr) {
  console.log(element, index, arr);
}

的方法, 三個參數分別是

  • 當前元素
  • 當前元素索引
  • 原始陣列

map() 轉換原始陣列變成另外一個陣列

let data = users.map( e => {
  return e.salary
});

console.log(data) // [3800,4000,8900,7000,5200]

Note! 有個很接近的方法叫做 forEach, 但是不建議使用, 他算是 for-loop 的callback版本

for(let i = 0; i users.length; ++i) {
  console.log(`index = ${i}, value = ${users[i]}`);
}

//等同
users.forEach((e,i) => {
  console.log(`index = ${i}, value = ${e}`);
})

let data = users.forEach((e,i) => {
  return e.salary
})

console.log(data) // undefined, forEach不回傳陣列

因為 forEach 可以被 map 取代, 且 forEach 不回傳陣列, 因此會有人用來修改原始陣列, 比方說

users.forEach((e,i, arr) => {
  arr[i] = e.salary;
})

這不是一個好做法, 因此不推薦使用 forEach

此外, 早期還未出現展開運算子之前, 可以用 map 複製陣列

// const data = [...arr]
const data = arr.map(e => e);

filter() 過濾原始陣列

let data= users.filter((e) => {
  return e.age > 30; // 必須回傳 boolean 值
})

console.log(data) /* data = [
  {
    name: 'Benson',
    age: 45,
    salary: 7000
  },
  {
    name: 'Neil',
    age: 35,
    salary: 5200
  }
] */

every() 以及 some() 判斷所有陣列

const flag1 = users.every(e => e.salary > 5000) // false, 因為有人薪水不足 5000
const flag2 = users.some(e => e.salary > 5000) // false, 因為有人超過 5000

every 的概念相當於C++的 all_of(全部符合), some 則是 any_of(任一符合)

sort 進行排序

users.sort((a,b) => a.age - b.age) // 根據 age 排序
users.sort((a,b) => a.name.localeCompare(b.name)) // 根據 name 排序
// string 要使用 string.localeCompare 進行比較

slice 進行分割

// users.slice(startIndex, count)
users.slice(0, 2) // 從index 0 開始, 取出兩個元素

find 傳入條件式, 尋找元素

// 因為 find 不一定會找到, 建議使用 ?. 來存取物件
const name = users.find(e => e.age === 22)?.name 

在實例上, 會建議通過一系列的轉換, 取出所需要的資料, 比方說:

const users = [
  {
    name: 'Peter',
    age: 20,
    salary: 3800
  },
  {
    name: 'Mary',
    age: 22,
    salary: 4000
  },
  {
    name: 'Jason',
    age: 30,
    salary: 8900
  },
  {
    name: 'Benson',
    age: 45,
    salary: 7000
  },
  {
    name: 'Neil',
    age: 35,
    salary: 5200
  },
  {
    name: 'Howard',
    age: 44,
    salary: 6666
  },
  {
    name: 'Alex',
    age: 35,
    salary: 7777
  },
]

const salarySigma = users
  .filter(e => e.salary > 5000)
  .map( e => e.salary )
  .reduce((sum, currentValue) => sum + currentValue, 0);

console.log(salarySigma) // 35543
const salaryAvg = salarySigma / users.filter(e => e.salary > 5000).length // 7108.6

通過一系列的轉換函式, 取出所需要的資料

絕大部分的 Web 應用, 都是處理資料的轉換, 因此字串、陣列相關的方法, 幾乎是必備的知識

反過來說, 雖然也有情況要進行CPU的密集計算, 但是該情況還是相對比較少的

前面提到的用法, 幾乎涵蓋大部分陣列的使用情境, 但也建議稍微看一下 String.prototypeRegExp.prototype 的資料

其中 RegExp 是 Regular Expression 正規表示法的意思, 允許使用模式來匹配字串, 舉例來說:

// 假定輸入的資料是日期格式, 且有可能是 YYYY-MM-DD 或是 YYYY/MM/DD

const date1 = "2001-01-05";
const date2 = "1998/01/02";
const rule = /(?<year>[0-9]{0,4})[\-\/](?<month>[0-9]{0,2})[\-\/](?<date>[0-9]{0,2})/;

RegExp

這只是一個很小的範例, 還有很多種用法, 比如說:驗證電子信箱、網址格式、特定的文字輸入...等, 且上方的RegExp 是有小瑕疵的(你可以嘗試找出來!)

總之, 字串與陣列的處理, 涵蓋大部分的操作情境, 且額外要注意的事情是:一些 ArrayLike 物件, 若有實現迭代器, 也是可以輕鬆轉換成陣列的

n2a

querySelectorAll 回傳的結果是 NodeList, 他並沒有陣列的成員或是屬性, 也無法通過 Array.isArray 的測試;

但是經過迭代器展開, 或是早期的轉換方式 Array.prototype.slice.call 後, 就成為了陣列型別

這些"像是"陣列樣子的物件, 被稱作 ArrayLike, 除了陣列本身是ArrayLike(因為陣列算是一種特殊的物件, JavaScript沒有真正意義的原生陣列), 還有String也是很經典的例子

JSON的序列化/反序列化

/* 反序列化 */
const strLikeJson = '{ "a":100, "b": 200, "c" :        3000 }';
const object = JSON.parse(strLikeJson);
/** 
 * object { 
 *  a: 100, 
 *  b: 200, 
 *  c: 30000 
 * }
 */

/* 序列化 */
const str = JSON.stringify({ a: 10, b: 20, c: 30 });
// string '{"a":10,"b":20,"c":30}'

JSON.parse 用來把 JSON 字串轉換為 JS物件, 且一定要是 JSON字串, 這個說法有一點模糊, 看下面的例子

const str1 = "{ a: 10, b: 20 }";
const str2 = "{ a: 10, b: 20, }";
const str3 = "{ 'a': 10, 'b': 20 }";
const str4 = "{ \"a\": 10, \"b\": 20 }";

在上述的例子中, 只有 str4 是合法的 JSON字串

JSON.parse 可以接受第二個參數 reviver function, 在 parse 解析完字串後, 可以進行一些修改:

const jsonStr = '{ "a":100, "b": 200, "c": 300 }';
let a = JSON.parse(jsonStr, (key, value) => {
  console.log(key, value);
  if(typeof value === "number")
    return value + 50;
  return value; // return origin value
});
// a = { a: 150, b: 250, c: 350 };

這允許在解析的同時進行一些修改或檢驗, 但是要注意, 最後一個解析會收到 key 為空字串結果, 雖然沒有特別註記, 但筆者猜測應該是告知開發者, Parse 結束的情況

如果 reviver 中沒有回傳值為 undefined, 則會刪除該屬性:

const jsonStr = '{ "a":100, "b": 200, "c": 300 }';
let a = JSON.parse(jsonStr, (key, value) => {
  switch( key ) {
    case "a":
    case "b":
      break;
    default:
      return value;
  }
});
// a = { c: 300 };

如果當次解析的物件是巢狀的, 則由深到淺解析

JSON.parse('{"a":{"b":{"c":{"d":true}}}}', (k,v) => { console.log(k); return v});
// output: d c b a

JSON.stringify 則是會把物件轉換成對應的 JSON字串, 優先調用 toJSON 方法, 然後把 可列舉可序列化 的屬性進行轉換

  • 可列舉:PropertyDescriptor 中 enumerable 為 true 的屬性
  • 可序列化:例如 function, 就不可進行轉換, 通常僅轉換:string, boolean, number;如果屬性是陣列或物件, 則深入檢查

JSON.stringify 可以額外接受兩個參數, 第二個參數是 replacer, 可以是function 或是 array:

const foo = {
  foundation: "Mozilla",
  model: "box",
  week: 45,
  transport: "car",
  month: 7
};

const jsonStr = JSON.stringify(foo, (key, value) => {
  /* replacer 為 Function 時, 如果 return 的值是 undefined, 則忽略該值 */
  if (typeof value === "string") {
    return undefined;
  }
  return value;
});

// jsonStr = '{"week":45,"month":7}'

/* replacer 為 array 時, 僅保留對應的key */
const jsonStr2 = JSON.stringify(foo, ['model', 'month', '??']);
// jsonStr = '{"model":"box","month":7}'

第三個參數是 space 只是控制縮排而已, 例如在輸出文件時, 可以使用 JSON.stringify(obj, null, 4) 保留 4個space 的縮排

匿名函數

當調用 Lib 的 function, 且內部調用極深時, 可以再某個地方使用 function(){} 而不是 () => {} 這裡使用 setTimeout() 來說明:


setTimeout(function myFn(){
  throw new Error();
}, 0)

/* Error stack:
Uncaught Error
    at myFn (<anonymous>:2:9)
*/

setTimeout(() => {
  throw new Error();
}, 0)

/* Error stack:
Uncaught Error
    at (<anonymous>:2:9)
*/

若自己開發Library, 或是使用了較深的function invoke, 可以加上 function name(){} 來進行除錯, 比方說:


lib(function myFn(){
  /* do something */
})


/* Error stack:
Uncaught Error
    at (<anonymous>:2:9)
    at (<anonymous>:2:9)
    at myFn (<anonymous>:2:9) <---- 也許某個部分會原本的函式名稱
    at (<anonymous>:2:9)
    at (<anonymous>:2:9)
    at (<anonymous>:2:9)
    at (<anonymous>:2:9)
    at (<anonymous>:2:9)
*/

// 反之, 若使用 arrow function:
lib(() => {
  /* do something */
})

/* Error stack:
Uncaught Error
    at (<anonymous>:2:9)
    at (<anonymous>:2:9)
    at (<anonymous>:2:9)
    at (<anonymous>:2:9)
    at (<anonymous>:2:9)
    at (<anonymous>:2:9)
    at (<anonymous>:2:9)
    at (<anonymous>:2:9)
會完全遺失錯誤堆疊的資訊
*/