【JavaScript】聊聊执行上下文、作用域和闭包吧

460 阅读10分钟

一、执行上下文

1、什么是执行上下文呢?

我的理解,执行上下文就是当前JavaScript代码被解析和执行时所在环境,那么自然,他被分为了以下两部分:

⑴ 编译解析阶段,也可以叫预处理阶段吧:

①在这个阶段它会将所有的var类型的变量存入上下文中并给它赋值为undefined

②除此之外,它还会将声明的函数存入栈中,并将函数体赋值给它

③还有一个就是this对象

⑵ 执行阶段:

①给词法环境中的变量进行赋值

②如果词法环境中不存在这个变量,就把它加入到词法环境并给它赋予相应的值

2、变量名、函数名的冲突

①在解析阶段,如果两个函数名相同,后面的会覆盖前面的,即使里面的参数个数不同,也会进行覆盖

test('second')
function test(){
    console.log('first')
}
function test(val){
    console.log(val)
}

//second

②在执行阶段

  • 执行阶段不会再处理声明方式定义的函数

  • 声明方式定义函数不会覆盖任何定义的函数和属性

  • 后赋值的属性会覆盖先赋值属性内容

③优先级

函数声明 > 用var定义的变量,当函数先声明,变量后声明,名字相同但是变量不会将函数进行覆盖,而是将它忽略

function test(){
    console.log('函数声明')
}

var test = function(){
    console.log('声明变量')
}

test()

//函数声明

3.案例

console.log(a)
console.log(b)
console.log(fn1)
console.log(fn2)
var a = 'cat'
b = 'dog'
fn1()
fn2()
function fn1(){
    console.log('pig')
}
var fn2 = ()=>{
    console.log('sheep')
}

//undefined
//报错
//函数体
//undefined
//pig
//报错

------首先编译解析阶段,声明变量a,声明函数fn1,声明变量fn2,并未声明变量b,然后进入执行阶段,首先打印a,此时a未进行赋值操作,因此为undefined,变量b未声明,因此报错,fn1声明为一个函数,因此打印出fn1这个函数体即fn1(){console.log('pig')},fn2此时也未进行赋值操作,因此也是undefined,接下去继续执行,变量a被赋值操作为cat,b这个变量未定义,忽略,fn1函数已声明,可执行,成功打印pig,继续,此时,fn2为undefined,因此会报错,最后fn2这个变量被赋值一个函数方法,到此,以上环境中的代码执行完毕

function fun(a,b){
    console.log(a)
    console.log(b)
    var a=20
    function b(){
        console.log("执行了b函数");
    }
}
fun(1,2)

//1
//函数体

------编译解析时由于在fun()中b发生了冲突,由于声明的函数比变量的优先级要高,所以b( ){...}会将变量b进行覆盖;当执行fun(1,2)这行代码时,a的值会变为1,而执行阶段不会再处理声明方式定义的函数,因此b的输出仍然函数体b( ){...}

二、作用域及作用域链

1.什么是作用域呢?

我们可以这样理解:作用域就是一个独立的地盘,让变量不会外泄、暴露出去。也就是说作用域最大的用处就是隔离变量,不同作用域下同名变量不会有冲突。ES6的到来,作用域被分为全局作用域函数作用域块级作用域

① 全局作用域:

  • 全局作用域有且只有一个,访问不了局部作用域中的变量

  • 直接编写在 script 标签之中的JS代码,都是全局作用域

  • 或者是一个单独的 JS 文件中的

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

  • 在全局作用域中有一个全局对象 window(代表的是一个浏览器的窗口,由浏览器创建),可以直接使用

  • 在全局作用域中,所创建的变量都会作为window对象的属性保存,所创建的函数都会作为window对象的方法保存

② 函数作用域:

  • 顾明思议,函数内部中的作用域
  • 函数内部的代码仅在函数内部起作用
  • 调用函数时创建函数作用域,函数执行完毕之后,函数作用域销毁
  • 每调用一次函数就会创建一个新的函数作用域,它们之间是相互独立的
  • 函数内部可访问全局作用域中的变量

③ 块级作用域:

  • ES6新增,用let命令新增了块级作用域,外层作用域无法获取到内层作用域,非常安全明了。即使外层和内层都使用相同变量名,也都互不干扰
<script>
    var num = 10
    var test = 99
    function fn(){
        var num = 20
        test = 100
        console.log(num)
        console.log(test)
    }
    fn()
    console.log(num)
    console.log(test)
    
    //20
    //100
    //10
    //100
    
</script>

全局作用域中定义了变量num,函数作用域中也定义了变量num,因为函数作用域与外界隔离,所以变量名虽然一样,但并不起冲突,因此首先打印的是20,而test变量是全局作用域中的,函数内部是可以访问全员作用域中的变量的,因此将全局作用域中的test赋值为100,所以,随后打印100,函数作用域外部访问不了函数内部变量,所以接下来打印的是全局作用域中的变量num,为10,最后,因为函数执行时修改了全局作用域中的变量test,因此最后打印的是100

2.全局变量和局部变量

在JavaScript中,根据作用域的不同,变量可以分为两种:全局变量 和 局部变量:

