作用域 Scope

  1. 函数的作用域是在函数定义时就已经确定,而不是在执行的时候确定
  2. JS 有两种作用域:全局作用域和函数作用域
    1. 内部的作用域能访问外部,反之不行,访问时从内向外依次查找;
    2. 如果在内部的作用域中访问了外部,则会产生闭包;
    3. 内部作用域能访问到外部,取决于函数定义的位置,和调用无关;
  3. 作用域内 var 定义的变量、函数声明会提升到作用域顶部;

作用域链 Scope Chain

  1. VO 中包含一个额外的属性,该属性指向创建该 VO 的函数本身;

    1. 每个函数在创建时,会有一个隐藏属性 [[scope]],它指向创建该函数时的 AO
    2. 当访问一个变量时,会先查找自身 VO 中没有查到值,就会向上级 [[scope]] 作用域去查,直到查到全局作用域;
  2. 某些浏览器会优化作用域链,函数的 [[scope]] 中仅保留需要用到的数据;

  3. 以下示例展示了作用域链的查找过程:

闭包

什么是闭包

  1. 闭包是一个封闭的空间,里面存储了在其他地方会引用到的该作用域的值,在 JavaScript 中是通过作用域链来实现的闭包;

    1. 只要在函数中使用了外部的数据,就创建了闭包,这种情况下所创建的闭包,在编码时是不需要去关心的;
    2. 还可以通过一些手段手动创建闭包,从而让外部环境访问到函数内部的局部变量,让局部变量持续保存下来,不随着它的上下文环境一起销毁;
  2. 如果是自动产生的闭包,无需操心闭包的销毁,而如果是手动创建的闭包,可以把被引用的变量设置为 null ,即手动清除变量,这样下次 JavaScript 垃圾回收器在进行垃圾回收时,发现此变量已经没有任何引用了,就会把设为 null 的变量给回收了;

闭包经典问题

  1. 在下面的代码中,预期的结果是过 1 秒后分别输出 i 变量的值为 1,2,3,但是,执行的结果是:4,4,4

    for (var i = 1; i <= 3; i++) {
        setTimeout(function () {
            console.log(i);
        }, 1000);
    }
    
  2. 实际上,问题就出在闭包身上,循环中的 setTimeout 访问了它的外部变量 i ,形成闭包;而 i 变量只有 1 个,所以循环 3 次的 setTimeout 中都访问的是同一个变量;循环到第 4 次,i 变量增加到 4 ,不满足循环条件,循环结束,代码执行完后上下文结束;但是,那 3setTimeout1 秒钟后才执行,由于闭包的原因,所以它们仍然能访问到变量 i ,不过此时 i 变量值已经是 4 了;

  3. 要解决这个问题,可以让 setTimeout 中的匿名函数不再访问外部变量,而是访问自己内部的变量,如下:

    for (var i = 1; i <= 3; i++) {
        (function (index) {
            setTimeout(function () {
                console.log(index);
            }, 1000);
        })(i)
    }
    
  4. 当然,解决这个问题还有个更简单的方法,就是使用 ES6 中的 let 关键字:它声明的变量有块作用域,如果将它放在循环中,那么每次循环都会有一个新的变量 i ,这样即使有闭包也没问题,因为每个闭包保存的都是不同的 i 变量,那么刚才的问题也就迎刃而解;

    for (let i = 1; i <= 3; i++) {
        setTimeout(function () {
            console.log(i);
        }, 1000);
    }
    

垃圾回收与内存泄漏

什么是内存泄露

  1. 程序的运行需要内存,只要程序提出要求,操作系统或者运行时(runtime)就必须供给内存;

  2. 对于持续运行的服务进程(daemon),必须及时释放不再用到的内存,否则内存占用越来越高,轻则影响系统性能,重则导致进程崩溃;

  3. 也就是说,不再用到的内存,如果没有及时释放,就叫做内存泄漏(memory leak);

