阅读 187

回望Javascript:执行上下文/作用域链/闭包

1.执行上下文

执行上下文是用来预处理JS代码的

执行上下文栈是用来管理执行上下文的

JS代码执行时它的代码位置分为一个全局代码和函数内的代码,因此执行上下文分为全局执行上下文和函数执行上下文,

全局执行上下文

在执行全局代码前会创建一个全局执行上下文环境将window将其确定为全局执行上下文;并且对全局数据进行一个预处理,这里面就包括:

  • var 定义的全局变量设为undefined,并且将其添加为window的属性
  • 声明function函数,添加为window的方法
  • this赋值为window

等全局执行上下文执行完毕后,才会真正开始执行代码

函数执行上下文

函数执行上下文就是函数执行前,也会创建类似于全局执行上下文的的环境,它的作用功能是对局部的数据进行预处理

  • 形参变量赋值为实参,添加为执行上下文的属性
  • argument赋值为实参列表,添加为执行上下文的属性
  • var 定义的局部变量设为undefined,添加为执行上下文的属性
  • function声明的函数赋值,添加为执行上下文的属性
  • this赋值为调用函数的对象或window

函数上下文指向完毕以后,开始执行我们函数体里的代码

我们总结一下就是不管是全局执行上下文还是函数执行上下文,在他们的JS代码执行前,都会有一个执行上下文环境,来给我们代码的里面的变量,函数,this等进行声明赋值,等这个操作结束以后,才真正开始执行代码

2.执行上下文栈

在全局代码执行前,JS引擎就会创建一个栈来管理所有的执行上下文对象

  • 全局执行上下文确定后,就会将其添加到栈中
  • 某个函数执行上下文确定后,将其添加到栈中
  • 函数执行上下文执行完毕后,出栈
  • 此时栈中只剩下window

因此我们执行上下文对象的个数永远都是n+1,n是函数执行上下文的个数,1是全局执行上下文window

一道面试题:

问题:执行结果以及有几个执行上下文

 console.log('g begin'+i);
  var i=1;
  foo(i);
  function foo(i) {
    if (i==4) {
      return
    }
    console.log('foo begin:'+i)
    foo(i+1);
    console.log('foo end:'+i)
  }

  console.log('g end:'+i)
复制代码

分析:首先这是一个递归函数。执行结果是:

  g beginundefined
  foo begin:1
  foo begin:2
  foo begin:3
  foo end:3
  foo end:2
  foo end:1
  g end:1
复制代码

函数共执行四次,并且加上一个全局执行上下文,因此一共有5个执行上下文对象

3.作用域

对于我的理解,我认为作用域就是一段代码可执行的区域,作用域是静态的,在我们编写代码时就已经决定了它的作用域。作用域的一个最大的功能就是隔离变量, 防止变名命名冲突。

作用域分为三种,全局作用域,局部作用域以及块作用域

**全局作用域:**全局作用域就是最外层的的代码执行范围,所有未定义直接赋值的变量自动声明为全局作用域;所有window对象的属性属于全局作用于范围;全局作用域有很大的弊端,过多的全局作用域变量会污染全局命名空间,引起变量冲突

**函数作用域:**函数作用域时声明在函数内部的变量,一般只有固定的代码片段可以访问到,并且函数作用域时分层的,内层作用域可以访问外层做作用域,而外层作用域不能访问到内层作用域

**块级作用域:**这是ES6独有的特性,使用新增的let额const指令可以声明块级作用域,块级作用域可以在一个函数中创建,也可以在一个作用域块中创建。let和const的声明的变量不会有便量提升,也不可以重复声明。

4.作用域链

作用域链实际上就是当我们在当前作用域寻找一个变量,但是这个作用域没有这个变量,那么这个变量就是自由变量,如果在自己的作用域找不到该变量, 就依次向上级作用域查找,直到访问到window就终止,这一层层的关系就叫做作用域链。

作用域链的作用就是保证对执行环境有权访问的所有变量和函数的有序访问,通过作用域链,可以访问到环境的变量和函数。

作用域链本质上是一个指向变量对象的指针列表。变量对象是一个包含了执行环境中所有变量和函数的对象。作用域链的前端始终都是当前执行上下文的变量对象。全局执行上下文的变量对象始终是作用域链的最后一个对象

5.闭包

**闭包如何产生:**当一个嵌套的内部子函数引用了嵌套的外部函数的变量时,就产生了闭包。

**闭包到底是是什么:**闭包是嵌套的内部函数?我认为闭包实际上是一个包含我们引用外部函数变量的一个对象(通过浏览器调试得出此结论)

闭包产生的条件:

  • 函数嵌套
  • 内部函数引用外部函数的数据
  • 执行外部函数(外部函数不执行, 内部函数不定义,函数定义就可产生闭包)

常见的闭包:

  1. 将函数作为另一个函数的返回值
function fn1() {
    var a=2;
    function fn2() {
      a++
      console.log(a)
    }
    return fn2
  }
  var f=fn1();//产生第一个闭包
  f();//3
  f();//4
  var f2=fn1();//产生第二个闭包
  f2();//3
  f2();//4
  fn1()()//产生第三个闭包
  fn1()()//产生第四个闭包
复制代码
  1. 将函数作为实参传递给另外一个函数
 function shwDelay(msg,time) {
    setTimeout(() => {
      console.log(msg)
    }, time);
  }
  shwDelay('123',1000)
复制代码

闭包的作用:

  1. 使用函数内部的变量(局部变量)执行完后,仍然存活在内存中(延长了局部变量的声明周期)
  2. 让函数外部可以操作(读写)到函数内部的数据

闭包的生命周期:

产生:在嵌套内部函数定义执行完时就产生了,也就是在函数执行上文时(变量提升,函数提升)

死亡:在嵌套的内部函数称为垃圾对象时(对此函数的引用指向null时)

闭包的缺陷:

  1. 如果执行闭包函数而且进行引用指向时,如果不手动释放,就会一直存在于内存中,造成内存泄露

  2. 函数执行完后,函数的局部变量没有释放,占用的内存时间就会变长

    function fnn() {
        var arr=new Array(10000)
        function fnn2() {
          console.log(arr.length)
        }
        return fnn
      }
      var fnn=fnn();
      fnn()
      fnn=null
    复制代码

上面代码就是在我们闭包中使用了一个占用很大空间的数组,如果我们不在使用它了,那么它会一直停留在内存中,因此我们需要手动将其指向null。使内部函数称为垃圾对象,从而回收我们的局部变量

一道闭包经典面试题:

  function fun(n, o) {
    console.log(o);
    return {
      fun: function (m) {
        return fun(m, n)
      }
    }
  }
  var a=fun(0); a.fun(1);a.fun(2);a.fun(3);//u,0,0,0
  var b=fun(0).fun(1).fun(2).fun(3)//u,0,1,2
  var c=fun(0).fun(1);c.fun(2);c.fun(3)//u,0,1,1
复制代码

6.内存溢出与内存泄露

内存溢出:当程序运行需要的内存超出了计算机为我么分配到内存空间就会造成内存溢出

内存泄露:可以理解为亚健康,和平时的编码的习惯相关,比如占用的内存没有及时释放,并且内存溢出最终造成的局面就是内存溢出,雪崩时没有一片雪花时无辜的。

常见的内存泄露有:

  1. 过多的局部变量
  2. 没有清理的订阅任务
  3. 闭包
文章分类
前端
文章标签