JavaScript ES6 JS函数的执行 闭包
注意,在我们的执行上下文中,是会含有三个部分的:
VO/GO | scope.chain(作用域链)| this
JavaScript 全局代码的执行流程
JavaScript 全局代码在执行前的工作
1. 初始化全局对象
JS 引擎在执行我们的JS代码之前,首先做的就是在堆内存中初始化创建一个全局化对象(Global Object【GO】)
该对象,因为是一个全局化的一个对象,所以说在所有的作用域(Scope)中都是可以进行访问的
再这个全局化对象中包含了: Date Array String Number setTimeout setInterval 等等对象或者方法
其中在内部还有一个 window 属性指向自己
实际上的话,在我们的 NodeJs 中,我们的全局对象才是 Global Object
但是在浏览器环境下的话,这个时候我们的全局对象就是 window 对象
2. 执行上下文(Execution Contexts)
JS引擎的话,内部是含有一个执行上下文栈的(Execution Context Stack,ECS),这个的是用于执行代码的调用栈
即是说,我们的 JS 代码的执行实际上的话是在栈区进行执行的
在我们的 Javascript 中全局的代码块实现执行的时候,首先进行的是创建一个全局的执行上下文(Global Execution Context)
GEC 是会被放在 ECS 中实现执行的
GEC 放置到我们的ECS 的两个步骤:
第一部分就是在执行代码前将JS代码通过解析parse ,解析为我们的 AST 抽象语法树,这个时候会将一些全局定义的变量和函数
- 等添加到 Global Object 中,但是不会进行赋值的操作
- 这个过程就是 变量的作用域提升(hoisting)
第二部分:在 ECS 中执行 JS 代码的收,这一步就需要实现我们的对变量进行赋值操作,或者说执行其他的函数
3.认识 VO 对象(variable object)
这个就是变量对象,变量和函数声明都是会添加进入这个 VO 对象中去的
同时需要注意的是,我们的每一个执行上下文都是会关联一个 VO
所以说通过这一点的了解,那么对于我们的全局的上下文(Global Content)对于的 VO 就是 Global Object(GO)
但是在我们的 VO 中的时候,变量,在这个对象模型中,都是没有赋值的,都是 undefined
只有等待这个 Contexts 进行压缩后,在添加到执行上下文栈后才可以实现赋值操作
在 VO 模型中的时候的设计模式是:
- 对于值类型的数据的话,就是实现的是不复制操作,赋值的为: undefined
- 但是对于函数,实现的是直接实现创建一个 Function Object,在 VO 模型中保存的函数变量指向这个内存地址
- 函数在 VO 模型中就创建的原因是:是为了我们的函数可以在定义之前就可以被使用
var message = "Global Message"
function foo() {
var message = "Foo Message"
}
var num01 = 10
var num02 = 15
var num03 = 10
console.log(num01, num02, num03)
JavaScript 全局代码执行时的工作
在我们的 JavaScript 代码执行时,那就直接进行开始赋值,从上往下开始执行代码,遇到函数直接跳过即可
JavaScript 函数代码执行流程
在我们的 JavaScript 中,只要遇到了一个需要单独执行的函数,那就会导致生成一个新的执行上下文出来
准确来说的话,这个就是因为作用域链的改变导致的
注意还是前面的一句话,我们的任意的一个执行上下文都是需要关联一个 VO 对象的
每一个执行上下文中定义的变量都是添加到了我们的 VO 对象中
在全局作用域中的执行上下文关联的 VO 是 GO(Global Object)
在函数中生成的执行下文关联的 VO 是 AO(Activation Object)
AO 对象的特点:
- 当我们进入函数执行上下文的时候,会创建一个 AO 对象(Activation object)
- 这个 AO 对象会使用一个 arguments 作为初始值,并且初始值是传入的参数
- 这个 AO 对象会作为执行上下文的 VO 来存储变量的初始值
在我们的函数开始执行的时候,那就是对函数中的而每一个变量实现初始值的赋值操作了
JavaScript 的作用域链问题(Scope chain)
回顾JavaScript 中的变量查找规则
- 首先是在本级的作用域中实现寻找我们的变量
- 如果在本层的作用域中没有找到的话,那就是往上一级作用域中寻找变量
console.log(message) // undefined
var message = "global message"
function foo() {
console.log(message) // global meaage
function bar() {
var message = "scope message"
console.log(message) // scope message
function foo_baar() {
console.log(message) // scope message
}
return foo_baar
}
return bar
}
foo()()()
通过上面的例子,我们就可以知道,在 foo_bar 中是没有实现定义我们的 message 的,这个时候,我们就可以实现了解到
如果说我们想要寻找的话message 的话,就是不断的想上一层实现寻找
这个例子还可以重新进行修改,就是在我们的 bar 和 foo_bar 函数之后定义我们的 message ,看看结果是什么
这个就可以使用我们的 VO 思想来实现解释了
出现这种现象的一大原因就是: 我们每一个作用域中的变量在代码执行前先进入 VO 对象模型
然后将其赋值初始值为 undefined ,然后在代码执行的时候才开始实现赋值的操作
但是我们的函数就不同的了,在本层的作用域中,是直接先把函数对象创建出来的,只是里面的变量又需要使用上面的思维
一条一条的来解析执行(还是先添加到 VO 模型中赋值初始值为 undefined,执行代码的时候,才开始真真的赋值操作)
需要我们注意的一点是: VO 只是我们的一个统一称呼,全局作用域为 GO(不是Golang语言哈),函数作用域叫 AO
console.log(message) // undefined
var message = "global message"
function foo() {
console.log(message) // global meaage
function bar() {
console.log(message) // undefined
var message = "scope message"
function foo_baar() {
console.log(message) // undefined
var message = "scope message"
}
return foo_baar
}
return bar
}
foo()()()
// 不建议这么进行书写,这中书写方式就是后期我们的回调地狱
JavaScript 内存管理的理解
通过前面的讲解是不是我们的只要出现以及调用的时候,都是会出现一个对象模型产生在堆区的???
这样的效果肯定是不好的,因为我们的每一台的硬件设备的话,内存肯定是有限的
如果我们一直创建对象的话,这个就有可能导致最终的硬件设备的内存直接吃满,让电脑出现卡顿或者直接退出应用程序
所以说为了解决这一点的忧患,我们上面一直创建出来的对象肯定是会被回收内存的,不会让他持续的存在于我们的堆区中
准确来说每一种编程语言中都是有自己的内存管理模型,大同小异,所以说这个时候,JavaScript 就出现了内存管理模型
不管是什么编程语言,代码的执行都是需要进行分配内存的,只是不同的编程语言有自己不同的分配内存的机制
有一些语言是自己来手动的分配内存,有一些编程语言是自动的分配内存
但是关于内存方面的问题的话,内存管理都是如下的声明周期
- 分配申请的内存
- 使用分配的内存
- 不使用的时候,对内存进行释放
手动分配内存: C | C++ 两个语言是手动的 申请内存和释放内存(malloc 和 free 函数)
自动管理内存: Java | JavaScript | Python | Swift | Dart 等,都是自动的分配内存
JavaScript 会在定义数据的时候,进行我们的分配内存
JS 对原始数据类型的内存分配是直接在栈空间中实现的分配
JS 对于复杂的数据类型而言的话就是在堆内存中进行的开辟一块空间实现的,并且将指向这块内存的指针返回让变量接收引用
JavaScript 中的垃圾回收机制
我们的内存是有限的,所以说当一个程序中不需要一部分数据的时候,那就需要进行的是对该部分的内存回收
现在的大部分的编程语言都是实现的是进行使用 GC (Garbage Collection)的垃圾回收机制
- 对于我们不需要使用的对象,这个就是我们的程序中的垃圾(此垃圾非彼垃圾),那就进行回后得到跟多空余的内存空间
常见的 GC 算法 —— 引用计数(Reference counting)
- 当一个对象有一个引用指向他的时候,那么他的对象引用就 +1
- 当一个对象的引用为 0 的时候,那么这个对象就立即被销毁
常见的引用算法 —— 标记清除(mark sweep)
- 标记清除的核心思想就是可达性(Reachability)
- 这个算法设计了一个根对象(root object),垃圾回收器会定时从这个根开始,找到所有的有引用到的对象,对于没有引用到的
- 对象,就被认为是不可用的对象,直接清除
JavaScript 闭包的使用
JavaScript 函数式编程
在我们的 JavaScript 函数的使用的话,我们主要书写的就是函数
我们可以实现的是自己书写一些高级的函数来实现完成一些特定的功能
或者说直接使用别人书写的内置的一些高阶的函数
现在的框架的开发模式中就含有我们的函数式的编程思想
vue3: composition api: setup函数 --> 代码(自定义 hooks ,实现定义函数)
react: class样式书写组件 -> function 样式的书写组件 -> hooks (所以说,当下的书写react的模式是使用函数式的开发模式)
JavaScript 闭包定义
我们实现的对闭包的定义就是在我们的计算机科学以及JavaScript(或者其他编程语言)中都是有定义的
比如说,我们在 python 书写闭包的时候,就可以自定义装饰器的书写
在计算机科学中对闭包的理解:
- 闭包(Closure),又是我们的词法闭包(Lexical Closure) 或者 函数闭包(function closure)
- 闭包的实现就是定义一个结构体,在我们的结构体里面存储一个函数和一个关联的环境
- 闭包跟函数的最大的区别就是: 当我们捕获闭包的时候,他的自由变量会在捕获时被确定,这样就可以实现脱离捕捉时的上下文,他照样可以继续运行 (计算机科学的描述说了等于没说,裂开!!!)
JavaScript 的由来
- JavaScript 的设计初期的话,是借鉴了 Scheme 这个编程语言的一些思想的
- 闭包就是实现的是: 一个函数和他所在的作用域共同组合成了我们的闭包
- 闭包的本质就是通过的是 作用域链 来实现的访问和他在同一个环境之下的其他变量的
MDN 的描述
- 一个函数和对其周围的状态(lexical environment)的引用捆绑到一起的,这样的组合就是闭包
- 闭包可以让我们实现的是在一个内层函数访问到其外层函数的作用域
- 准确来说,全局作用域就是一个环境,所以说只要我们创建了一个函数,那么同时形成闭包
总结:
- 就是如果说,我们的一种编程语言定义了一个函数后,可以实现访问到函数外层作用域的自由变量,那么这个函数就是闭包
- 可以理解为只要在函数内部对外层作用域中的数据进行了访问,那么这个就是闭包
var data = "我是函数外的数据" function foo() { console.log(data) } foo()function createAddNum(count) { function adder(num) { return num + count } return adder } // 开始实现调用我们的函数,实际上这个也算是后面的柯里化函数的简单书写吧!!! var addNum = createAddNum(5)(10) console.log(addNum)利用我们的这种作用域链的可达性就可以实现让我们的这些对象一直存储在堆区中不被销毁,这个就是我们的可达性算法
内存泄漏: 就是对于我们的永远不会使用的对象,对于我们的垃圾回收机制来说,他是不知道去清理的,因为从根开始,
这些永远是可达的,所以说一直销毁不掉
JavaScript 闭包清理内存
对于形成了闭包的某种情况下的话,为了实现清理我们的内存,这个时候就可以直接将变量实现赋值为 null
来实现清理内存即可
addNum = null=== 注意我们的这里实现作用域链闭环的时候,最关键的就是return的操作,没有这个的话就不会造成可达这样就可以实现手动的清理内存了,哈哈哈!!!
这一点就跟以前谈论基础数据类型的时候:我们为什么把 null 类型 和 对象类型联合起来讲述的原因了,我们形成闭环了!!!