4

【前端重温系列】闭包及其涉及知识点的理解

 3 years ago
source link: https://blog.vadxq.com/article/frontend-revisited-closure/
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

【前端重温系列】闭包及其涉及知识点的理解

时间已经是2020年了,马上也就2021年了,现如今的发展,ES6的实现几乎现代化浏览器都实现了。红宝书也出第四版了,删除了过旧的知识,引入了ES6,涵盖至ECMAScript 2019新标准。新的基础工具书的推出,自己也该好好重温一遍基础,重新梳理自己脑海的知识点,过时的删除,腾出空间给新知识。当然,内容主要还是从核心知识开始,扩展性涵盖其他知识点。

今天开始从闭包及其涉及知识点开始说起。

闭包的定义:闭包是什么

不同资料对闭包的解释

函数和对其周围状态(lexical environment,词法环境)的引用捆绑在一起构成闭包(closure)。也就是说,闭包可以让你从内部函数访问外部函数作用域。在 JavaScript 中,每当函数被创建,就会在函数生成时生成闭包。

其理解:闭包就是能够读取其他函数内部变量的函数。

由于在Javascript语言中,只有函数内部的子函数才能读取局部变量,因此可以把闭包简单理解成”定义在一个函数内部的函数”。所以,在本质上,闭包就是将函数内部和函数外部连接起来的一座桥梁。

在计算机科学中,闭包(英语:Closure),又称词法闭包(Lexical Closure)或函数闭包(function closures),是在支持头等函数的编程语言中实现词法绑定的一种技术。闭包在实现上是一个结构体,它存储了一个函数(通常是其入口地址)和一个关联的环境(相当于一个符号查找表)。环境里是若干对符号和值的对应关系,它既要包括约束变量(该函数内部绑定的符号),也要包括自由变量(在函数外部定义但在函数内被引用),有些函数也可能没有自由变量。闭包跟函数最大的不同在于,当捕捉闭包的时候,它的自由变量会在捕捉时被确定,这样即便脱离了捕捉时的上下文,它也能照常运行。捕捉时对于值的处理可以是值拷贝,也可以是名称引用,这通常由语言设计者决定,也可能由用户自行指定(如C++)。

在支持头等函数的语言中,如果函数f内定义了函数g,那么如果g存在自由变量,且这些自由变量没有在编译过程中被优化掉,那么将产生闭包。

闭包和匿名函数经常被用作同义词。但严格来说,匿名函数就是字面意义上没有被赋予名称的函数,而闭包则实际上是一个函数的实例,也就是说它是存在于内存里的某个结构体。如果从实现上来看的话,匿名函数如果没有捕捉自由变量,那么它其实可以被实现为一个函数指针,或者直接内联到调用点,如果它捕捉了自由变量那么它将是一个闭包;而闭包则意味着同时包括函数指针和环境两个关键元素。在编译优化当中,没有捕捉自由变量的闭包可以被优化成普通函数,这样就无需分配闭包结构体,这种编译技巧被称为函数跃升。

自我理解总结

包含了既不是函数参数、也不是函数的局部变量,而是一个不属于当前作用域的变量,相对于当前作用域来说,是一个自由变量的函数,就叫闭包。

闭包包含了自由变量和函数环境

为什么需要闭包,闭包优缺点

  • 可以引用外部函数的变量或者参数
  • 使该变量或者参数常驻内存,避免被垃圾回收机制所回收

在 js 中变量的作用域属于函数作用域, 在函数执行完后,作用域就会被清理,内存也会随之被回收,但是由于闭包可访问上级作用域,即使上级函数执行完, 作用域也不会随之销毁

总结:某个函数在定义时的词法作用域之外的地方被调用,闭包可以使该函数访问定义时的词法作用域

1)由于闭包会使得函数中的变量都被保存在内存中,内存消耗很大,所以不能滥用闭包,否则会造成网页的性能问题,在IE中可能导致内存泄露。解决方法是,在退出函数之前,将不使用的局部变量全部删除。

2)闭包会在父函数外部,改变父函数内部变量的值。所以,如果你把父函数当作对象(object)使用,把闭包当作它的公用方法(Public Method),把内部变量当作它的私有属性(private value),这时一定要小心,不要随便改变父函数内部变量的值。

闭包涉及知识:作用域及作用域链

作用域分为词法作用域和动态作用域,Javascript的作用域遵循的就是词法作用域模型

关于词法作用域和动态作用域区别

词法作用域

  • 也称为静态作用域。这是最普遍的一种作用域模型
  • 在代码书写的时候完成划分,作用域链沿着它定义的位置往外延伸

动态作用域

  • 相对“冷门”,但确实有一些语言采纳的是动态作用域,如:Bash 脚本、Perl 等
  • 在代码运行时完成划分,作用域链沿着它的调用栈往外延伸

