函数提升+上下文+内存清理及释放

news2025/1/9 6:25:51

文章目录

    • 函数提升+上下文
    • 函数释放
    • 拓展-垃圾回收机制
    • 垃圾回收之触发应用

函数提升+上下文

  1. 函数提升(Hoisting)

    • 概念:在JavaScript中,函数声明会被提升到当前作用域的顶部。这意味着可以在函数声明之前调用函数。例如:
    sayHello();
    function sayHello() {
        console.log("Hello!");
    }
    
    • 原理:当JavaScript引擎在执行代码时,会首先扫描整个作用域(例如全局作用域或者函数作用域),找到所有的函数声明,并将它们“提升”到作用域的顶部。但需要注意的是,函数表达式不会提升。例如:
    // 这会报错,因为函数表达式不会提升
    sayHi();
    var sayHi = function() {
        console.log("Hi!");
    };
    
    • 在上面的代码中,var sayHi这个变量声明会被提升,但是赋值(也就是函数表达式部分)不会提升。所以在调用sayHi的时候,它的值是undefined,调用就会出错。
  2. 函数执行上下文(Execution Context)

    • 概念:执行上下文是JavaScript中一个非常重要的概念,它定义了函数执行时的环境。当一个函数被调用时,就会创建一个新的执行上下文。这个执行上下文包括变量对象(VO)、作用域链和this的值。
    • 创建阶段
      • 变量对象(VO):在执行上下文的创建阶段,会创建变量对象。对于函数执行上下文,参数会作为变量对象的属性,函数内部声明的变量也会添加到变量对象中。例如:
      function add(num1, num2) {
          var result;
          result = num1 + num2;
          return result;
      }
      add(3, 5);
      
      add函数的执行上下文中,变量对象在创建阶段会有num1num2result这几个属性,其中num1num2会被初始化为传入的参数值(3和5),result会被初始化为undefined
      • 作用域链(Scope Chain):作用域链是由当前执行上下文的变量对象和它外层执行上下文的变量对象组成的一个链表。它用于查找变量的值。例如,在一个嵌套函数中:
      var globalVar = 10;
      function outer() {
          var outerVar = 20;
          function inner() {
              var innerVar = 30;
              console.log(globalVar + outerVar + innerVar);
          }
          inner();
      }
      outer();
      
      inner函数的执行上下文中,作用域链首先会查找自己的变量对象中的innerVar,然后查找outer函数执行上下文的变量对象中的outerVar,最后查找全局变量对象中的globalVar
      • this值this的值取决于函数的调用方式。在全局环境中,this指向window(在浏览器环境下)。在对象方法中,this指向调用该方法的对象。例如:
      var obj = {
          name: "John",
          sayName: function() {
              console.log(this.name);
          }
      };
      obj.sayName(); // 输出 "John"
      
    • 执行阶段:在执行阶段,JavaScript引擎会逐行执行函数中的代码,对变量进行赋值操作,执行函数调用等。
  3. 函数内部使用的对象

    • 函数参数作为对象:函数可以接收对象作为参数,然后在函数内部对这个对象进行操作。例如:
    function updateObject(obj) {
        obj.property = "new value";
        return obj;
    }
    var myObject = {
        property: "original value"
    };
    var updatedObject = updateObject(myObject);
    console.log(updatedObject.property); // 输出 "new value"
    
    • 在函数内部创建对象:可以在函数内部使用new关键字创建对象(如果是构造函数的话),或者使用对象字面量来创建对象。例如:
    function createObject() {
        var newObject = {
            name: "New Object",
            description: "This is a newly created object."
        };
        return newObject;
    }
    var createdObject = createObject();
    console.log(createdObject.name); // 输出 "New Object"
    
  4. 监听事件清除释放

    • 在DOM中清除事件监听器:在浏览器环境下,当给DOM元素添加事件监听器后,需要在适当的时候清除它,以避免内存泄漏等问题。如果使用addEventListener添加事件监听器,可以使用removeEventListener来清除。例如:
    var button = document.getElementById("myButton");
    function handleClick() {
        console.log("Button clicked!");
    }
    button.addEventListener("click", handleClick);
    // 之后想要清除事件监听器
    button.removeEventListener("click", handleClick);
    
    • 注意事项:在使用removeEventListener时,传递的函数必须是和添加监听器时完全相同的函数引用。如果是使用匿名函数添加的监听器,想要清除就会比较复杂。例如,下面这种情况就很难正确地清除监听器:
    button.addEventListener("click", function() {
        console.log("Anonymous function click!");
    });
    
    • 一种解决方法是将匿名函数赋值给一个变量,然后在removeEventListener中使用这个变量:
    var anonymousClickHandler = function() {
        console.log("Anonymous function click!");
    };
    button.addEventListener("click", anonymousClickHandler);
    // 清除监听器
    button.removeEventListener("click", anonymousClickHandler);
    

