史上最详细的作用域链和闭包解释

2,922 阅读6分钟

有关作用域链和原型链对于学习js的小伙伴都应该不陌生,今天咱们就看看最详细的作用域链解释,目录中可能有些一些不常见的名词,但是这些对于理解作用域链和闭包很有作用,而且基本都可以理解js运行机制了😁

目录

  • 执行环境
  • 函数创建时添加的[[Scopes]]属性
  • 函数执行时创建的 变量对象(活动对象)
  • 函数执行时创建的作用域链 为Scope
  • 全局执行环境
  • 函数执行环境
  • 闭包的形成
  • 闭包与变量


执行环境(ECMAScript中最重要的概念)

定义:
  • 定义了函数和变量有权访问其他数据
  • 决定了函数或变量的行为

代码实现:

 const result = add(3,2)// 使用function直接声明非匿名函数可以实现“函数声明提升”
 function add(a,b){
     return a+b
 }
 const value = add(1,2)

图片展示:

产物:

  • 为该环境中定义的函数添加了[[Scopes]]属性(稍后详细解释)
  • 在执行环境的时候创建的 变量对象(活动对象)(稍后详细解释)
  • 在执行环境的时候,通过复制该函数的[[Scopes]]属性创建的作用域链 Scope(稍后详细解释)

函数创建时添加的[[Scopes]]属性

定义:
在每一个函数被创建的时候都会为该函数添加一个[[Scopes]]属性,该属性包含了父级(可以理解为包含该函数的函数执行环境)的执行环境的作用域链



函数执行时创建的 变量对象(活动对象)

定义:
每个(函数)执行环境都会创建一个变量对象(活动对象),这个变量对象上面包含该执行环境中所有定义的变量和函数还有arguments参数,



函数执行时创建的作用域链

定义:
  • 每个函数创建的时候,都会预先为该函数添加一个[[Scopes]]属性,该属性是父级(可以理解为包含该函数的函数执行空间)执行环境的作用域链
  • 每个(函数)执行环境都会取该函数的[[Scopes]]属性,并copy一份作为当前的作用域链 Scope
  • 并把该函数执行环境的变量对象,推入到当前作用域的最前端

执行中需要读取的变量都会从该作用域链的前端查找(图中的Local对象),查找过程中如果没有找到需要的变量,就会一直找到作用链的最顶端window(图中的Global对象),这就是作用域链的查找



全局执行环境

定义:
全局执行环境是js最外层的执行环境,在不同的执行环境最外层的执行环境也不同,在web中window是全局执行环境。该执行环境一直存在,window的变量对象也一直存在的。

注意:
全局执行环境一直会存在,当关闭浏览器时才会被销毁



函数执行环境

执行步骤:
  • 1 当执行流到当前函数时,该函数的执行环境就会被推入到一个环境栈(可以理解为包含该函数的函数执行环境)中执行

    • 从该函数的[[Scopes]]属性copy一份,作为作用域链
    • 执行时创建变量对象并初始化它,把该函数中的arguments、定义的变量、定义的函数都会赋值到该变量对象上面。
    • 把当前的变量对象,推入到作用域链的最前端
    • 接下来的执行中需要读取的变量,从该作用域链的前端查找,查找过程中如果没有找到就会一直找到作用链的最顶端window
  • 2 当函数执行环境完毕后(无返回引用类型),该执行环境就会被弹出,把执行权还给之前的执行环境。

  • 3 此时函数的执行环境会被销毁,里面的变量对象和作用域链都会被销毁

闭包的形成

定义:
闭包是指有权访问另一个函数作用域的变量和函数:

语言实现:
当一个函数中返回一个引用地址并赋值给一个变量时,就会成一个闭包,只有当变量的引用地址改变或者为null时,js的垃圾回收机制不定时触发是,闭包才会被销毁

代码实现:

function process (a){
    return function(b){
        return a+b
    }
}
const add =process(3)
add(2)// 5
add=null

缺点:
由于闭包会携带包含它的函数的作用域,因此会比其他函数占用更多的内存。过 度使用闭包可能会导致内存占用过多,我们建议读者只在绝对必要时再考虑使用闭 包。虽然像 V8 等优化后的 JavaScript 引擎会尝试回收被闭包占用的内存,但请大家 还是要慎重使用闭包

闭包与变量

经典for中访问i的问题,看代码

 function createFn(){
    var result =[];
    for(var i=0;i<10;i++){
        result[i]=function(){  console.log(i)}
     }
    return result; 
}
var res=createFn()
res[0]()

这个问题大家应该都遇见过,打印的是0还是10呢,正确答案是10 为什么是10呢,咱们看看"res【0】()"的作用链里面包含了几个 变量对象 答案是3个

  • 作用域链的最前端是 当前匿名数的变量对象
  • 第二个是 createFn的变量对象 里面包含i 是最后一次赋值的i,这时的i已经是10了
  • 第三个是 window的变量对象

咱们看看,上面的代码的执行顺序

  • 当createFn()执行时

    • 会复制该函数的[[Scopes]],作为作用域链,
    • 接着创造 该函数的变量对象并把定义在函数内的i也放在 变量对象 上面
    • 把该变量对象推入到当前作用域链的最前端
    • 代码没for循环没执行一次, 变量对象i的值就会改变一次,for执行完毕的时候此时的i已经是10了
  • 当执行放在数组中的某个匿名函数的时候

    • 会复制该函数的[[Scopes]],作为作用域链,

    • 接着创造 该函数的变量对象并把定义在函数内变量和函数放在 变量对象,

    • 把该变量对象推入到当前作用域链的最前端

    • 当代码运行到 console.log(i) 会在当前的作用域的最前端开始查找i

      • 当前作用域链的最前端没有i,去查找下一个作用域
      • 这是发现了1,返回i停止作用域链的查找。此时的i已经是10

解决方案,咱们可以考虑一下,每次循环的i都放在一个变量对象上面,接下来在创建匿名函数的时候把当前的作用域链copy一份放在每个匿名函数的[[Scopes]],在每个匿名函数运行的时候,通过作用域链查找取得每次写入到变量对象的i的值

function createFn(){
    var result =[];
    for(var i=0;i<10;i++){
        result[i]=function(num){  
        return function(){ 
        debugger;
        console.log(num)
        }
        }(i)
     }
    return result; 
}
var res=createFn()
res[0]()

看图

接着看下图就会更明白

如果有不足的地方,请大家指出来,咱们一起交流,多谢 小伙伴 “Tim在掘金lv-1” 提出这个 for中闭包去i的经典案例