高階函式 - Higher Order Function

apolis發表於2019-06-24

一個函式 如果輸入引數包含函式 或 返回值包含函式,就稱為高階函式。

這篇文章介紹高階函式的一個子集:輸入 fn,輸出 fn'
fnfn'功能是否一致【即相同輸入是否始終對應相同輸出】,把這類高階函式的作用分為兩類:

  1. 包裝函式:功能一致
  2. 修改函式:功能不一致

包裝函式

從斐波那契數列開始。

const fib = n => (n <= 1 ? 1 : fib(n - 1) + fib(n - 2));

fib(42);

記錄執行時間

普通青年

const fib = n => (n <= 1 ? 1 : fib(n - 1) + fib(n - 2));

const start = new Date().getTime();
fib(42);
console.log(new Date().getTime() - start + "ms");

函式式青年

const timed = fn => (...args) => {
  const start = new Date().getTime();
  const result = fn(...args);
  console.log(new Date().getTime() - start + "ms");
  return result;
};

const fib = n => (n <= 1 ? 1 : fib(n - 1) + fib(n - 2));

timed(fib)(42);

效能優化 memorize

普通青年

const memory = {};
const fib = n => {
  if (n <= 1) return 1;
  else {
    if (memory[n]) return memory[n];
    else {
      memory[n] = fib(n - 1) + fib(n - 2);
      return memory[n];
    }
  }
};
const timed = fn => (...args) => {
  const start = new Date().getTime();
  const result = fn(...args);
  console.log(new Date().getTime() - start + "ms");
  return result;
};
timed(fib)(42);

函式式青年

const memorize = fn => {
  const memory = {};
  return arg => {
    if (memory[arg]) return memory[arg];
    else {
      memory[arg] = fn(arg);
      return memory[arg];
    }
  };
};
const fib = memorize(n => (n <= 1 ? 1 : fib(n - 1) + fib(n - 2)));
const timed = fn => (...args) => {
  const start = new Date().getTime();
  const result = fn(...args);
  console.log(new Date().getTime() - start + "ms");
  return result;
};
timed(fib)(42);

修改函式

once

場景:
傳送請求,如果後臺返回 session 超時,彈出重新登入提示框。
發出多個請求,後臺都返回 session 超時錯誤,只希望彈一個重新登入提示框。

const once = fn => {
  let executed = false;
  return (...args) => {
    if (!executed) {
      executed = true;
      fn(...args);
    }
  };
};
const showLogoutWin = once(function() {
  // ...
});

debounce

場景:
輸入框 change 事件觸發向後臺查詢
為消除不必要的查詢
使用者連續輸入時不觸發查詢,當 200ms 內沒有新的輸入時,才向後臺查詢

const debounce = (fn, ms = 200) => {
  let timeoutId;
  return (...args) => {
    // YOU MAY HAVE A TRY
  };
};

更多實際場景

validateRequired

根據 rule.required 判斷空值時是否報錯,這段邏輯出現在多個 validator 中。

const ipv4Validator = (rule, value, callback) => {
  if (value) {
    if (ipv4RegExp.test(value)) {
      callback();
    } else {
      callback("請輸入合法IP");
    }
  } else {
    if (rule.required) {
      callback("該域為必填項");
    } else {
      callback();
    }
  }
};
const validateRequired = (validator, msg = "該域為必填項") => (
  rule,
  value,
  callback
) => {
  if (value) {
    validator(rule, value, callback);
  } else {
    if (rule.required) {
      callback(msg);
    } else {
      callback();
    }
  }
};

const ipv4Validator = validateRequired((rule, value, callback) => {
  if (ipV4Regexp.test(value)) {
    callback();
  } else {
    callback("請輸入合法IP");
  }
});

tryUntilSucceeded

因為網路不穩定,請求可能出錯,出錯後重新請求,直到得到響應為止。

let res;
while (true) {
  try {
    res = await get(path);
    break;
  } catch (err) {
    console.log(err);
  }
}

每個請求都套一層while,寫起來太費事、太重複。

const tryUntilSucceeded = fn => async (...args) => {
  // YOU MAY HAVE A TRY
};

const enhancedGet = tryUntilSucceeded(get);
const enhancedPost = tryUntilSucceeded(post);

const resGet = await enhancedGet(path);
const resPost = await enhancedPost(path);

小結

恰當使用高階函式有以下好處:

  • 函式做的事情更單一
    像上面斐波那契數列的例子,fib 只關心數列的計算邏輯
    記錄時間、效能優化的事情交給 timedmemorized 處理
    這樣 fib 邏輯簡單,不容易出錯
  • 程式碼可複用,減少了程式碼重複
    像上面的timed, memorized......tryUntilSucceeded都可以提取到公共庫,供別的地方使用

相關文章