闭包、执行上下文和作用域

文章目录

  • 写在前面
  • 一、闭包
    • 1. 定义
    • 2. 作用
    • 3. 注意
    • 4. 与普通函数的区别
  • 二、执行上下文
    • 执行上下文的定义
      • 1. 执行上下文:
      • 2. 执行上下文的作用
      • 3. 创建原因
      • 4. 运行环境
      • 执行栈
      • 执行栈的调用关系
        • 执行上下文的调用关系:
        • 执行栈的调用关系
      • 5. 创建执行上下文的过程
        • 5.1. 创建变量对象VO
        • 5.2. 创建作用域链
          • 5.2.1. 作用域
            • 1. 定义
            • 2. 作用域的解析规则【LHS、RHS】
            • 3. 作用域的分类
            • 4. 暂时性死区和变量提升的区别:
          • 5.2.2 作用域链
            • 1. 作用域链作用
            • 2. 作用域链的顺序
        • 5.3. 确定this指向
          • 5.3.1. 定义
          • 5.3.2. this指向
          • 5.3.3. 改变this指向

写在前面

  • 闭包:在js中指的是一个函数
  • 执行上下文:
    • this:该记录的一个属性,在函数执行的过程中用到
    • 作用域:是一个在何处用/如何查找变量的规则
    • 变量对象VO:创建执行上下文时,会有一个变量用来存储执行上下文中的函数或变量
    • 活动对象AO:进入执行上下文时,该上下文中的所有变量和对象都可以被访问到【激活状态】

  • 拓展【js垃圾清除】方式
  • 标记清除【对象是否可以获得】
    1. 给内存中的所有变量加上标记
    2. 去掉被使用变量的标记
    3. 下次再加上标记的就是准备被清除
  • 引用计数【对象是否不再需要】
    1. 当一个对象被引用的时候,该对象的引用次数就+1
    2. 如果引用该对象的对象又引用了其他值,那么之前被引用的对象的引用次数就-1
    3. 垃圾回收会清除引用次数为0的对象所占的内存

一、闭包

1. 定义

有权访问其他函数内部作用域变量的函数
连接函数内部和外部的桥梁

闭包的严格定义:

  • 环境部分
    • 函数的词法环境
    • 标识符列表:函数中用到的未声明变量
  • 表达式部分:函数体

2. 作用

延长作用域链【让一个变量一直保存在内存里,不污染全局变量】

  function a(){  //在这个函数中,b依赖于a
    var count = 0;
    function b(){
      count++
      console.log(count)
    }
    return b;
  }
  var c= a() //c调用a函数,return的是b,所以c引用了b,但是a被间接引用,a不会被垃圾回收
  c()//1
  c()//2
  c()//3
  //每次调用c都会使count+1,因为a中的count一直保存咋内存中  

3. 注意

  1. 使得函数的变量保存在内存中,使得内存消耗很大,还可能造成内存泄漏【解决:在退出函数之前,将局部变量设置为null/删除】
  2. 闭包可能改变函数内部变量的私有属性

4. 与普通函数的区别

  1. 携带了执行环境
  2. 占用更多的内存

二、执行上下文

以下:是三种概念的变化


ES3:

  • 作用域【scope】
  • 变量对象【variable object】:用于存储变量的对象
  • this值【this value】

ES5:

  • 词法环境【lexical environment】:获取变量时使用
  • 变量环境【variable environment】:声明变量时使用
  • this值

ES2018

  • 词法环境【lexcal environment】:获取变量/this值时使用
  • 变量环境【variable environment】:声明变量时使用
  • 用于恢复代码的执行位置
  • 执行的任务时函数的使用,表示正在被执行的函数
  • 执行的任务是脚本/模块化时调用,表示正在被执行的代码
  • 使用的基础库和内置对象实例
  • 仅生成器有上下文这个属性,表示当前生成器

执行上下文的定义

1. 执行上下文:

函数调用时,创建的活动记录。这个记录里包含了函数调用时需要的信息

2. 执行上下文的作用

  • 定义函数或变量访问其他数据的权利
  • 可以决定他们的各自行为

3. 创建原因

当js引擎进入不同的运行环境就会创建对应的一个执行上下文

