全局作用域(var)
基于早期ECMA的版本规范:
GEC(global excution context)全局执行上下文:执行全局代码
FEC(functional excution context)函数执行上下文:执行函数代码
每一个执行上下文会被关联到一个变量环境(variable object,VO),在源代码中的变量和函数声明会被作为属性添加到VO中
对于函数来说,参数也会被添加到VO中
- 解释
- 不管执行的是哪个,都会关联到VO对象(variable object),只是这个VO对象所代表的东西不一样而已
- 参数被添加到VO中,是形参的那个地方
代码执行过程
foo
函数
function foo(){ //foo函数并没有特殊的含义,是编程约定俗成的一种习惯(定义我们不知道要取什么名字的东西) }
- 函数变量提升
function foo(){ console.log("小余") } foo() //会在控制台打印出"小余"
foo() function foo(){ console.log('小余') } //一样在控制台能够打印出来'小余'
全局函数执行过程
编译阶段是js->AST的时候就确立了
变量对象的定义:变量对象是一个特殊的对象,并且与执行上下文息息相关,VO(变量对象)里面会存有下列内容:
- variables(var,variableDeclaration);
- function declarations(FD)
- function formal parameters
AO对象
函数执行的前一刻,会创建一个称为执行期上下文的内部对象(AO)。一个执行期上下文定义了一个函数执行时的环境,函数每次执行时对应的执行上下文都是独一无二的,所以多次调用一个函数会导致创建多个执行上下文,当函数执行完毕时,它所产生的执行上下文会被销毁。
- 讲人话就是:
AO对象
在函数执行上下文里面
- 当我们创建了函数的时候,js引擎会重新开辟一块空间来进行存储(编译阶段)
- 保存父级作用域(函数的上一层作用域)
- 保存函数的执行体(就是执行的代码块)
- 开辟的内存空间一般是0x开头
- 我们在
GlobalObject
(全局对象)中一般是放入了我们函数的名字,例如foo。 - 然后在全局对象中的foo再引用了对应存储函数的内存空间,也就是保存的是指向该函数的内存地址的指针
- foo()的()是调用的意思,执行之后就会放入函数的调用栈中(调用栈会再创建出来一个
函数执行上下文(Functional Execution Context)
,在里面会有一个类似GOglobal object
的东西,叫做AOActivation Object
- 在执行函数之前会先创建AO对象,会将函数的内容提升到AO对象中,此时函数里面的内容都是undefined,当我们在AO开始执行函数代码的时候,函数内的undefined逐渐被替换掉执行的内容
- 函数里所有代码执行完了之后,函数的执行上下文就会被弹出栈,执行上下文就会销毁掉。此时AO如果没有人指向它的话,也会跟着一起销毁掉
- 如果后续在后面又调用了一遍foo(),然后没准还传了一些参数进去,那刚刚的过程又会重复执行了一遍
作用域链
- 当我们查找一个变量时,真实的查找路径是沿着作用域链来查找的
- 作用域链组成
- VO(variable Object 变量对象)
- Parent(父级作用域)
- 只有函数才会产生作用域,父级作用域在编译阶段就被确认了
全局代码执行过程(函数嵌套)
- 函数里面如果嵌套函数的话,这个时候进行执行的时候,嵌套函数是没有被编译的,而是预编译。等AO对象被创建的时候,它才会被正式编译
var name="why" foo(123) function foo(num){ console.log("1",m); var n = 10 var m = 20 function bar(){ console.log("2",name) } bar() } //结果如下 1 undefined 2 why
- 此时如果我们把
var name="why"
注销掉,会发现控制台的2 why
并没有消失
- 因为windows的属性上面本来就有name这个属性,当我们注销掉的时候,值已经保存在windows的name里面了,控制台打印一下windows能找到
- 所以最好的办法就是我们给name换一个变量名,比如XiaoYu
- 然后重复上面操作注销掉XiaoYu这个变量,此时就出现我们想要看到的结果:找不到了
函数调用函数执行过程
- 函数调用函数的作用域查找
//message打印出来的是哪里的内容 var message = "小余" function foo(){ console.log(message) } function bar(){ var message = "大余" foo() } bar() //打印结果:小余
- bar()执行的时候会调用一个AO,也就是函数执行上下文
- AO里面是一个message,此时还是undefined。下一步往里面填入"大余"
- 再下一步执行foo(),这个时候就又创建出来一个执行上下文的空间用来执行bar里面的foo函数。这个执行上下文就又是一个AO,但此时这个AO里面是空的
- 此时要取找message,是按照作用域链去查找的,查找的话首先是从自己身上查找,foo自己身上是没有message的,这里的执行上下文是空的,那下一个就是父级作用域,注意这里的父级作用域,不是看你foo()在哪调用的上一级,而是你函数写在哪的上一级,foo函数跟bar函数是并列的,他们的上一级都是全局作用域了,在全局作用域上面的也就只有
var message="小余"
,所以打印的结果为小余 - 对于函数来说AO其实就是VO
以上的都是ES6之前的概念(也就是ES5)
新的ECMA标准-变量环境和环境记录
在最新的ECMA的版本规范中,对一些词汇进行了修改
每一个执行上下文会关联到一个变量环境
(Variable Environment)中,在执行代码中变量和函数的声明会作为环境记录
(Environment Record)添加到变量环境中。
对于函数来说,参数会被作为环境记录添加到变量环境中
- 解释
- VO对象(对应AO)被Variable Environment(变量环境)替代
- VO对象(对应GO)被VE,也就是Variable Environment,都是变量环境进行了替代
- 变量环境里的是环境记录
作用域提升面试题
面试题1
在foo()里面没有声明n,自己没有就会向父级作用域去找,在父级作用域找到var n = 100,然后进行了覆盖操作,此时我们再打印n出来就是200了
我们打印的时候,console.log(n)的n其实不是值,而是存放指向值存放的内存空间的指针
var n = 100 function foo(){ n = 200 } foo() console.log(n) //答案是200
面试题2
函数内的是foo对象,函数外面的是go(global Object)对象、下面全部统一称呼go对象
流程步骤:
- 最先触发var m = "大余",go对象:{m:undefined} => {m:"大余"}
- 然后执行foo()函数,创造出一个AO对象(执行上下文),此时还处于编译状态,AO对象:{m:undefined},注意了,这个时候查找顺序是从自身开始查找,自身没有才去父级查找,而
大余
这个变量信息是在foo()的父级作用域中,所以之后才会去查找。此时执行上下文里面要去打印console.log(m),先在自己的作用域链中查找,而此时m已经在自身作用域中被赋值了一个undefined,但为什么不往父级找呢?首先这边执行上下文并没有结束,而在下面的时候就找到了var m = "小余"了,所以他就不会在将AO对象的执行上下文都执行结束后再去父级作用域寻找 - 最后执行了执行上下文中var m = "小余",将AO对象里的m的内容替代成了
小余
,,然后继续往下执行打印,打印出结果
function foo(){ console.log(m) var m = "小余" console.log(m); } var m = "大余" foo() //结果如下 //undefined //小余
面试题3
跟前面已经大同小异了
- foo1函数的执行结果,如果自身作用域内没有找到n,就会沿着父级作用域寻找,然后foo1是在foo2函数内调用的,父级作用域并不取决于在哪调用,而取决于你函数体处于哪里,foo1的作用域是跟foo2的作用域平级的,他们的父级作用域都是最外层的全局作用域。
- 然后foo2内部首先自己创建出来一个AO对象,再AO对象里创建一个执行上下文,里面先对编译阶段的{n:undefined}进行赋值200,然后通过console.log进行了打印,接着调用了foo1()函数,这foo1()函数答案为一百,在上一步中我们已经进行分析了
- 接着就是调用了foo2(),先打印了foo2中赋值的200,再打印foo1中的100。最后打印了最外层的n,100。这里最外层的打印只能打印100,100如果注销掉就报错,因为显而易见的,全局作用域基本上已经是最大的作用域了,再往上就找不到了,而这个是不会向函数内部去往下找的,且函数执行完后,他的执行上下文就销毁掉了
var n = 100 function foo1(){ console.log("这是foo1内部",n); } function foo2(){ var n = 200 console.log("这是foo2内部",n); foo1() } foo2() console.log("这是最外层",n); //执行结果顺序如下 //这是foo2内部 200 //这是foo1内部 100 //这是最外层 100
面试题4
- 首先最外层,一个GO对象(Global Object):{a:undefined,foo:0xa00},foo的0xa00是内存地址,然后a被赋值为100
- 然后到foo函数部分,生成AO对象,AO对象里面是执行上下文,首先a的内容肯定是先为undefined,接着就return了,后面的var a = 100都还没生效foo函数就结束了,在编辑器中会给出提示:检测到无法访问的代码。但是还是请注意,这个执行上下文中还是出现了a这个变量,虽然完全没有用上,但是他意味着我们的执行上下文中还是出现了a这个变量,阻止了我们向父级作用域继续寻找的道路,所以我们访问不到全局作用域的100
- 最后就只能返回undefined了
var a = 100 function foo(){ console.log(a) return var a = 200 } foo() //undefined
面试题5
var a = b = 10会转化为两行代码
- var a = 10
- b = 10(没错,b没有被var声明),从右向左,先给b赋值
所以很显然,外面作用域是访问不到a,但是能访问到b的,不然你把console.log(a)注释掉,就可以正常显示控制台信息的b为10了
function foo(){ var a = b = 10 } foo() console.log(a); console.log(b); //会报错
作用域补充
没有声明直接使用,严格来说,语法都错了,应该要报错的,因为我们甚至不知道这个变量是怎么来的,但是JavaScript的语法太灵活了,他允许了这种写法,但是最好不要这样写,就当作了解就行
function foo(){ m = 200 } foo() console.log(m); //200
内存管理
认识内存管理
- 不管什么样的编程语言,在代码的执行过程中都是需要给它分配内存的,不同的是某些编程语言需要我们自己手动的管理内存,某些编程语言会自动帮助我们管理内存
- 不管以什么样的方式来管理内存,内存的管理都会有如下的生命周期:
- 第一步:跟配申请你需要的内存(申请)
- 第二步:使用分配的内存(存放一些东西,比如对象等)
- 第三步:不需要使用时,对其进行释放
- 不同的编程语言对于第一步和第三步会有不同的实现
- 手动管理内存:比如C、C++,包括早起的OC,都是需要手动来管理内存的申请和释放的(malloc和free函数);
- 自动管理内存:比如Java、JavaScript、python、Swift、Dart等,它们有自动帮助我们管理内存
- 通常情况下JavaScript是不需要手动管理的,JavaScript会在定义变量的时候为我们分配内存
//创建对象 //Java代码 Person p = new Person() //JavaScript代码 var obj = {name:"why"}
内存分配方式
- JavaScript对于基本数据类型内存的分配会在执行时,直接在栈空间进行分配
- JavaScript对于复杂数据类型内存的分配会在堆内存中开辟一块空间,并且将这块空间的指针返回值变量引用。我们一般也称呼这个为引用类型
- 栈空间存放的是地址,真正的对象实例存放在堆空间中
知识点补充:
- 简单类型和复杂类型
简单类型
简单类型又叫做基本数据类型或者值类型
- 值类型: 简单数据类型/基本数据类型,在存储变量中存储的是值本身,因此叫做值类型
String、number、Boolean、undefined、null - 复杂类型
复杂类型又叫做引用类型 - 引用类型: 复杂数据类型,在存储变量中存储的仅仅是地址(引用),因此叫做引用数据类型,通过new关键字创建的对象(系统对象、自定义对象),如Object、Array、Data等
JavaScript的垃圾回收
- 因为内存的大小是有限的,所以当内存不再需要的时候,我们需要对其进行释放,以便腾出更多的内存空间
- 在手动管理内存的语言中,我们需要通过一些方式自己来释放不再需要的内存,比如free函数
- 但是这种管理的方式其实非常的低效,影响我们编写逻辑的代码的效率;
- 并且这种方式对开发者的要求也很高,并且一不小心就会产生内存泄露;
- 所以现在大部分现代的编程语言都是有自己的垃圾回收机制:
- 垃圾回收的英文是Garbage Collection,简称
GC
; - 对于那些不再使用的对象,我们都称之为垃圾,它需要被回收,以释放更多的内存空间
- 而我们的语言允许环境,比如Java的运行环境JVM,JavaScript的运行环境js引擎都会内存(内置) 垃圾回收器;
- 垃圾回收器我们也是简称GC,所以在很多地方你看到的GC其实是指垃圾回收器
- 关键问题:GC怎么知道那些对象不再使用的呢?
- 涉及到GC的算法 =>作为子标题内容进行扩展
常见的GC算法 - 引用计数
对象里面有一个专门的空间,叫做 retain count,专门记录有多少个指针指向自己的retain count(一个指向加1),默认为0,但通常最少是1,因为你在栈里面存放的地址已经就指向堆内存了),这个计数器(retain count)是实时更新的,当这个计数器为0的时候,垃圾回收机制就知道这个对象已经没有人在使用了,就会触发回收机制销毁掉
引用计数弊端:
- 循环引用
var obj1 = {friend:obj2} var obj2 = {friend:obj1} //这样互相引用如果不obj1 = null结束的话,会产生内存泄漏的
var obj = {name:"小余"} var info = {name:"大余",friend:obj} var p = {name:"超大余",friend:obj}
常见的GC算法 - 标记清除
- 这个算法是设置一个根对象(root object),其实就是GO(Global Object),垃圾回收器会定期从这个根开始,找所有从根开始有引用到的对象,对于哪些没有引用到的对象,就认为是不可用(不可达)的对象
- 这个算法可以很好的解决循环引用的问题(因为被认为不可用的对象会在下一回中被回收掉)
- JS引擎笔记广泛的采用就是标记清除算法,当然类似V8引擎为了进行更好的优化,它在算法的实现细节上也会结合一些其他的算法
JavaScript高级笔记-coderwhy版本(二)https://developer.aliyun.com/article/1469638