函数释放

  1. JavaScript的内存管理基础

    • 在JavaScript中,内存管理主要是由垃圾回收器(Garbage Collector,GC)来自动完成的。垃圾回收器会定期扫描内存,找出那些不再被使用的对象,并释放它们所占用的内存空间。
    • 当一个对象没有任何引用指向它时,就会被垃圾回收器认为是“垃圾”,从而被回收。例如,在下面的代码中:
    function createObject() {
        let myObject = {
            name: "Example"
        };
        return myObject;
    }
    let newObject = createObject();
    // 此时myObject对象仍然被newObject引用,不会被回收
    newObject = null;
    // 现在没有引用指向myObject对象了,它会在下次垃圾回收时被回收
    
  2. 函数内部局部变量的释放

    • 手动设置为null
      • 在函数内部,如果有一些比较占用内存的对象,如大型数组、复杂的对象等,可以在函数执行结束前手动将它们设置为null。例如:
      function processData() {
          let largeArray = new Array(1000000).fill(0);
          // 对大型数组进行一些操作...
          // 操作完成后,手动将其设置为null
          largeArray = null;
      }
      
      • 当把largeArray设置为null后,就切断了对这个大型数组对象的引用。这样,在下一次垃圾回收时,这个数组对象占用的内存就有更大的机会被回收。不过,需要注意的是,设置为null并不意味着立即释放内存,只是告诉垃圾回收器这个对象可以被回收了。
    • 让变量超出作用域
      • 函数内部的局部变量在函数执行结束后会自动超出作用域。例如:
      function limitedScope() {
          let localVariable = "This is a local variable";
          console.log(localVariable);
      }
      limitedScope();
      // 在这里,localVariable已经超出了作用域,它所占用的内存会由垃圾回收器来管理
      
      • 当函数limitedScope执行完毕后,localVariable就不再存在于当前的执行上下文中,它所占用的内存会在适当的时候被垃圾回收器回收。但是,如果这个变量所引用的对象还被其他地方(如全局变量或者闭包)引用,那么它不会被回收。
  3. 闭包中的内存释放

    • 理解闭包对内存的影响
      • 闭包是指有权访问另一个函数内部变量的函数。当一个函数返回一个闭包时,这个闭包会保留对其外部函数的变量的引用,即使外部函数已经执行完毕。例如:
      function outerFunction() {
          let outerVariable = "I'm from outer function";
          return function innerFunction() {
              console.log(outerVariable);
          };
      }
      let closureFunction = outerFunction();
      closureFunction();
      // 此时,即使outerFunction已经执行完毕,
      // outerVariable仍然被closureFunction引用,不会被回收
      
    • 释放闭包中的内存
      • 要释放闭包中引用的内存,可以通过将闭包函数设置为null来切断引用。例如:
      closureFunction = null;
      // 现在没有引用指向outerVariable了,它会在下次垃圾回收时被回收
      
      • 另外,如果闭包中的变量是一个比较复杂的对象,也可以在闭包内部手动将其设置为null来帮助垃圾回收。
  4. 处理事件监听器和定时器对内存的影响

    • 事件监听器
      • 在函数内部添加的事件监听器,如果没有正确移除,会导致内存泄漏。例如,在一个函数中给DOM元素添加了一个点击事件监听器:
      function addEventListenerFunction() {
          let button = document.getElementById("myButton");
          button.addEventListener("click", function() {
              console.log("Button clicked");
          });
      }
      addEventListenerFunction();
      
      • 每次调用这个函数,都会添加一个新的点击事件监听器,但是这些监听器不会自动被移除。要释放内存,需要在合适的时候(如组件卸载或者不再需要监听事件时)使用removeEventListener来移除事件监听器。
    • 定时器
      • 类似地,在函数内部设置的定时器(如setTimeoutsetInterval)也可能会导致内存问题。例如:
      function setTimerFunction() {
          let timerId = setTimeout(function() {
              console.log("Timer expired");
          }, 1000);
      }
      setTimerFunction();
      
      • 要释放定时器占用的资源,可以使用clearTimeout(对于setTimeout)或者clearInterval(对于setInterval)来取消定时器。例如:
      function cancelTimerFunction() {
          let timerId = setTimeout(function() {
              console.log("Timer expired");
          }, 1000);
          clearTimeout(timerId);
      }
      cancelTimerFunction();
      

