Open hacker0limbo opened 5 years ago
最近根据一些教程尝试实现一个简易的 react-hooks. 里面用到了大量的闭包, 翻阅了一些文章加上一些自己的思考, 整理一下
这篇文章更多的是想谈一谈为什么要用闭包, 以及我们可以用闭包来做些什么. 很多网上的教程只是讲一下什么是闭包(很多甚至都没有讲明白), 而对于闭包的实际应用往往是一笔带过. 知乎上有这么一个问题: Python 所谓的“闭包”是不是本着故意把人搞晕的态度发明出来的?
本人也是初学者, 可能很多地方理解的也不是很准确, 也欢迎和我交流
函数也是可以看做有生命周期的, 如下图:
具体可以参考这篇文章, 这里就不多描述了
网上看到一个比较精简的解释:
一个函数在调用的时候, 内部的自由变量, 要到这个函数被定义的地方去找, 而不是在这个函数当前被调用的地方去找 这个函数连同它被定义时的环境一起, 构成了一个数据结构, 就是闭包
真正想要去了解闭包的整个执行流程以及基本原理, 我推荐看这篇教程. 讲的非常深入浅出, 从词法环境, 讲到作用域链和活动对象, 是非常清晰的.
不过这里还是总结一下, 有这么几点:
每个函数创建的时候, 就已经会创建一个词法环境. 当在运行的时候, 创建一个新的词法环境, 这两个词法环境很有可能是不同的. 所以分析的时候一定是要看最新的词法环境, 比如如下的例子:
let phrase = 'Hello' function say(name) { alert(`${phrase}, ${name}`) } say('John') // Hello, John
上图是函数say创建时的词法环境
say
上图是函数say()调用时的词法环境
say()
所以可以看出来, 创建时和运行时的词法环境是截然不同的, 同时由于引用到了全局环境下的phrase变量, 如果在say()调用前修改该变量, 那么调用say()的时候, 其词法环境又会发生变化, 比如将代码改成如下形式:
phrase
let phrase = 'Hello' function say(name) { alert(`${phrase}, ${name}`) } phrase = 'World' say('John') // World, John
很明显的, 在调用say('John')这个函数的时候, 其词法环境中引用的phrase变量已经被修改, 因此最后结果为 "World, John"
say('John')
当然如果将phrase的修改放到函数执行结束后, 那么词法环境并不会改变, 毕竟变量的修改是在函数结束之后发生的, 在执行say()函数的时候, 其词法环境中的phrase并没有被修改掉, 代码如下:
let phrase = 'Hello' function say(name) { alert(`${phrase}, ${name}`) } say('John') // Hello, John phrase = 'World'
所以总结来说, 一定要去看函数被调用时的词法环境, 由于词法环境里面往往引用到了外部的变量, 环境等, 很有可能在被调用时已经和创建的时候发生了很大的变化(例如上面的say函数例子)
say函数
词法环境包括自己内部的环境, 和引用的外部的环境. 当存在嵌套函数的时候, 外部环境可能还引用更加外部的环境, 就形成了一条作用域链. 最最里层的函数在执行的时候, 会根据这条链子, 由内而外寻找需要的变量. 然后可以对找到的变量进行修改, 修改是在该变量所在的作用域内修改, 也就是说对于闭包来讲, 修改的地方在该函数的外部环境修改, 而非克隆一份放低自己的内部环境内修改. 比如下面代码:
function makeCounter() { let count = 0 return function() { return count++ } } let counter = makeCounter() alert(counter())
执行闭包, 创建对外部环境的引用
修改外部环境变量
每次调用一个函数, 如果这个函数存在闭包, 那么都会创建一个单独的闭包环境, 里面有该闭包的状态. 多次调用这个函数, 会创建多个闭包, 这些闭包内部的环境状态都是独立的. 类似于有一个类, 你可以进行多次实例化, 每次实例化出来的都是不同的对象. 例如下面的例子:
function makeCounter() { let count = 0 return function() { return count++ } } let counter1 = makeCounter(); let counter2 = makeCounter(); alert(counter1()) // 0 alert(counter1()) // 1 alert(counter2()) // 0 (独立的)
如果想要深入了解整个的流程还是需要去看上面推荐的那篇文章, 也有英文版的. 另提一句, 这个教程的其他内容质量也是不错的, 也有对应的中文翻译, 可以参考
两道闭包的小练习题:
第一题
function foo() { let x = 100 function add(_x) { x += _x console.log('change closure x', x) } return [x, add] } const [x1, add1] = foo() console.log('x1 before', x1) // x1 before, 100 add1(5) // change closure x, 105 console.log('x1 after', x1) // x1 after, 100 const [x2, add2] = foo() console.log('x2 before', x2) // x2 before, 100 add2(10) // change closure x, 110 console.log('x2 after', x2) // x2 after, 100
这里有两个点需要注意:
引用问题, 再调用完add1()函数以后, x已经改变了地址, 不再指向 100, 而是指向了 105, 然而x1还是指向原始的 100, 同理add2(). 不理解的可以看下面的示例代码:
add1()
x
x1
add2()
let a = 0 let b = a // a 和 b 指向同一地址 a = 100 // a 的指向改变, b 不变 console.log({ a, b }) // { a: 100, b: 0 }
第二题
function foo() { let x = 100 function render() { const _x = x function inner() { x += 5 console.log('innerrender', { _x, x }) } console.log('render', { _x, x }) return inner } function clear() { x = 0 } return [add, render, clear] } const [add, render, clear] = foo() inner = render() // render { _x: 100, x: 100 } clear() inner() // innerrender { _x: 100, x: 5 }
这里存在多个嵌套的闭包, 在调用最里层的函数inner()的时候需要分析清楚此时它所在的环境里引用的x和_x的值是怎样的
inner()
_x
上面花了很大的篇幅将闭包是什么, 这里谈一谈闭包的实际应用
在之前, 先总结一下闭包的几个特点:
可以想到, 普通(纯)函数是没有状态的, 闭包可以使得函数有状态, 有状态就意味着有记忆, 毕竟状态可以发生改变. 当然, 这里的状态, 往往是在外部环境所提供的, 不过内部函数可以读取并操作这个状态.
所以...如果你想进行一些状态的管理与保存, 你实际上可以有两个选择:
闭包和类都是保存状态用的
如果熟悉React, 类和闭包好像有点 class 组件 和 React hooks 的味道!
React
class 组件
React hooks
不过这里先不讲React, 先考虑如下例子, 想要实现一个计数器, 有这么两个功能:
假设我们想要用普通纯函数实现, 可能会这么写:
const Counter = function() { let count = 0 count++ return count } Counter() // 1 Counter() // 1
这里的函数是没有状态的, 调用多少次永远都是 1. 同时内部变量count在函数调用完毕之后也被垃圾回收了. 所以这个计数器的实现是失败的
count
那如果用类实现, 可能代码是这样:
class Counter { constructor() { this.count = 0 } add() { this.count += 1 } getCount() { return this.count } } c1 = new Counter() c1.add() c1.add() c1.getCount() // 2 c2 = new Counter() c2.add() c2.getCount() // 1
使用类模拟后, 每一次实例化都新生成了一个新的对象, 每个对象里面的count属性都是独立的, 我们可以对获取它, 也可以操作它
如果用闭包来实现:
const Counter = function() { let count = 0 const add = function() { count += 1 } const getCount = function() { return count } // return { getCount, add } return [getCount, add] // 返回一个数组或者对象都可以 } const c1 = Counter() c1[1]() c1[1]() c1[0]() // 2 const c2 = Counter() c2[1]() c2[0]() // 1
使用闭包模拟, 每次运行Counter()产生的闭包环境都是独立的, 不受其他闭包影响. 注意这里的闭包指的是getCount和add两个闭包, Counter只是提供这两个闭包的一部分环境(count), count作为状态, 被这两个函数读取操作
Counter()
getCount
add
Counter
通过以上例子, 我们用闭包, 实现了类才能实现的功能, 这是一件非常神奇的事情.
当然, 闭包的实际应用还有很多, 比如React Hooks, 写这篇文章的原因很大程度上也是因为Hooks的使用还是有很多心智负担的, 想要更好的使用还是需要去了解一些稍微底层的原理.
React Hooks
Hooks
如果你对 React-hooks 的实现比较感兴趣, 可以参考这篇文章, 这篇文章使用原生JavaScript实现了最最简单的Hooks. 不过还是有很多地方说的比较简略. 后续可能会再写一篇博客将里面代码做更加详细的分析
JavaScript
如果对 React 或者 hooks 不了解的, 我非常推荐去看一看 Redux 作者 Dan Abramov 在 2018 年 React Conf 上关于 hooks 介绍的一篇演讲: React Today and Tomorrow and 90% Cleaner React With Hooks. 相信你会非常震撼的.
最近根据一些教程尝试实现一个简易的 react-hooks. 里面用到了大量的闭包, 翻阅了一些文章加上一些自己的思考, 整理一下
这篇文章更多的是想谈一谈为什么要用闭包, 以及我们可以用闭包来做些什么. 很多网上的教程只是讲一下什么是闭包(很多甚至都没有讲明白), 而对于闭包的实际应用往往是一笔带过. 知乎上有这么一个问题: Python 所谓的“闭包”是不是本着故意把人搞晕的态度发明出来的?
本人也是初学者, 可能很多地方理解的也不是很准确, 也欢迎和我交流
从函数的生命周期讲起
函数也是可以看做有生命周期的, 如下图:
具体可以参考这篇文章, 这里就不多描述了
什么是闭包
网上看到一个比较精简的解释:
真正想要去了解闭包的整个执行流程以及基本原理, 我推荐看这篇教程. 讲的非常深入浅出, 从词法环境, 讲到作用域链和活动对象, 是非常清晰的.
不过这里还是总结一下, 有这么几点:
每个函数创建的时候, 就已经会创建一个词法环境. 当在运行的时候, 创建一个新的词法环境, 这两个词法环境很有可能是不同的. 所以分析的时候一定是要看最新的词法环境, 比如如下的例子:
上图是函数
say
创建时的词法环境上图是函数
say()
调用时的词法环境所以可以看出来, 创建时和运行时的词法环境是截然不同的, 同时由于引用到了全局环境下的
phrase
变量, 如果在say()
调用前修改该变量, 那么调用say()
的时候, 其词法环境又会发生变化, 比如将代码改成如下形式:很明显的, 在调用
say('John')
这个函数的时候, 其词法环境中引用的phrase
变量已经被修改, 因此最后结果为 "World, John"当然如果将
phrase
的修改放到函数执行结束后, 那么词法环境并不会改变, 毕竟变量的修改是在函数结束之后发生的, 在执行say()
函数的时候, 其词法环境中的phrase
并没有被修改掉, 代码如下:所以总结来说, 一定要去看函数被调用时的词法环境, 由于词法环境里面往往引用到了外部的变量, 环境等, 很有可能在被调用时已经和创建的时候发生了很大的变化(例如上面的
say函数
例子)词法环境包括自己内部的环境, 和引用的外部的环境. 当存在嵌套函数的时候, 外部环境可能还引用更加外部的环境, 就形成了一条作用域链. 最最里层的函数在执行的时候, 会根据这条链子, 由内而外寻找需要的变量. 然后可以对找到的变量进行修改, 修改是在该变量所在的作用域内修改, 也就是说对于闭包来讲, 修改的地方在该函数的外部环境修改, 而非克隆一份放低自己的内部环境内修改. 比如下面代码:
执行闭包, 创建对外部环境的引用
修改外部环境变量
每次调用一个函数, 如果这个函数存在闭包, 那么都会创建一个单独的闭包环境, 里面有该闭包的状态. 多次调用这个函数, 会创建多个闭包, 这些闭包内部的环境状态都是独立的. 类似于有一个类, 你可以进行多次实例化, 每次实例化出来的都是不同的对象. 例如下面的例子:
如果想要深入了解整个的流程还是需要去看上面推荐的那篇文章, 也有英文版的. 另提一句, 这个教程的其他内容质量也是不错的, 也有对应的中文翻译, 可以参考
闭包的几个小练习
两道闭包的小练习题:
第一题
这里有两个点需要注意:
引用问题, 再调用完
add1()
函数以后,x
已经改变了地址, 不再指向 100, 而是指向了 105, 然而x1
还是指向原始的 100, 同理add2()
. 不理解的可以看下面的示例代码:第二题
这里存在多个嵌套的闭包, 在调用最里层的函数
inner()
的时候需要分析清楚此时它所在的环境里引用的x
和_x
的值是怎样的闭包的应用
上面花了很大的篇幅将闭包是什么, 这里谈一谈闭包的实际应用
在之前, 先总结一下闭包的几个特点:
可以想到, 普通(纯)函数是没有状态的, 闭包可以使得函数有状态, 有状态就意味着有记忆, 毕竟状态可以发生改变. 当然, 这里的状态, 往往是在外部环境所提供的, 不过内部函数可以读取并操作这个状态.
所以...如果你想进行一些状态的管理与保存, 你实际上可以有两个选择:
如果熟悉
React
, 类和闭包好像有点class 组件
和React hooks
的味道!不过这里先不讲
React
, 先考虑如下例子, 想要实现一个计数器, 有这么两个功能:假设我们想要用普通纯函数实现, 可能会这么写:
这里的函数是没有状态的, 调用多少次永远都是 1. 同时内部变量
count
在函数调用完毕之后也被垃圾回收了. 所以这个计数器的实现是失败的那如果用类实现, 可能代码是这样:
使用类模拟后, 每一次实例化都新生成了一个新的对象, 每个对象里面的
count
属性都是独立的, 我们可以对获取它, 也可以操作它如果用闭包来实现:
使用闭包模拟, 每次运行
Counter()
产生的闭包环境都是独立的, 不受其他闭包影响. 注意这里的闭包指的是getCount
和add
两个闭包,Counter
只是提供这两个闭包的一部分环境(count
),count
作为状态, 被这两个函数读取操作通过以上例子, 我们用闭包, 实现了类才能实现的功能, 这是一件非常神奇的事情.
闭包和 React Hooks
当然, 闭包的实际应用还有很多, 比如
React Hooks
, 写这篇文章的原因很大程度上也是因为Hooks
的使用还是有很多心智负担的, 想要更好的使用还是需要去了解一些稍微底层的原理.如果你对 React-hooks 的实现比较感兴趣, 可以参考这篇文章, 这篇文章使用原生
JavaScript
实现了最最简单的Hooks
. 不过还是有很多地方说的比较简略. 后续可能会再写一篇博客将里面代码做更加详细的分析如果对 React 或者 hooks 不了解的, 我非常推荐去看一看 Redux 作者 Dan Abramov 在 2018 年 React Conf 上关于 hooks 介绍的一篇演讲: React Today and Tomorrow and 90% Cleaner React With Hooks. 相信你会非常震撼的.
参考