JavaScript执行上下文和作用域及闭包

JavaScript执行上下文和作用域

  • 作用域与执行环境
    • 作用域
    • 执行环境
    • 两者区别
  • JS代码的执行流程
    • 变量提升
    • 变量对象
  • 作用域链
    • 函数的创建与调用
  • 执行上下文栈
  • 块级作用域
    • 块级作用域的实现
  • 闭包
    • 闭包的回收


作用域与执行环境

执行环境与作用域不是同一种东西!


作用域

作用域是指在程序中定义变量的区域,决定了变量的生命周期,是一套存储规则。作用域就是变量与函数的可访问范围,即作用域控制着变量和函数的可见性和生命周期

  • 全局作用域
    其中的对象在代码中任何地方都能访问,其生命周期伴随着页面的生命周期
  • 函数作用域
    在函数内部定义的变量包括函数只能在函数内部被访问。函数执行结束后函数内部定义的变量会被销毁
  • 注意
    • 除了全局作用域,只有函数才能创建作用域即函数作用域 ,JS没有块级作用域
    • 作用域可以嵌套,不可以重叠
var a=1;      //全局作用域
function fn1(){
     
    var a=2;     //fn1作用域
}

执行环境

即执行上下文,是当前代码的运行环境,与this关键字相关联,所有的变量都存在其中
上下文环境主要分为全局环境局部(函数)环境每个函数执行的上下文环境都不同

this.a=1;      //全局执行上下文
function fn1(){
     
   	this.a=2;   //fn1执行上下文
}
var obj=new fn1();
  • 创建执行上下文的条件
    • 当js执行全局代码时会编译全局代码并创建全局执行上下文。在整个页面生存周期内,全局执行上下文只有一份
    • 调用一个函数时,函数体内的代码被编译并创建函数执行上下文;函数执行结束,函数执行上下文被销毁
    • 使用eval()方法时,eval()中的代码被编译并为其创建执行上下文

两者区别

  • 作用域是在定义时即编译阶段确定,不会改变;执行上下文是在执行、调用阶段才创建
  • 作用域是静态观念的,而执行上下文环境是动态上的
  • 一个作用域下可能包含若干个上下文环境,也可能没有,但其中处于活动状态的执行上下文环境只有一个
    JavaScript执行上下文和作用域及闭包_第1张图片JavaScript执行上下文和作用域及闭包_第2张图片

JS代码的执行流程

  1. 编译阶段
    输入一段代码经过编译后会生成两部分内容:
    • 执行上下文
      执行上下文是js执行一段代码时的运行环境。其中包括一个变量环境的对象,该对象中保存了变量提升的内容;还包括一个词法环境,ES6中letconst声明的变量保存在其中
    • 可执行代码
      js引擎将声明以外的代码编译为字节码,成为可执行代码
  2. 执行阶段
    js引擎开始按照顺序执行可执行代码
    JavaScript执行上下文和作用域及闭包_第3张图片

变量提升

指在js代码执行过程中,js引擎把变量和函数声明部分提升到代码开头的行为。变量被提升后会被设置默认值undefined


变量对象

每个执行环境都有一个与之关联的变量对象,环境中定义的所有变量和函数都保存在这个对象中
若环境是函数,则其活动对象会作为变量对象,其中最开始只包含一个变量即arguments对象

  • 注意
    var声明的变量会自动被添加到最接近的环境中;若没有使用var声明,则会自动被添加到全局环境

作用域链

访问规则,保证对执行环境有权访问的所有变量和函数的有序访问

  • 每进入一个新执行环境,都会创建一个用于搜索变量和函数的作用域链
  • 每个执行环境都包含一个外部引用即outer,指向其外部的执行环境。若某个函数中使用了外部变量,则js引擎沿着这条作用域链进行变量查找
  • 作用域链的前端是当前执行代码所在环境的变量对象,最后一个对象始终是全局执行环境的变量对象。搜索从作用域链的前端开始逐级向后回溯访问,寻找目标。
  • 内部环境可以通过作用域链访问所有外部环境,但外部环境不能访问内部环境。环境之间的联系是线性的、有次序的

函数的创建与调用

  • 创建
    1. 首先js引擎编译全局代码并创建全局执行上下文,其中包含着全局变量对象,代码中的全局变量包括声明的函数都保存在其中
    2. 在全局中创建一个函数,会创建一个预先包含全局变量对象的作用域链,这个作用域链被保存在函数内部的[[Scope]]属性中
  • 调用
    1. 为函数创建一个执行环境,然后通过复制函数的[[Scope]]属性中的对象,构建起执行环境的作用域链
    2. 该函数执行环境的活动对象(作为变量对象)被创建并推入作用域链的前端,并生成可执行代码
    3. 执行完毕后,局部活动对象即函数执行环境就会被销毁,内存中仅保存全局执行环境的变量对象
  • 例子1
    function compare(value1, value2){
             
    	// code...
    } 
    var result = compare(5, 10); 
    

