由两道题扩展的对作用域,作用域链,闭包,立即执行函数,匿名函数的认识总结

1,027 阅读7分钟

前言

最近在学JS,前几天看到两道题,刚开始看懵懵懂懂,这几天通过各种查资料,慢慢的理解,顿悟了,对匿名函数,闭包,立即执行函数的理解也更深了一点,在此分享给大家我的理解与总结,希望能帮助大家理解.因为这篇文章是我用心总结的,查阅了很多的资料,所以总结的比较细,篇幅较长,如果没耐心,建议跳出,点个收藏,以后如果要用到,有耐心想看时,方便查阅.另外如果有啥错误,还望指正


题目一

function fn() {
        for (var i = 0; i < 2; i++) {
            var variate = i;
            setTimeout(function () {
                console.log("setTimeout执行后:" + variate);
            }, 1000);
        }
        console.log(i);
    }
    fn();

最后结果是啥呢?

结果是,先打印2,再打印2个1 为什么呢? 先来梳理下函数执行过程:

  • 首先for循环遍历i,(0,1)的时候分别将遍历值传给variate变量,variate变量最后保存的值为1
  • 当i值为2时,指针跳出循环,执行到打印i值这步,此时i=2
  • 执行函数fn(),执行完毕后,触发setTimeout事件,因为循环2次,而且最后保存在这个作用域中变量的值为1,所以最后输出2个1
  • 所以最后的打印的值为2,1,1

分析完了,先不急,我们先来了解下setTimeout事件

setTimeout事件

  • setTimeout事件有两个参数:事件,时间开始执行时间
  • setTimeout事件是异步的
  • 当调用setTimeout事件时,会把函数参数,放到事件队列中。等主程序运行完,再调用

理解这个后,答案就很容易得出了


题目二

function fn() {
           for (var i = 0; i < 2; i++) {
               (function () {
                   var variate = i;
                   setTimeout(function () {
                       alert(variate);
                   }, 1000);
               })();
                            
           }
          console.log(i);
          console.log(variate);
       }
       fn(); 

先分析下整体结构: 函数体内包含一个for循环体,循环体内又包含一个匿名函数,形成闭包,加上两个小括号-->(匿名函数)()形成立即执行函数

再思考下函数执行过程

  1. i=0时,进入函数体内,因为是立即执行,所以i值进入匿名函数,通过作用域链,变量variate获得i值,匿名函数体内的setTimeout中的变量variate获得i值,第一轮循环结束;

  2. i=1时,执行与1同样的过程;

  3. i=2,跳出循环,打印i,variate;

结果是啥呢?

Excuse me?竟然有错误?

好,那就让我们来解决错误,错误显示variate is not defined,原来是这样,没定义,那分析一波,为什么会显示未定义呢? 首先我们看函数内部,内部已经定义了,所以我们想到作用域的问题

作用域和作用域链

  • 作用域

变量和函数的访问区域,分全局作用域函数作用域,在es6中添加let关键字后有了块级作用域概念.

变量提升: JS在解析代码前会先将所有函数体内的变量,提升至函数体顶端,来看个例子

var Gscope = "global";
        function t() {
            var Gscope;
            console.log("这是全局变量:"+Gscope);//这是全局变量:undefined
            let Lscope = "local";
            console.log("这是局部变量"+Lscope);//这是局部变量local
        }  
    t();
    

为什么第一个值为undefined?因为函数体内的Gscope变量被提升至函数体顶端,但是未赋值,so,undefined.

let关键字:let用于声明变量,但是let声明的变量只在let所在的代码块(块级作用域)有用,OK,show code

for (let i = 0; i < 2; i++) {
            let i = 'a';
            console.log(i);//a a
        }
    console.log(i);//i is not defined
  • 作用域链

什么是作用域链?有什么用途?怎么创建起来的?

先引用一句高级程序设计里的话:

作用域链本质上是一个指向变量对象的指针列表,它只引用但不实际包含变量对象

我的理解是:

作用域链就相当于是沟通执行环境内的各个变量与函数的桥梁,通过作用域链,同一执行环境里面的变量和函数都有权利访问对方;

