有关作用域链和原型链对于学习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的经典案例