【譯】理解JavaScript:閉包

原文連接javascript

爲何深度學習JavaScript?

JavaScript現在是最流行的編程語言之一。它運行在瀏覽器、服務器、移動設備、桌面應用,也可能包括冰箱。無需我舉其餘再多不相干的例子,只要你正從事web開發,你就不可避免地要寫JavaScript。java

不少web開發者僅僅由於能寫能夠運行的代碼就聲稱瞭解JavaScript。對於JavaScript,你能夠用一個月就能寫代碼,掌握它以後終生收益。(If there are no errors and nobody’s complaining why should you need to learn more?)(譯者注:不知所云)git

好吧,我就是曾經聲稱很瞭解此語言的一員。幾年前我用AngularJS和Node寫應用,當時對本身的能力很是自信。拋開功能,我堅信我已經征服了JavaScript。github

當面試中讓我解釋一下閉包時我懵逼了。我感受本身知道一點,和回調有關,我當時一直用回調(當時還不知道Promise),但就是不知道怎麼描述其原理。web

在個人開發職業生涯中那次失敗的JavaScript面試是最恥辱和最具教育意義的經歷。從那時起我歷時一年半致力於JavaScript的高價段位,並決定分享於世人。先從一個最多見的JavaScript面試題開始:面試

什麼是閉包?

毫無疑問你已經在各類應用中使用過閉包。你每次爲事件處理器添加回調時你都在用閉包的神奇屬性。編程

我遇到過不少關於此概念的解釋,但我最信服是Kyle Simpson下的定義:瀏覽器

當一個方法執行完脫離了本身的詞法做用域,但仍然可以記住並訪問其詞法做用域,這就是閉包。

這個解釋開始可能有點晦澀,讓咱們抽絲剝繭摘下閉包的真面目。服務器

此文不詳述做用域(有專門的主題闡述),不過做用域是理解閉包原理的基礎。做用域就是包含某些屬性和方法的區域。每一個JavaScript方法都會建立一個新的做用域,它內部的變量和入參都只能在其內部訪問。閉包

若是你在函數內聲明一個變量,函數外是訪問不到的。不過,咱們能夠在函數內部定義擁有做用域的內部函數。這些內嵌函數的特別之處在於它們能夠訪問父做用域的變量。

坦白說這也算不上什麼特別之處,由於每個在全局做用域中定義的函數都能訪問全局變量。雖然咱們提到的這些內嵌函數能夠訪問父函數的做用域,但它們不能在父函數以外被調用。除非咱們將其暴露出來。

咱們將內部函數暴露出來就能夠在全局做用域中使用。牛逼!如今咱們就能夠爲所欲爲了。不過,暴露出來的內部函數實際上引用了它父做用域的變量,會不會有問題?不會!絕對不會,這就是閉包!

閉包是暴露出來的內嵌方法

我不肯定這是不是給閉包下的最好的定義,但這確實可以很好地抓住此術語的本質。閉包就是咱們在函數外部就能訪問其父做用域的內部函數。你可否經過咱們以前提到的詞法做用域理解此解釋呢?

function person(name) {
  return {
    greet: function() {
      console.log('hello from ' + name)
    }
  }
}

let alex = person('alex');
alex.greet(); // hello from alex
console.log(alex.name); // undefined
console.log(name); // will throw ReferenceError

咱們在此定義了只有一個參數nameperson函數。它返回一個以greet爲屬性的對象。如今咱們知道,暴露出的greet函數能夠訪問父函數參數。儘管name變量並無定義在greet的做用域中,由於它是閉包,因此greet能夠從其父做用域中獲取。

並非特別難理解,你可能都用了不少次了。我學閉包前從沒把它想象的多難,理解了其背後的原理,我就明白了封裝並使用模塊。

哇唔,哇唔...模塊?封裝?出乎意料。

模塊和用閉包封裝

我深陷JavaScript漩渦以前首先了解到其中不少高深詞彙都有實踐解釋。模塊和封裝就是這類術語很完美的例子。我先從封裝開始,用相同的策略各個擊破去理解它們。

封裝是基本的編程原則之一。學過OOP(面向對象編程)的人對此概念很是熟悉,但對於沒學過的人來講---封裝就是容許咱們保持數據私有的基本隱藏機制。咱們不想把方法的全部內容暴露給全局做用域,咱們想讓大多數內容保持私有且不可訪問。