JavaScript 中的垃圾回收

  1. 浏览器的 Javascript 具有自动垃圾回收机制(GCGarbage Collecation),也就是说,执行环境会负责管理代码执行过程中使用的内存,其原理是:垃圾收集器会定期(周期性)找出那些不在继续使用的变量,然后释放其内存

  2. 但是这个过程不是实时的,因为其开销比较大并且 GC 时停止响应其他操作,所以垃圾回收器会按照固定的时间间隔周期性的执行;

  3. 不再使用的变量也就是生命周期结束的变量,当然只可能是局部变量,全局变量的生命周期直至浏览器卸载页面才会结束,局部变量只在函数的执行过程中存在,而在这个过程中会为局部变量在栈或堆上分配相应的空间,以存储它们的值,然后在函数中使用这些变量,直至函数结束,而闭包中由于内部函数的原因,外部函数并不能算是结束;

标记清除

  1. JavaScript 中最常用的垃圾回收方式就是标记清除;

  2. 当变量进入环境时,例如,在函数中声明一个变量,就将这个变量标记为 进入环境;从逻辑上讲,永远不能释放进入环境的变量所占用的内存,因为只要执行流进入相应的环境,就可能会用到它们;而当变量离开环境时,则将其标记为 离开环境

    function test(){
      var a = 10 ; // 被标记 ,进入环境 
      var b = 20 ; // 被标记 ,进入环境
    }
    test(); // 执行完毕 之后 a、b 又被标离开环境,被回收
    
  3. 垃圾回收器在运行的时候会给存储在内存中的所有变量都加上标记(当然,可以使用任何标记方式);然后,它会去掉环境中的变量以及被环境中的变量引用的变量的标记(闭包),而在此之后再被加上标记的变量将被视为准备删除的变量,原因是环境中的变量已经无法访问到这些变量了;最后,垃圾回收器完成内存清除工作,销毁那些带标记的值并回收它们所占用的内存空间;

  4. 到目前为止,IE9+、Firefox、Opera、Chrome、SafariJS 实现使用的都是标记清除的垃圾回收策略或类似的策略,只不过垃圾收集的时间间隔互不相同;

引用计数

  1. 引用计数的含义是跟踪记录每个值被引用的次数;

  2. 当声明了一个变量并将一个引用类型值赋给该变量时,则这个值的引用次数就是 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
    }
    
  3. Netscape Navigator3 是最早使用引用计数策略的浏览器,但很快它就遇到一个严重的问题:循环引用;循环引用指的是对象 A 中包含一个指向对象 B 的指针,而对象 B 中也包含一个指向对象 A 的引用;

    function fn() {
        var a = {};
        var b = {};
        a.pro = b;
        b.pro = a;
    }
    fn();
    
  4. 以上代码 ab 的引用次数都是 2fn 执行完毕后,两个对象都已经离开环境,在标记清除方式下是没有问题的,但是在引用计数策略下,因为 ab 的引用次数不为 0 ,所以不会被垃圾回收器回收内存,如果 fn 函数被大量调用,就会造成内存泄露;在 IE7IE8 上,内存直线上升;

面试题

第 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(此次和上一次输出的图一样,则省略掉….)

闭包造成内存泄漏举例

  1. 意外的全局变量;

  2. 未被清空的计时器或回调函数;

  3. 脱离 DOM 的引用;

  4. 未被销毁的事件监听;

打赏作者
您的打赏是我前进的动力
微信
支付宝
评论

中午好👏🏻,我是 ✍🏻   疯狂 codding 中...

粽子

这有关于前端开发的技术文档和你分享。

相信你可以在这里找到对你有用的知识和教程。

了解更多

目录

  1. 1. 作用域 Scope
  2. 2. 作用域链 Scope Chain
  3. 3. 闭包
    1. 3.1. 什么是闭包
    2. 3.2. 闭包经典问题
  4. 4. 垃圾回收与内存泄漏
    1. 4.1. 什么是内存泄露
    2. 4.2. JavaScript 中的垃圾回收
    3. 4.3. 标记清除
    4. 4.4. 引用计数
  5. 5. 面试题
    1. 5.1. 第 1 题
    2. 5.2. 第 2 题
    3. 5.3. 第 3 题
    4. 5.4. 第 4 题
    5. 5.5. 第 5 题
    6. 5.6. 第 6 题
    7. 5.7. 第 7 题
    8. 5.8. 第 8 题
    9. 5.9. 闭包造成内存泄漏举例