目录
执行上下文
全局执行上下文
执行上下文栈
this
作用域
全局作用域
函数作用域
作用域与执行上下文的区别
作用域链
闭包
如何产生闭包
闭包到底是什么?
产生闭包的条件
常见的闭包
闭包1:将一个函数作为另一个函数的返回值
闭包2. 将函数作为实参传递给另一个函数调用
闭包的作用
闭包的生命周期
闭包的应用:定义具有特定功能的js模块
方式一
方式二
闭包的缺点及解决
内存泄漏内存溢出
内存泄漏
内存溢出(一种程序运行出现的错误)
首页 web前端 js教程 作用域和闭包详解

作用域和闭包详解

Mar 19, 2018 pm 04:36 PM
作用域 详解

这次给大家带来作用域和闭包详解,使用作用域与闭包的注意事项有哪些,下面就是实战案例,一起来看一下。

执行上下文

执行上下文主要有两种情况:

  • 全局代码: 一段<script>标签里,有一个全局的执行上下文。所做的事情是:变量定义、函数声明

  • 函数代码:每个函数里有一个上下文。所做的事情是:变量定义、函数声明、this、arguments

PS:注意“函数声明”和“函数表达式”的区别。

全局执行上下文

在执行全局代码前将window确定为全局执行上下文。

(1)对全局数据进行预处理:(并没有赋值)

  • var定义的全局变量==>undefined, 添加为window的属性

  • function声明的全局函数==>赋值(fun), 添加为window的方法

  • this==>赋值(window)

(2)开始执行全局代码

在调用函数, 准备执行函数体之前, 创建对应的函数执行上下文对象(虚拟的, 存在于栈中)。

(1)对局部数据进行预处理:

  • 形参变量==>赋值(实参)==>添加为执行上下文的属性

  • arguments==>赋值(实参列表), 添加为执行上下文的属性

  • var定义的局部变量==>undefined, 添加为执行上下文的属性

  • function声明的函数 ==>赋值(fun), 添加为执行上下文的方法

  • this==>赋值(调用函数的对象)

(2)开始执行函数体代码

执行上下文栈

  • 1.在全局代码执行前, JS引擎就会创建一个栈来存储管理所有的执行上下文对象

  • 2.在全局执行上下文(window)确定后, 将其添加到栈中(压栈)

  • 3.在函数执行上下文创建后, 将其添加到栈中(压栈)

  • 4.在当前函数执行完后,将栈顶的对象移除(出栈)

  • 5.当所有的代码执行完后, 栈中只剩下window

this

this指的是,调用函数的那个对象。this永远指向函数运行时所在的对象。

解析器在调用函数每次都会向函数内部传递进一个隐含的参数,这个隐含的参数就是this。

根据函数的调用方式的不同,this会指向不同的对象:【重要】

  • 1.以函数的形式调用时,this永远都是window。比如fun();相当于window.fun();

  • 2.以方法的形式调用时,this是调用方法的那个对象

  • 3.以构造函数的形式调用时,this是新创建的那个对象

  • 4.使用call和apply调用时,this是指定的那个对象

需要特别提醒的是:this的指向在函数定义时无法确认,只有函数执行时才能确定。this的几种场景:

  • 1、作为构造函数执行

例如:

    function Foo(name) {
        //this = {};
        this.name = name;
        //return this;
    }
    var foo = new Foo();
登录后复制
  • 2、作为对象的属性执行

    var obj = {
        name: 'A',
        printName: function () {
            console.log(this.name);
        }
    }
    obj.printName();
登录后复制
  • 3、作为普通函数执行

    function fn() {
        console.log(this); //this === window
    }
    fn();
登录后复制
  • 4、call apply bind

作用域

作用域指一个变量的作用范围。它是静态的(相对于上下文对象), 在编写代码时就确定了。

作用:隔离变量,不同作用域下同名变量不会有冲突。

作用域的分类:

