一文帶你掌握JS高階編程技巧!猛!

編者薦語: 本文旨在幫助你們掌握JavaScript中的幾大重要高階編程技巧,如:高階函數高級單例模式惰性函數思想柯理化函數思想compose扁平化函數思想javascript

高階編程

這一篇,咱們主要來說解下,在JavaScript中,高階編程思想都有哪些,它們在項目中又有哪些實際性的用途呢?前端

單例設計模式

用單獨的實例來管理當前事物的相關特徵,泛指屬性方法,相似於實現分組的特色,把一個實例的全部特徵描述綁定在一個分組裏。vue

來看一下簡單的單例設計模式:java

let module1 = (function () {
  function tools() {}
  function share() {}

  return {
    name: 'house',
    tools
  };
})();
module1.tools(); 
複製代碼

這裏咱們能夠把module1模塊定義的方法暴露出來,給外部模塊用。react

還有一種基於閉包實現的單例模式稱爲:高級單例設計模式,在vue/react出來以前,是團隊協做最經常使用的模塊化思想,經常使用來以此模塊劃分編程

閉包形式的單例模式以下:設計模式

let A = (function () {
  function fn() {
    B.getXxx();
  }
  function query() {}

  return {
    query
  }
})();

let B = (function () {
  function fn() {}
  function getXxx() {}
  
  A.query();

  return {
    getXxx
  }
})(); 
複製代碼

在上面這個例子中,咱們能夠分別定義A、B兩個模塊,先在本身模塊內聲明函數,再暴露出去,給對方的模塊使用,這即是最先的模塊化思想,也是高級單例設計模式數組

惰性函數

咱們先來看這樣一個例子,封裝一個獲取元素樣式的函數。瀏覽器

獲取樣式,咱們一般想到window.getComputedStyleelement.currentStyle這兩個API,但他們的執行也是有條件的。markdown

好比:前者只有Google瀏覽器兼容,後者IE瀏覽器兼容。

function getCss(element, attr) {
  if ('getComputedStyle' in window) {
    return window.getComputedStyle(element)[attr];
  }
  return element.currentStyle[attr];
} 

getCss(document.body, 'margin');
getCss(document.body, 'padding');
getCss(document.body, 'width'); 
複製代碼

從這個例子中,咱們能夠看出來,若是須要3次獲取元素的樣式,明顯每一次進入函數都須要判斷該方法兼容與否,這就形成了沒必要要的浪費。最好的解決方案——惰性函數思想。

通俗的來講,惰性函數初始化第一次渲染的時候執行,若是第二遍執行仍是同樣的效果,咱們須要用閉包的思想解決這一問題。

function getCss(element, attr) {
  if ('getComputedStyle' in window) {
    getCss = function (element, attr) {
      return window.getComputedStyle(element)[attr];
    };
  } else {
    getCss = function (element, attr) {
      return element.currentStyle[attr];
    };
  }
  // 爲了第一次也能拿到值
  return getCss(element, attr);
}
getCss(document.body, 'margin');
getCss(document.body, 'padding');
getCss(document.body, 'width'); 
複製代碼

在這個函數中,當咱們第一次執行getCss函數的時候,就已經能夠肯定getComputedStyle兼容與否了,因此在第二次就不必再判斷了,根據第一次判斷完返回的結果,直接肯定第二次,第三次的函數執行到底用哪一個API,這樣以來每次函數執行,都將少了一層判斷,必定程度上提升了js的運行速度。

那麼咱們來看,這個惰性體如今哪裏呢?

過程:其實,是第一次getCss函數執行完建立了全局做用域下的私有執行上下文,而在其裏面從新生成了getCss函數,將其引用地址從新賦值給全局函數getCss,致使全局下的getCss不能被釋放,造成了一個閉包。在之後每次執行時,都執行裏面的小函數getCss

柯理化函數

柯理化函數含義是給函數分步傳遞參數,每次傳遞部分參數,並返回一個更具體的函數接收剩下的參數,這中間可嵌套多層這樣的接收部分參數的函數,直至返回最後結果。

