JS 基礎:Closure 閉包

文章目錄

  • JS 基礎:Closure 閉包
    • 簡介
    • 參考
  • 正文
    • IIFE 立即執行函數(Immediately Invoked Functions Expression)
    • Closure 閉包
      • for 循環:使用 IIFE 建立塊級作用域
      • PrimeCreator 質數(素數)製造機
      • Module 模塊化
      • Module Loader 模塊加載器
  • 結語

簡介

在 ES6 新增 letconst 的塊級作用域變量聲明,以及 import/export 等模塊化語法之前,只能透過 var 以及,IIFE 來區隔命名空間以及作用域,寫法之複雜和繁瑣,並且大大降低代碼的可讀性。

本篇將會重點介紹 立即執行函數(IIFE)閉包(Closure)的使用方法和具體應用。

參考

你懂 JavaScript 嗎?#15 閉包(Closure) https://cythilya.github.io/2018/10/22/closure/
[筆記] 談談JavaScript中的IIFEs(Immediately Invoked Functions Expressions) https://pjchender.blogspot.com/2016/05/javascriptiifesimmediately-invoked.html

正文

IIFE 立即執行函數(Immediately Invoked Functions Expression)

立即執行函數(IIFE)顧名思義,就是立即執行的函數(廢話!
),我們先來看一下他的使用形式:

// 加載模塊時立即執行的函數
;(function loaded(w) {console.log('inovke immediately when js file was loaded')
})(window)// 立即執行函數生成初始化值
const greeting = (function (name) {return `hello ${name}`
})('John')

你可能會想,阿就是一個立即執行的函數啊有什麼了不起的。先別急,IIFE 是實現閉包(closure)非常重要的一環,我們只要先知道 IIFE 有以下特性:

  1. 解析到 IIFE 時將立即執行函數表達式,而不用聲明一個函數再調用(相當於生成一個暫時的函數,然後執行完後馬上消失)
  2. 能夠區隔作用域,由於遠古時代只有 var 的函數作用域,所以可以使用 IIFE 的區隔命名空間,如下代碼:
var b = (function (i) {var a = ireturn a
})(1)
console.log(b)
// 1
console.log(a)
// ReferenceError: a is not defined

接下來讓我們先繼續看下去

Closure 閉包

在上面我們知道 IIFE 可以區隔作用域,因為 var 具有函數作用域的特性,所以我們可以將變量"鎖"在 IIFE 的函數作用域內,而避免裡面聲明的變量污染到全局環境。

所謂的閉包(closure)的作用在於,將變量聲明在一個暫時存在的作用域(也就是一個函數)當中,並且將訪問作用域內部的值的一些方法返回暴露出來。由於 JavaScript 與 Java 相似的是都有自己的垃圾回收處理機制,但是由於 IIFE 的返回值保留了暫時作用域的變量訪問,因此就會出現類似內存泄露的情況,在作用域結束之後分配給變量的內存依舊存在。

var { greeting, setName } = (function () {var name = 'John'function greeting() {console.log(`Hello ${name}`)}function setName(val) {name = val}return {greeting,setName}
})()greeting()
setName('Andy')
greeting()// output:
// Hello John
// Hello Andy

我們可以發現 name 變量就這樣被"鎖"在 IIFE 裡面了,我們只能透過返回的 greetingsetName 函數來修改和訪問這個值(這邊使用了 ES6 的對象解構賦值),並且由於 greeting 存在對變量 name 的引用關係,所以 name 不會被垃圾回收器回收內存,而這也是閉包(closure)的命名由來。

接下來我們邊舉例子邊了解閉包的特性以及應用

for 循環:使用 IIFE 建立塊級作用域

首先第一個例子我們先來改造一個經典的面試題:

  • 原題
for (var i = 0; i < 10; i++) {setTimeout(function () {console.log(i)}, 0)
}
// output:
// 10
// 10
// 10
// 10
// 10
// 10
// 10
// 10
// 10
// 10

這個問題是因為 var 的作用域問題,在我上一篇有說到

  • 解法一:使用 let
for (let i = 0; i < 10; i++) {setTimeout(function () {console.log(i)}, 0)
}
// output:
// 1
// 2
// 3
// 4
// 5
// 6
// 7
// 8
// 9
// 10

由於 let 聲明的變量屬於塊級作用域,所以 setTimeout 函數會調用正確的塊中的 i 值。但是在 ES6 之前大家是怎麼解決的呢?

  • 解法二:使用 IIFE
for (var i = 0; i < 10; i++) {;(function (i) {setTimeout(function () {console.log(i)}, 0)})(i)
}
// output:
// 1
// 2
// 3
// 4
// 5
// 6
// 7
// 8
// 9
// 10