if (true) {
    var name = 'smyhvae';}console.log(name);
登录后复制

上方代码中,并不会报错,因为:虽然 name 是在块里面定义的,但是 name 是全局变量。

全局作用域

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

在全局作用域中:

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

  • 创建的变量都会作为window对象的属性保存。

  • 创建的函数都会作为window对象的方法保存。

全局作用域中的变量都是全局变量,在页面的任意的部分都可以访问到。

变量的声明提前:

使用var关键字声明的变量( 比如 var a = 1),会在所有的代码执行之前被声明(但是不会赋值),但是如果声明变量时不是用var关键字(比如直接写a = 1),则变量不会被声明提前。

举例1:

    console.log(a);
    var a = 123;
登录后复制

打印结果:undefined

举例2:

    console.log(a);
    a = 123;   //此时a相当于window.a
登录后复制

程序会报错:

作用域和闭包详解

函数的声明提前:

  • 使用函数声明的形式创建的函数function foo(){}会被声明提前

也就是说,它会在所有的代码执行之前就被创建,所以我们可以在函数声明之前,调用函数。

  • 使用函数表达式创建的函数var foo = function(){}不会被声明提前,所以不能在声明前调用。

很好理解,因为此时foo被声明了,且为undefined,并没有给其赋值function(){}

所以说,下面的例子,会报错:

作用域和闭包详解

函数作用域

调用函数时创建函数作用域,函数执行完毕以后,函数作用域销毁。

每调用一次函数就会创建一个新的函数作用域,他们之间是互相独立的。

在函数作用域中可以访问到全局作用域的变量,在全局作用域中无法访问到函数作用域的变量。