JavaScript执行上下文和作用域及闭包_第4张图片

  • 例子2
    function bar() {
           
    	console.log(myName);
    }
    function foo() {
           
    	var myName = '123';
    	bar();
    }
    var myName = '456';
    foo(); //456
    /*
    因为bar函数和foo函数都在全局作用域中被声明创建,所以它们的外部引用outer都指向的是全局上下文
    因此bar函数中js引擎查找myName变量沿着作用域链向上一级找到的是全局作用域中的myName
    */
    

执行上下文栈

js引擎利用执行上下文栈(调用栈)来管理执行上下文,追踪函数执行

  • 例子
    var a = 2;
    function add(b, c) {
           
    	return b+c;
    }
    function addAll(b, c) {
           
    	var d = 10;
    	result = add(b, c)
        return a+result+d
    }
    addAll(3, 6);
    
    1. 创建全局上下文并将其压入栈底
    2. 执行全局代码,执行到调用addAll函数,js引擎编译该函数,为其创建一个执行上下文并压入栈中,再执行函数中的代码
    3. 执行到调用add函数,js引擎编译该函数,为其创建一个执行上下文并压入栈中,再执行函数中的代码
    4. add函数执行完毕,该函数执行上下文从栈顶弹出
    5. addAll函数执行完毕,执行上下文从栈顶弹出
    6. 全局上下文留在栈中
  • 栈溢出
    调用栈有大小,当入栈的执行上下文超过一定数目就会报错,写递归代码时易发生栈溢出错误
  • 调用栈信息查看
    • 在浏览器控制台Sources中可以给代码加断点,执行代码时执行流程会在断点处暂停,然后可以在Call Stack查看调用栈情况
    • 在代码中加上console.trace()来输出当前函数调用关系

块级作用域

ES6引入letconst关键字实现块级作用域

  • let声明的是变量,值可以被修改
  • const声明的是常量,值不可以被修改

块级作用域的实现

例子

//例子
function foo() {
     
    var a = 1;
    let b = 2;
    {
     
        let b = 3;
        var c = 4;
        let d = 5;
        console.log(a); //1
        console.log(b); //3
    }
    console.log(b); //2
    console.log(c); //4
    console.log(d); //报错
}
foo();
  1. 编译阶段
    JavaScript执行上下文和作用域及闭包_第5张图片
    • 函数内部通过var声明的变量在编译阶段被存放到变量环境中
    • 函数内部通过let声明的变量被存放到执行上下文的词法环境
    • 在函数块级作用域内部let声明的变量没有被放到词法环境中
  2. 执行阶段
    词法环境内部维护了一个小型栈结构,栈底是函数最外层的letconst变量,进入一个块级作用域后就会把该作用域块中的letconst变量压到栈顶,当作用域块执行完成后该作用域的信息就会从栈顶弹出
    JavaScript执行上下文和作用域及闭包_第6张图片

闭包

有权访问另一个函数作用域中变量的函数,创建闭包的常见方式就是在一个函数内部创建另一个函数

function createComparisonFunction(propertyName) {
             
    return function(object1, object2){
          //匿名函数
        //code... 
    };
}
//创建函数
var compare = createComparisonFunction("name"); 
//调用函数
var result = compare({
      name: "Nicholas" }, {
      name: "Greg" })
//解除对匿名函数的引用(以便释放内存) 
compareNames = null;

JavaScript执行上下文和作用域及闭包_第7张图片
createComparisonFunction()函数在执行完毕后,其活动对象也不会被销毁,因为匿名函数的作用域链仍在引用这个活动对象。即当 createComparisonFunction()函数返回后,其执行环境的作用域链会被销毁,但它的活动对象仍然会留在内存中;直到匿名函数被销毁后,createComparisonFunction()的活动对象才会被销毁

  • 注意
    • 匿名函数的执行环境具有全局性,因此this对象通常指向window
    • 闭包只能取得包含函数中任何变量的最后一个值
      function createFunctions(){
                  
      	var result = new Array(); 
      	for (var i=0; i < 10; i++){
                    
          	result[i] = function(){
                         
              	return i;    
          	};    
      	} 
      	return result; 
      }
      /*
      因为result数组中每个函数作用域链中都保存着其包含函数的活动对象
      所以它们引用的都是同一个变量i
      所以每个函数都返回10
      */
      

闭包的回收

  • 若引用闭包的函数是一个局部变量,等函数销毁后,js引擎执行垃圾回收时判断闭包不再被使用就会将这块内存回收
  • 若引用闭包的函数是一个全局变量,则闭包会一直存在直到页面关闭;但若此闭包以后不再使用,会造成内存泄漏
    • 内核泄漏
      由于外部函数的活动对象被引用于内部匿名函数的作用链中,若匿名函数存在,即使外部函数调用完销毁后,它所占用的内存也不会被回收,这样就容易导致内存泄漏。因此要手动设置引用匿名函数的变量为null解除引用

参考:
① 极客时间《浏览器工作原理与实践》
② https://www.cnblogs.com/wangfupeng1988/p/4000798.html
③ https://blog.csdn.net/qq_38563845/article/details/78206729

你可能感兴趣的:(JavaScript,javascript)