作用域、作用域链、浅谈闭包

134 阅读7分钟

作用域是可访问变量的集合。JavaScript中函数和对象也算是变量,简单来说,作用域就是一个区域,包含了其中变量,常量,函数等等定义信息和赋值信息,以及这个区域内代码书写的结构信息。作用域决定了这些变量的可访问性(可见性),作用域就是一个独立的地盘,让变量不会外泄、暴露出去,也就是说作用域最大的用处就是隔离变量,不同作用域下同名变量不会有冲突。

ES6之前作用域分为全局作用域局部作用域,ES6新增了块级作用域

1.全局作用域

在代码中任何地方都能访问到的变量的集合称为全局作用域,拥有全局作用域的变量称为全局变量,一般来说以下几种情形拥有全局作用域

  1. 直接编写在 script 标签之中定或者是一个单独的 JS 文件中的代码拥有全局作用域
var outVariable = "我是最外层变量";
function outFun() { //最外层函数
    var inVariable = "内层变量";
    function innerFun() { //内层函数
        console.log(inVariable);
    }
    innerFun();
}
console.log(outVariable); //我是最外层变量
outFun(); //内层变量
console.log(inVariable); //inVariable is not defined
innerFun(); //innerFun is not defined
  1. 所有末定义直接赋值的变量会被自动声明为拥有全局作用域
function outFun2() {
    variable = "未定义直接赋值的变量";
    var inVariable2 = "内层变量2";
}
outFun2();
console.log(variable); //未定义直接赋值的变量
console.log(inVariable2); //inVariable2 is not defined

虽然variable是在函数内部的,但是它并未被定义

  1. 所有window对象的属性拥有全局作用域

一般情况下,window对象的内置属性都拥有全局作用域,例如window.name、window.location、window.top等等。

全局变量其实会转化为window对象的属性。

<script>
    var a=1
    console.log(window.a); //1
 </script>

全局作用域在页面打开时创建,页面关闭时销毁。

全局作用域的弊端:如果我们写了很多行 JS 代码,变量定义都没有用函数包括,那么它们就全部都在全局作用域中。这样就会污染全局命名空间, 容易引起命名冲突。

2.局部作用域

也叫做函数作用域,即定义在函数中的变量的集合,拥有局部作用域的变量称为局部变量,全局作用域不能访问到局部作用域中的变量

调用函数时创建函数作用域,函数执行完毕之后,函数作用域销毁,每调用一次函数就会创建一个新的函数作用域,它们之间是相互独立的。

<script>
    var num = 10;
    function nu(){
        var num = 20;
        var num2=30
        console.log(num);
    }
    nu();//20
    comsole.log(num2)//num2 is not defined
    console.log(num);//10
</script>

num2是局部变量,外面访问不到,两个num一个在全局作用域下,另一个在局部作用域下,虽然两个变量的变量名相冲突,但是并没有影响。所以,在不同的作用域下,变量名相同也不受影响,这样就很有效的减少了命名冲突。 jQuery、Zepto 等库的源码,所有的代码都会放在(function(){....})()中。因为放在里面的所有变量,都不会被外泄和暴露,不会污染到外面,不会对其他的库或者 JS 脚本造成影响,这是函数作用域的一个体现。

3.块级作用域

在{}中声明的变量,如if{....},for{...},function{....},当通过新增命令let和const声明时,该变量就有了块级作用域,外面的作用域是不能访问到块级作用域。

function fun() {
      var a = 10
      console.log(c)
      if (3 < 4) {
        let c = 20
      }
    }
    fun()//c is not defined

块级变量有如下特点

  1. 声明变量不会提升到代码块顶部
<script>
    console.log(a)
    let a = 10
  </script>// Cannot access 'a' before initialization

如果是用var来声明,就可以提升变量声明,此时结果是undefined

  1. 禁止重复声明

如果一个标识符已经在代码块内部被定义,那么在此代码块内使用同一个标识符进行 let 声明就会导致抛出错误。