第二種解法的精髓在於,for 循環中每一圈我們都調用一個 IIFE,並將當圈的 i 值作為參數傳入,所以實際上每一圈都會創造出一個新的函數(也就是一個新的作用域),然後每個作用域的參數 i 都將保存當圈的 i 值。

在 for 循環的例子裡面,我們透過 IIFE 來模擬塊級作用域的表現,也就是我們可以使用 IIFE 來建立塊級作用域

PrimeCreator 質數(素數)製造機

const PrimeCreator = (function () {let primes = [2, 3, 5]let index = -1function reset() {index = -1}function nextPrime() {index++if (index >= primes.length) {createNextPrime()}return primes[index]}function createNextPrime() {let nextPrime = primes[primes.length - 1] + 1let isPrime = falsewhile (!isPrime) {isPrime = truefor (let prime of primes) {if (nextPrime % prime === 0) {nextPrime++isPrime = falsebreak}}if (isPrime) {break}}primes.push(nextPrime)}return {reset,nextPrime}
})()

這邊使用 IIFE 保存了一個質數列表,上次調用時建造過的質數會被保存在 primes 變量,
如過調用 reset 後,下次遍歷的時候就不需要重新計算,可以直接提取上次計算出來的結果。

這邊的重點不在於這個函數本身,而只是一個思想。當我們需要某個方法,而他可能需要保存自己的私有變量,或是索引表(Hash Table、Map Structure 等),我們可以透過閉包的特性將一些狀態封裝到 IIFE 內部。這就有很多應用,如某某製造機(保存索引表,或是上次調用後的遺產)、狀態機(保存內部狀態),以及後面將要提到的模塊加載器(保存各模塊又能夠區隔命名空間和作用域)

Module 模塊化

接下來第三個例子我們來嘗試使用閉包(closure)來建立模塊化(modules)的概念。

由於在 ES6 引入 import/export 之前,JS 一直都沒辦法實現真正的模塊化,因此遠古時代的人們就想出了一個辦法,使用閉包來模擬模塊化的情形:

const SquareModule = (function () {var _length = 100function setLength(length) {console.log(`set length = ${length}`)_length = length}function area() {return _length * _length}return {setLength,area}
})()console.log(`area = ${SquareModule.area()}`)
SquareModule.setLength(20)
console.log(`area = ${SquareModule.area()}`)// output:
// area = 10000
// set length = 20
// area = 400

我們透過 IIFE 建立一個內部作用域來模擬生成一個模塊,模塊內部的屬性(變量)對於外部是不可見的。

Module Loader 模塊加載器

接下來是閉包最重要的應用,就是可以用來模擬 ES6 的模塊化系統:

// 模塊管理器
const ModuleManager = (function Manager() {// 保存模塊,外部不可訪問const modules = {}// 加載模塊函數// name: 模塊名// deps: 依賴模塊名的數組// 模塊加載器: 動態解析模塊function load(name, deps, impl) {for (let i = 0; i < deps.length; i++) {deps[i] = modules[deps[i]]}modules[name] = impl.apply(null, deps)}// 提取模塊的接口function get(name) {return modules[name]}return {load,get}
})()ModuleManager.load('Add', [], function () {function add(x, y) {return x + y}return {add}
})ModuleManager.load('Mul', ['Add'], function (Add) {function mul(x, y) {let sum = 0while (y-- > 0) {sum = Add.add(sum, x)}return sum}return {mul}
})const Add = ModuleManager.get('Add')
const Mul = ModuleManager.get('Mul')
console.log(Add.add(1, 2))
console.log(Mul.mul(3, 4))// output:
// 3
// 12

這邊我們透過閉包隱藏真正保存模塊的對象(modules 變量),並且加載器接收的第三個參數為其他模塊的生成函數,我們也可以在 impl 函數內部保存模塊自己的狀態。其實上面的寫法已經非常接近於 ES6 的模塊化系統了,非常值得參考(兩者具體比較如下)

  • 閉包寫法
ModuleManager.load('Mul', ['Add'], function (Add) {function mul(x, y) {let sum = 0while (y-- > 0) {sum = Add.add(sum, x)}return sum}return {mul}
})
  • ES6 Module 寫法
// Mul.js
import { add } from 'Add'function mul(x, y) {let sum = 0while (y-- > 0) {sum = add(sum, x)}return sum
}export { mul }

結語

本篇介紹了閉包(closure)的概念,以及數個應用的概念和示例。即便 ES6 以及其他模塊化技術已經非常普及,了解閉包的運行機制和理念也是非常重要。

