JavaScript - 讓我容易搞混的 ()

這次 code review 時,花了一些時間釐清觀念,討論「()」在陣列方法或測試方法語法裡面的箭頭函式的意思,reviewer 分享 () 有兩個意思:

// 1. 用來定義函式接收的參數
function hello(name){
	// do something
	console.log(`Hello, ${name}!`);
	return
}

// 2. 用來表示要執行 () 前的函式
hello();

// 函式表達式和箭頭函式
// 當作一般函式使用時,可以視為同義寫法
const hello = function (name){...};
const hello = (name) => {...};

// 3. 除了上述跟函式相關的用法,還可以用來改變運算優先順序,跟數學算式的用法類似
// 1.、2. 是函式語法的一部分,3. 是群組運算子
2 + 3 * 4; // 先乘除後加減
(2 + 3) * 4; // 括號算完才由左到右運算

目前練習過程遇到容易搞混的情境(容易弄錯括號要包的範圍)

// 陣列方法
arr.filter((item) => item.active);
arr.map((item) => item.name);
arr.reduce((sum, item) => sum + item.price, 0);

// 陣列方法的同語意寫法
arr.filter(function (item) { return item.active; });
arr.map(function (item) { return item.name; });
arr.reduce(function (sum, item) { return sum + item.price; }, 0);

// Vitest 測試語法
test('adds', () => {
  expect(add(1, 2)).toBe(3);
});

test.each([
  [1, 1, 2],
  [1, 2, 3],
  [2, 1, 3],
])('add(%i, %i) -> %i', (a, b, expected) => {
  expect(add(a, b)).toBe(expected);
});

// Vitest 測試語法的同語意寫法
test('adds', function () {
  expect(add(1, 2)).toBe(3);
});

test.each([
  [1, 1, 2],
  [1, 2, 3],
  [2, 1, 3],
])('add(%i, %i) -> %i', function (a, b, expected) {
  expect(add(a, b)).toBe(expected);
});

看到一長串 () ,可以練習的拆解思考方法

先用一般函式表達式理解箭頭函式

在「不使用 this / arguments、也不拿來 new」的情境下,可以把這兩種寫法視為同語意。目前練習還沒碰到。

// 這兩個在「當作一般函式使用」時,語意可以視為相同
const hello = function (name) {
  // ...
};

const hello = (name) => {
  // ...
};

  • ():定義函式接收的參數

  • => 或 function:定義函式的內容

// 箭頭函式
(item) => item.active;

// 從定義拆解箭頭函式
function (item) {
  return item.active;
}

// 回到陣列方法中拆解
arr.filter((item) => item.active);

arr.filter(function (item) {
  return item.active;
});

分層拆解陣列方法 () 思路:

  1. arr.filter(...):filter 是 HOF 高階函式,() 要放一個函式當作判斷條件

  2. (item) => item.active:定義一個匿名函式

    1. 參數:(item) 表示函式接收自訂名稱為 item 的參數,在這裡會是陣列中的每個元素

    2. 函式內容:item.active 表示函式處理運算參數的內容和回傳值

  3. 不同陣列方法的參數數量和回傳值內容不同

// Vitest 測試語法中的箭頭函式
// 單一測試
test('adds', () => {
  expect(add(1, 2)).toBe(3);
});

// 從定義拆解單一測試方法中的箭頭函式
test('adds', function () {
  expect(add(1, 2)).toBe(3);
});

// 參數化測試(巢狀陣列)
test.each([
  [1, 1, 2],
  [1, 2, 3],
  [2, 1, 3],
])('add(%i, %i) -> %i', (a, b, expected) => {
  expect(add(a, b)).toBe(expected);
});

// 結構是
const eachFn = test.each([
  [1, 1, 2],
  [1, 2, 3],
  [2, 1, 3],
]);

eachFn(
  'add(%i, %i) -> %i',
  (a, b, expected) => {
    expect(add(a, b)).toBe(expected);
  },
);

// 從定義拆解參數化測試方法中的箭頭函式
// (a, b, expected) => {...} 同語意寫法
function (a, b, expected) {
  expect(add(a, b)).toBe(expected);
}

分層拆解測試方法 () 思路:

單一測試
  1. test('測試描述句', ...) : test 是一個函式,第一個參數是描述測試內容的字串,第二個參數是測試內容的函式(在這裡是() => {...} )

  2. () => {...} : 定義一個「不接收任何參數」的函式

    1. 參數:() 表示沒有參數

    2. 函式內容:{expect(add(1, 2)).toBe(3);} 表示測試步驟

  3. expect():接收實際算出來的值,所以常常會在裡面直接呼叫被測試函式 add(1, 2) 拿回傳值

  4. toBe() :斷言,接收預期的測試函式輸出結果

參數化測試

1.test.each([...])('測試描述句', 這個函式) : 整體語意是「把這個匿名函式交給 Vitest,請 Vitest 依照前面那個資料表,重複呼叫它很多次」。 1. 相當於 eachFn('測試描述句', 這個函式) 這個對照單一測試函式的語法 2. test.each([...]) : 把測試資料表(巢狀陣列)給 Vitest,回傳一個函式,內層陣列每一組代表一組測資 3. (a, b, expected) => {...} : 被 Vitest 多次呼叫的匿名函式

從語意定義判斷 () 在做什麼

大部分搞混「括號要包到哪裡」的問題,其實是在搞清楚:「這裡是在『定義函式』,還是在『呼叫函式』,還是在『包一坨運算式』」