Javascript難點知識運用---遞歸,閉包,柯里化等

前言

ps: 2018/05/13 經指正以後發現惰性加載函數細節有問題,已改正
在這裏也補充一下,這些都是根據本身理解寫的例子,不必定說的都對,有些只能查看不能運行的要謹慎,由於我可能只是將方法思路寫出來,沒有實際跑過的.html

面向對象編程 && 面向過程編程

面向對象編程(Object Oriented Programming,OOP)

是一種以事物爲中心的編程思想,把構成問題事務分解成各個對象,創建對象的目的不是爲了完成一個步驟,而是爲了描敘某個事物在整個解決問題的步驟中的行爲,三大特色缺一不可。前端

特色 做用
封裝 將其說明(用戶可見的外部接口)與實現(用戶不可見的內部實現)顯式地分開,其內部實現按其具體定義的做用域提供保護
繼承 子類自動共享父類數據結構和方法的機制
多態 相同的操做或函數、過程可做用於多種類型的對象上並得到不一樣的結果。不一樣的對象,收到同一消息能夠產生不一樣的結果

面向過程編程(Procedure Oriented Programming, POP)

是一種以過程爲中心的編程思想,分析出解決問題所須要的步驟,而後用函數把這些步驟一步一步實現,使用的時候一個一個依次調用就能夠了。程序員

百度百科有個很形象的比喻,編程

例如五子棋:
面向過程的設計思路步驟:
一、開始遊戲,
二、黑子先走,
三、繪製畫面,
四、判斷輸贏,
五、輪到白子,
六、繪製畫面,
七、判斷輸贏,
八、返回步驟2,
九、輸出最後結果

面向對象的設計思路步驟:整個五子棋能夠分爲
一、黑白雙方,這兩方的行爲是如出一轍的,
二、棋盤系統,負責繪製畫面,
三、規則系統,負責斷定諸如犯規、輸贏等。
第一類對象(玩家對象)負責接受用戶輸入,並告知第二類對象(棋盤對象)棋子佈局的變化,棋盤對象接收到了棋子的變化就要負責在屏幕上面顯示出這種變化,同時利用第三類對象(規則系統)來對棋局進行斷定。 segmentfault

面向對象是以功能來劃分問題,而不是步驟。一樣是繪製棋局,這樣的行爲在面向過程的設計中分散在了多個步驟中,極可能出現不一樣的繪製版本,由於一般設計人員會考慮到實際狀況進行各類各樣的簡化。而面向對象的設計中,繪圖只可能在棋盤對象中出現,從而保證了繪圖的統一。數組

(更多內容請自行查閱,本節到此爲止了.)瀏覽器

基本類型和引用類型

以前已經寫過這個文章,就不復述了
詳情能夠參考我以前寫的文章關於Javascript基本類型和引用類型小知識數據結構

執行環境(execution context)及做用域(scope)

來自Javascript高級程序設計3:閉包

解析

執行環境定義了變量或函數有權訪問的其餘數據,決定了它們各自的行爲。每一個執行環境都有一個與之關聯的 變量對象(variable object),環境中定義的全部變量和函數都保存在這個對象中。雖然咱們編寫的代碼沒法訪問這個對象,但解析器在處理數據時會在後臺使用它。

全局執行環境是最外圍的一個執行環境。根據 ECMAScript 實現所在的宿主環境不一樣,表示執行環境的對象也不同。在 Web 瀏覽器中,全局執行環境被認爲是 window 對象,所以全部全局變量和函數都是做爲 window (全局)對象的屬性和方法建立的。某個執行環境中的全部代碼執行完畢後,該環境被銷燬,保存在其中的全部變量和函數定義也隨之銷燬(全局執行環境直到應用程序退出——例如關閉網頁或瀏覽器——時纔會被銷燬)。app

每一個函數都有本身的執行環境。當執行流進入一個函數時,函數的環境就會被推入一個環境棧中。而在函數執行以後,棧將其環境彈出,把控制權返回給以前的執行環境。 ECMAScript 程序中的執行流正是由這個方便的機制控制着。

當代碼在一個環境中執行時,會建立變量對象的一個做用域鏈(scope chain)。做用域鏈的用途,是保證對執行環境有權訪問的全部變量和函數的有序訪問。做用域鏈的前端,始終都是當前執行的代碼所在環境的變量對象。若是這個環境是函數,則將其活動對象(activation object)做爲變量對象。活動對象在最開始時只包含一個變量,即 arguments 對象(這個對象在全局環境中是不存在的)。做用域鏈中的下一個變量對象來自包含(外部)環境,而再下一個變量對象則來自下一個包含環境。這樣,一直延續到全局執行環境;全局執行環境的變量對象始終都是做用域鏈中的最後一個對象。

標識符解析是沿着做用域鏈一級一級地搜索標識符的過程。搜索過程始終從做用域鏈的前端開始,而後逐級地向後回溯,直至找到標識符爲止(若是找不到標識符,一般會致使錯誤發生).

延長做用域鏈

  • try-catch 語句的 catch 塊
  • with 語句(不推薦)

這兩個語句都會在做用域鏈的前端添加一個變量對象。
對 with 語句來講,會將指定的對象添加到做用域鏈中。
對 catch 語句來講,會建立一個新的變量對象,其中包含的是被拋出的錯誤對象的聲明。

沒有塊級做用域

function test() {
  if (true) {
    var num = 0;
  }
  //打印if語句內聲明變量
  console.log(num);//0
  for (var i = 0; i < 4; i++) { }
  //打印for語句內聲明變量
  console.log(i);//4
}
test()//0 4

在其餘類 C 的語言中,由花括號封閉的代碼塊都有本身的做用域,在if ,for語句執行完畢後被銷燬,但在 JavaScript 中, if ,for語句中的變量聲明會將變量添加到當前的執行環境中。若是向模擬塊狀做用域的話能夠利用閉包等方法,下文會提到.
(更多內容請自行查閱,本節到此爲止了.)

垃圾收集

JavaScript 具備自動垃圾收集機制,也就是說,執行環境會負責管理代碼執行過程當中使用的內存。這種垃圾收集機制的原理其實很簡單:找出那些再也不繼續使用的變量,而後釋放其佔用的內存。爲此,垃圾收集器會按照固定的時間間隔(或代碼執行中預約的收集時間),週期性地執行這一操做。

下面咱們來分析一下函數中局部變量的正常生命週期。
局部變量只在函數執行的過程當中存在。而在這個過程當中,會爲局部變量在棧(或堆)內存上分配相應的空間,以便存儲它們的值。而後在函數中使用這些變量,直至函數執行結束。此時,局部變量就沒有存在的必要了,所以能夠釋放它們的內存以供未來使用。在這種狀況下,很容易判斷變量是否還有存在的必要;
但並不是全部狀況下都這麼容易就能得出結論。垃圾收集器必須跟蹤哪一個變量有用哪一個變量沒用,對於再也不有用的變量打上標記,以備未來收回其佔用的內存。用於標識無用變量的策略可能會因實現而異,但具體到瀏覽器中的實現,則一般有兩個策略。

  • 標記清除
    當變量進入環境(例如,在函數中聲明一個變量)時,就將這個變量標記爲「進入環境」。從邏輯上講,永遠不能釋放進入環境的變量所佔用的內存,由於只要執行流進入相應的環境,就可能會用到它們。而當變量離開環境時,則將其標記爲「離開環境」。
    可使用任何方式來標記變量。好比,能夠經過翻轉某個特殊的位來記錄一個變量什麼時候進入環境,或者使用一個「進入環境的」變量列表及一個「離開環境的」變量列表來跟蹤哪一個變量發生了變化。說到底,如何標記變量其實並不重要,關鍵在於採起什麼策略。
    垃圾收集器在運行的時候會給存儲在內存中的全部變量都加上標記(固然,可使用任何標記方式)。而後,它會去掉環境中的變量以及被環境中的變量引用的變量的標記。而在此以後再被加上標記的變量將被視爲準備刪除的變量,緣由是環境中的變量已經沒法訪問到這些變量了。最後,垃圾收集器完成內存清除工做,銷燬那些帶標記的值並回收它們所佔用的內存空間。
  • 引用計數
    引用計數的含義是跟蹤記錄每一個值被引用的次數。當聲明瞭一個變量並將一個引用類型值賦給該變量時,則這個值的引用次數就是 1。若是同一個值又被賦給另外一個變量,則該值的引用次數加 1。相反,若是包含對這個值引用的變量又取得了另一個值,則這個值的引用次數減 1。當這個值的引用次數變成 0 時,則說明沒有辦法再訪問這個值了,於是就能夠將其佔用的內存空間回收回來。這樣,當垃圾收集器下次再運行時,它就會釋放那些引用次數爲零的值所佔用的內存。
    問題一, 循環引用會一直沒法回收;
    問題二, 低版本IE有一部分對象並非原生 JavaScript 對象;