① 全局变量:

  • 在全局作用域下声明的变量叫做 全局变量(在函数外部定义的变量)
  • 全局变量在全局(代码的任何位置)下都可以使用;全局作用域中无法访问到局部作用域中的变量
  • 全局变量第一种创建方式:在全局作用域下 var声明的变量是全局变量
  • 全局变量第二种创建方式:如果在函数内部,没有使用 var关键字声明直接赋值的变量也属于 全局变量(不建议使用)

② 局部变量:

  • 在局部作用域下声明的变量叫做局部变量(在函数内部定义的变量)
  • 局部变量只能在函数内部使用,在局部作用域中可以访问到全局变量
  • 在函数内部 var 声明的变量就是局部变量
  • 函数的形参实际上就是局部变量

③ 全局变量和局部变量的区别:

  • 全局变量:在任何一个地方都可以使用,全局变量只有在浏览器关闭的时候才会销毁,比较占用内存资源
  • 局部变量:只能在函数内部使用,当其所在代码块被执行时,会被初始化;当代码块执行完毕就会销毁,因此更节省节约内存空间

3.作用域链:

只要是代码,就有一个作用域,写在函数内部的就叫做局部作用域;

如果函数中还有函数,那么在这个作用域中又可以诞生一个作用域;

当在函数作用域中操作一个变量的时候,会先在自身作用域中查找,如果有就直接使用,如果没有就向上级作用域中寻找。如果全局作用域中也没有,那么就报错。

根据内部函数可以访问可以访问外部函数变量的这种机制,用链式查找决定哪些数据能被内部函数访问,就称为函数作用域链   

var a = 1;
function fn1(){
    var a=2;
    var b='22';
    fn2();
    function fn2(){
        var a =3;
        fn3();
        function fn3(){
            var a=4;
            console.log(a);  
            console.log(b);  
        }
    }
}
fn1();

//4
//22

三、闭包

1.什么是闭包?

我们都知道,js的作用域分两种,全局和局部,基于我们所熟悉的作用域链相关知识,我们知道在js作用域环境中访问变量的权利是由内向外的,内部作用域可以获得当前作用域下的变量并且可以获得当前包含当前作用域的外层作用域下的变量,反之则不能,也就是说在外层作用域下无法获取内层作用域下的变量,同样在不同的函数作用域中也是不能相互访问彼此变量的,那么我们想在一个函数内部也有限权访问另一个函数内部的变量该怎么办呢?闭包就是用来解决这一需求的,闭包的本质就是在一个函数内部创建另一个函数

2.闭包的特性

①函数嵌套函数

②函数内部可以引用函数外部的参数和变量

③参数和变量不会被垃圾回收机制回收

3.案例分析

function a(){
    var name = 'liang'
    return function(){
        return name
    }
}
var b = a()
console.log(b())

//liang

在这段代码中,a()中的返回值是一个匿名函数,这个函数在a()作用域内部,所以它可以获取a()作用域下变量name的值,将这个值作为返回值赋给全局作用域下的变量b,实现了在全局变量下获取到局部变量中的变量的值

function fn(){
    var num = 3
    return function(){
        var n = 0
        console.log(++n)
        console.log(++num)
    }
}
var fn1 = fn()
fn1()  //1 4
fn1()  //1 5

一般情况下,在函数fn执行完后,就应该连同它里面的变量一同被销毁,但是在这个例子中,匿名函数作为fn的返回值被赋值给了fn1,这时候相当于fn1=function(){var n = 0 ... },并且匿名函数内部引用着fn里的变量num,所以变量num无法被销毁,而变量n是每次被调用时新创建的,所以每次fn1执行完后它就把属于自己的变量连同自己一起销毁,于是乎最后就剩下孤零零的num,于是这里就产生了内存消耗的问题

定时器与闭包结合会怎么样呢? 写一个for循环,让它按顺序打印出当前循环次数

for(var i = 0 ; i < 5 ; i++){
    setTimeout(()=>{
        console.log(i)
    },1000)
}

//5 5 5 5 5

按照预期它应该依次输出1 2 3 4 5,而结果它输出了五次5,这是为什么呢?原来由于js是单线程的,所以在执行for循环的时候定时器setTimeout被安排到任务队列中排队等待执行,而在等待过程中for循环就已经在执行,等到setTimeout可以执行的时候,for循环已经结束,i的值也已经编程5,所以打印出来五个5,那么我们为了实现预期结果应该怎么改这段代码呢?(ps:如果把for循环里面的var变成let,也能实现预期结果)

for(var i = 0 ; i < 5 ; i++){
    (function(i){
        setTimeout(()=>{
            console.log(i)
        },i*1000)
    })(i)
}

//0
//1
//2
//3
//4

4.闭包的优缺点

优点:

①保护函数内的变量安全 ,实现封装,防止变量流入其他环境发生命名冲突

②在内存中维持一个变量,可以做缓存(但使用多了同时也是一项缺点,消耗内存)

③匿名自执行函数可以减少内存消耗

缺点:

①被引用的私有变量不能被销毁,增大了内存消耗,容易造成内存泄漏,解决方法是可以在使用完变量后手动为它赋值为null;

②其次由于闭包涉及跨域访问,所以会导致性能损失,我们可以通过把跨作用域变量存储在局部变量中,然后直接访问局部变量,来减轻对执行速度的影响