柯里化(Currying) 學習

首先柯里化是什麼,柯里化簡單來說就是將有複數arg的函數變成一次只接收單一個arg的函數。

我自己看了許多的別人寫的說明,最簡單易懂的說明就是下面的這個用法

var add = function(x) {
  return function(y) {
    return x+ y;
  }
}add(1)(2)// 3

但及時看了這種簡單的說明,實際使用場景是怎樣我也搞不太清楚,結果今天看到一個短視頻介紹柯里化,講法非常簡單,也比較好理解,於是我想要順便把自己的理解記錄下來。視頻位置

先來跟視頻走一遍

  1. 參數的重複使用
function url(protocol,hostname,pathname){
  return `${protocol}${hostname}${pathname}`
}

const myURL = url('https://','www.google.com','/page1')
console.log(nyURL)
// result:"https://www.google.com/page1"

通常我們最常遇到需要使用重複性參數的就是XMLHttpRequest,因為我們可能包含很多重複性的參數在裡面,例如action是使用post還是get,或是前面的網址都是一樣的,這種時候我們就可以使用柯里化,來達到優化代碼的功效

const url = function(protocol){
  return function(hostname,pathname){
    return `${protocol}${hostname}${pathname}`;
  }
}

const myURL = url('https://')
console.log(myURL('www.google.com','/page1'))
console.log(myURL('www.google.com','/page2'))
console.log(myURL('www.google.com','/page3'))

結果如下:

藉由柯里化我們能夠將共通的部分,重複的部分,整合。

2.提前返回

假設我們要考慮到瀏覽器兼容性問題,需要判斷是哪個瀏覽器,來去調用能夠使用的函數的話,也能利用柯里化的提前返回,來達到優化的效果

這樣做我們就能夠自定義函數,並套上我們想要的邏輯在外層。

這邊使用立即函式,讓瀏覽器在初始化頁面的時候能夠先執行到
使用立即函式還有另一個優點是,不論我們之後調用幾次這個function,因為我們已經在頁面初始化的時候先把能夠使用的事件監聽套到我們宣告的變數上面,所以不會在之後每次調用時再做一次if判斷。

//提前判斷
/**
 * addEventListener
 * @param element - 要對哪個元素做事件監聽
 * @param type - 元素要添加什麼事件
 * @param listener - 要執行的回調函數
 * @param useCapture - 是事件冒泡還是事件補捉
 * attachEvent
 * @param element - 要對哪個元素做事件監聽
 * @param type - 元素要添加什麼事件
 * @param handler - 要執行的回調函數
 * 
 */

const whichEvent = ( function() {
    if(window.addEventListener){
      return function (element,type,listener,useCapture) {
        element.addEventListener(type,function(e){
            //規避this指向問題,使用call來指向元素,同時把event對象帶入到參數裡
          listener.call(element,e);
        },useCapture)
      }  
    }else if(window.attachEvent){
        return function (element,type,handler) {
          element.attachEvent('on' + type,function(e){
            handler.call(element,e);
          })
        }  
      }
  })();

3. 延時執行

延時執行大概就是柯里化最明顯的表現之一,也是面試題的其中之一
add(1)(2)(3) = 6;
add(1,2,3)(4) = 10;
add(1)(2)(3)(4)(5) = 15;

這樣便能看出這個函數要有幾個特點
1.一次能夠帶入的參數不設限制
2.可以被不限次數的呼叫
3.可以得到一個累加的結果

如果有看到這裡的各位建議各位可以先自己試看看再去看答案
比較能夠了解自己的想法是不是對的

我原本不知道怎麼取不定量的參數,看了視頻才知道可以利用

function add(){
   let args = arguments
}

來取得所有的參數
取得參數後我們要把參數放到一個陣列裡面

let args = Array.prototype.slice.call(arguments);

利用這個原生方法可以取得一個參數所組成的陣列
不然原本參數的型別是[object Arguments],算是物件型別需要做轉換才能變成陣列
再來就是最重要的,回傳一個函示並能夠重複執行
我原本的想法是

return function(){
  return add()
}

但這樣會報錯
看了視頻才知道如果先宣告一個變量把function塞進去執行結束後再回傳變量就能得到想要的結果了
利用不斷返回自己達到能夠無限次被調用
之後就是用reduce方法把陣列累加,就能得到我們想要的值

let global = 0
function add() {
  let args = Array.prototype.slice.call(arguments);
  let inner = function () {
    args.push(...arguments);
    console.log(args)
    global = args.reduce(function(num,arg){
      return num+=arg
    },0)
    return inner;
  }
  return inner;
}

console.log(add(1)(2)(3))
console.log(global)

我是這樣寫的但實際上跟視頻講的不一樣,因為我有在全域宣告一個變數

但其實還有另外更好的辦法

實際上當javascript console.log(function(){})它是印出function.toString()

也就是說我們看到的是一個字串