拓展-垃圾回收机制

  1. 垃圾回收的概念和重要性

    • 概念:垃圾回收(Garbage Collection,GC)是一种自动内存管理机制,用于回收程序中不再使用的内存。在JavaScript等高级编程语言中,开发人员不需要手动分配和释放内存来存储对象和数据,垃圾回收器会自动处理这些事情。这大大简化了编程工作,但也需要开发人员理解其基本原理,以避免潜在的内存泄漏等问题。
    • 重要性:如果没有垃圾回收机制,随着程序的运行,内存中会积累大量不再被使用的对象,导致内存泄漏。内存泄漏会逐渐耗尽系统的内存资源,最终可能使程序崩溃或者系统运行缓慢。例如,在一个长期运行的Web应用程序中,如果存在内存泄漏,用户在浏览页面时会发现页面越来越卡顿,甚至浏览器可能会因为内存耗尽而无响应。
  2. 引用计数垃圾回收算法

    • 原理
      • 引用计数算法是一种比较简单的垃圾回收算法。它的基本思想是为每个对象维护一个引用计数。当一个对象被创建并赋值给一个变量时,它的引用计数为1;当有新的变量引用这个对象时,引用计数加1;当一个引用该对象的变量不再使用(例如,变量被重新赋值或者超出了作用域),引用计数减1。当一个对象的引用计数为0时,就表示这个对象不再被使用,可以被回收。
      • 例如,在下面的代码中:
      let a = {
          name: 'objectA'
      };
      let b = a;
      // 此时对象a的引用计数为2
      a = null;
      // 引用计数减1,变为1
      b = null;
      // 引用计数变为0,对象a可以被垃圾回收
      
    • 局限性
      • 循环引用问题是引用计数算法的主要缺陷。当两个或多个对象相互引用形成一个循环时,它们的引用计数永远不会为0,即使这些对象从程序的其他部分无法访问。例如:
      function createCycle() {
          let obj1 = {};
          let obj2 = {};
          obj1.other = obj2;
          obj2.other = obj1;
      }
      createCycle();
      
      • 在这个例子中,obj1obj2相互引用,它们的引用计数都为2(自身的引用和对方的引用)。即使createCycle函数执行完毕,这两个对象在引用计数算法下也不会被回收,从而导致内存泄漏。
  3. 标记 - 清除垃圾回收算法

    • 原理
      • 标记 - 清除算法是现代JavaScript引擎中常用的垃圾回收算法之一。它的基本过程包括两个阶段:标记阶段和清除阶段。
      • 在标记阶段,垃圾回收器从一组被称为“根”(roots)的对象开始,这些根对象通常包括全局对象(在浏览器环境中是window对象)、当前执行栈中的变量等。然后,沿着对象之间的引用关系进行遍历,标记所有从根对象可达的对象。
      • 在清除阶段,垃圾回收器遍历整个堆内存,回收那些没有被标记的对象,即将它们所占用的内存释放掉。例如,在一个简单的JavaScript程序中,假设全局变量globalObj引用了一个对象,这个对象又引用了其他对象,垃圾回收器会从globalObj开始标记所有可达的对象,然后清除那些不可达的对象。
    • 与引用计数算法对比
      • 标记 - 清除算法能够很好地解决循环引用的问题。在上面的循环引用示例中,虽然obj1obj2相互引用,但如果它们无法从根对象到达,那么在标记 - 清除算法下,它们会在清除阶段被回收。
      • 不过,标记 - 清除算法也有一些缺点。它在标记和清除过程中需要暂停程序的执行(称为“STW”,Stop - The - World),这可能会导致短暂的性能卡顿。而且,清除后的内存空间是不连续的,可能会产生内存碎片,影响后续内存分配的效率。
  4. 标记 - 整理垃圾回收算法

    • 原理
      • 标记 - 整理算法是在标记 - 清除算法的基础上发展而来的。它同样包括标记阶段,标记从根对象可达的对象。在清除阶段,它不是简单地回收未标记的对象,而是将所有存活的(被标记的)对象向一端移动,然后将剩余的内存空间一次性清理掉。这样就解决了标记 - 清除算法产生内存碎片的问题。
    • 性能考虑
      • 标记 - 整理算法虽然解决了内存碎片问题,但在移动对象的过程中也需要消耗更多的时间和资源。因此,不同的JavaScript引擎会根据具体的应用场景和性能需求,选择合适的垃圾回收算法或者结合使用多种算法。例如,V8引擎在新生代(对象刚创建时所处的内存区域)主要使用复制算法(一种特殊的高效的内存回收算法,通过将存活对象复制到新的内存空间来实现回收),在老生代(对象经过一段时间后,从新生代晋升到的内存区域)可能会结合使用标记 - 清除和标记 - 整理算法。
  5. 分代垃圾回收(以V8引擎为例)

    • 新生代和老生代的划分
      • V8引擎将内存分为新生代和老生代两个区域。新生代主要用于存储新创建的对象,通常这些对象的生命周期较短。老生代用于存储经过多次垃圾回收后仍然存活的对象,这些对象的生命周期较长。
      • 新生代内存空间相对较小,一般采用更高效的复制算法进行垃圾回收。在复制算法中,新生代内存被划分为两个等大小的区域,称为From空间和To空间。当进行垃圾回收时,将From空间中存活的对象复制到To空间,然后清空From空间,最后将From空间和To空间的角色互换。
    • 对象晋升
      • 当一个对象在新生代中经过多次垃圾回收后仍然存活,它会被晋升到老生代。晋升的条件可能包括对象的存活时间达到一定阈值、对象的大小超过一定限制等。老生代的垃圾回收相对复杂,因为其中的对象数量较多且生命周期较长,通常会结合使用标记 - 清除和标记 - 整理算法。

