重學JavaScript之匿名函數

注意: 本文章爲 《重學js之JavaScript高級程序設計》系列第七章。

關於《重學js之JavaScript高級程序設計》是從新回顧js基礎的學習。

歡迎關注 前端公衆號【小夭同窗】 前端

1. 什麼是匿名函數?

匿名函數就是沒有名字的函數,有時候也稱爲《 拉姆達函數》。匿名函數是一種強大的使人難以置信的工具。以下:segmentfault

function a(a1, a2, a3) {
    // 函數體
}
複製代碼

==其餘函數表達式==數組

var a = function(a1, a2, a3) {
    // 函數體
}
複製代碼

以上兩個例子在邏輯上等價,其主要的區別是: 前者會在代碼執行前被加載到做用域中,然後者則是在代碼執行到那一行的時候纔會有定義。另外一個重要的區別就是:函數聲明會給函數一個指定的名字,而函數表達式則是:建立一個匿名函數,而後將這個匿名函數賦給一個變量。bash

function(a1, a2, a3) {
    // 函數體
}
複製代碼

上面例子也是徹底能夠的,可是卻沒法調用這個函數,由於沒有指向這個函數的指針,可是能夠將這個函數做爲參數傳入另一個函數,或者從一個函數中返回另外一個函數時就可使用這種形式來定義匿名函數。微信

2. 遞歸

遞歸函數是在一個函數經過名字調用自身的狀況下構成的閉包

function f(num) {
    if (num <= 1) {
        retrun 1
    } else {
        return num * f(num - 1)
    }
}
複製代碼

以上,這是一個經典的遞歸階乘函數,表面上沒有任何問題,可是卻會被如下代碼致使出錯:函數