4. 运行环境

  • 全局 window
  • 函数
  • eval

下面介绍一下执行上下文

var name = 'window';

outer();

function outer(){
    var name = 'outer';
    inner();
    //函数内部的函数
    function inner(){
        var name = 'inner';
        console.log(name);
    }
}

画了一张草图,介绍了一下他们之间的关系:【自行理解一下啊】
闭包、执行上下文和作用域_第1张图片

执行栈

  • 多个执行上下文形成执行上下文栈
  • 顶层是当前执行上下文
  • 底层是全局执行上下文

执行栈的调用关系

在ECMAScript中,代码又三种类型:global、function、eval
每一种代码的执行都需要依赖自身的上下文【当然,global可能涵盖了更多的function和eval实例】
函数的每一次调用,都会进入函数执行中的上下文,来计算函数中变量的值
对于eval也是一样,也会进入eval的执行上下文,判断应该从何处获取变量的值

注意
一个函数可能产生无限的执行上下文,因为函数的调用(递归)产生了一个新的上下文环境

function foo(bar){

}
// 调用相同的function,每次都会产生3个不同的上下文
//(包含不同的状态,例如参数bar的值)

foo(10);
foo(20);
foo(30);

一个执行上下文可以激活另一个上下文,【函数调用】

  • 激活其他上下文的某个上下文被称为调用者(caller)
  • 被激活的上下文被称为被调用者(callee)
    被调用者也可能是调用者【比如一个在全局上下文中调用自身的内部方法】

在arguments中有介绍,
caller返回调用该函数的对象
calee返回正在被执行的函数


执行上下文的调用关系:

  1. 当一个caller 调用一个callee,那么这个caller就会暂停自己的执行,将控制权交给这个callee
  2. 这个callee会进入堆栈【进行中的上下文(active execution context)】,但这个callee的上下文执行结束后,会把控制权交回给caller
  3. caller会继续执行。这个caller结束之后,会继续触发其他的上下文
  4. 一个callee可以用return/exception 来结束自身的上下文

如下图,所有的 ECMAScript 的程序执行都可以看做是一个执行上下文堆栈 [execution context (EC) stack]。堆栈的顶部就是处于激活状态的上下文。
闭包、执行上下文和作用域_第2张图片

执行栈的调用关系

  1. 一段程序开始时,会先进入全局执行上下文环境【也就是执行栈最底部的元素】,全局执行程序会初始化,生成必要的对象和函数
  2. 在执行过程中,可能会激活一些方法【也有可能是已经初始化过的】,然后进入他们的执行上下文,将新的元素入栈
  3. 当这些初始化结束之后,这个系统会等待一些事件【鼠标点击】,会触发一些方法,然后进入他们的执行上下文

见下图,有一个函数上下文“EC1″和一个全局上下文“Global EC”,下图展现了从“Global EC”进入和退出“EC1″时栈的变化:
闭包、执行上下文和作用域_第3张图片

5. 创建执行上下文的过程

5.1. 创建变量对象VO

创建执行上下文时,会用一个变量对象,存储执行上下文中的变量/函数
分类

  • 形参
  • 函数
  • 变量

5.2. 创建作用域链

5.2.1. 作用域
1. 定义

用于在何处以及如何查找变量的【规则】

2. 作用域的解析规则【LHS、RHS】
  1. 先声明
  2. 在赋值
var a = 2
1. var a //声明
2. a = 2  //赋值
3. 作用域的分类
  1. 全局作用域
    • 最外层函数
    • 未定义直接赋值的变量
    • windows对象
  2. 函数作用域【函数内部】
  3. 块级作用域【let/const/{}】
    • 不存在变量提升【不允许变量在声明之前使用】
    • 暂时性死区【let/const声明的变量绑定这个区域,不受外部的影响,如果在声明之前使用就会报错】

4. 暂时性死区和变量提升的区别:

变量提升:

	console.log(a)//ReferenceError
    let a = 1

暂时性死区:

   //代码1
    var a = 2
    if(true){
      console.log(a)//ReferenceError
      let a = 3
    }

