一、 前言
第六弹内容是闭包。 距离上次函数的发布已经过去了一个多月, 最近事情比较多,很少有时间去写文章, 低质量还得保证所以本章放草稿箱一个月了,终于补齐了,其实还有很多细节要展开说明,想着拖太久了,还是先发出来吧,后续慢慢补充。感谢各位佬的支持,希望多多点赞收藏,如果发现有什么不好的点也可以评论或私信我。
本系列为一周一更,计划历时6个月左右。从JS最基础【变量与作用域】到【异步编程,密码学与混淆】。希望自己能坚持下来, 也希望给准备入行JS逆向的朋友一些帮助, 我现在脸皮厚度还行。先要点赞,评论和收藏。也是希望如果本专栏真的对大家有帮助可以点个赞,有建议或者疑惑可以在下方随时问。
先预告一下【V少JS基础班】的全部内容,我做了一些调整。看着很少,其实,正儿八经细分下来其实挺多的,第一个月的东西也一点不少。
第一个月【变量 、作用域 、BOM 、DOM 、 数据类型 、操作符】
第二个月【函数、闭包、this、面向对象编程】
第三个月【原型链、异步编程、nodejs】
第四个月【密码学、各类加密函数】
第五个月【jsdom、vm2、express】
第六个月【基本请求库、前端知识对接】
==========================================================
二、本节涉及知识点
闭包
==========================================================
三、重点内容
一、概念
1- 闭包
概念:
首先我们看下闭包的权威解释(来自MDN)
A closure is the combination of a function bundled together (enclosed) with references to its surrounding state (the lexical environment).
翻译过来就是:
闭包是由捆绑起来(封闭的)的函数和函数周围状态(词法环境)的引用组合而成。换言之,闭包让函数能访问它的外部作用域。在 JavaScript 中,闭包会随着函数的创建而同时创建。
翻译:
英文 | 中文 | 解释 |
---|---|---|
combination | 组合,结合 | 结合体 |
bundled together / enclosed | 封装在一起 | 函数和变量一起被打包 |
references | 引用 | 指向外部变量的引用 |
surrounding state | 周围状态 | 外层作用域中的变量 |
lexical environment | 词法环境 | 在函数创建时所在的作用域链 |
提炼:
closure是闭包
闭包不是函数, 闭包是包含函数与函数所带的词法环境绑定的结合体
口语解释:
闭包是一个结构体。 他是一个函数和一个函数所携带的词法环境一起构成的结构体。
那我们现在就有一个问题了, 函数我们知道,词法环境是什么
2-词法环境&词法作用域
还是MDN:
A Lexical Environment is a specification type used to define the association of Identifiers (names) with specific variables and functions based on the lexical nesting structure of ECMAScript code.
a specification type: 规格类型
the association of: 关联
Identifiers: 标识符
specific variables: 特定变量
nesting structure of: 嵌套结构
提炼:
Lexical Environment 是引擎内部用来描述和追踪当前代码上下文中的变量、函数绑定信息的机制。
口语:
Lexical Environment 和 Lexical Scope 在代码调试过程中的可见性上可以默认是同一个东西。
但是 Lexical Environment, 他是引擎运行时创建的一个结构(对象)。而Lexical Scope则是代码层静态的结构。
在代码执行时,均被存放在上下文中。
好。 说了这么多概念。 我们可能还是不理解。 那我们直接上代码。 我们先从词法作用域【Lexical Scope】开始
【Lexical Scope】
作用域分为: 全局作用域,函数作用域,块作用域[ES6新增]
二、实例讲【作用域】
作用域其实在第一弹的时候就有讲过了。当时没有涉及到这么深,简单的说明了一下, 现在我们再次总结回顾一下:
作用域是在程序运行时代码中的某些特定部分中变量、函数和对象的可访问性。
作用域就是代码的执行环境,全局作用域就是全局执行环境,局部作用域就是函数的执行环境,它们都是栈内存
作用域又分为全局作用域和局部作用域。在ES6之前,局部作用域只包含了函数作用域,ES6的到来为我们提供了 ‘块级作用域’(由一对花括号包裹),可以通过新增命令let和const来实现;而对于全局作用域
在 Web 浏览器中,全局作用域被认为是 window 对象,因此所有全局变量和函数都是作为 window 对象的属性和方法创建的。
.在一个函数内部
2.在一个代码块(由一对花括号包裹)内部
let 声明的语法与 var 的语法一致。基本上可以用 let 来代替 var 进行变量声明,但会将变量的作用域限制在当前代码块中 (注意:块级作用域并不影响var声明的变量)
以上是对作用域的一个总结回顾。 具体细节我们再来讨论。
作用域分为: 全局作用域,函数作用域,块作用域[ES6新增]
1- 函数作用域:
指的是在 JavaScript 中,函数内部声明的变量和函数只能在该函数内部访问,外部无法直接访问。
也就是说,每当你声明一个函数,函数内部就创建了一个独立的作用域。
function foo() {let a = 10;console.log(a); // 10
}
foo();
console.log(a); // ReferenceError: a is not defined
变量a在函数foo内声明,函数外无法访问,会报错。
额外小知识:
作用域链: 当函数内访问变量时,会先查找自己作用域内有没有这个变量,如果没有,就去外层作用域找,直到找到全局作用域。
2- 块级作用域:
块级作用域就是被一对花括号 {} 包围起来的代码块,里面用 let 或 const 声明的变量,只在这对花括号内有效。
这意味着变量的生命周期和可访问范围严格限制在该代码块内。
3- 全局作用域(Global Scope):是 JavaScript 中最顶层的作用域,
任何在函数、块外部声明的变量和函数,都属于全局作用域。
这里就要说一下window对象了。 在ES6之前, window就等同于全局作用域。
但是在ES6之后, JavaScript引入了 let和const的概念。
就导致,window其实只是全局作用域的一部分了。
我们用以下这个图并配上代码去理解
当你运行 JS 时,JavaScript 引擎为每段代码创建一个执行上下文(Execution Context),它包含三个核心组件:
执行上下文(Global / Function)
├── VariableEnvironment(var/function)
├── LexicalEnvironment(let/const/class)
├── ThisBinding(this 绑定)
以下代码中的b,c在全局作用域中,但并未挂载到window上
var a = 1;
let b = 2;
const c = 3;console.log(window.a); // 1
console.log(window.b); // undefined
console.log(window.c); // undefined
原理图如下:
┌──────────────┐
│ GlobalExecutionContext │
│ ┌──────────────┐ │
│ │ VariableEnv │ → window (global object)
│ │ a: 1 │ │
│ └──────────────┘ │
│ ┌──────────────┐ │
│ │ LexicalEnv │ ← 你访问不到
│ │ b: 2 │
│ │ c: 3 │
│ └──────────────┘
└──────────────┘
好的,那我们接下来进入到了关键点了。
三、let和const 的引入与作用域链
我们先看下以下代码:
debugger;
var data = [];
for (var i = 0; i < 3; i++) {data[i] = function () {console.log(i);};
};
data[0]();
data[1]();
data[2]()输出的结果为:
3
3
3
为什么呢?我们可以看下流程。 代码执行就是顺序执行。
先用var声明了一个 data=[]
然后我们进入循环, 3次循环分别赋值
data[0] = function () {console.log(i);}
data[1] = function () {console.log(i);}
data[2] = function () {console.log(i);}
而此时的i是var声明的,所以挂载在window上。经过3次循环
window.i 的值就为3。 向下执行的时候。
data[0]();
data[1]();
data[2]()开始调用函数, 将window.i传入。 返回的值就是3
那有没有办法去解决这个问题呢,我就想打印出0,1,2。 肯定是有的, 这就是let的作用。 我们只用将 var i = 0 换成 let i = 0
var data = [];
for (let i = 0; i < 3; i++) {data[i] = function () {console.log(i);};
};
data[0]();
data[1]();
data[2]()此时的输出结果就是:
0
1
2
为什么呢? 我们再看下流程
先用var声明了一个 data=[]
然后我们进入循环, 3次循环分别赋值
data[0] = function () {console.log(i);}
data[1] = function () {console.log(i);}
data[2] = function () {console.log(i);}
而此时的i是let声明的,所以他挂载在块级作用域上。 三次循环生成3个块级作用域,分别绑定在三个函数上。
data[0]();
data[1]();
data[2]()
向下执行的时候, 三个函数分别使用绑定的块级作用域中的i。 返回值就是0, 1 , 2
总结:
这个案例我们了解到了。
第一: 作用域链的存在,代码在函数或者块级作用域中(局部作用域)执行的时候。会优先获取当前作用域中的变量,如果找不到,会向上层查找。
第二:let和const在块级作用域中声明的变量不会穿挂载到window上。 而var声明的变量可以挂在到window上。
第三:window 与 全局作用域不全等
四、闭包初始模样
到此,我们其实已经离闭包越来越近了。 甚至我们已经用到了闭包。
for (let i = 0; i < 3; i++) {data[i] = function () {console.log(i);};
};
以上代码,就是一个很标准的闭包结构。
我们一直都在背诵两个概念。
1- 函数内部嵌套另一个函数
2- 内函数返回外函数
满足这两个条件就是闭包。
其实这是完全错误的说法。 我们再次回顾一下开头我们从MDN中查找的概念:
A closure is the combination of a function bundled together (enclosed) with references to its surrounding state (the lexical environment).
翻译:
闭包是由捆绑起来(封闭的)的函数和函数周围状态(词法环境)的引用组合而成。换言之,闭包让函数能访问它的外部作用域。在 JavaScript 中,闭包会随着函数的创建而同时创建。
首先闭包是个结构,他不是一个函数。闭包是有函数和他组成的词法环境构成。
其次,闭包让函数能访问他的外部作用域
我们对照这个案例看一下:
我们有函数:
function () {console.log(i);};
也有作用域:
块级作用域中的i,被内层函数访问。
所以 function 函数 和 块级作用域 共同构建成了一个闭包结构。
五、我们熟悉的闭包
好的, 至此。我们已经完全理解了闭包。 那我们把块级作用域改写成函数作用域。
再来看一下我们一直在背诵的逻辑
var result = [];
var a = 3;
function foo(a) {let total = 0; for (var i = 0; i < 3; i++) {result[i] = function () {total += i * a;console.log(total);}}
}
foo(1);
result[0]();
result[1]();
result[2]();
此时,我们把块级作用域跟换成了函数作用域。 虽然我们使用了var去声明了var i=0; 但是var一直在函数作用域内。
并且有内部的result[i]指向的函数使用。 此时就是我们最常熟知的闭包结构。
最后在这里, 我再次声明一下。 闭包是一个结构,他不是一个函数。 闭包是包含了 函数和它所关联的作用域,一起构成的一个结构
另外我们如果用outer函数来表示外部函数,inner函数表示内部函数。 我们的闭包应该是inner和他的作用域一起构成了一个闭包。
===========================================================
六、闭包的实际应用
ok,闭包的原理我们算是真正的理解了。
我们现在知道了原理,那我们要思考的应该是,我们为什么要用闭包呢?
其实在循环中使用let,已经跟我们说明了。 为什么我们要使用闭包。
当我们需要私有化变量的时候,我们就需要用到闭包。 我们不想var出来的变量直接穿透我们的作用域。
不想再我们的结构外层还有操作可以改动我们的变量时,就需要使用闭包了。
看以下代码:
const counter = (function () {let privateCounter = 0;function changeBy(val) {privateCounter += val;}return {increment() {changeBy(1);},decrement() {changeBy(-1);},value() {return privateCounter;},};
})();console.log(counter.value()); // 0counter.increment();
counter.increment();
console.log(counter.value()); // 2counter.decrement();
console.log(counter.value()); // 1
此时,在counter对象中。 privateCounter就是counter的私有变量。 在外面是无法改动函数内部privateCounter的值。
这就是闭包的应用
七、慎用闭包
其实对我们爬虫来说, 我们是不需要使用闭包的,我们要做到的是理解闭包的原理。 从而让我们更好的懂开发逻辑,懂逆向。
我们学习完闭包之后,可能就会觉得,闭包真是个好东西。我们应该多用闭包,甚至每次要私有化变量的时候都用一个外函数套内函数的方式好了。
但是过多的使用闭包会有一个问题, 每个闭包都有它的私有化变量。他们互相隔离,单独的占用一块内存。其实对性能的损耗是很大的。
所以在正常开发中,正确的处理思路是,把需要共享的变量绑定在原型链上,我们可以通过操作原型链来控制对应的变量。
这样就能在解决属性绑定的问题同时解决内存。那原型链我们还不清楚,我们下一章节就是原型链的讲解
最后的最后
本章就到这里。拖得太久,感谢各位佬的收看。如果觉得写得好,还请麻烦点赞收藏。确实是因为之前写文章大家都比较积极的点赞收藏给了我很大的动力。感谢各位大佬