var a = f
f = null
console.log(a(4) // 報錯
複製代碼

以上代碼先把 f() 函數保存在變量 a 中,而後將f變量設置爲 null ,結果指向原始函數的引用只剩下一個。但在接下來調用 a() 時,因爲必須執行 f(),但 f 已經不是函數,全部就會報錯。這個時候可使用 arguments.callee工具

function f(num) {
    if (num <= 1) {
        return 1
    } else {
        return num * arguments.callee(num - 1)
        // 經過 arguments.callee 代替函數名,能夠保證不會出問題
    }
}

var a = f
a = null
a(4)    // 24
複製代碼

3. 閉包

閉包是指有權訪問另外一個函數做用域中的變量的函數。建立閉包的方式:在一個函數內部建立另外一個函數。post

function c(p) {
    retrun function(o1,o2){
        // var v1 = o1[p]
        // var v2 = o2[p]
        
        if (v1 < v2) {
            return -1
        } else if (v1 > v2) {
            retrun 1
        }else {
            retrun 0
        }
    }
}
複製代碼

在上面代碼中,有標記的兩行是匿名函數中的代碼。這兩行代碼訪問了外部函數中的變量 p。即便這個內部函數被返回了,並且被其餘地方調用了,但它仍然能夠訪問變量 p。之因此還可以訪問這個變量,是由於函數的做用域鏈中包含了c()的做用域。學習

當某個函數第一次被調用時,會建立一個執行環境及相應的做用域鏈,並把做用域鏈賦值給一個特殊的內部屬性([Scope])。而後,使用 this、arguments和其餘命名參數的值來初始化函數的活動對象。但在做用域鏈中,外部函數的活動對象始終處於第二位,外部函數的外部活動對象處於第三位。直到做爲做用域鏈重點的全局執行環境。

  • 在函數執行過程當中,爲讀取和寫入變量的值,就須要在做用域鏈中查找變量。後臺的每一個執行環境都有一個表示變量的對象--變量對象
  • 全局環境的變量對象始終存在,而局部環境的變量對象,則只在函數執行的過程當中存在。咱們在建立函數的時候會建立一個預先包含全局變量對象的做用域鏈,這個做用域鏈被保存在內部的[Scope]屬性中,當調用函數時,會爲函數建立一個執行環境,而後經過賦值函數的[Scope]屬性中的對象構建起執行環境的做用域鏈。
  • 若是這時候有一個變量對象被建立並被推入執行環境做用域鏈的前端,對於一開始建立的函數的執行環境而言,其做用域鏈中包含兩個變量:==本地活動對象和全局變量對象==。因此,做用域鏈本質上是一個指向變量對象的指針列表,它只引用但不實際包含變量對象。

不管何時函數在訪問一個變量時,就會從做用域鏈中搜索具備相同名字的變量,函數執行完成後,局部活動對象將被銷燬,內存中僅保存全局做用域。可是因爲閉包會攜帶包含它的函數的做用域,所以會比其餘函數佔用更多的內存。過分使用閉包可能會致使內存佔用過多。

在一個函數內部定義的函數會將外部函數的活動對象添加到它的做用域鏈中。內部函數在外部函數中被返回後,它的做用域鏈被初始化爲包含外部函數的==活動對象和全局變量對象==,這樣內部函數就能夠訪問外部函數中定義的全部的變量。因此在外部函數執行結束後,它並不會被銷燬,由於內部函數的做用域鏈還在引用這個活動對象。也就是說外部函數執行結束後,它的做用域鏈會被銷燬,可是活動對象還在內存中,直到內部函數被銷燬後。

3.1 閉包與變量

做用域鏈的這種配置引出了一個反作用,閉包只能取得包含函數中任何變量的最後一個值。

3.2 關於 this 對象

在閉包中使用this 也可能會致使一些問題。由於this對象是在運行時基於函數的執行環境綁定的。在全局函數中 this === window,函數被做爲某個對象的方法調用時,this就等於那個對象。匿名函數的執行環境具備全局性,所以其this 對象一般指向window。==可是這並非絕對的。==

在函數被調用的時候,其活動對象都會自動得到兩個特殊變量:==this 和 arguments。== 內部函數在搜索這兩個變量時,只會搜索到其活動對象爲止,所以永遠不可能直接訪問外部函數中的這兩個變量。若是把外部做用域中的this對象保存在一個閉包可以訪問的變量裏,就可讓閉包訪問該對象了。

3.3 內存泄露

因爲IE對JS對象和 COM對象使用不一樣的垃圾收集例程,所以閉包在IE中會致使一些特殊的問題。也就是說,若是閉包的做用域鏈中保存着一個HTML元素,那麼就意味着該元素將沒法被銷燬。

==注意==:閉包會引用包含函數的整個活動對象,而其中包含着變量,即便閉包不直接引用變量,包含函數的活動對象中也仍然會保存一個引用。所以把變量設置爲 null ,這樣就可以解除對DOM對象的引用,減小其引用數,確保正常回收其佔用的內存。

四、 模仿塊級做用域

vJS沒有塊級做用域的概念,這意味着在塊語句中定義的變量,其實是在包含函數中而非語句中建立的。JS歷來不會告訴你是否屢次聲明瞭同一個變量,它老是對後續的聲明視而不見。咱們能夠經過==匿名函數來模仿塊級做用域==從而避免這個問題。

(function () {
    // 塊級做用域
})()
複製代碼

五、私有變量

嚴格來講在JS中並無私有成員的概念:==全部對象屬性都是公有的==。不過卻是有一個私有變量的概念。任何在函數中定義的變量均可以認爲是私有變量,由於不能在函數的外部訪問這些變量。私有變量包括函數的參數、局部變量和在函數內部定義的其餘函數。

在函數內部若是有私有變量,那麼在函數內部能夠訪問這個變量,但在函數外部則不能訪問它們。若是==在這個函數內部建立一個閉包,那麼閉包經過本身的做用域鏈也能夠訪問這些變量==。

咱們把有權訪問私有變量和私有函數的公有方法稱爲==特權方法==。有兩種在對象上建立特權方法的方式,

第一種:在構造函數中定義
function m (){
    let p = 10
    function p (){
        retrun false
    }
    
    // 特權方法
    this.pb = function () {
        p++
        retrun p()
    }
}
複製代碼
第二種:靜態私有變量

經過在私有做用域中定義私有變量或函數,一樣也能夠建立特權方法。和在構造函數中定義特權方法的區別在於私有變量和函數是由實例共享的,因爲特權方法是在原型上定義的,所以全部實例都使用同一個函數。

多查找做用域鏈中的一個層次,就會在必定程度上影響查找速度。這正是閉包和私有變量一個不足之處。

5.1 模塊模式

指的是爲單例建立私有變量和特權方法。所謂單例,指的就是隻有一個實例對象,按照慣例,JS是以對象字面量的方式來建立單例對象的:

var s = {
    name : v,
    method: function(){
        // 方法的代碼
    }
}
複製代碼

六、總結

匿名函數,也稱爲拉姆達函數,是一種使用JS函數的強大方式。有以下特色:

  1. 任何函數表達式從技術上說都是匿名函數,由於沒有引用它們的肯定的方式
  2. 在沒法肯定如何引用函數的狀況下,遞歸函數就會變得比較複雜
  3. 遞歸函數應該始終使用 argument.callee來遞歸地調用自身,不要使用函數名,由於函數名可能會發生變化。

當函數內部定義了其餘函數時,就建立s了閉包,閉包有權訪問包含函數內部的全部變量。

  1. 在後臺執行環境彙總,閉包的做用域鏈包含着它本身的做用域、包含函數的做用域和全局做用域;
  2. 一般,函數的做用域及全部變量都會在函數執行結束後被銷燬
  3. 可是,若是函數返回了一個閉包時, 這個函數的做用域將會一直在內存中保存到閉包不存在爲止

使用閉包能夠在JS中模仿塊級做用域

  1. 建立並當即調用一個函數,這樣便可以執行其中的代碼,又不會在內存中留下對該函數的引用
  2. 結果就是函數內部的全部變量都會被當即銷燬--除非將某些變量賦值給了包含做用域中的變量

閉包能夠用於對象中建立私有變量

  1. 即便JS中沒有正式的私有對象屬性概念,但可使用閉包來實現公有方法,而經過公有方法能夠訪問在包含做用域中定義的變量。
  2. 有權訪問私有變量的公有方法叫作 特權方法
  3. 可使用構造函數、原型模式來實現自定義類型的特權方法,也可使用模塊模式、加強的模塊模式來實現單例的特權方法。

JS中的匿名函數和閉包都是很是的特性,可是要注意使用場景和方法。

重學js系列

重學js之JavaScript簡介

重學 JS 之在 HTML 中使用 JavaScript

重學js之JavaScript基本概念(上)=> 數據類型

重學js之JavaScript基本概念(中)=> 操做符

重學js之JavaScript基本概念(下)=> 運算符

重學js之JavaScript變量、做用域和內存問題

重學js之JavaScript引用類型

重學js之JavaScript 面向對象的程序設計(建立對象)

重學JavaScript之面向對象的程序設計 => 繼承

ES6入門系列

ES6入門之let、cont

ES6入門之變量的解構賦值

ES6入門之字符串的擴展

ES6入門之正則的擴展

ES6入門之數值的擴展

ES6入門之函數的擴展

ES6入門之數組的擴展

ES6入門之對象的擴展

ES6入門之Symbol

ES6入門之Set 和 Map

Es6入門之proxy

ES6入門之Promise對象

Git教程

前端Git基礎教程

Python玩轉微信

Python 實現微信自動經過好友添加請求!!!

Python + Wxpy 實現微信防撤回。

Python獲取好友地區分佈及好友性別分佈

相關文章
相關標籤/搜索