一、作用域、作用域链
1. 作用域
作用域指的是代码中的变量、函数和对象的可访问性。ES5 只有全局作用域和函数作用域,ES6 新增了块级作用域,可通过let / const体现。
「JS 采用的是 静态/词法作用域,也就是说变量的作用域在定义时就确定了,一个函数能够访问到的上层作用域,在函数"创建"时就已经被确定,和函数在哪里"调用执行"无关。」 如果是动态作用域,那么作用域和函数执行的环境有关。
2. 作用域链
2.1 自由变量
当前作用域中没有定义的变量,就叫做自由变量。在函数 f 中,取自由变量的值时,要到 "创建 f 函数"的那个作用域中取,无论 f 函数在哪里被"调用"。,这也就是 “静态作用域”。
var a = 2;
function f() {
console.log(a); // 自由变量
}
function show(f) {
var a = 20;
(function() {
f();
})();
}
show(f); // 2
2.2 作用域链
查找一个自由变量的值时,会先在当前作用域寻找,找不到的话就会到它的父级作用域寻找,逐层向外查找直到找到全局window对象,如果全局作用域还没有就返回undefined,这一层层的嵌套关系就是 作用域链。
3. 块级作用域
花括号内的区域就是块级作用域。
自执行函数
函数定义后想要立即调用的解决方法:
(1)使用 IIFE:将整个 function 放在一个圆括号里。
// IIFE:立即调用的函数表达式,记得后面加分号!
(function() {})();
// or
(function() {}());
IIFE 的目的:不用给函数命名,避免污染了全局变量;IIFE 内部形成了一个单独的作用域(类似块级作用域),可以封装一些外部无法读取的私有变量。
(2)在函数表达式后直接加圆括号调用
var f = function f(){}();
二、执行上下文
1. 什么是执行上下文
执行上下文:当前代码执行的一个环境与作用域。
当 JS 引擎解析到可执行代码时(通常是函数调用阶段),会先做一些执行前的准备工作,这个 “准备工作” 就叫做 执行上下文 或 执行环境。JS 的任何代码都是在执行上下文中运行的。
2. 执行上下文的三种类型
-
全局执行上下文:一个程序只会存在一个全局上下文,函数外的代码都在全局执行上下文中。它做了两件事:
- 创建一个全局对象,以浏览器环境为例就是 window。
- 将 this 值绑定到这个全局对象上。
-
函数执行上下文:每当调用函数时,都会为该函数创建一个新的函数执行上下文(不管这个函数是不是被重复调用的)。
-
eval 函数执行上下文:运行在 eval 函数中的代码也有自己的执行上下文,使用少。
3. 执行上下文栈【重要】
执行上下文栈,也叫调用栈,用来存储在代码执行期间创建的所有执行上下文。
当 JS 引擎首次读取代码时,会创建一个 全局执行上下文 并将其 推到 当前的执行栈,以后每调用一次函数,引擎都会为函数创建一个 函数执行上下文 并将其 推到 当前执行栈内。
「 js 引擎会运行执行上下文在执行栈顶端的函数,当栈顶函数运行完成后,将其弹出,上下文控制权将移到当前执行栈的下一个执行上下文。」
代码示例:
function first(){
second();
console.log(1);
}
function second(){
third();
console.log(2);
}
function third(){
console.log(3);
}
first(); // 3 2 1
执行过程如下:
// 1. 代码执行前创建全局执行上下文
ECStack = [globalContext];
// 2. first 调用
ECStack.push('first functionContext');
// 3. first 调用了 second ,等待 second 执行完毕再输出 1
ECStack.push('second functionContext');
// 4. second 调用了 third ,等待 third 执行完毕再输出 2
ECStack.push('third functionContext');
// 5. third 执行完毕,输出 3 并弹出栈
ECStack.pop();
// 6. second 执行完毕,输出 2 并弹出栈
ECStack.pop();
// 7. first 执行完毕,输出 1 并弹出栈
ECStack.pop();
// 此时执行栈中只剩下一个全局执行上下文
4. 执行上下文的生命周期(ES5 版)
(1) 创建阶段
- this 值绑定 - This Binding
- 词法环境组件创建
- 变量环境组件创建
(2)执行阶段
(3) 回收阶段
三、作用域 与 执行上下文
当 JS 引擎处理一段脚本内容的时候,它是以怎样的顺序解析和执行的?脚本中的那些变量是何时被定义的?它们之间错综复杂的访问关系又是怎样创建和链接的? JS 是解释型语言,代码的执行分为 解释 和 执行 两个过程。
解释阶段
- 词法分析:将字符流分解为一些有意义的单词。
- 语法分析:对词法集合分析最终转换成一个 AST抽象语法树。
- 作用域规则确定:因此作用域在定义时就确定、不会再改变。
执行阶段
- 创建执行上下文:执行上下文在运行时确定,随时可能改变,this 指向也是执行时确定。
- 执行函数代码
- 垃圾回收
四、闭包
1. 闭包是什么
有时,我们需要从外部获取到函数内部的局部变量,但正常情况下无法实现。通常采用“在函数内部再定义一个函数” 的方法来实现。这就是闭包,相比于局部变量无法共享和长久地保存,而全局变量可能造成全局污染。闭包这种机制既可以长久地保存变量又不会造成全局污染!
闭包:
能够访问其他函数内部变量的函数。(实际上就是 定义在一个函数内部的函数),闭包中引用的变量的存储位置是堆内存,它的三个特点是:
- 外部函数嵌套内部函数
- 内部函数使用外部函数的变量(自由变量)
- 把内部函数返回出去,并在外部调用
因为内部函数返回出去了,counter 函数还在使用,所以不会对外部函数 counterCreator 垃圾回收、不会销毁内部作用域,闭包使得函数可以继续访问定义时的词法作用域。
function counterCreator(){
let index = 1;
function counter(){ //闭包函数
console.log(index++);
}
return counter;
}
// 测试
let counterA = counterCreator();
let counterB = counterCreator();
counterA(); //1
counterA(); //2,闭包可以记住它的执行环境
counterB(); //1
counterB(); //2
「 闭包是基于词法作用域的,正因为 js 采用词法作用域,函数的作用域在定义时就确定了,所以内部函数可以记住并访问所在的词法作用域(外部函数)」。
2. 闭包的优缺点(应用场景)
1. 优点 或 应用场景
闭包的应用,大多数是在需要维护内部变量的场景下(从外部读取函数内部的变量)。
- 模拟私有属性,让函数的私有变量不受外部的干扰。(name getName())
- 当希望一个变量长期保持在内存中时。
- 函数的柯里化。
- 节流防抖
2. 缺点
function foo() {
var a = 2;
function bar() {
console.log(a);
}
return bar;
}
var x = foo();
- 容易导致内存泄漏:闭包会导致全局作用域中始终存在着一个变量
x在引用着内部函数bar,bar又用到了foo的变量a,所以垃圾回收机制不能把函数销毁,只能让这些数据占用着内存。
解决方法:dom 的事件处理函数定义在外部,解除闭包。或者删除对 dom 元素的引用。
- 常驻内存,增加内存使用量。
五、垃圾回收
垃圾回收(GC)就是 释放那些程序不用的内存。两种常见的垃圾回收策略是:标记清除法 和 引用计数法。
1. 标记清除法 - 是否可达
大多数浏览器的 JS引擎 都是基于标记清除法优化,分为 标记 和 清除 两个阶段。
-
标记:垃圾回收器会定期从根对象(全局对象) 开始遍历,将 能从根对象访问到的对象 标记为 可到达对象。
-
清除:清理没被标记的对象,回收它们所占用的内存空间。然后将原来的标记都清除,等待下一轮垃圾回收。
优点:实现简单,打标记可以用 1 位二进制位就能解决。
缺点:垃圾清除后,剩余的对象内存位置是不变的,因此空闲内存空间是不连续的,可能出现大量内存碎片。
2. 引用计数法 - 是否有其他变量引用它
这是早期的垃圾回收算法,现在几乎不用了。
它的策略是记录每个变量值被引用的次数。如果没有引用指向该对象,引用数为0,那么对象将被回收。
- 当声明了一个变量并将一个引用类型赋值给该变量,那么这个值的引用次数为 1。
- 如果同一个值又被赋值给另一个变量,值的引用次数加 1。
- 如果该变量的值被其他值覆盖了,值的引用次数减 1。
- 当这个值的引用次数等于 0 的时候,就回收空间。
缺点:无法处理循环引用。
3. Chrome V8 引擎的垃圾回收算法
对于栈内存,执行上下文栈切换后就被回收,比较简单。
分代回收策略:V8 将 「堆内存」 分为新生代和老生代两个区域,新生代存放存活时间较短的对象(经过一次垃圾回收就被回收掉),老生代存放存活时间较长的对象(经过一次垃圾回收还存在)。
- 新生代内存回收机制:容量小,64位系统仅有32M。新生代内存分为
From(正在使用的内存)和To(闲置的内存)两部分(各占一半),当From快填满时会垃圾回收扫描From,根据标记清除法将非存活对象回收,将存活对象复制到To中,清空From然后调换From/To,等待下一次回收。 - 老生代内存回收机制:
- 晋升:如果新生代的变量经过垃圾回收依然存在,那么会晋升到老生代内存中。
- 标记清除:同上。
- 整理内存碎片:清除结束后,把存活的对象全部挪到内存的一端。
六、内存泄漏
内存泄漏:不再用到的内存,没有及时释放。可能会导致应用程序卡顿或崩溃。
1. 引起内存泄漏的原因
(1) 意外的全局变量
- 变量未声明直接使用,会创建一个全局变量,在页面关闭之前都不会被释放。
function fn() { a = 1; }
- 使用 this 创建的变量。
function fn() { this.a = 1; }
(2)闭包引起的内存泄漏
(3)DOM的事件绑定没有及时清理 。移除 DOM 元素前没有注销掉其绑定的事件方法,也会造成内存泄漏。
2. 内存泄漏的解决方案
(1)使用严格模式 use strict;,避免意外的全局变量。
(2)对 DOM 元素,在销毁阶段记得解绑相关事件。
(3)避免过度使用闭包。
参考文章