這纔是閉包的真正便利之處。咱們能夠利用閉包訪問父做用域,甚至在外部訪問的時候得到適當地封裝。在父函數中可能有不少方法和變量,經過利用閉包咱們能夠將其暴露給咱們須要的函數。

咱們能夠用閉包爲咱們的方法定義一個公共API,並保持方法中全部東西私有。

咱們如今已經掌握了封裝,只需實踐便可。在JavaScript中對此概念的實踐就是使用模塊。

模塊

在ES6中可使用importexport關鍵字產生以文件爲基礎的模塊,但要注意這些只是語法糖而已。

function Person(firstName, lastName, age) {
  var private = 'this is a private member';

  return {
    getName: function() {
      console.log('My name is ' + firstName + ' ' + lastName);
    },
    getAge: function() {
      console.log('I am ' + age + ' years old')
    }
  }
}

let person = new Person('Alex', 'Kondov', 22);
person.getName();
person.getAge();
console.log(person.private); //undefined

這是一個咱們能夠保持一些數據私有的簡單例子。咱們能夠有其餘內嵌方法,儘管導出後可使用,但並無都暴露出來。

function Order (items) {
  const total = items => {
    return items.reduce((acc, curr) => {
      return acc + curr.price
    }, 0)
  }
  
  const addTaxToPrice = price => price + (price * 0.2)
  
  return {
    calculateTotal: () => {
      return addTaxToPrice(total(items)).toFixed(2)
    }
  }
}

const items = [
  { name: 'Toy', price: 14.99 },
  { name: 'Candy', price: 7.99 }
]

const order = Order(items)
console.log(order.total) // undefined
console.log(order.addTaxToPrice) // undefined
console.log(order.calculateTotal()) // 27.58

在這個更接近真實的例子中方法返回了一個order對象,惟一暴露出來的方法是calculateTotalOrder函數有一個閉包,容許此閉包使用它的變量和入參。在你計算訂單總價時隱藏了內部邏輯,也方便之後擴展。

怪異之處

JavaScript也有其怪異之處。實際上有些怪異之處讓人很是蛋疼。閉包使用不當就會很坑。

下面的代碼常常出如今JavaScript面試中讓猜它的輸出。

for (var i = 1; i <= 5; i++) {
  setTimeout(function timer () {
    console.log(i);
  }, i * 1000);
}

從1循環到5並在一段時間後打印出當前的數字。正常感受會輸出1,2,3,4,5,對嗎?

讓我驚奇的是上面的代碼會在輸出臺上連續5次打印出6。若是循環之中沒有setTimeout不會有任何問題,由於日誌輸出會被當即執行。很明顯,排隊操做引起了這個問題。

咱們指望每次調用setTimeout都會獲取i變量自身的拷貝,但實際狀況倒是它訪問的是它的父做用域。又由於都在排隊,第一個日誌會在它排隊1秒後發生。當1000毫秒過去的時候,循環早已結束,i變量也早已被賦值爲6。

我明白了這個問題但如何修復呢?setTimeout會在全局做用域尋找i變量,沒法打印出咱們想要的數字。咱們能夠把setTimeout包裹到一個方法中並將咱們想要輸出的變量傳進去。這樣setTimeout會從它的父做用域而不是全局做用域進行訪問。

for (var i = 1; i <= 5; i++) {
  (function(index) {
    setTimeout(function timer () {
      console.log(index);
    }, index * 1000);
  })(i)
}

咱們使用IIFE(當即執行函數,Immediately Invoked Function Expression)並把想輸出的數字傳進去。IIFE是一種定義後當即調用的函數,它經常使用於這種狀況---咱們想要建立做用域。這種方式每次函數調用都用它們本身的變量拷貝,這也意味着setTimeout運行時會訪問對應的數字。因此上面的例子咱們會達到期待的結果:1,2,3,4,5

結束語

此文介紹了閉包的本質,但還有不少須要學習和更多的邊際狀況須要考慮。若是你想更進一步瞭解閉包,我強烈推薦Kyle Simpson的書中Scope & Closures的部分。

相關文章
相關標籤/搜索