作用域 Scope
- 函数的作用域是在函数定义时就已经确定,而不是在执行的时候确定
- JS 有两种作用域:全局作用域和函数作用域
- 内部的作用域能访问外部,反之不行,访问时从内向外依次查找;
- 如果在内部的作用域中访问了外部,则会产生闭包;
- 内部作用域能访问到外部,取决于函数定义的位置,和调用无关;
- 作用域内 var 定义的变量、函数声明会提升到作用域顶部;
作用域链 Scope Chain
VO 中包含一个额外的属性,该属性指向创建该 VO 的函数本身;
- 每个函数在创建时,会有一个隐藏属性
[[scope]]
,它指向创建该函数时的 AO;- 当访问一个变量时,会先查找自身 VO 中没有查到值,就会向上级
[[scope]]
作用域去查,直到查到全局作用域;某些浏览器会优化作用域链,函数的
[[scope]]
中仅保留需要用到的数据;以下示例展示了作用域链的查找过程:
闭包
什么是闭包
闭包是一个封闭的空间,里面存储了在其他地方会引用到的该作用域的值,在 JavaScript 中是通过作用域链来实现的闭包;
- 只要在函数中使用了外部的数据,就创建了闭包,这种情况下所创建的闭包,在编码时是不需要去关心的;
- 还可以通过一些手段手动创建闭包,从而让外部环境访问到函数内部的局部变量,让局部变量持续保存下来,不随着它的上下文环境一起销毁;
如果是自动产生的闭包,无需操心闭包的销毁,而如果是手动创建的闭包,可以把被引用的变量设置为 null ,即手动清除变量,这样下次 JavaScript 垃圾回收器在进行垃圾回收时,发现此变量已经没有任何引用了,就会把设为 null 的变量给回收了;
闭包经典问题
-
在下面的代码中,预期的结果是过 1 秒后分别输出 i 变量的值为 1,2,3,但是,执行的结果是:4,4,4;
for (var i = 1; i <= 3; i++) { setTimeout(function () { console.log(i); }, 1000); }
-
实际上,问题就出在闭包身上,循环中的 setTimeout 访问了它的外部变量 i ,形成闭包;而 i 变量只有 1 个,所以循环 3 次的 setTimeout 中都访问的是同一个变量;循环到第 4 次,i 变量增加到 4 ,不满足循环条件,循环结束,代码执行完后上下文结束;但是,那 3 个 setTimeout 等 1 秒钟后才执行,由于闭包的原因,所以它们仍然能访问到变量 i ,不过此时 i 变量值已经是 4 了;
-
要解决这个问题,可以让 setTimeout 中的匿名函数不再访问外部变量,而是访问自己内部的变量,如下:
for (var i = 1; i <= 3; i++) { (function (index) { setTimeout(function () { console.log(index); }, 1000); })(i) }
-
当然,解决这个问题还有个更简单的方法,就是使用 ES6 中的 let 关键字:它声明的变量有块作用域,如果将它放在循环中,那么每次循环都会有一个新的变量 i ,这样即使有闭包也没问题,因为每个闭包保存的都是不同的 i 变量,那么刚才的问题也就迎刃而解;
for (let i = 1; i <= 3; i++) { setTimeout(function () { console.log(i); }, 1000); }
垃圾回收与内存泄漏
什么是内存泄露
程序的运行需要内存,只要程序提出要求,操作系统或者运行时(runtime)就必须供给内存;
对于持续运行的服务进程(daemon),必须及时释放不再用到的内存,否则内存占用越来越高,轻则影响系统性能,重则导致进程崩溃;
也就是说,不再用到的内存,如果没有及时释放,就叫做内存泄漏(memory leak);
JavaScript 中的垃圾回收
浏览器的 Javascript 具有自动垃圾回收机制(GC:Garbage Collecation),也就是说,执行环境会负责管理代码执行过程中使用的内存,其原理是:垃圾收集器会定期(周期性)找出那些不在继续使用的变量,然后释放其内存;
但是这个过程不是实时的,因为其开销比较大并且 GC 时停止响应其他操作,所以垃圾回收器会按照固定的时间间隔周期性的执行;
不再使用的变量也就是生命周期结束的变量,当然只可能是局部变量,全局变量的生命周期直至浏览器卸载页面才会结束,局部变量只在函数的执行过程中存在,而在这个过程中会为局部变量在栈或堆上分配相应的空间,以存储它们的值,然后在函数中使用这些变量,直至函数结束,而闭包中由于内部函数的原因,外部函数并不能算是结束;
标记清除
-
JavaScript 中最常用的垃圾回收方式就是标记清除;
-
当变量进入环境时,例如,在函数中声明一个变量,就将这个变量标记为 进入环境;从逻辑上讲,永远不能释放进入环境的变量所占用的内存,因为只要执行流进入相应的环境,就可能会用到它们;而当变量离开环境时,则将其标记为 离开环境;
function test(){ var a = 10 ; // 被标记 ,进入环境 var b = 20 ; // 被标记 ,进入环境 } test(); // 执行完毕 之后 a、b 又被标离开环境,被回收
-
垃圾回收器在运行的时候会给存储在内存中的所有变量都加上标记(当然,可以使用任何标记方式);然后,它会去掉环境中的变量以及被环境中的变量引用的变量的标记(闭包),而在此之后再被加上标记的变量将被视为准备删除的变量,原因是环境中的变量已经无法访问到这些变量了;最后,垃圾回收器完成内存清除工作,销毁那些带标记的值并回收它们所占用的内存空间;
-
到目前为止,IE9+、Firefox、Opera、Chrome、Safari 的 JS 实现使用的都是标记清除的垃圾回收策略或类似的策略,只不过垃圾收集的时间间隔互不相同;
引用计数
-
引用计数的含义是跟踪记录每个值被引用的次数;
-
当声明了一个变量并将一个引用类型值赋给该变量时,则这个值的引用次数就是 1 ,如果同一个值又被赋给另一个变量,则该值的引用次数加 1;相反,如果包含对这个值引用的变量又取得了另外一个值,则这个值的引用次数减 1 ,当这个值的引用次数变成 0 时,则说明没有办法再访问这个值了,因而就可以将其占用的内存空间回收回来;这样,当垃圾回收器下次再运行时,它就会释放那些引用次数为 0 的值所占用的内存;
function test() { var a = {}; // a 指向对象的引用次数为 1 var b = a; // a 指向对象的引用次数加 1,为 2 var c = a; // a 指向对象的引用次数再加 1,为 3 var b = {}; // a 指向对象的引用次数减 1,为 2 }
-
Netscape Navigator3 是最早使用引用计数策略的浏览器,但很快它就遇到一个严重的问题:循环引用;循环引用指的是对象 A 中包含一个指向对象 B 的指针,而对象 B 中也包含一个指向对象 A 的引用;
function fn() { var a = {}; var b = {}; a.pro = b; b.pro = a; } fn();
-
以上代码 a 和 b 的引用次数都是 2,fn 执行完毕后,两个对象都已经离开环境,在标记清除方式下是没有问题的,但是在引用计数策略下,因为 a 和 b 的引用次数不为 0 ,所以不会被垃圾回收器回收内存,如果 fn 函数被大量调用,就会造成内存泄露;在 IE7 与 IE8 上,内存直线上升;
面试题
第 1 题
console.log(a, b, c);
var a = 1;
var b = function () { };
function c() { }
第 2 题
// 前置知识点
/*
var a = 12, b = 13; // b 带 var
var a = b = 12; // b 不带 var
*/
var a = 1,
b = 2;
function m1() {
console.log(a);
var a = 3;
function m2() {
console.log(a, b);
}
m2();
}
m1();
第 3 题
var a = 1;
function m1() {
a++;
}
function m2() {
var a = 2;
m1();
console.log(a);
}
m2();
console.log(a);
第 4 题
let x = 5;
function fn(x) {
return function (y) {
console.log(y + (++x));
}
}
let f = fn(6);
f(7);
fn(8)(9);
f(10);
console.log(x);
第 5 题
let x = 5;
function fn() {
return function (y) {
console.log(y + (++x));
}
}
let f = fn(6);
f(7);
fn(8)(9);
f(10);
console.log(x);
第 6 题
let a = 0, b = 0;
function A(a) {
A = function (b) {
alert(a + b++);
}
alert(a++);
}
A(1);
A(2);
第 7 题
var x = 3, obj = { x: 5 };
obj.fn = (function () {
this.x *= ++x;
return function (y) {
this.x *= (++x) + y;
console.log(x);
}
})();
var fn = obj.fn;
obj.fn(6);
fn(4);
console.log(obj.x, x);
第 8 题
function fun(n, o) {
console.log(o);
return {
fun: function (m) {
return fun(m, n);
}
};
}
var c = fun(0).fun(1);
c.fun(2);
c.fun(3);
// 输出:
// undefined
// 0
// 1
// 1(此次和上一次输出的图一样,则省略掉….)
闭包造成内存泄漏举例
意外的全局变量;
未被清空的计时器或回调函数;
脱离 DOM 的引用;
未被销毁的事件监听;
第 1️⃣ 座大山:堆栈内存、执行上下文
上一篇