var count = 30;
let count = 40; // Uncaught SyntaxError: Identifier 'count' has already been declared

但如果在嵌套的作用域内使用 let 声明一个同名的新变量,则不会抛出错误。

var count = 30;
if (condition) {
let count = 40;
// 其他代码
}
  1. 循环中的绑定块作用域的妙用

开发者可能最希望实现for循环的块级作用域了,因为可以把声明的计数器变量限制在循环内,例如:

for (let i = 0; i < 10; i++) {
  // ...
}
console.log(i);
// ReferenceError: i is not defined

上面代码中,计数器i只在for循环体内有效,在循环体外引用就会报错。

var a = [];
for (var i = 0; i < 10; i++) {
  a[i] = function () {
    console.log(i);
  };
}
a[6](); // 10

如果使用let,声明的变量仅在块级作用域内有效,最后输出的是 6。

var a = [];
for (let i = 0; i < 10; i++) {
  a[i] = function () {
    console.log(i);
  };
}
a[6](); // 6

上面代码中,变量i是let声明的,当前的i只在本轮循环有效,所以每一次循环的i其实都是一个新的变量,所以最后输出的是6。你可能会问,如果每一轮循环的变量i都是重新声明的,那它怎么知道上一轮循环的值,从而计算出本轮循环的值?这是因为 JavaScript 引擎内部会记住上一轮循环的值,初始化本轮的变量i时,就在上一轮循环的基础上进行计算。

相当于下面这样写:

var a = [];
{ let k;
    for (k = 0; k < 10; k++) {
      let i = k; //注意这里,每次循环都会创建一个新的i变量
      a[i] = function () {
        console.log(i);
      };
    }
}
a[6](); // 6var a = [];
{ let k;
    for (k = 0; k < 10; k++) {
      let i = k; //注意这里,每次循环都会创建一个新的i变量
      a[i] = function () {
        console.log(i);
      };
    }
}
a[6](); // 6

4.作用域链

函数也可以看做是对象,js中有些对象的隐式属性是无法访问到的,[[scope]]是与作用域有关的隐式属性,在函数创建时生成,它是函数存储作用域链的容器

<script>
    function a(){
      function b(){
        var b=1;
      }
      var a=1;
      b();
    }
    var c=3;
    a();
</script>

当函数a被定义时,系统会生成函数a的[[scoped]]属性,该属性会存储函数的作用域链,作用域链的第0位存储当前环境下的全局执行期上下位GO,GO里面存着全局下的所有属性和方法

image-20220802134857187.png

当函数a被执行时(前一刻), 作用域链的顶端存储a函数生成的函数执行期上下文AO,同时第一位存储GO,查找变量是在函数a中存储的作用域链中从顶端开始向下依次查找

image-20220802135826203.png

当函数b被定义时, 它也会生成自己的[[scope]]属性存储自己的作用域链,此时的作用域链是和a函数被执行时的作用域链是一样的

image-20220802140828455.png

当函数b被执行时(前一刻) ,它会生成自己的AO,存储在作用域链的最顶端

image-20220802141229042.png

函数b执行结束后,它的AO会被销毁,即重新回到被定义的状态

image-20220802141607547.png

函数a执行完后,它的AO被销毁,因为函数b是在AO中被定义的,所以函数b的[[scope]]也不存在了,函数a回到被定义时的状态

image-20220802141900025.png

5.闭包

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

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

test1函数被定义的时候,系统生成[[scope]]属性,存放作用域链,作用域链有GO

image-20220802171240117.png

当test1被执行时候,test1生成自己的AO,同时test2被定义,生成自己的[[scope]]并存储作用域链

image-20220802171542951.png

当test1执行结束后,test1的AO并不会被销毁,因为我们把test2函数返回到全局变量test3上,而test2的作用链有test1的AO

image-20220802171856131.png

当test3执行的时候,test2的作用域链上增加自己的AO,当打印a时,会在作用域链上从上往下找,这时在test1的AO上找到a

image-20220802173000444.png