我觉得大家看了上面两段代码,肯定跟我一样,第一眼就觉得这两个概念好像一样,但是其实不是,我们将下面的代码改一下

	//代码2
    var a = 2
    if(true){
      console.log(a)//2     
      //let a = 3  注释let声明后,就会输出a的值,原因【内部作用域可以访问全局作用域】
    }

当我们改成let声明使,也会输出2,原因和上面一样【内部作用域可以访问全局作用域】

	//代码3
    let a = 2
    if(true){
      console.log(a)//2
      //let a = 3
    }

解答:
细心的朋友可以发现,代码1比下面两段代码多了一句就是在,if内部使用了let重新声明了变量a
也就是说,在块级作用域中,如果使用了let 声明,那么这个变量就会绑定这个区域,不受外部影响,如果变量在声明之前使用,则会报错【对于const也是一样】

    let b = 5
    {
      console.log(b)//ReferenceError
      let b = 10
    }

注释let声明后,又对了

    let b = 5
    {
      console.log(b)//5
     // let b = 10
    }
5.2.2 作用域链
1. 作用域链作用
  1. 内部作用域可以访问外部作用域
  2. 保证执行环境对变量/函数的有序访问
2. 作用域链的顺序
  1. 最前端【当前代码所在环境的变量对象】
  • 如果是函数环境【活动对象】
    活动对象:进入到执行上下文中,该上下文中的所有变量和对象都可以被访问到,激活状态
    也就是说在作用域链的最前端,如果是函数的话,活动对象作为变量对象
    活动对象最开始只包含一个变量:arguments对象
  • 如果是全局环境,则不存在
  1. 来自下一个包含(外部)环境的变量对象
  2. 全局执行环境

5.3. 确定this指向

5.3.1. 定义

this指向上下文这个记录的属性,在函数执行过程中用到

5.3.2. this指向

取决于函数在哪里调用【函数调用时发生的绑定】

  • 全局,this指向window
  • 当函数被某个对象方法调用时,this指向那个对象
  var name = 'hahaha'
  function test(){
    var name = 1
    console.log(this.name)//hahaha
  }
  test()//全局调用了
  console.log(this.name)//hahaha
  var name = 'i am window'
  var a = {
    name: 'i am an apple',
    fn: function(){
      console.log(this.name)
    }
  }
  var f = a.fn//定义了一个全局变量,并赋值了一个方法
  f();//i am window

下面这段代码是将a.fn()的调用后的结果赋给f

  var name = 'i am window'
  var a = {
    name: 'i am an apple',
    fn: function(){
      console.log(this.name)
    }
  }
  var f = a.fn()//i am an apple

下面这段代码,最后调用是在全局中的test(),所以还是window

  var name = 'window'
  function test(){
    var name = 'apple'
    innerFun();
    function innerFun(){
      console.log(this.name)
    }
  }
  test()//window
5.3.3. 改变this指向
  1. call和apply改变this指向【区别只是参数传递方式不同】

call

    var name = 'i am window'
    var a = {
      name: 'i am an apple',
      fn: function (a, b) {
        console.log(this.name)
        console.log(a + b)
      }
    }
    var f = a.fn
    f.call(a,1,9)
    //i am an apple
    // 10

apply

    var name = 'i am window'
    var a = {
      name: 'i am an apple',
      fn: function (a, b) {
        console.log(this.name)
        console.log(a + b)
      }
    }
    var f = a.fn
    f.apply(a,[1,9])
    //i am an apple
    // 10
  1. bind【忽略当前this的绑定,将this绑定在提供的对象上,并生成一个新函数】
    var name = 'i am window'
    var a = {
      name: 'i am an apple',
      fn: function (a, b) {
        console.log(this.name)
        console.log(a + b)
      }
    }
    var f = a.fn
    f.bind(a,1,9)//没有输出
    f.bind(a,1,9)()//i am an apple  10
    //或者用下面的方式输出【切记,test指向a】
    var test = f.bind(a,1,9)
    test() //i am an apple  10

参考文献:
https://www.cnblogs.com/TomXu/archive/2012/01/12/2308594.html
https://www.cnblogs.com/TomXu/archive/2012/01/12/2308594.html

你可能感兴趣的:(javascrip)