垃圾回收之触发应用

  1. 垃圾回收器运行的情况

    • 内存达到一定阈值
      • 不同的JavaScript引擎(如V8引擎等)有自己的内存管理策略。当内存占用达到一定的阈值时,垃圾回收器就会自动启动。这个阈值是由JavaScript引擎内部设定的,目的是平衡性能和内存使用。例如,在浏览器环境中,V8引擎会监控堆内存(用于存储对象等数据)的使用情况。当堆内存的占用接近其容量上限时,垃圾回收器就会被触发,开始清理那些不可达的对象,释放内存空间。
    • 程序空闲时段
      • 为了尽量减少垃圾回收对程序性能的影响,JavaScript引擎通常会选择在程序相对空闲的时段运行垃圾回收。比如,当事件循环(Event Loop)中的任务队列暂时为空,没有正在执行的JavaScript代码,且浏览器没有其他高优先级的任务(如页面渲染)时,垃圾回收器就可能会启动。这就像是在打扫房间,选择在房间没人活动的时候进行打扫,以避免干扰正常的活动。
    • 全局变量或对象的生命周期结束(理论情况)
      • 在一个理想的模型中,如果一个全局变量所引用的对象不再被需要,且这个全局变量被重新赋值或者删除,那么垃圾回收器应该回收这个对象。例如,在一个简单的脚本中:
      let globalObject = {
          property: 'value'
      };
      // 使用globalObject进行一些操作...
      globalObject = null;
      // 理论上,此时这个对象应该在之后被垃圾回收
      
      • 不过,在实际情况中,由于JavaScript引擎的复杂性和各种优化策略,即使这样设置为null,垃圾回收器也不一定会立即运行,而是会根据上述提到的内存阈值和空闲时间等因素来决定何时运行。
  2. 无法主动触发垃圾回收机制(在标准JavaScript中)

    • 在标准的JavaScript规范中,没有提供直接触发垃圾回收的方法。这是因为垃圾回收是一个复杂的过程,由JavaScript引擎自动管理,目的是确保内存的高效利用和程序的性能稳定。如果允许随意触发垃圾回收,可能会导致性能问题,例如,频繁地触发垃圾回收可能会打断程序的正常执行,导致程序卡顿。
    • 虽然不能直接触发,但可以通过优化代码来间接地影响垃圾回收的效果。例如,及时释放不再使用的对象引用(如将变量设置为null),避免创建不必要的全局变量,以及正确地管理闭包等,这些做法可以让垃圾回收器更容易识别出哪些对象是不可达的,从而更高效地回收内存。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/2273622.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

