D10 - 点一笼热呼呼的小笼闭包 Closure

前言

闭包,一个完全无法从字面意思了解的专有名词,若是改叫小笼闭包,是不是马上联想到这个画面

一个个汤包被安放在蒸笼中,呼应到闭包在程序码中的封闭区块概念,是不是具象化许多呢

为什麽需要闭包

让我们从以下的举例一步步来了解为什麽要有闭包的设计

let cookie = 1;

function addCookie(){
    cookie++;
}

addCookie();
addCookie();
console.log(cookie) // 2

在全域下宣告一个值为 1 的变数 cookie及宣告一个 addCookie function,每次呼叫 cookie 值增加 1
若希望 cookie 的值 +2,呼叫两次 addCookie,果然这时 cookie 的值增加为 3

但有个问题来了,cookie 因为存在全域变数下,有极大的风险容易被修改,若有另外的变数也叫 cookie,但起算值想从 10 开始不就撞车了?

既然变数放全域不安全,那将 cookie 宣告放到 addCookie 内试试看

function addCookie(){
    let cookie = 1;
    cookie++
    console.log(cookie);
}

addCookie(); // 2
addCookie(); // 2

稍微修改一下,变数宣告放到函式内、cookie++ 後再 return 抛出,
一样进行两次呼叫,结果结果的值都是 2 ?!

虽然 cookie 变数被好好的存在 addCookie 中,但每次呼叫 cookie 都重新被赋值 1,所以不管今天呼叫 addCookie 几次得到的值都只会是 1+1 = 2 的结果

那怎麽做到每次呼叫 cookie +1 ,变数又不会被随意修改呢?
透过 闭包 Closure 就对啦

透过闭包模拟私有变数

诸如 Java 之类的程序语言,提供了私有方法宣告的能力,意味着它们只能被同一个 class 的其他方法呼叫。

JavaScript 并没有的提供原生的方法完成这种事,不过它藉由闭包来模拟私有方法。私有方法不只能限制程序码的存取,它还提供了强而有力的方式来管理全域命名空间,避免非必要的方法弄乱公开介面。 - MDN

闭包的标准起手式就是之前提过的 巢状结构 Nested function,在 function 内 return function

function addCookie(){
    let cookie = 1;
    return function (){
        cookie++
        console.log(cookie);
    } 
}

let cookieNumber = addCookie();
cookieNumber() // 呼叫匿名函式,cookie++ 变成 2
cookieNumber() // 再次呼叫匿名函式,cookie++ 变成 3

将 cookie + 1 的动作包在内层的匿名函式内,在外层将 addCookie 的呼叫结果存入变数 cookieNumber

存入 cookieNumber 的是什麽?就是闭包中的匿名函式
所以呼叫 cookieNumber 等同於执行匿名函式,得到 cookie + 1 的结果
那第二次呼叫 cookieNumber 呢? 为什麽是取得 3

记得前几天讲过的 字汇环境 和 Scope chain 吗
匿名函式中并没有记录 cookie 的值,需要往外一层到 addCookie 的字汇环境查找,此时 cookie 的值因为上一次的呼叫被重新赋予值为 2,所以再加 1 的结果就是 3

那如果没有将内部的匿名函式存入一个变数,而是用 cookieNumber( )( ) 的方式执行匿名函式呢?

function addCookie(){
    let cookie = 1;
    return function (){
        cookie++
        return cookie;
    } 
}

// 存入变数
let cookieNumber = addCookie();
cookieNumber()// 2
cookieNumber()// 3

// 使用()()
addCookie()() // 2
addCookie()() // 2

想当初又傻又天真的我以为使用两个小括号的意思就跟存入变数後再呼叫一样,直到被馒头大大敲醒

如果你也跟当初的我有一样的困惑,没关系,加上一个 console.log 你就懂了

function addCookie(){
    let cookie = 1;
    console.log('you have to pass me first!')
    return function (){
        cookie++
        return cookie;
    } 
}

addCookie()();
// you have to pass me first!
// 2

You have to pass me first !

bytecode 内的 createClosure

记得昨天提到的使用 node 指令印出 bytecode 吗?
输入 node --print-bytecode --print-bytecode-filter=addCookie test.js 後印出 addCookie function 的 bytecode,发现其中一行的内容显示为 createClosure

对应上 closure 不仅是理论上的名词定义,在 V8 内部的实作内也确实区分出 closure 特性的操作

结语

学习过 Lexical Environment、Scope Chain 和 Execution Context 後看闭包能更快上手,一切的一切都关乎作用域的概念呀

Reference

忍者JavaScript 开发技巧探秘2 by John Resig、Bear Bibeault、Josip Maras
所有的函式都是闭包:谈 JS 中的作用域与 Closure Huli 大大在详尽的 closure 文章
MDN - 闭包
W3School
I never understood JavaScript closures
你懂 JavaScript 吗?#15 闭包(Closure)


<<:  Day 10 Eventrouter + ELK + Filebeat 来收集k8s丛集的events

>>:  [D10] 影像杂讯与滤波(1)

TypeScript 能手养成之旅 Day 2 环境安装

前言 每当要学习一个新的语言时,都有一个 SOP 开始流程,就是 安装环境 => 运行程序码 ...

【Day05】Git 版本控制 - Git 基本指令(1)

在开始讲解指令前,先推荐给大家一个平台:六角学院,会提到这个平台是因为!六角学院的 Git &...

Day-30 特集:回圈实例题

for/传统for/高阶函式for回圈比较 const lists = [2, 4, 1, 8, 7...

Day 16 留言是种互动!

好奇是知识的萌芽,萌芽之後,就要给予养分,让知识茁壮,没有养分的知识,只是一个没有办法萌芽的种子而已...

[Python]决策数01─运用CART做决策树

Hi! 大家好,我是Eric,这次要来用Python做决策树。 缘起:决策树因为相对於其他机器学习...