最基本的柯里化拆分

// 原函數
function add(a, b, c) {
  return a + b + c;
}

// 柯里化函數
function addCurrying(a) {
  return function (b) {
    return function (c) {
      return a + b + c;
    }
  }
}

// 調用原函數
add(1, 2, 3); // 6

// 調用柯里化函數
addCurrying(1)(2)(3) // 6
複製代碼

被柯里化的函數 addCurrying 每次的返回值都爲一個函數,並使用下一個參數做爲形參,直到三個參數都被傳入後,返回的最後一個函數內部執行求和操做,實際上是充分的利用了閉包的特性來實現的。

封裝柯理化通用式

上面的柯里化函數沒涉及到高階函數,也不具有通用性,沒法轉換形參個數任意或未知的函數

咱們來封裝一個通用的柯里化轉換函數,能夠將任意函數轉換成柯里化。

// add的參數不固定,看有幾個數字累計相加
function add (a,b,c,d) {
  return a+b+c+d
}

function currying (fn, ...args) {
  // fn.length 回調函數的參數的總和
  // args.length currying函數 後面的參數總和 
  // 如:add (a,b,c,d) currying(add,1,2,3,4)
  if (fn.length === args.length) {  
    return fn(...args)
  } else {
    // 繼續分步傳遞參數 newArgs 新一次傳遞的參數
    return function anonymous(...newArgs) {
      // 將先傳遞的參數和後傳遞的參數 結合在一塊兒
      let allArgs = [...args, ...newArgs]
      return currying(fn, ...allArgs)
    }
  }
}

let fn1 = currying(add, 1, 2) // 3
let fn2 = fn1(3)  // 6
let fn3 = fn2(4)  // 10
複製代碼

柯理化函數思想

利用閉包保存機制,把一些信息預先存儲下來(預處理的思想)

咱們用一道求和的題來描述:

function currying(...outerArgs) {
  return function anonymous (...innerArgs) {
    let args = outerArgs.concat(innerArgs)
    return args.reduce((previousValue, currentValue) => previousValue + currentValue)
  }
}

let fn1 = currying(1, 2)
let fn2 = fn1(3)
let fn3 = fn2(4)
複製代碼

補充 什麼是預處理的思想:

在第一次fn1函數執行的時候,會建立一個函數執行上下文,將 1, 2 存儲在該上下文中的局部變量裏,在之後執行 fn2 fn3的時候,須要先取得 fn1 的返回結果,使用存儲的 1, 2 參數,這種機制便造成了閉包

不理解reduce的小夥伴看過來:

狀況1:reduce沒有第二個參數時

let Array = [10, 20, 30, 40, 50];
Array.reduce(previousValue, currentValue, currentIndex, arr){
  return previousValue + currentValue;
}
複製代碼
  • 第一次觸發函數時,previousValue爲第一項(10),currentValue爲第二項(20)
  • 第二次觸發函數時,previousValue爲回調函數的返回值(30),currentValue爲第三項(30)
  • 第X次觸發函數時,previousValue爲回調函數的返回值,currentValue爲第X+1
  • currentIndex:對應的就是currentValue的索引值
  • arr:是個常量 數組自己

狀況2:reduce有第二個參數時

let Array = [10, 20, 30, 40, 50];
Array.reduce((previousValue, currentValue, currentIndex, arr){
  return previousValue + currentValue;
}, 0)
複製代碼
  • 第一次觸發函數時,previousValue爲第二個參數(0),currentValue爲第一項(10)
  • 第二次觸發函數時,previousValue爲回調函數的返回值(10),currentValue爲第二項(20)
  • 第X次觸發函數時,previousValue爲回調函數的返回值,currentValue爲第X
  • currentIndex:對應的就是currentValue的索引值
  • arr:是個常量 數組自己

compose函數

compose組合函數:把多層函數嵌套調用扁平化

