一、作用域
JavaScript中最为重要的⼀个概念:执⾏环境(作用域),定义了变量或函数有权访问的其他数据,决定了它们各⾃的⾏为。每个执⾏环境都有⼀个与之关联的变量对像(variable object),环境中定义的所有变量和函数都保存在这个对象中。虽然我们编写的代码⽆法访问这个对象,但解析器在处理数据时会在后台使用它。
在Web浏览器中,全局执⾏环境被认为是window对象,因此所有全局变量和函数都是作为window对象的属性和⽅法创建的。
注:某个执⾏环境中的所有代码执⾏完毕后,该环境被销毁,保存在其中的所有变量和函数定义也随之销毁(全局执⾏环境直到应⽤程序退出——例如关闭网页或浏览器时才会被销毁)。
作用域:变量起作用的区域。
注:ES6块作用域只let const能识别,var声明的变量不支持块作用域。
JS: ES5 全局作用域(独立js文件 script,所有的全局作用域互通),函数作用域。
ES6 新增块级作用域 {}
函数作用域:函数的花括号{}里面所有的区域。函数身体出现在什么位置,该函数作用域就在那个位置,跟函数的调用没有关系。
作用域因素:
(1)全局变量:在整个程序的任何位置都能使用。
(2)局部变量:定义在函数内部的变量,只有在函数的范围内才能使用。
作用域应用:
全局作用域:
全局作用域在页面打开时创建全局对象GO(window对象),页面关闭时销毁GO对象。
全局作用域中的变量是GO对象的属性名,变量的值是GO对象的属性值。
全局执行步骤:
1.创建全局GO对象仓库
2.找到全局变量声明,提升变量声明
3.找到函数声明,提升函数声明
例如:
GO{
n : 10
fn : function fn(){} (函数)
fn1: function (){} (函数体)
}
函数作用域:
函数作用域在函数执行时创建AO对象,在函数结束时销毁AO对象。
函数作用域中的变量是AO对象的属性名,变量的值是AO对象的属性值。
下一次执行函数时,会创建全新的A0对象。
函数作用域(局部)执行步骤:
function fn(a) {
处理函数内部的代码
语法解析:预解析(预编译)四步
1. 创建AO对象
2.找到变量声明和形参,赋予默认值为undefined
3. 将实参的值赋值给形参
4. 找到函数声明,然后提升
例如:
注:js是单线程的语句,所以会解释一行执行一行,但是函数声明的提升不受限。
二、JS的执行
执行:从全局作用域进入。先执行全局作用域GO,再执行局部作用域AO。
(每激活一个作用域时,都会有两个步骤,1,预编译期, 2. 执行期)
1. 预编译期:
找var声明;
找function声明;
注意: function声明的函数名如果跟var声明的某个变量重名了,function 统一会覆盖var声明的变量
2. 执行(变量赋值,算术运算、)
例如:
注:
(1)函数调用时表达式的调用:
函数声明直接加括号是不能调用函数的
(function sum(a, b) {
console.log(a + b)
})(10, 20);
则这种写法整个函数将不再是函数声明而是一个函数表达式。
(2)重名问题:
给一个变量赋予不同的值,普遍都是后面的覆盖前面的。
特例:如果变量名和函数名重名的话, 以函数为优先。
如果遇见标识符相同冲突,这个标识符给函数。
(3)提升问题:
函数表达式会提升,但是提升的是变量的声明,不是函数。
函数的提升只有函数声明方式定义的函数才会进行提升。
(4)特殊事项:
不声明直接赋值的变量会成为全局变量。
两个函数同时操作同一个变量,一个增加,一个减少,即:函数和函数的通信。
初始值定义在函数外,累加重复调用函数的时候,不会重置。
全局的变量或者函数会自动成为window的属性。
在使用变量或调用函数是可以使用window.变量或window.函数()。
函数的形参会默认定义为函数的局部变量。
三、作用域链
作用域链:当函数执行时会生成AO对象,并且把这个AO对象放在[[scope]]范围内和函数创建时的环境里,形成链式结构。作用域链就是函数的[[Scopes]]属性中保存了多少个作用域 ,其中存储了AO对象的集合。在函数内使用某个变量的时候,会按照[[Scopes]]里的作用域的先后属性查找变量。
即:作用域链 = 函数执行时的AO对象 + 函数创建时的环境
先判断标识符(变量名和函数名)的查询规则,再将一个一个作用域连接在一起解析。
步骤事项:
1.当遇见一个定义的变量名时,JS引擎会从其所在的作用域依次向外层查找,
会在查找到第一个匹配的标识符的时候停止,并赋匹配值给变量。
2.多层嵌套:如果有同名的变量,并且都有值,那么就会发生“遮蔽效应”:
隔绝相同变量作用,不会产生值的覆盖,在变量作用的作用域内寻找匹配值赋值。
3.一个变量在使用的时候得属性值几呢?就会在当前层去寻找它的定义,找不到,
找上一层function,直到找到全局变量,如果全局也没有,就报错。
4.函数是一个对象,有些属性我们可以访问,比如name,length,arguments.
但有些不可以,因为有些属性仅供Js解释引擎存取,[[scope]]就是其中一个。
5.变量查找规则:沿着当前函数作用域链到作用域链顶端,自上而下寻找变量,
作用域由内而外查找变量属性值。
四、垃圾收集
JavaScript具有⾃动垃圾收集机制,也就是说,执⾏环境会负责管理代码执⾏过程中使⽤的内存。在编写JavaScript程序时,开发⼈员不⽤再关⼼内存使⽤问题,所需内存的分配以及⽆⽤内存的回收完全实现了⾃动管理。这种垃圾收集机制的原理为:找出那些不再继续使⽤的变量,然后释放其占⽤的内存。因此,垃圾收集器会按照固定的时间间隔(或代码执⾏中预定的收集时间),周期性地执⾏这⼀操作。
函数中局部变量的正常⽣命周期:局部变量只在函数执⾏的过程中存在。⽽在这个过程中,会为局部变量在栈(或堆)内存上分配相应的空间,以便存储它们的值。然后在函数中使⽤这些变量,直⾄函数执⾏结束。此时,局部变量就没有存在的必要了,因此可以释放它们的内存以供将来使⽤。在这种情况下,很容易判断变量是否还有存在的必要,但并⾮所有情况下都这么容易就能得出结论。垃圾收集器必须跟踪哪个变量有⽤、哪个变量没⽤,对于不再有⽤的变量打上标记,以备将来收回其占⽤的内存。⽤于标识⽆⽤变量的策略可能会因实现⽽异,但具体到浏览器中的实现,则通常有两个策略:
1、标记清除
垃圾收集器在运⾏的时候会给存储在内存中的所有变量都加上标记(当然,可以使⽤任何标记⽅式)。然后,它会去掉环境中的变量以及被环境中的变量引⽤的变量的标记。⽽在此之后再被加上标记的变量将被视为准备删除的变量,原因是环境中的变量已经⽆法访问到这些变量了。最后,垃圾收集器完成内存清除⼯作,销毁那些带标记的值并回收它们所占⽤的内存空间。
2、引用计数
另⼀种不太常⻅的垃圾收集策略叫做引⽤计数。引⽤计数的含义是跟踪记录每个值被引⽤的次数。当声明了⼀个变量并将⼀个引⽤类型值赋给该变量时,则这个值的引⽤次数就是1。如果同⼀个值⼜被赋给另⼀个变量,则该值的引⽤次数加1。相反,如果包含对这个值引⽤的变量⼜取得了另外⼀个值,则这个值的引⽤次数减1。当这个值的引⽤次数变成0时,则说明没有办法再访问这个值了,因⽽就可以将其占⽤的内存空间回收回来。这样,当垃圾收集器下次再运⾏时,它就会释放那些引⽤次数为零的值所占⽤的内存。
注:垃圾收集机制对性能的影响问题有待考察,策略有待更新。
五、闭包
闭包:一个函数可以把它自己内部的语句和自己声明时所处的作用域一起封装成了一个密闭环境,称为“闭包” (Closures)。
形成闭包特点:把一个函数从它定义的那个作用域整体挪走运行。会发现这个函数能够记忆住定义时的那个作用域。不论函数走到哪里,定义时的作用域就会带到哪里。
闭包的全新特性:定义不同属性名,将同一具有闭包特点的函数声明调动赋给属性名,则每次重新引用函数的时候,闭包是全新的。互不影响。
无论它在何处被调用,它总是能访问它定义时所处作用域中的全部变量。
闭包的产生:
1. 函数嵌套函数
2. 内部函数引用外部函数的变量或者参数,并使用return返回了该内部函数。
闭包的作用:
实现共有变量:比如:累加或累减功能
可以做缓存数据(存储结构)
闭包宜处:
因为闭包不会被销毁, 导致闭包内的变量一直存在, 可以用来保存调用长久会使用到的数据。
变量可以常驻内存。不能从外部去干扰该变量,保证了数据的安全。
闭包缺陷:
闭包内的变量不会被垃圾回收机制回收,会一直常驻内存里。
闭包会导致作用域不会被销毁, 作用所占用的内存不会被释放, 占内存影响性能。