Seata的部署与微服务集成

文章目录 Seata的部署与微服务集成1. Seata介绍2. 部署TC服务2.1 数据准备2.2 配置文件2.3 docker 部署2.4 访问 3. 微服务集成Seata3.1 引入服务3.2 改造配置3.3 添加数据库表3.4 注解标记 Seata的部署与微服务集成 1. Seata介绍 Seata 是一款开源的分布式事务解决方案&…

NFS 组件容器化部署实战指南

文章目录 前言部署NFS服务器K8S部署NFS问题记录 前言 使用nfs-client-provisioner这个应用,利用nfs server给kubernets提供作为持久化后端,并且动态提供pv。所有节点需要安装nfs-utils组件,并且nfs服务器与kubernets worker节点都能网络连通…

【江协STM32】10-2/3 MPU6050简介、软件I2C读写MPU6050

1. MPU6050简介 MPU6050是一个6轴姿态传感器,可以测量芯片自身X、Y、Z轴的加速度、角速度参数,通过数据融合,可进一步得到姿态角,常应用于平衡车、飞行器等需要检测自身姿态的场景3轴加速度计(Accelerometer&#xff…

裸机器搭建k8s部署 1.28.10版本

问了搭建k8s集群踩了很多坑,问题主要出现在网络插件处,因此主要是master节点操作问题。重新走一下流程整理一下笔记。 目录 虚拟机准备 虚拟机 系统版本信息 修改镜像地址 配置静态ip 关闭防火前和交换分区 转发 IPv4 并让 iptables 看到桥接流量…

HCIE-day10-ISIS

ISIS ISIS(Intermediate System-to-Intermediate System)中间系统到中间系统,属于IGP(内部网关协议);是一种链路状态协议,使用最短路径优先SPF算法进行路由计算,与ospf协议有很多相…

70.爬楼梯 python

爬楼梯 题目题目描述示例 1:示例 2:提示: 题解思路分析Python 实现代码空间优化代码解释提交结果 题目 题目描述 假设你正在爬楼梯。需要 n 阶你才能到达楼顶。 每次你可以爬 1 或 2 个台阶。你有多少种不同的方法可以爬到楼顶呢&#xff…

优质内容在个人IP运营中的重要性:以开源AI智能名片商城小程序为应用实例的深度探讨

摘要:在数字化时代,个人品牌(IP)的塑造与传播已成为各行各业提升影响力、吸引用户关注、促进商业转化的关键策略。优质内容作为连接个人IP与目标受众的桥梁,其在个人IP运营中的重要性不言而喻。本文旨在深入探讨优质内…

有限元分析学习——Anasys Workbanch第一阶段笔记(8)水杯案例的对称与轴对称处理

目录 1 序言 2 对称处理 2.1 模型处理 2.2 网格划分、约束载荷及接触设置 2.3 计算结果 3 轴对称处理 3.1 对称与轴对称概念 3.2 轴对称问题的应用 3.2.1 创建分析案例 3.2.2 导入并处理模型 3.2.3 网格划分、约束载荷及接触设置 3.2.4 后处理计算结果 1 序言 本章…

网络安全-web渗透环境搭建-BWAPP(基础篇)

01--所需系统环境: 虚拟主机系统部署(vmware,虚拟主机创建、虚拟主机网络配置(桥接,便于网络中多个主机都能访问虚拟主机)、虚拟软件功能,快照、克隆、镜像文件加载,ova文件制作&am…

Java 实现 Elasticsearch 查询当前索引全部数据

Java 实现 Elasticsearch 查询当前索引全部数据 需求背景通常情况Java 实现查询 Elasticsearch 全部数据写在最后 需求背景 通常情况下,Elasticsearch 为了提高查询效率,对于不指定分页查询条数的查询语句,默认会返回10条数据。那么这就会有…

算能AI计算服务器SE5设备树的二次修改实操

目录 1.大纲 2.实操 2.下载对应文件包 3.解包启动文件 4.修改对应的设备树 5.重启后 教程链接:https://github.com/sophgo/sophon-tools/tree/main/source/pmemory_edit 1.大纲 2.实操 2.1 选择串口,波特率115200,重启设备&#xff0…

Python的Matplotlib库应用(超详细教程)

目录 一、环境搭建 1.1 配置matplotlib库 1.2 配置seaborn库 1.3 配置Skimage库 二、二维图像 2.1 曲线(直线)可视化 2.2 曲线(虚线)可视化 2.3 直方图 2.4 阶梯图 三、三维图像 3.1 3D曲面图 3.2 3D散点图 3.3 3D散…

Flutter:封装一个自用的bottom_picker选择器

效果图&#xff1a;单列选择器 使用bottom_picker: ^2.9.0实现&#xff0c;单列选择器&#xff0c;官方文档 pubspec.yaml # 底部选择 bottom_picker: ^2.9.0picker_utils.dart AppTheme&#xff1a;自定义的颜色 TextWidget.body Text() <Widget>[].toRow Row()下边代…

牛客网刷题 ——C语言初阶(6指针)——BC106 上三角矩阵判定

1. 题目描述——BC106 上三角矩阵判定 牛客网OJ题链接 描述 KiKi想知道一个n阶方矩是否为上三角矩阵&#xff0c;请帮他编程判定。上三角矩阵即主对角线以下的元素都为0的矩阵&#xff0c;主对角线为从矩阵的左上角至右下角的连线。 示例 输入&#xff1a; 3 1 2 3 0 4 5 0 0…

力扣刷题:数组OJ篇(下)

大家好&#xff0c;这里是小编的博客频道 小编的博客&#xff1a;就爱学编程 很高兴在CSDN这个大家庭与大家相识&#xff0c;希望能在这里与大家共同进步&#xff0c;共同收获更好的自己&#xff01;&#xff01;&#xff01; 目录 1.轮转数组&#xff08;1&#xff09;题目描述…

《(限)战斗天赋VR》V02122024官方中文学习版

《(限)战斗天赋VR》官方中文版https://pan.xunlei.com/s/VODaeHDXSxw4BNDNl39dxJXnA1?pwdusm5# 一款具有挑战性的基于物理的roguelite剑术格斗游戏&#xff0c;你可以在一个超级无缝的程序地牢中创造自己的战斗风格&#xff0c;体验无与伦比的游戏体验。有80多种敌人变种、10…

《Spring Framework实战》3:概览

欢迎观看《Spring Framework实战》视频教程 Spring Framework 为基于现代 Java 的企业应用程序提供了全面的编程和配置模型 - 在任何类型的部署平台上。 Spring 的一个关键要素是应用程序级别的基础设施支持&#xff1a;Spring 专注于企业应用程序的 “管道”&#xff0c;以便…

基于SpringBoot+Vue动漫交流系统平台设计和实现

系统介绍&#xff1a; 免费的源码&#xff0c;我刚找到的&#xff0c;给大家推荐一下源码下载 动漫交流系统平台是一个专为动漫爱好者设计的在线社区平台&#xff0c;旨在为用户提供一个便捷的环境来分享动漫。这个系统包含了多种功能&#xff0c;如动漫分类、动漫视频、动漫…

微信小程序获取图片使用session(上篇)

概述&#xff1a; 我们开发微信小程序&#xff0c;从后台获取图片现实的时候&#xff0c;通常采用http get的方式&#xff0c;例如以下代码 <image class"user_logo" src"{{logoUrl}}"></image>变量logoUrl为ur图片l的请求地址 但是对于很多…

HTML5实现好看的中秋节网页源码

HTML5实现好看的中秋节网页源码 前言一、设计来源1.1 网站首页界面1.2 登录注册界面1.3 节日由来界面1.4 节日习俗界面1.5 节日文化界面1.6 节日美食界面1.7 节日故事界面1.8 节日民谣界面1.9 联系我们界面 二、效果和源码2.1 动态效果2.2 源代码 源码下载结束语 HTML5实现好看…