侧边栏壁纸
博主头像
woku博主等级

成功的路上并不拥挤

  • 累计撰写 50 篇文章
  • 累计创建 13 个标签
  • 累计收到 3 条评论

作用域、作用域链、闭包基础

woku
2022-03-07 / 0 评论 / 0 点赞 / 89 阅读 / 2,913 字

前置知识:预编译 GO(全局执行期上下文) AO(函数执行期上下文)

在认识作用域和作用域之前,先来看看对象

  var obj = {
    name:'yg'
    age:24,
    sayHi: function() {
      console.log('heiyg')
    }
  }

  • 上面的obj对象中,我们可以 访问到obj的name,age和sayHi的属性和方法。

  • 函数也是对象,我们也可以访问函数的属性,例如fn.name、fn.length、fn.protorype

  • 在js中,有些属性我们是不能访问的,也就是js引擎内部的隐式属性[[scope]]

什么是[[scope]]作用域

函数在创建时,会生成js内部的一个隐式属性

这个属性存储函数的作用域链

有什么用?

任何程序中,都会涉及到在变量中存储值和获取值,那么这些变量怎么存储,又该如何获取,为了满足这一目标,需要制定规则,那么这套规则就叫作用域

全局作用域

程序的任意地方都能拿到定义的变量,这个变量就是全局变量,他拥有的作用域就是全局作用域。

在函数外部定义的变量基本上是全局变量

注意:

var a = 1
console.log(a)
b = 2
console.log(b)

通过var 定义的a是全局变量,拥有全局作用域

不通过var定义的b看上去也是全局变量,但严格意义上来说,他是window的一个属性,不是全局变量

变量不能被删除,只有属性才能删除,那么我们测试一下效果

在浏览器中使用:

delete a 返回false (没有删除成功)

delete b 返回true (删除成功)

在函数外部使用var定义的变量是全局变量,没有使用var定义的是window的一个属性,不是真正意义上的全局变量

var a = 1
console.log(a)
function test() {
    b = 2
}
test()
console.log(b)

在函数内部没有使用var定义的变量会挂载到window下,作为window的一个属性,不是真正意义上的全局变量

b是window的属性,为啥能直接使用console.log(b)呢,而不是console.log(window.b)
window是一个全局对象
b找不到的时候就去全局对象window上找

局部作用域(函数作用域)

函数内部定义的变量拥有局部作用域,局部作用域可以屏蔽变量

var a = 1
console.log(a)
function test() {
    var b = 2
}
test()
console.log(b)

b变量的声明是在函数内部,全局原则上无法访问局部作用域中的变量,如果要访问,可以使用闭包喔!

ES6中块级作用域

var a = 1
console.log(a)
function test() {
    var b = 2
    if(b == 2) {
        var c = 3
    }else {
        var d = 4
    }
    console.log(c)
}
test()

在程序中用{}包起立的我们都称为块

if(b == 2) else

这里分部是2块,在块中使用var定义的变量,在块的外面能访问到,要形成块级作用域(也就是在块中定义的变量,只能自己块能访问),需要结合ES6中的let和const

  • let和const声明的变量预编译时不会进行变量提升
  • let和const声明的变量拥有块级作用域

动态作用域

在运行的时候才知道结果,一般和this指向有关

var a = 1
console.log(a)
function test() {
   console.log(this.a)
}
test.call({a:111})

深入理解作用域

如下代码

<script>
function a(){
  function b(){}
  var a = 1
  b()
}
var c = 3
a()
</script>
  • 在全局执行的前一刻,会生成GO。此时函数已经被定义
  • 函数被定义的时候,生成js内部的一个隐式属性,也就是[[scope]] (作用域),保存着这个函数a的作用域链,第0位保存当前环境下全局执行期上下文GO 每个函数的作用域链上都会有GO
  • 一句句执行,当a函数被执行的前一刻,a函数的AO生成,此时a函数的作用域的第0位存储a函数的AO,相应的GO就往下移。查找变量是从作用域的顶端依次向下查找。

  • 当a函数在执行的时候,b函数定义,同样生成作用域,存储作用域链。此时作用域链拿的是他所在的环境下的,也就是a函数在执行期的作用域链

​ 和上图一样

  • 继续往下执行,当b函数执行前一刻,就生成自己的AO。排在最上面,b函数的作用域链上的第一位就是自己的AO。

  • b函数执行完后,b函数的AO销毁,回归被定义时候的状态。

  • b函数执行完了,a函数也执行完了,销魂a函数的AO,a函数的这个AO上包含了函数b,销魂了AO后b函数整个就不存在了,当然b函数的作用域也没有了

  • 最后a函数回归到被定义时的状态。

整个过程

<script>
function a() {
  function b() {
    function c() {}
    c()
  }
  b()
}
a()
// a定义 a [[scope]]   0 -> GO
// a执行 a [[scope]]   0 -> a AO
//                    1 -> GO
// b定义 b [[scope]]   0 -> a AO
//                    1 -> GO
// b执行 b [[scope]]   0 -> b AO
//                    1 -> a AO
//                    2 -> GO
// c定义 c [[scope]]   0 -> b AO
//                    1 -> a AO
//                    2 -> GO
// c执行 c [[scope]]   0 -> c AO
//                    1 -> b AO
//                    2 -> a A0
//                    3 -> GO
// c结束 c [[scope]]   0 -> b AO
//                    1 -> a AO
//                    2 -> GO
// b结束 b [[scope]]   0 -> a AO
//                    1 -> GO
//       c [[scope]] 没有了
// a结束 a [[scope]]   0 ->GO
//       b [[scope]] 没有了

</script>

闭包基础

<script>
function test1() {
  function test2() {
     a = 2
     console.log(a) 
  }
  var a = 1
  return test2
}
var test3 = test1()
test3()
</script>

根据上面所学的作用域和作用域链,来分析

  • test1函数被定义时,生成[[scope]]属性,保存该函数的作用域链,第0位存储GO

  • 当test1被执行时前一刻, 生成test1的AO,当函数test1在执行的时候test2被定义,test2生成[[scope]]属性,作用域链就是它所处的环境

  • 当test1执行完结束时,test2被返回到了外部,此时test2的作用域链中拽着test1的AO,所以test1的AO并没有销毁,只是把线剪断了

  • 当test2返回到外部被test3接收,test3执行前一刻,生成了自己的AO,

执行时,a = 2,首先在自己的AO中没有找到a,于是去text1的AO中寻找,再次执行test3时,实际操作的仍然是原来test1的AO

当test2执行完后,test2的AO被销毁,但原来的test1的AO还在text2的作用域链上。

总结

当内部函数被返回到外部并保存时,就产生了闭包。闭包会让原来的作用域链不释放,过度闭包可能会导致内存泄漏或加载过慢。

0

评论区