compose函數常基於reduce 柯理化函數思想解決 函數調用扁平化的問題:

在項目中,咱們常常遇到這樣一個問題:

const fn1 = x => x + 10;
const fn2 = x => x - 10;
const fn3 = x => x * 10;
const fn4 = x => x / 10;

console.log(fn3(fn1(fn2(fn1(4)))))
複製代碼

上面這個例子中,明顯看出層級嵌套較深,這時候就須要調用函數扁平化的思想

函數調用扁平化: 若是是多層級嵌套調用的函數,把一個函數調用完,看成另外一個函數的實參傳遞到下一個函數中

解決方案:從左到右依次執行

/** * @param funcs 存儲按照順序執行的函數(數組) => [fn1, fn3, fn2, fn4] * @param args 存儲第一個函數執行須要傳遞的實參信息(數組) => [5] */
function compose(...funcs) {
  return function anonymous(...args) {
    if(funcs.length === 0) return args;
    if(funcs.length === 1) return funcs[0](...args);
    // funcs 裏有 多個函數時
    return funcs.reduce((n, func) => {
      // 第一次執行:
      // n:第一個函數執行的實參 func:第一個函數
      // 第二次執行:
      // n的值:上一次func執行後的返回值,做爲實參傳遞給下一個函數執行 func:第二個函數
      return Array.isArray(n) ? func(...n) : func(n);
    }, args)
  }
}

let res = compose(fn1, fn3, fn2, fn4)(5)

// 執行過程:
console.log(compose()(5)); //=>5
console.log(compose(fn1)(5)); //=>5+10 = 15
console.log(compose(fn1, fn3)(5)); //=>fn1(5)=15 fn3(15)=150
console.log(compose(fn1, fn3, fn2)(5)); //=>fn1(5)=15 fn3(15)=150 fn2(150)=140
console.log(compose(fn1, fn3, fn2, fn4)(5)); //=>fn1(5)=15 fn3(15)=150 fn2(150)=140 fn4(140)=14
複製代碼

compose函數扁平化的執行過程:

  1. compose()(5),如compose()函數中無函數參數傳遞進來,則直接返回後面的參數 5

  2. compose(fn1, fn3, fn2, fn4)(5),先執行fn1(5),再將fn1(5)的結果傳遞到fn3中,如fn3(fn1(5)) => fn3(15),以此類推fn2(fn3(fn1(5))) => fn4(150),後來每一次函數調用的參數都是前一個函數返回的結果,天然咱們就想到了Array.prototype.reduce方法

  3. 設置reduce的第二個參數,是爲了統一n爲每一次函數的返回結果(第一次n爲5,func爲fn1),恰好又須要讓fn1(5)函數執行,可見第二個參數的必要性,是爲了統一每次的回調函數返回結果都是number

函數式編程與命令式編程

回調函數:把一個函數做爲值傳遞給另一個函數,在另一個函數中把這個函數執行(這是實現函數式編程重要的知識)

函數式編程注重結果,不在意過程,過程交給別人處理,體現函數封裝性思想(提倡)

命令式編程注重過程,須要本身去實現過程

函數式編程:把邏輯如何實現封裝成爲API方法,咱們之後只要調取API方法,便可獲取想要的結果便可

let arr = [10, 20, 30, 40, 50];

let res = arr.reduce((n, item) => {
  return n + item;
});
複製代碼

命令式編程:用代碼實現主要的邏輯,注重過程

let res = 0;
for (let i = 0; i < arr.length; i++) {
  res += arr[i];
}
複製代碼

看完三件事❤

若是你以爲這篇內容對你還蠻有幫助,我想邀請你幫我三個小忙:

  1. 點贊,轉發讓更多的人也能看到介紹內容(收藏不點贊,皆是耍流氓!!)
  2. 關注公衆號 「前端時光屋」,不按期分享原創知識。
  3. 同時能夠期待後續文章ing

也能夠來個人我的博客:

前端時光屋:www.javascriptlab.top/

相關文章
相關標籤/搜索