在函数中要访问全局变量可以使用window对象。(比如说,全局作用域和函数作用域都定义了变量a,如果想访问全局变量,可以使用window.a

提醒1:

在函数作用域也有声明提前的特性:

  • 使用var关键字声明的变量,会在函数中所有的代码执行之前被声明

  • 函数声明也会在函数中所有的代码执行之前执行

因此,在函数中,没有var声明的变量都会成为全局变量,而且并不会提前声明。

举例1:

        var a = 1;
        function foo() {
            console.log(a);
            a = 2;     // 此处的a相当于window.a
        }
        foo();
        console.log(a);   //打印结果是2
登录后复制

上方代码中,foo()的打印结果是1。如果去掉第一行代码,打印结果是Uncaught ReferenceError: a is not defined

提醒2:定义形参就相当于在函数作用域中声明了变量。

        function fun6(e) {
            console.log(e);
        }
        fun6();  //打印结果为 undefined
        fun6(123);//打印结果为123
登录后复制

作用域与执行上下文的区别

区别1:

  • 全局作用域之外,每个函数都会创建自己的作用域,作用域在函数定义时就已经确定了。而不是在函数调用时

  • 全局执行上下文环境是在全局作用域确定之后, js代码马上执行之前创建

  • 函数执行上下文是在调用函数时, 函数体代码执行之前创建

区别2:

  • 作用域是静态的, 只要函数定义好了就一直存在, 且不会再变化

  • 执行上下文是动态的, 调用函数时创建, 函数调用结束时就会自动释放

联系:

  • 执行上下文(对象)是从属于所在的作用域

  • 全局上下文环境==>全局作用域

  • 函数上下文环境==>对应的函数使用域

作用域链

当在函数作用域操作一个变量时,它会先在自身作用域中寻找,如果有就直接使用(就近原则)。如果没有则向上一级作用域中寻找,直到找到全局作用域;如果全局作用域中依然没有找到,则会报错ReferenceError。

外部函数定义的变量可以被内部函数所使用,反之则不行。

<!DOCTYPE html><html lang="en"><head>
    <meta charset="UTF-8">
    <title>Title</title>
    <script>
        //只要是函数就可以创造作用域
        //函数中又可以再创建函数
        //函数内部的作用域可以访问函数外部的作用域
        //如果有多个函数嵌套,那么就会构成一个链式访问结构,这就是作用域链
        //f1--->全局
        function f1(){
            //f2--->f1--->全局
            function f2(){
                //f3---->f2--->f1--->全局
                function f3(){
                }
                //f4--->f2--->f1---->全局
                function f4(){
                }
            }
            //f5--->f1---->全局
            function f5(){
            }
        }
    </script></head><body></body></html>
登录后复制

理解:

  • 多个上下级关系的作用域形成的链, 它的方向是从下向上的(从内到外)

  • 查找变量时就是沿着作用域链来查找的

查找一个变量的查找规则:

    var a = 1
    function fn1() {
      var b = 2
      function fn2() {
        var c = 3
        console.log(c)        console.log(b)        console.log(a)        console.log(d)      }
      fn2()    }
    fn1()
登录后复制
  • 在当前作用域下的执行上下文中查找对应的属性, 如果有直接返回, 否则进入2

  • 在上一级作用域的执行上下文中查找对应的属性, 如果有直接返回, 否则进入3

  • 再次执行2的相同操作, 直到全局作用域, 如果还找不到就抛出找不到的异常

闭包

闭包就是能够读取其他函数内部数据(变量/函数)的函数。

只有函数内部的子函数才能读取局部变量,因此可以把闭包简单理解成"定义在一个函数内部的函数"。

上面这两句话,是阮一峰的文章里的,你不一定能理解,来看下面的讲解和举例。

如何产生闭包

当一个嵌套的内部(子)函数引用了嵌套的外部(父)函数的变量或函数时, 就产生了闭包。

闭包到底是什么?

使用chrome调试查看

  • 理解一: 闭包是嵌套的内部函数(绝大部分人)

  • 理解二: 包含被引用变量 or 函数的对象(极少数人)

注意: 闭包存在于嵌套的内部函数中。

产生闭包的条件

  • 1.函数嵌套

  • 2.内部函数引用了外部函数的数据(变量/函数)。

来看看条件2:

    function fn1() {
        function fn2() {
        }
        return fn2;
    }
    fn1();
登录后复制

上面的代码不会产生闭包,因为内部函数fn2并没有引用外部函数fn1的变量。

PS:还有一个条件是外部函数被调用,内部函数被声明。比如:

    function fn1() {
        var a = 2
        var b = 'abc'
        function fn2() { //fn2内部函数被提前声明,就会产生闭包(不用调用内部函数)
            console.log(a)        }
    }
    fn1();
    function fn3() {
        var a = 3
        var fun4 = function () {  //fun4采用的是“函数表达式”创建的函数,此时内部函数的声明并没有提前
            console.log(a)        }
    }
    fn3();
登录后复制

常见的闭包


  1. 将一个函数作为另一个函数的返回值


    1. 将函数作为实参传递给另一个函数调用。

    闭包1:将一个函数作为另一个函数的返回值

        function fn1() {
          var a = 2
          function fn2() {
            a++
            console.log(a)      }
          return fn2    }
        var f = fn1();   //执行外部函数fn1,返回的是内部函数fn2
        f() // 3       //执行fn2
        f() // 4       //再次执行fn2
    登录后复制
    登录后复制

    当f()第二次执行的时候,a加1了,也就说明了:闭包里的数据没有消失,而是保存在了内存中。如果没有闭包,代码执行完倒数第三行后,变量a就消失了。

    上面的代码中,虽然调用了内部函数两次,但是,闭包对象只创建了一个。

    也就是说,要看闭包对象创建了一个,就看:外部函数执行了几次(与内部函数执行几次无关)。

    闭包2. 将函数作为实参传递给另一个函数调用

        function showDelay(msg, time) {
          setTimeout(function() {  //这个function是闭包,因为是嵌套的子函数,而且引用了外部函数的变量msg
            alert(msg)      }, time)    }
        showDelay('atguigu', 2000)
    登录后复制

    上面的代码中,闭包是里面的funciton,因为它是嵌套的子函数,而且引用了外部函数的变量msg。

    闭包的作用

    • 作用1. 使用函数内部的变量在函数执行完后, 仍然存活在内存中(延长了局部变量的生命周期)

    • 作用2. 让函数外部可以操作(读写)到函数内部的数据(变量/函数)

    我们让然拿这段代码来分析:

        function fn1() {
          var a = 2
          function fn2() {
            a++
            console.log(a)      }
          return fn2    }
        var f = fn1();   //执行外部函数fn1,返回的是内部函数fn2
        f() // 3       //执行fn2
        f() // 4       //再次执行fn2
    登录后复制
    登录后复制

    作用1分析

    上方代码中,外部函数fn1执行完毕后,变量a并没有立即消失,而是保存在内存当中。

    作用2分析:

    函数fn1中的变量a,是在fn1这个函数作用域内,因此外部无法访问。但是通过闭包,外部就可以操作到变量a。

    达到的效果是:外界看不到变量a,但可以操作a

    比如上面达到的效果是:我看不到变量a,但是每次执行函数后,让a加1。当然,如果我真想看到a,我可以在fn2中将a返回即可。

    回答几个问题:

    • 问题1. 函数执行完后, 函数内部声明的局部变量是否还存在?

    答案:一般是不存在, 存在于闭中的变量才可能存在。

    闭包能够一直存在的根本原因是f,因为f接收了fn1(),这个是闭包,闭包里有a。注意,此时,fn2并不存在了,但是里面的对象(即闭包)依然存在,因为用f接收了。

    • 问题2. 在函数外部能直接访问函数内部的局部变量吗?

    不能,但我们可以通过闭包让外部操作它。

    闭包的生命周期

    1. 产生: 嵌套内部函数fn2被声明时就产生了(不是在调用)

    2. 死亡: 嵌套的内部函数成为垃圾对象时。(比如f = null,就可以让f成为垃圾对象。意思是,此时f不再引用闭包这个对象了)

    闭包的应用:定义具有特定功能的js模块

    • 将所有的数据和功能都封装在一个函数内部(私有的),只向外暴露一个包含n个方法的对象或函数。

    • 模块的使用者, 只需要通过模块暴露的对象调用方法来实现对应的功能。

    方式一

    (1)myModule.js:(定义一个模块,向外暴露多个函数,供外界调用)

    function myModule() {
        //私有数据
        var msg = 'Smyhvae Haha'
        //操作私有数据的函数
        function doSomething() {
            console.log('doSomething() ' + msg.toUpperCase()); //字符串大写
        }
        function doOtherthing() {
            console.log('doOtherthing() ' + msg.toLowerCase()) //字符串小写
        }
        //通过【对象字面量】的形式进行包裹,向外暴露多个函数
        return {
            doSomething1: doSomething,
            doOtherthing2: doOtherthing    }}
    登录后复制

    上方代码中,外界可以通过doSomething1和doOtherthing2来操作里面的数据,但不让外界看到。

    (2)index.html:

    <!DOCTYPE html><html lang="en"><head>
        <meta charset="UTF-8">
        <title>05_闭包的应用_自定义JS模块</title></head><body><!--闭包的应用 : 定义JS模块  * 具有特定功能的js文件  * 将所有的数据和功能都封装在一个函数内部(私有的)  * 【重要】只向外暴露一个包含n个方法的对象或函数  * 模块的使用者, 只需要通过模块暴露的对象调用方法来实现对应的功能--><script type="text/javascript" src="myModule.js"></script><script type="text/javascript">
        var module = myModule();
        module.doSomething1();
        module.doOtherthing2();</script></body></html>
    登录后复制

    方式二

    同样是实现方式一种的功能,这里我们采取另外一种方式。

    (1)myModule2.js:(是一个立即执行的匿名函数)

    (function () {
        //私有数据
        var msg = 'Smyhvae Haha'
        //操作私有数据的函数
        function doSomething() {
            console.log('doSomething() ' + msg.toUpperCase())    }
        function doOtherthing() {
            console.log('doOtherthing() ' + msg.toLowerCase())    }
        //外部函数是即使运行的匿名函数,我们可以把两个方法直接传给window对象
        window.myModule = {
            doSomething1: doSomething,
            doOtherthing2: doOtherthing    }})()
    登录后复制

    (2)index.html:

    <!DOCTYPE html><html lang="en"><head>
        <meta charset="UTF-8">
        <title>05_闭包的应用_自定义JS模块2</title></head><body><!--闭包的应用2 : 定义JS模块  * 具有特定功能的js文件  * 将所有的数据和功能都封装在一个函数内部(私有的)  * 只向外暴露一个包信n个方法的对象或函数  * 模块的使用者, 只需要通过模块暴露的对象调用方法来实现对应的功能--><!--引入myModule文件--><script type="text/javascript" src="myModule2.js"></script><script type="text/javascript">
        myModule.doSomething1()    myModule.doOtherthing2()</script></body></html>
    登录后复制

    上方两个文件中,我们在myModule2.js里直接把两个方法直接传递给window对象了。于是,在index.html中引入这个js文件后,会立即执行里面的匿名函数。在index.html中把myModule直接拿来用即可。

    总结:

    当然,方式一和方式二对比后,我们更建议采用方式二,因为很方便。

    但无论如何,两种方式都采用了闭包。

    闭包的缺点及解决

    缺点:函数执行完后, 函数内的局部变量没有释放,占用内存时间会变长,容易造成内存泄露。

    解决:能不用闭包就不用,及时释放。比如:

        f = null;  // 让内部函数成为垃圾对象 -->回收闭包
    登录后复制

    总而言之,你需要它,就是优点;你不需要它,就成了缺点。

    内存泄漏内存溢出

    内存泄漏

    内存泄漏:占用的内存没有及时释放。内存泄露积累多了就容易导致内存溢出。

    常见的内存泄露:

    • 1.意外的全局变量

    • 2.没有及时清理的计时器或回调函数

    • 3.闭包

    情况1举例:

        // 意外的全局变量
        function fn() {
            a = new Array(10000000);
            console.log(a);
        }
        fn();
    登录后复制

    情况2举例:

        // 没有及时清理的计时器或回调函数
        var intervalId = setInterval(function () { //启动循环定时器后不清理
            console.log('----')    }, 1000)    // clearInterval(intervalId);  //清理定时器
    登录后复制

    情况3举例:

    <script type="text/javascript">
      function fn1() {
        var arr = new Array[100000];   //这个数组占用了很大的内存空间
        function fn2() {
          console.log(arr.length)    }
        return fn2  }
      var f = fn1()  f()
      f = null //让内部函数成为垃圾对象-->回收闭包</script>
    登录后复制

    内存溢出(一种程序运行出现的错误)

    内存溢出:当程序运行需要的内存超过了剩余的内存时,就出抛出内存溢出的错误。

      //内存溢出
      var obj = {}
      for (var i = 0; i < 10000; i++) {
        obj[i] = new Array(10000000);   //把所有的数组内容都放到obj里保存,导致obj占用了很大的内存空间
        console.log('-----')  }
    登录后复制

    相信看了本文案例你已经掌握了方法,更多精彩请关注php中文网其它相关文章!

    推荐阅读:

    Vue指令的使用

    JS闭包的使用

    JavaScript的字符串怎样使用

    以上是作用域和闭包详解的详细内容。更多信息请关注PHP中文网其他相关文章!

    本站声明
    本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系admin@php.cn

    热AI工具

    Undresser.AI Undress

    Undresser.AI Undress

    人工智能驱动的应用程序,用于创建逼真的裸体照片

    AI Clothes Remover

    AI Clothes Remover

    用于从照片中去除衣服的在线人工智能工具。

    Undress AI Tool

    Undress AI Tool

    免费脱衣服图片

    Clothoff.io

    Clothoff.io

    AI脱衣机

    Video Face Swap

    Video Face Swap

    使用我们完全免费的人工智能换脸工具轻松在任何视频中换脸!

    热工具

    记事本++7.3.1

    记事本++7.3.1

    好用且免费的代码编辑器

    SublimeText3汉化版

    SublimeText3汉化版

    中文版,非常好用

    禅工作室 13.0.1

    禅工作室 13.0.1

    功能强大的PHP集成开发环境

    Dreamweaver CS6

    Dreamweaver CS6

    视觉化网页开发工具

    SublimeText3 Mac版

    SublimeText3 Mac版

    神级代码编辑软件(SublimeText3)

    c语言中typedef struct的用法 c语言中typedef struct的用法 May 09, 2024 am 10:15 AM

    typedef struct 在 C 语言中用于创建结构体类型别名,简化结构体使用。它通过指定结构体别名将一个新的数据类型作为现有结构体的别名。优点包括增强可读性、代码重用和类型检查。注意:在使用别名前必须定义结构体,别名在程序中必须唯一并且仅在其声明的作用域内有效。

    java中的variable expected怎么解决 java中的variable expected怎么解决 May 07, 2024 am 02:48 AM

    Java 中的变量期望值异常可以通过以下方法解决:初始化变量;使用默认值;使用 null 值;使用检查和赋值;了解局部变量的作用域。

    js中闭包的优缺点 js中闭包的优缺点 May 10, 2024 am 04:39 AM

    JavaScript 闭包的优点包括保持变量作用域、实现模块化代码、延迟执行和事件处理;缺点包括内存泄漏、增加了复杂性、性能开销和作用域链影响。

    c++中的include什么意思 c++中的include什么意思 May 09, 2024 am 01:45 AM

    C++ 中的 #include 预处理器指令将外部源文件的内容插入到当前源文件中,以复制其内容到当前源文件的相应位置。主要用于包含头文件,这些头文件包含代码中需要的声明,例如 #include <iostream> 是包含标准输入/输出函数。

    C++ 智能指针:全面剖析其生命周期 C++ 智能指针:全面剖析其生命周期 May 09, 2024 am 11:06 AM

    C++智能指针的生命周期:创建:分配内存时创建智能指针。所有权转移:通过移动操作转移所有权。释放:智能指针离开作用域或被明确释放时释放内存。对象销毁:所指向对象被销毁时,智能指针成为无效指针。

    c++中函数的定义和调用可以嵌套吗 c++中函数的定义和调用可以嵌套吗 May 06, 2024 pm 06:36 PM

    可以。C++ 允许函数嵌套定义和调用。外部函数可定义内置函数,内部函数可在作用域内直接调用。嵌套函数增强了封装性、可复用性和作用域控制。但内部函数无法直接访问外部函数的局部变量,且返回值类型需与外部函数声明一致,内部函数不能自递归。

    vue中let和var的区别 vue中let和var的区别 May 08, 2024 pm 04:21 PM

    在 Vue 中,let 和 var 声明变量时在作用域上存在差异:作用域:var 具有全局作用域,let 具有块级作用域。块级作用域:var 不创建块级作用域,let 创建块级作用域。重新声明:var 允许在同一作用域内重新声明变量,let 不允许。

    js中this的指向有几种情况 js中this的指向有几种情况 May 06, 2024 pm 02:03 PM

    JavaScript 中,this 的指向类型有:1. 全局对象;2. 函数调用;3. 构造函数调用;4. 事件处理程序;5. 箭头函数(继承外层 this)。此外,可以使用 bind()、call() 和 apply() 方法显式设置 this 的指向。

    See all articles