重学JavaScript【做用域、执行上下文和垃圾回收】

重学JavaScript 篇的目的是回顾基础,方便学习框架和源码的时候能够快速定位知识点,查漏补缺,全部文章都同步在 公众号(道道里的前端栈)github 上。html

原始值和引用值

在JavaScript中,数据分为 原始值引用值,原始值就是最简单的数据,通常也称为 值类型,引用值就是由多个值构成的对象,通常被叫作 引用类型。保存原始值的变量是按值访问的,因此操做的是存储在变量中的实际值。引用值是保存在内存中的对象,要想改变它,实际上操做的是对该对象的 引用前端

  • 原始值不能添加属性,引用值能够添加属性git

  • 原始值复制给另外一个变量是两个独立的栈,引用值复制给另外一个变量是复制的引用地址,对象所在的堆不变。github

  • 对象被传入方法并改变它的属性时,对象在外部访问的仍是原来的值面试

    function setName(obj){
      obj.name = "adc";
      obj = new Object();
      obj.name = "def"
    }
    let person = new Object();
    setName(person);
    console.log(person.name) // abc
    复制代码

上面可得出,方法传入的对象实际上是按值传递的,内部obj被重写以后,obj会变成一个指向本地对象的指针,而这个指向本地的对象在函数结束后会被销毁。数组

执行上下文

在JavaScript中,上下文 的概念特别重要,由于上下文决定了它们能够访问哪些数据和行为。每一个上下文都有一个关联的变量对象,这个对象里包括了上下文中定义的全部东西。浏览器

  • 全局上下文,也就是最外层的上下文,通常就是 window
  • 函数上下文,执行的时候会被推入到一个上下文栈上,函数执行完毕后,上下文栈会弹出该函数上下文,将控制权返还给以前的执行上下文
  • 上下文是在函数调用的时候才会生效的

如今咱们来模拟一个执行上下文的行为:markdown

首先要知道的是,JavaScript的整个执行过程分为两个阶段:编译阶段(由做用域规则肯定,编译成可执行代码),执行阶段(引擎完成,该阶段建立执行上下文)。闭包

咱们定义一个执行上下文栈是一个数组:ECStack = [],当JavaScript开始解释执行代码的时候,首先会遇到全局代码,因此此时咱们压入一个全局执行上下文 globalContext,当整个应用程序结束的时候,ECStack才会被清空,因此ECStack底部永远会有一个 globalContext。框架

ECStack = [
	globalContext
]
复制代码

此时,若是碰到了一个函数:

// 要执行下面的函数
function fn(){
  function inFn(){}
  inFn()
}
fn()
复制代码

那么执行上下文栈会经历如下过程:

// 压栈
ECStack.push(globalContext)
ECStack.push(fnContext)
ECStack.push(inFnContext)

//弹出
ECStack.pop(inFnContext)
ECStack.pop(fnContext)
ECStack.pop(globalCotext)
复制代码

执行上下文在建立阶段,会发生三件事:

  1. 建立变量对象

  2. 建立做用域链

  3. this的指向

每一个执行上下文都会分配一个 变量对象(variable object,VO) ,它的属性由 变量函数声明 构成,在函数上下文的状况下,参数列表也会被加入到变量对象中做为属性,不一样做用域的变量对象也不一样。

注意:只有函数声明会被加入到变量对象中,而函数表达式不会!

// 函数声明
function a(){}
typeof a //function

//函数表达式
var a - function fn(){}
typeof fn // undefined

复制代码

当一个函数被激活的时候,会建立一个活动对象(activation object,AO)并分配给执行上下文,活动对象由 arguments 初始化构成,随后它会被当作 变量对象 用于变量初始化。

function a(name, age){
  var gender = "male";
  function b() } a("小明", 20) 复制代码

a 被调用时,在a的执行上下文会建立一个活动对象AO,而且被初始化为:AO = [arguments],随后AO又被当作变量对象VO进行变量初始化,此时:VO = [arguments].concat([name.age,gender,b])

通常状况下变量对象包括:形参,函数声明,和变量声明,下面用代码来表示一下某刻的变量对象都是什么:

function fn(value){
  console.log(a);
  console.log(inFn);
  var a = 2;
  function inFn(){};
  var c = function() {};
  a = 3;
}
fn(1);
复制代码

在进入执行上下文后,此时的AO是:

AO = {
	arguments: {
		0: 1,
    length: 1
	}
	value: 1,
  a: undefined,
  b: reference to function inFn(){},
  c: undefined
}
复制代码

接下来代码开始执行,执行完后,此时的AO是:

AO = {
	arguments: {
    0: 1,
    length: 1
  },
  value: 1,
  a: 3,
  b: reference to function inFn(){},
  c: reference to FunctionExpression "c"
}
复制代码

从上面来看,代码总体的执行顺序应该是:

function fn(value){
  var a;
  function inFn(){};
  var d;
  console.log(a);
  console.log(inFn);
  a = 2;
  function inFn(){};
  c = function(){};
  a = 3;
}
复制代码

每一个时间,只会存在一个激活的变量对象。

做用域

做用域决定了查找变量的方法,JavaScript里采用的是 静态做用域动态做用域,静态做用域是在函数定义的时候才会被决定,动态做用域是在函数被调用的时候定义,下面是一道经典面试题:

var a = 1;
function out(){
  var a = 2;
  inner();
}
function inner(){
  console.log(a)
}
out()
// 1
复制代码

做用域和做用域之间是有连接关系的,在查找变量的时候,若是当前上下文没有找到,就从父级执行上下文的变量对象中找,直到全局上下文。

函数的做用域在建立的时候决定,是由于内部有个属性叫:[[scope]],它会保留全部的父变量,换句话讲,它就是全部父变量对象的层级链,咱们能够从控制台找到某个函数里的 [[scope]] ,可是他不表明完整的做用域链!

function out(){
  function inner(){}
}
复制代码

函数建立时,各自的 [[scope]] 为:

out.[[scope]] = [
	globalContext.VO
]
inner.[[scope]] = [
  outContext.AO,
  globalContext.VO
]
复制代码

当函数被激活时,进入函数上下文,建立AO后,会将活动对象添加到做用域链的顶端,此时执行上下文的做用域链,咱们叫 Scope

Scope = [AO].concat([[scope]])
复制代码

到如今为止,做用域链建立完毕。

下面把执行上下文和做用域结合起来,看一下它的执行过程是怎样的:

var scope = "global scope";
function fn(){
  var a = "local scope";
  return a;
}
scope();
复制代码
  1. fn函数被建立,此时fn会维护一个私有属性[[scope]],把当前环境的做用域链初始化到这个[[scope]]上

    fn.[[scope]] = [
    	globalContext.VO
    ]
    复制代码
  2. 执行fn函数,建立fn的执行上下文,以后fn函数的执行上下文被压入执行上下文栈

    ECStack = [
      fnContext,
      globalContext
    ]
    复制代码
  3. fn函数复制内部的[[scope]]属性,从而建立做用域链

    fnContext = {
    	Scope: fn.[[scope]]
    }
    复制代码
  4. 此时fn的执行上下文和做用域链构建完毕,开始用 arguments 建立并初始化活动对象,加入形参,函数声明和变量声明

    fnContext = {
      AO: {
    		arguments: {
          length: 0
        },
        a: undefined
      },
      Scope: fn.[[scope]]
    }
    复制代码
  5. 此时fn内部也构建完毕,开始将本身的活动对象AO压入本身做用域链的顶端

    fnContext = {
      AO: {
    		arguments: {
          length: 0
        },
        a: undefined
      },
      Scope: [AO, [[Scope]]]
    }
    复制代码

    注意,此时的做用域链就包括了 本身的AO 和 前面经过复制内部[[scope]]建立好的做用域链

  6. 此时,fn的做用域链,变量,执行上下文都完毕了,开始执行fn函数,接下来的每一步就是修改 AO 的值,而后把AO压栈出栈,最终:

    ECStack = [
    	globalContext
    ]
    复制代码

    有一个讲的比较细的例子在这里:一道JS面试题引起的思考

垃圾回收

在函数中,局部变量会在函数执行的时候存在,若是函数结束了,变量就不被须要了,它所占用的内存就能够释放出来。经常使用的两种机制为 标记清理引用计数

标记清理是最经常使用的,就是每用到一次,该变量就会被标记一次,依次叠加,每不用一次(即离开上下文),标记就会减小一个,依次递减。

引用计数就是对每一个值的引用作一个记录,引用一次就加1,浏览器记录的是引用的次数,若是该值引用的变量被其余值覆盖了,就减1,当引用数为0时,释放内存。

若是对变量引用不当,或者执行的最终做用域没有释放掉,那么它就不会被标记和引用计数,此时就会形成 内存泄漏,又一道经典面试题:

function fn(value){
	return function(name){
		return value + name
	}
}
var fn2 = fn("123");
var name = fn2("小明")
复制代码

经典闭包题,函数内返回一个匿名函数!咱们再来分析一遍: fn2调用了fn,返回了一个匿名函数,该匿名函数会持有fn函数做用域的VO,包括arguments和value。当fn执行结束被销毁后,它的VO仍是会一直保存在内存中,它的VO仍然在匿名函数中存在,也就是说这个VO一直被用着,因此浏览器的垃圾回收机制不会对它作处理,此刻就成了内存泄漏。

要想避免内存泄漏,经常使用的方法就是:赋值为null

fn2 = null
复制代码

强制把fn2内部清空,这样匿名函数的引用就成了null,此时它所使用的fn的VO就能够被回收了。

参考连接:

个人公众号:道道里的前端栈,每一天一篇前端文章,嚼碎的感受真奇妙~

相关文章
相关标签/搜索