(更多內容請自行查閱,本節到此爲止了.)

JavaScript原型對象與原型鏈

以前已經寫過這個文章,就不復述了,特地修改了以前的排版補充之類
詳情能夠參考我以前寫的文章關於Javascript中的new運算符,構造函數與原型鏈一些理解

遞歸

一種會在函數內部重複調用自身的寫法.

function factorial(num) {
  if (num <= 1) {
    return 1;
  } else {
    return num * factorial(num - 1);
  }
}
console.log(factorial(5));//120

一開始的常規寫法,可是有個問題是內部調用自身是使用函數名字,若是在將factorial賦值到一個變量以後,儘管仍是調用原factorial函數,但不是指望的調用函數自身的寫法了.

function factorial(num) {
  if (num <= 1) {
    return 1;
  } else {
    return num * factorial(num - 1);
  }
}
var another = factorial;
factorial = null;

console.log(another(5)); //TypeError: factorial is not a function

如上,其實是在another上調用factorial,並且若是factorial不存在以後會引發錯誤.

解決方案:

1, arguments.callee(不推薦)
是一個指向正在執行的函數的指針屬性.

function factorial(num) {
  if (num <= 1) {
    return 1;
  } else {
    return num * arguments.callee(num - 1);
  }
}
var another = factorial;
factorial = null;

console.log(another(5)); // 120

缺點:

  1. 嚴格模式下,不能經過腳本訪問 arguments.callee,訪問這個屬性會致使錯誤;
  2. arguments是龐大且變化的,每次訪問須要消耗大量性能;

2, 命名函數表達式

var factorial = (function f(num) {
  if (num <= 1) {
    return 1;
  } else {
    return num * f(num - 1);
  }
});
var another = factorial;
factorial = null;

console.log(another(5)); // 120

這種方式在嚴格模式和非嚴格模式下都行得通.
(更多內容請自行查閱,本節到此爲止了.)

閉包

定義:

  • 未知來源: 指函數變量能夠保存在函數做用域內,所以看起來是函數將變量「包裹」了起來;
  • 未知來源: 指在函數聲明時的做用域之外的地方被調用的函數;
  • 官方: 一個擁有許多變量和綁定了這些變量的環境的表達式;
  • Javascript高級程序設計3: 閉包是指有權訪問其餘函數做用域中的變量的函數;
  • JavaScript語言精粹: JavaScript中的函數運行在它們被定義的做用域裏,而不是它們被執行的做用域裏;
  • 阮一峯: 閉包就是可以讀取其餘函數內部變量的函數;

這是一個很難界定的點,每一個人的說法都不一樣,包括各類專業資料,權威大神,可是惟一不變的是它們都有提到訪問其餘做用域的能力.
例如這個例子,不只能夠在外部讀取函數內部變量,還能修改.