词法作用域及其作用域链

词法(lexical)一词指的是,词法作用域根据源代码中声明变量的位置来确定该变量在何处可用。嵌套函数可访问声明于它们外部作用域的变量。

任何变量(不管包含的是原始值还是引用值)都存在于某个执行上下文中(也称为作用域)。这个上下文(作用域)决定了变量的生命周期,以及它们可以访问代码的哪些部分。执行上下文可以总结如下。

  • 执行上下文(作用域)分全局上下文(全局作用域)、函数上下文(局部作用域)和块级上下文(块级作用域)。

  • 代码执行流每进入一个新上下文,都会创建一个作用域链,用于搜索变量和函数。

  • 函数或块的局部上下文不仅可以访问自己作用域内的变量,而且也可以访问任何包含上下文乃至全局上下文中的变量。

  • 全局上下文只能访问全局上下文中的变量和函数,不能直接访问局部上下文中的任何数据。

  • 变量的执行上下文用于确定什么时候释放内存。

  • 整个代码结构中只有函数可以限定作用域(待考证)

  • 作用域规则优先使用变量提升规则分析

  • 如果当前作用规则中有名字了, 就不考虑外面的名字

闭包涉及知识点:js内存管理

JavaScript是使用垃圾回收的编程语言,开发者不需要操心内存分配和回收。JavaScript的垃圾回收程序可以总结如下。

  • 离开作用域的值会被自动标记为可回收,然后在垃圾回收期间被删除。
  • 主流的垃圾回收算法是标记清理,即先给当前不使用的值加上标记,再回来回收它们的内存。
  • 引用计数是另一种垃圾回收策略,需要记录值被引用了多少次。JavaScript引擎不再使用这种算法,但某些旧版本的IE仍然会受这种算法的影响,原因是JavaScript会访问非原生JavaScript对象(如DOM元素)。
  • 引用计数在代码中存在循环引用时会出现问题。
  • 解除变量的引用不仅可以消除循环引用,而且对垃圾回收也有帮助。为促进内存回收,全局对象、全局对象的属性和循环引用都应该在不需要时解除引用。

闭包的实现

若函数作为参数被传递

// 函数作为参数被传递
function print(fn) {
const a = 200
fn()
}

const a = 100
function fn() {
console.log(a)
}
print(fn) // 100

函数作为返回值被返回

// 函数作为返回值
function create() {
const a = 100
return function () {
console.log(a)
}
}

const fn = create()
const a = 200
fn() // 100

闭包的应用

实现数据(变量和方法)私有化

函数柯里化(函数式编程

闭包相关例题

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

// 5 5 5 5 5 5
// 创建的5个setTimeout闭包共享一个词法作用域,优先打印外层5
// 闭包只能取得包含函数中任何变量赋值最后一个值 // 5


for (var i = 0; i < 5; i++) {
((j) => {
setTimeout(() => {
console.log(j)
}, 1000 * j)
})(i)
}
// 0 1 2 3 4
// 5个setTimeout闭包有自己独立的词法环境
// 闭包读取到不同的i值

var output = function (i) {
setTimeout(function() {
console.log(i);
}, 1000);
};

for (var i = 0; i < 5; i++) {
output(i);
}

// 0 1 2 3 4
// 这里的 i 被赋值给了 output 作用域内的变量 i

// 变种
function test (){
var num = []
var i

for (i = 0; i < 10; i++) {
num[i] = function () {
console.log(i)
}
}

return num[9]
}

test()()
// 10

var test = (function() {
var num = 0
return () => {
return num++
}
}())

for (var i = 0; i < 10; i++) {
test()
}

console.log(test())
// 10


var a = 1;
function test(){
a = 2;
return function(){
console.log(a);
}
var a = 3;
}
test()();
// 2
// 变量提升,test a提升了。后来又赋值2
// 我们作用域的划分,是在书写的过程中,根据你把它写在哪个位置来决定的。像这样划分出来的作用域,遵循的就是词法作用域模型。这里我们匿名函数被定义的时候 a = 3 的赋值动作还没有发生(只有声明会被提前!),因此它拿到的 a 就是 2!


function foo(a,b){
console.log(b);
return {
foo:function(c){
return foo(c,a);
}
}
}

var func1=foo(0);
func1.foo(1);
func1.foo(2);
func1.foo(3);
var func2=foo(0).foo(1).foo(2).foo(3);
var func3=foo(0).foo(1);
func3.foo(2);
func3.foo(3);
// undefined
// 0
// 0
// 0
// undefined
// 0
// 1
// 2
// undefined
// 0
// 1
// 1
// {foo: ƒ}

About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK