文章目录
写在前面
- 闭包:在js中指的是一个函数
- 执行上下文:
- this:该记录的一个属性,在函数执行的过程中用到
- 作用域:是一个在何处用/如何查找变量的规则
- 变量对象VO:创建执行上下文时,会有一个变量用来存储执行上下文中的函数或变量
- 活动对象AO:进入执行上下文时,该上下文中的所有变量和对象都可以被访问到【激活状态】
- 拓展【js垃圾清除】方式
- 标记清除【对象是否可以获得】
- 给内存中的所有变量加上标记
- 去掉被使用变量的标记
- 下次再加上标记的就是准备被清除
- 引用计数【对象是否不再需要】
- 当一个对象被引用的时候,该对象的引用次数就+1
- 如果引用该对象的对象又引用了其他值,那么之前被引用的对象的引用次数就-1
- 垃圾回收会清除引用次数为0的对象所占的内存
一、闭包
1. 定义
有权访问其他函数内部作用域变量的函数
连接函数内部和外部的桥梁
闭包的严格定义:
- 环境部分
- 函数的词法环境
- 标识符列表:函数中用到的未声明变量
- 表达式部分:函数体
2. 作用
延长作用域链【让一个变量一直保存在内存里,不污染全局变量】
function a(){ //在这个函数中,b依赖于a
var count = 0;
function b(){
count++
console.log(count)
}
return b;
}
var c= a() //c调用a函数,return的是b,所以c引用了b,但是a被间接引用,a不会被垃圾回收
c()//1
c()//2
c()//3
//每次调用c都会使count+1,因为a中的count一直保存咋内存中
3. 注意
- 使得函数的变量保存在内存中,使得内存消耗很大,还可能造成内存泄漏【解决:在退出函数之前,将局部变量设置为null/删除】
- 闭包可能改变函数内部变量的私有属性
4. 与普通函数的区别
- 携带了执行环境
- 占用更多的内存
二、执行上下文
以下:是三种概念的变化
ES3:
- 作用域【scope】
- 变量对象【variable object】:用于存储变量的对象
- this值【this value】
ES5:
- 词法环境【lexical environment】:获取变量时使用
- 变量环境【variable environment】:声明变量时使用
- this值
ES2018
- 词法环境【lexcal environment】:获取变量/this值时使用
- 变量环境【variable environment】:声明变量时使用
- 用于恢复代码的执行位置
- 执行的任务时函数的使用,表示正在被执行的函数
- 执行的任务是脚本/模块化时调用,表示正在被执行的代码
- 使用的基础库和内置对象实例
- 仅生成器有上下文这个属性,表示当前生成器
执行上下文的定义
1. 执行上下文:
函数调用时,创建的活动记录。这个记录里包含了函数调用时需要的信息
2. 执行上下文的作用
- 定义函数或变量访问其他数据的权利
- 可以决定他们的各自行为
3. 创建原因
当js引擎进入不同的运行环境就会创建对应的一个执行上下文
4. 运行环境
- 全局 window
- 函数
- eval
下面介绍一下执行上下文
var name = 'window';
outer();
function outer(){
var name = 'outer';
inner();
//函数内部的函数
function inner(){
var name = 'inner';
console.log(name);
}
}
画了一张草图,介绍了一下他们之间的关系:【自行理解一下啊】
执行栈
- 多个执行上下文形成执行上下文栈
- 顶层是当前执行上下文
- 底层是全局执行上下文
执行栈的调用关系
在ECMAScript中,代码又三种类型:global、function、eval
每一种代码的执行都需要依赖自身的上下文【当然,global可能涵盖了更多的function和eval实例】
函数的每一次调用,都会进入函数执行中的上下文,来计算函数中变量的值
对于eval也是一样,也会进入eval的执行上下文,判断应该从何处获取变量的值
注意
一个函数可能产生无限的执行上下文,因为函数的调用(递归)产生了一个新的上下文环境
function foo(bar){
}
// 调用相同的function,每次都会产生3个不同的上下文
//(包含不同的状态,例如参数bar的值)
foo(10);
foo(20);
foo(30);
一个执行上下文可以激活另一个上下文,【函数调用】
- 激活其他上下文的某个上下文被称为调用者(caller)
- 被激活的上下文被称为被调用者(callee)
被调用者也可能是调用者【比如一个在全局上下文中调用自身的内部方法】
在arguments中有介绍,
caller返回调用该函数的对象
calee返回正在被执行的函数
执行上下文的调用关系:
- 当一个caller 调用一个callee,那么这个caller就会暂停自己的执行,将控制权交给这个callee
- 这个callee会进入堆栈【进行中的上下文(active execution context)】,但这个callee的上下文执行结束后,会把控制权交回给caller
- caller会继续执行。这个caller结束之后,会继续触发其他的上下文
- 一个callee可以用return/exception 来结束自身的上下文
如下图,所有的 ECMAScript 的程序执行都可以看做是一个执行上下文堆栈 [execution context (EC) stack]。堆栈的顶部就是处于激活状态的上下文。
执行栈的调用关系
- 一段程序开始时,会先进入全局执行上下文环境【也就是执行栈最底部的元素】,全局执行程序会初始化,生成必要的对象和函数
- 在执行过程中,可能会激活一些方法【也有可能是已经初始化过的】,然后进入他们的执行上下文,将新的元素入栈
- 当这些初始化结束之后,这个系统会等待一些事件【鼠标点击】,会触发一些方法,然后进入他们的执行上下文
见下图,有一个函数上下文“EC1″和一个全局上下文“Global EC”,下图展现了从“Global EC”进入和退出“EC1″时栈的变化:
5. 创建执行上下文的过程
5.1. 创建变量对象VO
创建执行上下文时,会用一个变量对象,存储执行上下文中的变量/函数
分类
- 形参
- 函数
- 变量
5.2. 创建作用域链
5.2.1. 作用域
1. 定义
用于在何处以及如何查找变量的【规则】
2. 作用域的解析规则【LHS、RHS】
- 先声明
- 在赋值
var a = 2
1. var a //声明
2. a = 2 //赋值
3. 作用域的分类
- 全局作用域
- 最外层函数
- 未定义直接赋值的变量
- windows对象
- 函数作用域【函数内部】
- 块级作用域【let/const/{}】
- 不存在变量提升【不允许变量在声明之前使用】
- 暂时性死区【let/const声明的变量绑定这个区域,不受外部的影响,如果在声明之前使用就会报错】
4. 暂时性死区和变量提升的区别:
变量提升:
console.log(a)//ReferenceError
let a = 1
暂时性死区:
//代码1
var a = 2
if(true){
console.log(a)//ReferenceError
let a = 3
}
我觉得大家看了上面两段代码,肯定跟我一样,第一眼就觉得这两个概念好像一样,但是其实不是,我们将下面的代码改一下
//代码2
var a = 2
if(true){
console.log(a)//2
//let a = 3 注释let声明后,就会输出a的值,原因【内部作用域可以访问全局作用域】
}
当我们改成let声明使,也会输出2,原因和上面一样【内部作用域可以访问全局作用域】
//代码3
let a = 2
if(true){
console.log(a)//2
//let a = 3
}
解答:
细心的朋友可以发现,代码1比下面两段代码多了一句就是在,if内部使用了let重新声明了变量a
也就是说,在块级作用域中,如果使用了let 声明,那么这个变量就会绑定这个区域,不受外部影响,如果变量在声明之前使用,则会报错【对于const也是一样】
let b = 5
{
console.log(b)//ReferenceError
let b = 10
}
注释let声明后,又对了
let b = 5
{
console.log(b)//5
// let b = 10
}
5.2.2 作用域链
1. 作用域链作用
- 内部作用域可以访问外部作用域
- 保证执行环境对变量/函数的有序访问
2. 作用域链的顺序
- 最前端【当前代码所在环境的变量对象】
- 如果是函数环境【活动对象】
活动对象:进入到执行上下文中,该上下文中的所有变量和对象都可以被访问到,激活状态
也就是说在作用域链的最前端,如果是函数的话,活动对象作为变量对象
活动对象最开始只包含一个变量:arguments对象 - 如果是全局环境,则不存在
- 来自下一个包含(外部)环境的变量对象
- 全局执行环境
5.3. 确定this指向
5.3.1. 定义
this指向上下文这个记录的属性,在函数执行过程中用到
5.3.2. this指向
取决于函数在哪里调用【函数调用时发生的绑定】
- 全局,this指向window
- 当函数被某个对象方法调用时,this指向那个对象
var name = 'hahaha'
function test(){
var name = 1
console.log(this.name)//hahaha
}
test()//全局调用了
console.log(this.name)//hahaha
var name = 'i am window'
var a = {
name: 'i am an apple',
fn: function(){
console.log(this.name)
}
}
var f = a.fn//定义了一个全局变量,并赋值了一个方法
f();//i am window
下面这段代码是将a.fn()的调用后的结果赋给f
var name = 'i am window'
var a = {
name: 'i am an apple',
fn: function(){
console.log(this.name)
}
}
var f = a.fn()//i am an apple
下面这段代码,最后调用是在全局中的test(),所以还是window
var name = 'window'
function test(){
var name = 'apple'
innerFun();
function innerFun(){
console.log(this.name)
}
}
test()//window
5.3.3. 改变this指向
- call和apply改变this指向【区别只是参数传递方式不同】
call
var name = 'i am window'
var a = {
name: 'i am an apple',
fn: function (a, b) {
console.log(this.name)
console.log(a + b)
}
}
var f = a.fn
f.call(a,1,9)
//i am an apple
// 10
apply
var name = 'i am window'
var a = {
name: 'i am an apple',
fn: function (a, b) {
console.log(this.name)
console.log(a + b)
}
}
var f = a.fn
f.apply(a,[1,9])
//i am an apple
// 10
- bind【忽略当前this的绑定,将this绑定在提供的对象上,并生成一个新函数】
var name = 'i am window'
var a = {
name: 'i am an apple',
fn: function (a, b) {
console.log(this.name)
console.log(a + b)
}
}
var f = a.fn
f.bind(a,1,9)//没有输出
f.bind(a,1,9)()//i am an apple 10
//或者用下面的方式输出【切记,test指向a】
var test = f.bind(a,1,9)
test() //i am an apple 10
参考文献:
https://www.cnblogs.com/TomXu/archive/2012/01/12/2308594.html
https://www.cnblogs.com/TomXu/archive/2012/01/12/2308594.html