function test() {
  var num = 1;
  return {
    get: function () {
      console.log(num);
    },
    add: function () {
      console.log(++num);
    }
  }
}
var result = test();
result.get(); // 1
result.add(); // 2

注意:

1, 匿名函數和閉包函數沒有必然關係;
匿名函數: 不須要函數名字,沒污染全局命名空間的風險而且執行後自動銷燬環境.
不少人說匿名函數也是閉包的用法,可是在我看來這只不過是使用匿名函數的寫法來寫閉包,讓開發省掉多餘步驟而已.例如:

//閉包寫法
function test1() {
  return function () {
    console.log(1);
  }
}
test1()(); // 1
//匿名函數寫法
var test2 = (function () {
  return function () {
    console.log(1);
  }
})()
test2(); // 1

2, 閉包所保存的是整個變量對象,而不是某個特殊的變量,因此只能取得包含函數中任何變量的最後一個值。
這就是爲何for循環返回的i永遠是最後一個的緣由了

<!DOCTYPE html>

<head>
    <meta charset="UTF-8">
    <title></title>
</head>

<body>
    <ol>
        <li>點我吧!!</li>
        <li>點我吧!!</li>
        <li>點我吧!!</li>
        <li>點我吧!!</li>
    </ol>
    <script>
        var btn = document.getElementsByTagName('li');

        for (var i = 0; i < 4; i++) {
            btn[i].onclick = function () {
                alert(i);
            };
        }
    </script>
</body>

</html>

咱們能夠經過建立閉包環境模擬塊級做用域讓行爲符合預期

<!DOCTYPE html>

<head>
    <meta charset="UTF-8">
    <title></title>
</head>

<body>
    <ol>
        <li>點我吧!!</li>
        <li>點我吧!!</li>
        <li>點我吧!!</li>
        <li>點我吧!!</li>
    </ol>
    <script>
        var btn = document.getElementsByTagName('li');

        for (var i = 0; i < 4; i++) {
            btn[i].onclick = function (num) {
                return function () {
                    alert(num);
                }
            }(i);
        }
    </script>
</body>

</html>

3, this指向問題
this 對象是在運行時基於函數的執行環境綁定的,閉包的執行環境具備全局性,所以其 this 對象一般指向 window;

function test() {
  return function () {
    console.log(this === window);
  }
}
test()(); // true

優勢:

  • 封裝性好,避免全局污染;
  • 能夠延長變量生命週期,保存當前變量不被清除;
  • 能夠在模擬塊級做用域;
  • 在對象中建立私有變量方法,只暴露出想提供的訪問權限;

缺點:

  • 因爲閉包會使得函數中的變量都被保存在內存中,內存消耗很大,因此不能濫用閉包,不然會形成網頁的性能問題,在IE中可能致使內存泄露。解決方法是,在退出函數以前,將不使用的局部變量所有刪除(delete是沒用的,要設成null)。
  • 閉包會在父函數外部,改變父函數內部變量的值。因此,若是把父函數看成對象(object)使用,把閉包看成它的公用方法(Public Method),把內部變量看成它的私有屬性(private value),這時必定要當心,不要隨便改變父函數內部變量的值。

(更多內容請自行查閱,本節到此爲止了.)

Currying(柯里化)

在計算機科學中,柯里化(Currying)是把接受多個參數的函數變換成接受一個單一參數(最初函數的第一個參數)的函數,而且返回接受餘下的參數且返回結果的新函數的技術。特色是:

  1. 參數複用;
  2. 提早返回;
  3. 延遲計算/運行。

單說有點抽象,例如你有一個簡單的計算函數

function add(a, b) {
  return a + b
}

若是你想再加多寫計算量怎麼辦?繼續加入參加參數,
咱們分析下上面的函數有什麼特色:
1, 計算方式固定,返回全部參數相加的結果;
2, 參數不固定,可能須要增減;

既然計算方法固定,那咱們就只須要在參數上想辦法解決,有沒一種方法可讓函數保存參數直到某個時刻再執行?天然是有的,並且仍是利用以前學過的知識點:
1, 函數做用域內的 arguments 入參變量;
2, 遞歸;
3, 閉包;