function add() {
  let args = Array.prototype.slice.call(arguments);
  let inner = function () {
    args.push(...arguments);
//     args.reduce(function(num,arg){
//       return num+=arg
//     },0)
    return inner;
  }
  
  inner.toString = function(){
    return 'test'
  }
  
  return inner;
}

console.log(add(1)(2)(3))
// test

改良後

function add() {
  let args = Array.prototype.slice.call(arguments);
  let inner = function () {
    args.push(...arguments);
    return inner;
  }
  
  inner.toString = function(){
    
    let sum = args.reduce(function(num,arg){
      return num+=arg
    });
    //這裡跟視頻走會報錯,因為應該是要回傳Stirng型別才對
    return sum.toString()
  };
  
  return inner;
}

let result = add(1)(2)(3)
console.log(result) // 6

但其實這樣會多因出一個 f ,如果用typeof去查看的話它的型別其實還是function

4.用於有callback的函數

視頻裡的代碼是這樣的

const nameList1 = [
  { mid: "哈傻K", profession: "中單" },
  { mid: "沙皇", profession: "中單" },
  { mid: "卡牌", profession: "中單" },
  { mid: "發條", profession: "中單" },
];

const nameList2 = [
  { adc: "輪子媽", profession: "ADC" },
  { adc: "VN", profession: "ADC" },
  { adc: "老鼠", profession: "ADC" },
];

const currying = name => element => element[name];
const name_mid = currying('mid')
const name_adc = currying('adc')

console.log(nameList1.map(name_mid))
console.log(nameList2.map(name_adc))

執行結果

這時候我就傻了,我搞不懂這怎麼可以這樣用
於是我把它拆開來看

currying方法不用箭頭

function currying(name) {
    return function (element) {
      return element[name];
    };
}

或者是直接使用map

let test = nameList1.map((el) => {
   return el["mid"];
});
console.log(test);

把name 套進來

let test2 = nameList1.map(function (element) {
   return element[name];
});
console.log("test2", test2);

這樣就能夠理解了,實際上currying只是應該要寫在map 裡面的function 再套一層在外面,讓它能夠接收參數的設定

const nameList1 = [
  { mid: "哈傻K", profession: "中單" },
  { mid: "沙皇", profession: "中單" },
  { mid: "卡牌", profession: "中單" },
  { mid: "發條", profession: "中單" },
];

const nameList2 = [
  { adc: "輪子媽", profession: "ADC" },
  { adc: "VN", profession: "ADC" },
  { adc: "老鼠", profession: "ADC" },
];

//   const curring = name => element => element[name];

function curring(name) {
  return function (element) {
    return element[name];
  };
}
name_mid = curring("mid");
name_adc = curring("adc");

let test = nameList1.map((el) => {
  return el["mid"];
});
console.log(test);
//如果我想要回傳element[name]我就必需在哪裡先把[name]給傳進去
// 但因為map本身就是一個callback function
// 所以如果我們要傳一個name進去,正常來說應該是要將name作為全域變量來傳入才能達到我要的效果
// 但如果透過curry化就能夠提前將name傳進去
// 問題是怎麼做到的
// 原理應該是map需要回傳一個function,所以我們就給他一個function,
//但在給他一個function 的時候 我們要先傳入name所以
// 雖然是return了一個function但我們也共通化了,所需要的name
let test2 = nameList1.map(function (element) {
  return element[name];
});
console.log("test2", test2);

由此可知只要是使用callback function 的方法都可以使用柯里化,如filter

總結柯里化的使用場景

  1. 參數重複使用
  2. 提前返回
  3. 延時執行
  4. 用於有callback的函數

結論

我目前的理解是,雖然簡單來說柯里化就是讓一次傳遞的參數能夠減少,達到分層的效果,但這也只是一個概念,我們也不需要一定要一次只能傳遞一個參數來限制自己,代碼是死的人是活的,只要能夠達到結果,都是好代碼,當然還要考慮效能跟可讀性。

所以我思考了一下自己會使用的場景大概就是參數的重複使用和用於有callback的函數,我會把想要重複性的參數套再外面

大概就是像這樣

import request from '@/utils/request'

export function myRequest(method){
	return function({url,data},errorKey){
		return request({
			url,
			method,
			data
		}).then(response => {
      //加上一些判斷是不是成功的response
			return Promise.resolve(response.data)
		})
	}
}

//使用currying
const customizeRequest = myRequest('get')

export function Login(query){
	return customizeRequest({
		url: 'https://1234567890.com,		
		data: query
	},'LoginError')
}
view raw

補充:實際上用柯里化並不會提升效能,相反的還會比一般直接使用一個function來的多花上一點。

以上是我的學習分享,謝謝大家!

charlie ku
charlie ku

程式撰寫是我的專業,思考與成長則是我永不停歇的追求。在這裡,我分享一些對生活和技術的思考,從技術探討到閱讀心得,這些都是我在探索自己和這個世界時所發現的靈感。希望我的分享能夠啟發你,也讓我們在彼此的交流中共同進步。

文章: 18

發佈留言

發佈留言必須填寫的電子郵件地址不會公開。 必填欄位標示為 *