不同的执行环境间是怎样的呢?

不同执行环境间的交流还是通过桥梁(作用域链),但是现在桥梁变成单行道了,只能允许内部环境访问外部环境,但外部环境不能访问内部环境.内部环境通过桥梁能够向上搜索查询变量和函数,但外部却不能向下搜索进入另一个执行环境.理解这个后,出现题目二的问题,variate is not defined,就很容易理解了:

因为他们两个压根不在同一个执行环境,而且,里面的变量对象通过闭包能够访问外部环境变量,但外部环境变量无权访问内部的变量variate.

这时可能又蹦出一个问题了,"桥梁"(执行环境的作用域链)怎么搭建起来的呢?

  1. 先创建一个预先包含全局变量对象的作用域链,保存在内部的[scope]属性中
  2. 调用函数时,为函数搭建一个执行环境
  3. 复制函数的[scope]中的对象构建起执行环境的作用域链
  4. 创建活动对象,并将活动对象推入执行环境的前端

分析完后,再重新阅读下作用域的概念,会发现很有道理!


闭包

首先提出几个问题:什么是闭包? 为什么要用它?它有啥缺点?怎么创建?

什么是闭包?

闭包是指有权访问另一个函数作用域中变量的函数

先贴上刚刚那一段代码

function fn() {
           for (var i = 0; i < 2; i++) {
               (function () {
                   var variate = i;
                   setTimeout(function () {
                       console.log("setTimeout执行后:"+variate);
                   }, 1000);
               })();//闭包,立即执行函数,匿名函数
                            
           }
          console.log(i);//2
          console.log(variate);//variate is not defined
       }
       fn(); 

通过定义可以知道,闭包本质还是作用域链的问题. 那为什么内部环境能访问外部环境呢? 那就先探讨下,函数调用时会发生什么吧!

  1. 先创建执行环境和作用域链;
  2. 初始化函数的活动对象(命名参数值,arguments);
  3. 在作用链中搜索具有相应名字的变量,实现对变量的读取和写入;
  4. 调用执行完毕,销毁局部活动对象,仅保存全局作用域. 所以关键还是内部函数作用域链将外部的活动对象添加到自己作用域中了

这个例子中函数fn()内部嵌套了一个匿名函数形成闭包,内部的variate变量变为私有成员变量,所以外部无法访问,因而会报错variate is not defined

为什么用闭包?

  • 因为在闭包内部保持了对外部活动对象的访问,但外部的变量却无法直接访问内部,避免了全局污染;
  • 可以当做私有成员,弥补了因js语法带来的面向对象编程的不足;
  • 可以长久的在内存中保存一个自己想要保存的变量.

闭包有啥缺点呢?

  1. 可能导致内存占用过多,因为闭包携带了自身的函数作用域
  2. 闭包只能取得外部包含函数中得最后一个值

怎么创建闭包? 在函数内部嵌套使用函数


匿名函数

什么是匿名函数? 顾名思义,就是没有名字的函数 如例子中的代码就是一个匿名函数

function () {
                   var variate = i;
                   setTimeout(function () {
                       console.log("setTimeout执行后:"+variate);
                   }, 1000);
               }

匿名函数优缺点?
优点:可以通过var关键字创建函数表达式,函数表达式不会出现变量提升的情况,只有在真正被解释执行的时候才会执行到函数表达式所在的代码行,有效避免了全局污染;

缺点:匿名函数绑定的事件不能解绑


立即执行函数

什么是立即执行函数?有什么作用?

什么是立即执行函数?

声明一个匿名函数,并且马上调用它{通过加()的形式}

立即执行函数的形式

(匿名函数)();

 (function () {
                   var variate = i;
                   setTimeout(function () {
                       console.log("setTimeout执行后:"+variate);
                   }, 1000);
               })()

为什么要用小括号将匿名函数包裹起来?

为了通过浏览器的语法检查

作用? 创建一个独立的作用域,避免全局污染


小结

通过两道题扩展出来知识点,并且总结出来,现在对知识点的基础概念,以及一些实现原理有了很清晰的认识,这种感觉很棒