咱們能夠先看看柯里化後執行過程大概如此

add(a)(b)(c);
========至關於===========
function add(a) {
  return function (b) {
    return function (c) {
      return a + b + c
    }
  }
}

實踐開始

接下來咱們就分階段寫一個方法讓原函數轉換成這種柯里化函數用法.

咱們先寫一個最簡單的柯里化函數以下

function toCurry(fn) {
  //轉入參數組
  var args = [].slice.call(arguments),
    //提取執行方法
    fn = args.shift();
  return function () {
    //拼接上次入參和此次入參執行方法
    return fn.apply(null, args.concat([].slice.call(arguments)));
  };
}

function add(a, b) {
  return a + b
}

console.log(toCurry(add, 1)(2)); // 3

已經初步走向柯里化的道路了,而後繼續擴展延伸,

把接受多個參數的函數變換成接受一個單一參數

function toCurry(fn) {
  //保存回調函數
  return function curry() {
    var args = [].slice.call(arguments);
    //判斷目前入參是否達到函數要求入參
    if (args.length < fn.length) {
      return function () {
        //遞歸調用curry,拼接當前arguments和下次入參的arguments
        return curry.apply(null, args.concat([].slice.call(arguments)))
      }
    } else {
      //執行函數
      return fn.apply(null, args)
    }
  }
}

function add(a, b, c) {
  return a + b + c
}

var fn = toCurry(add);
console.log(fn(1, 2)(3)); //6
console.log(fn(1)(2)(3)); //6

目前無論怎麼傳只要達到數量就會執行,接下來咱們要把固定入參轉成自動入參方式,因此要在執行判斷那裏下功夫,咱們如今是根據函數的入參fn.length來判斷是否執行,這須要在函數預先定義好,先嚐試一下把這個改爲人手控制.

//新增入參數目限制
function toCurry(fn, len) {
  var len = len || fn.length;
  //保存回調函數
  return function curry() {
    var args = [].slice.call(arguments);
    //判斷目前入參是否達到函數要求入參
    if (args.length < len) {
      return function () {
        //遞歸調用curry,拼接當前arguments和下次入參的arguments
        return curry.apply(null, args.concat([].slice.call(arguments)))
      }
    } else {
      //執行函數
      return fn.apply(null, args)
    }
  }
}

function add(a, b, c) {
  return a + b + c
}

var fn = toCurry(add, 3);
console.log(fn(1)(2)(3));//6

如今已經能夠自定義配置執行時機,可是可不能夠更進一步,把配置這一步驟都免掉呢?這個就讓你們自由發揮吧,下面貼出一個網上流傳的寫法

function add() {
  var args = [].slice.call(arguments);
  var fn = function () {
    var newArgs = args.concat([].slice.call(arguments));
    return add.apply(null, newArgs);
  }
  fn.toString = function () {
    return args.reduce(function (a, b) {
      return a + b;
    })
  }
  return fn;
}
console.log(add(1)(2)(3));

這算是一種取巧的寫法,偷換toString寫法達到執行的目的.由於我暫時還沒什麼想法,就不說了,
接下來還有一個問題,有些函數須要用到tihs指向的時候,咱們已經早就丟失了,因此在通用函數還要保存指針

//新增入參數目限制
function toCurry(fn, len) {
  var len = len || fn.length;
  //保存回調函數
  return function curry() {
    var args = [].slice.call(arguments),
      self = this;
    //判斷目前入參是否達到函數要求入參
    if (args.length < len) {
      return function () {
        //遞歸調用curry,拼接當前arguments和下次入參的arguments
        return curry.apply(self, args.concat([].slice.call(arguments)))
      }
    } else {
      //執行函數
      return fn.apply(self, args)
    }
  }
}
var num = {
  name: 'mike',
  add: function add(a, b, c) {
    console.log(this.name);
    return a + b + b
  }
}