JS基礎:Closure 閉包相关推荐

  1. JS基礎:Hoisting 變量提升、TDZ 暫時性死區(Temporal Dead Zone)

    JS 基礎:Hoisting 變量提升.TDZ 暫時性死區(Temporal Dead Zone) 文章目錄 JS 基礎:Hoisting 變量提升.TDZ 暫時性死區(Temporal Dead Z ...

  2. JS基礎:void冷知識

    JS 基礎:void 冷知識 文章目錄 JS 基礎:void 冷知識 簡介 參考 正文 語法 Usage 作用 Application `` IIFE(Immediately Invoked Func ...

  3. JS基礎:Prototype Chain 原型鏈

    JS 基礎:Prototype Chain 原型鏈 文章目錄 JS 基礎:Prototype Chain 原型鏈 簡介 參考 正文 Object 對象創建 直接量 `{}` 內置構造函數 `new O ...

  4. 【繁中】Python 教學 爬蟲基礎

    Python 文章目录 Python __init__.__new__和__call__ 型態 len(資料) Tuple 特殊字串 成員運算子 input 集合Set 基本語法 Set 運算子 字典 ...

  5. 從turtle海龜動畫 學習 Python - 高中彈性課程系列 3 烏龜繪圖 所需之Python基礎

    "Talk is cheap. Show me the code." ― Linus Torvalds 老子第41章 上德若谷 大白若辱 大方無隅 大器晚成 大音希聲 大象無形 道 ...

  6. html td线怎么显示,html基礎 table標記 設置邊框線粗細 td標簽 相鄰列/行的單元格合並...

    鎮場詩: 清心感悟智慧語,不着世間名與利.學水處下納百川,舍盡貢高我慢意. 學有小成返哺根,願鑄一良心博客.誠心於此寫經驗,願見文者得啟發. ----------------------------- ...

  7. PHP Plurk Api基礎教學(一) - 如何製作自動發噗機器人

    PHP Plurk Api基礎教學(一) - 如何製作自動發噗機器人 1.第一步 到Plurk PHP APi Google code官網下載最新的PHP API 上面的連結是1.6.2版(聽說已經是 ...

  8. mysql數據庫的增刪改查_MySQL數據庫之基礎增刪改查操作

    作者:        守望幸福 最后修改日期:2014-7-27 所操作的MySQL數據庫名:firstdb 所用的兩個表結構如下: student表 number name socre born_d ...

  9. web静态资源访问规则||webjars的访问配置——webjars是maven库里面对css js image打的一个jar包

    Html css js image  txt   web项目中 放在 Webapp 在springboot项目中  静态资源放置的位置 Springboot默认的静态资源目录 (1)在src/main ...

最新文章

  1. 行情跌宕起伏,或许你可以买这个代币?
  2. python threading模块多线程源码示例(二)
  3. python setdefault,Python笔记setdefault用法
  4. Azure运维系列10:跨订阅迁移ARM虚拟机
  5. SAP零售行业解决方案初阶 1
  6. 配置Keil C51配置开发 STC51单片机过程
  7. Maven入门指南① :Maven 快速入门及简单使用
  8. python基础教程视频教程百度云-Python零基础入门学习视频教程全42集百度云网盘下载...
  9. Arduino总结一
  10. QT5.14.2基于PCL1.11.1显示点云(基于Windows VS2019开发环境)
  11. c语言程序设计夏宽理第三版答案,(100)西南大学2021计算机考研数据速览(特别更新版),民族脊梁袁隆平院士的母校!...
  12. Linux 技巧: Bash 测试和比较函数 (shell编程)
  13. c语言程序女设计教学效果分析,C语言程序设计的教学论文
  14. ./wls1036_linux32.bin: /lib/ld-linux.so.2: bad ELF interpreter
  15. 您企业的邮件系统够安全吗
  16. 【mysql乱码】解决php中向mysql插入中文数据乱码的问题
  17. 批量输出lib文件名(PCL或者opencv等环境配置)
  18. PCB 18种特殊走线的画法与技巧!
  19. 如何恢复删除的文件?4种常用方法教你恢复被删除的文件
  20. 在页面引入项目路径 ${webRoot}

热门文章

  1. win7升级正版win10方法(精华篇)!
  2. android 画爱心进度条_Android 开发之 HeartProgress 自定义心形进度条
  3. 项目log4j日志管理详解
  4. 五十六、Fluent空化模型理论
  5. 【Python】P1747 好奇怪的游戏
  6. 我们应该如何正确使用计算机,电脑怎么正确使用
  7. Linux操作系统下连接闪讯的方法(支持有线与无线)
  8. python对时间的灵活处理
  9. Android修炼之道——GoogleGlass开发指南
  10. 必须掌握的网络安全知识