num.add = toCurry(num.add, 3);
console.log(num.add(1)(2)(3));

好的,目前除了固定參數那一塊還沒其餘思路,基本方法時成型了,另外再給一個延遲執行的例子寫法你們看

var add = (function (fn) {
  var ary = [];
  return function curry() {
    var args = [].slice.call(arguments);
    if (args.length) {
      ary = ary.concat(args)
      return curry
    } else {
      return fn.apply(null, ary)
    }

    return args.length ? curry : fn.apply(null, ary)
  }
})(function () {
  console.log([].slice.call(arguments).reduce((total, cur) => total += cur));
})

add(1)(2)(3)(); // 6

有參數就保存,沒參數就執行,這種使用場景也比較多,根據狀況寫法不一樣,例如這裏就沒有保存this指向.

總的來講,柯里化主要更改在於傳入的參數個數,以及它如何影響代碼的結果.
(更多內容請自行查閱,本節到此爲止了.)

惰性載入函數

若是平時有本身寫一些兼容不一樣瀏覽器差別代碼的話,確定以後中間夾雜着不少判斷分支,做爲強迫症的程序員怎麼能夠忍受這些東西,例如:

function addEvent(element, eType, handle, bol) {
  if (element.addEventListener) {
    element.addEventListener(eType, handle, bol);
  } else if (element.attachEvent) {
    element.attachEvent("on" + eType, handle);
  } else {
    element["on" + eType] = handle;
  }
}

解決方案

1, 在第一次調用的過程當中,該函數會被覆蓋爲另一個按合適方式執行的函數

function addEvent() {
  if (document.addEventListener) {
    addEvent = function (element, eType, handle, bol) {
      element.addEventListener(eType, handle, bol);
    }
  } else if (document.attachEvent) {
    addEvent = function (element, eType, handle, bol) {
      element.attachEvent("on" + eType, handle);
    }
  } else {
    addEvent = function (element, eType, handle, bol) {
      element["on" + eType] = handle;
    }
  }
  return addEvent.apply(this, arguments);
}

除了第一次會執行判斷,而後原函數被覆蓋成分支流程代碼再返回函數

2, 上面提過的匿名自執行閉包寫法,比第一種好處是省去手動觸發第一次執行.

var addEvent = (function () {
  if (document.addEventListener) {
    return function (element, eType, handle, bol) {
      element.addEventListener(eType, handle, bol);
    }
  } else if (document.attachEvent) {
    return function (element, eType, handle, bol) {
      element.attachEvent("on" + eType, handle);
    }
  } else {
    return function (element, eType, handle, bol) {
      element["on" + eType] = handle;
    }
  }
})();

尾調用優化

函數的最後一步是調用另外一個函數,之因此有這種寫法就是由於上面說的執行環境(execution context)及做用域(scope),關鍵就是最後一步能不能把損耗降到最低,例如:

//沒錯,又是咱們的熟面孔函數
function add() {
  var a = 1,
    b = 2;
  return result(a + b)
}

function result(num) {
  return num;
}
console.log(add()); // 3

因爲是函數的最後一步操做,因此不須要保留外層函數的環境變量,由於都不會再用到了,只要直接只保留內層函數的環境變量(代碼8)就能夠了。

尾遞歸

還用上面的遞歸例子,這是一個一直累積環境變量的寫法.

function factorial(num) {
  if (num <= 1) {
    return 1;
  } else {
    return num * factorial(num - 1);
  }
}
console.log(factorial(5));//120

若是咱們能利用尾調用優化寫法就可讓它一直保持一層的環境變量記錄.

function factorial(num, total) {
  if (num === 1) return total;
  return factorial((num - 1), total * num);
}
console.log(factorial(5, 1)); //120

缺點

  • 把運算放在方法入參中,可讀性語義化都是個問題;
  • 須要傳入初始值;(能夠採用上面尾調用方法,柯里化等寫法解決,實際意義不大)
相關文章
相關標籤/搜索