Skip to content

1-13作用域和立即执行函数 ​

本页对应 第一章JavaScript基础,共 5 段视频,约 50 分钟。

学习目标 ​

  • 能说明作用域的含义与作用,并准确区分全局作用域、函数作用域和块级作用域,判断给定变量属于全局变量还是局部变量。
  • 能依据变量访问的就近原则,分析含嵌套函数和同名变量的代码片段,写出最终输出结果并说明变量查找路径。
  • 能写出匿名函数的函数表达式形式,正确完成声明、调用、传参与返回值,并指出“必须先声明后使用”的原因。
  • 能写出立即执行函数的两种语法形式,正确传参,说明其避免全局变量污染的作用,并处理多个立即执行函数需用分号隔开的注意点。
  • 能识别 flexible.js 源码中立即执行函数的写法,指出实参与形参的对应关系,并分析该写法在隔离变量、防止全局污染方面的作用。
  • 能完成综合案例:在给定代码中定位作用域相关错误,按规范改写为函数表达式或立即执行函数并验证运行结果。

知识点 ​

先看需求:把一串总秒数变成“时:分:秒” ​

综合案例效果图

需求很直白:用户在弹窗里输入一个总秒数,程序自动算出对应的小时、分钟、秒,并显示在页面上。

为什么需要:真实项目里,倒计时、视频时长、订单剩余时间,后端往往只给一个“总秒数”,比如 5000,而界面上必须显示成 01:23:20。这个转换是前端最常见的格式化工作之一。

怎么想:拆成三步——拿数据、算数据、显示数据。拿数据用 prompt,算数据封装成一个函数,显示数据用 document.write 把拼好的 HTML 写到页面。

易错点:prompt 的返回值是字符串,后面虽然做除法会自动转数字,但为了明确,笔记里用了 +prompt(...) 做隐式转换;另外用户点“取消”时返回 null,+null 是 0,正好被函数形参的默认值兜住。

为什么要把计算封装进 getTime 函数 ​

为什么需要:如果三行计算直接写在全局,h、m、s 就成了全局变量。转写稿里反复强调过:同一个作用域下变量名不能重复,重复声明会直接报错;而把变量放进函数里,它就变成局部变量,只在这个函数范围内有效,外面根本访问不到,也就不会和页面里其他 h、m 冲突。这就是作用域“提高程序逻辑局部性、减少变量名冲突”的价值。

怎么想:函数既然能把变量“关”起来,那计算逻辑就应该关进去;同时函数还能接收参数、返回结果——转写稿讲函数表达式时明确说过,函数可以传参,也可以有返回值,这两点正好用上:总秒数通过形参传进去,拼好的 HTML 通过 return 送出来。

怎么做:

js
function getTime(t = 0) { ... }

形参 t 按转写稿的注意点,要“当局部变量来看”,只在函数内部使用;t = 0 是默认值,防止实参是 undefined 时算出 NaN。

易错点:形参名别和外面的 totalSeconds 混着用;返回值别漏写 return,否则 str 拿到的是 undefined,页面会显示 “undefined”。

三个公式是怎么推出来的 ​

怎么想:先把“秒”换算成“分”,再换算成“时”。

  • 总秒数除以 60,得到总分钟;再除以 60,得到总小时。所以小时是 t / 60 / 60。
  • 分钟要的是“不足一小时的零头”,所以对 60 取余:t / 60 % 60。
  • 秒数要的是“不足一分钟的零头”:t % 60。

怎么做:除法结果可能是小数,用 parseInt 取整,得到笔记里的三行:

js
let h = parseInt(t / 60 / 60 % 24)
let m = parseInt(t / 60 % 60)
let s = parseInt(t % 60)

易错点:分钟千万别写成 parseInt(t / 60),那是总分钟数,输入 5000 会得到 83;% 24 表示只保留一天之内的小时数,如果需求是“总共多少小时”,就不能取模。取整用 parseInt 对正数相当于向下取整,负数场景不在本案例讨论范围。

数字补 0:让 9 秒显示成 09 ​

为什么需要:倒计时里如果 9 秒直接显示成一位数,宽度会跳,视觉上很别扭,所以统一补成两位。

怎么做:用三元表达式判断,小于 10 就在前面拼个 '0':

js
h = h < 10 ? '0' + h : h
m = m < 10 ? '0' + m : m
s = s < 10 ? '0' + s : s

易错点:补 0 之后 h 变成了字符串类型,如果后面还要拿它做数学运算就会出问题,所以补 0 要放在计算全部完成之后;判断条件写 < 10,别写成 <= 10,否则 10 会被补成 010。

拼 HTML 并输出到页面 ​

怎么想:页面结构里三个 span 分别放时、分、秒,正好用模板字符串把变量塞进去,函数直接返回这段 HTML:

js
return `
  <span class="hour">${h}</span>
  <span class="minute">${m}</span>
  <span class="second">${s}</span>
`

怎么做:函数外面用 let str = getTime(totalSeconds) 接收返回值,再 document.write 把整块 .box 结构写到页面,\${str} 嵌在 .timer 里。

易错点:转写稿讲函数表达式时特别提醒“必须先声明后使用”,所以调用 getTime 一定写在函数声明之后;document.write 是在脚本执行时往文档流里追加内容,本案例脚本写在 body 内,输出正常,如果放到页面加载完成之后再调用,就会覆盖整个文档,这点要留意。

完整代码 ​

html
<!DOCTYPE html>
<html lang="en">

<head>
  <meta charset="UTF-8">
  <meta http-equiv="X-UA-Compatible" content="IE=edge">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>综合案例-转换时间案例</title>
  <style>
    .box {
      position: relative;
      width: 190px;
      height: 260px;
      color: #fff;
      background: #e83632 url(./images/bg.png) 50%/contain no-repeat;
    }

    .title {
      width: 100%;
      text-align: center;
      font-size: 30px;
      font-weight: 700;
      padding-top: 31px;
    }

    .count {
      margin-top: 90px;
      font-size: 14px;
      text-align: center;
    }

    .timer {
      margin-left: auto;
      margin-right: auto;
      width: 130px;
      height: 30px;
      margin-top: 10px;
      display: block;
    }

    .timer span {
      position: relative;
      float: left;
      width: 30px;
      height: 30px;
      text-align: center;
      background-color: #2f3430;
      margin-right: 20px;
      color: white;
      font-size: 20px;
    }

    .timer span:nth-child(-n+2)::after {
      content: ":";
      display: block;
      position: absolute;
      right: -20px;
      font-weight: bolder;
      font-size: 18px;
      width: 20px;
      height: 100%;
      top: 0;
    }

    .timer span:last-child {
      margin-right: 0;
    }
  </style>
</head>

<body>
  <!-- <div class="box">
    <div class="title">计算时间</div>
`<div>`
      <div class="count">转换为时分秒</div>
      <div class="timer">
        <span class="hour">01</span>
        <span class="minute">22</span>
        <span class="second">58</span>
      </div>
    </div>
  </div> -->

  <script>
    // 转换时分秒案例 
    // 计算公式:计算时分秒
    // 小时:  h = parseInt(总秒数 / 60 / 60 % 24)
    // 分钟:  m = parseInt(总秒数 / 60 % 60)
    // 秒数:   s = parseInt(总秒数 % 60) 

    // 1. 用户输入总的秒数
    let totalSeconds = +prompt('请您输入总的秒数:')

    // 2. 封装函数 getTime,把总的秒数转换为时分秒,注意记得数字补0 哦
    function getTime(t = 0) {
      // 转换小时
      let h = parseInt(t / 60 / 60 % 24)
      // 转换分钟
      let m = parseInt(t / 60 % 60)
      // 转换秒数
      let s = parseInt(t % 60)
      // 数字补0
      h = h < 10 ? '0' + h : h
      m = m < 10 ? '0' + m : m
      s = s < 10 ? '0' + s : s
      console.log(h, m, s)
      return `
        <span class="hour">\${h}</span>
        <span class="minute">\${m}</span>
        <span class="second">\${s}</span>
      `
    }
    let str = getTime(totalSeconds) // 实参
    console.log(str)
    // 3. 页面打印输出
    document.write(`
       <div class="box">
        <div class="title">计算时间</div>
`<div>`
          <div class="count">转换为时分秒</div>
          <div class="timer">
            \${str}
          </div>
        </div>
      </div>
    `)
  </script>
</body>

</html>

调试时按这个顺序排查 ​

先看 console.log(h, m, s) 输出的原始值对不对,再看补 0 后的值,最后看 console.log(str) 里的 HTML 有没有正确插入变量。如果整块都显示 undefined,八成是 return 漏了;如果小时数永远不超过 23,那是 % 24 在起作用,属于预期行为。函数内部的变量是局部的,调试时只能在函数里打印,这也是作用域带来的好处——出问题时范围清晰,不会牵一发动全身。

课件图示 ​

课件第4页:函数-Function 函数: 是可以被重复使用的代码块 作用:函数可以把具有相

课件第6页:函数基本使用 定义一个函数并不会自动执行它,需要调用函数 2. 调用函数 利用关

课件第7页:总结 总结 1. 函数是什么?有什么好处? Ø 可以被重复使用的代码块 Ø 代码

课件第11页:1.2 函数整体认知 问题:这样的函数只能求 10 + 20, 而且结果只能函数

课件第12页:1.2 函数整体认知 l 函数语法 l 说明: Ø 函数参数,如果有多个则用逗号

课件第15页:1.3 函数参数 Ø 形参:声明函数时小括号里的叫形参(形式上的参数) Ø 实参

课件第17页:总结 总结 1. 函数中实参在哪里?形参在哪里? Ø 函数调用时,小括号里面的是

课件第18页:逻辑中断 逻辑中断: 存在于逻辑运算符 && 和 || 中,左边如果满足一定条件

代码示例 ​

代码一:综合案例 —— 秒数转换为时分秒(完整页面) ​

js
<!DOCTYPE html>
<html lang="en">

<head>
  <meta charset="UTF-8">
  <meta http-equiv="X-UA-Compatible" content="IE=edge">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>综合案例-转换时间案例</title>
  <style>
    .box {
      position: relative;
      width: 190px;
      height: 260px;
      color: #fff;
      background: #e83632 url(./images/bg.png) 50%/contain no-repeat;
    }

    .title {
      width: 100%;
      text-align: center;
      font-size: 30px;
      font-weight: 700;
      padding-top: 31px;
    }

    .count {
      margin-top: 90px;
      font-size: 14px;
      text-align: center;
    }

    .timer {
      margin-left: auto;
      margin-right: auto;
      width: 130px;
      height: 30px;
      margin-top: 10px;
      display: block;
    }

    .timer span {
      position: relative;
      float: left;
      width: 30px;
      height: 30px;
      text-align: center;
      background-color: #2f3430;
      margin-right: 20px;
      color: white;
      font-size: 20px;
    }

    .timer span:nth-child(-n+2)::after {
      content: ":";
      display: block;
      position: absolute;
      right: -20px;
      font-weight: bolder;
      font-size: 18px;
      width: 20px;
      height: 100%;
      top: 0;
    }

    .timer span:last-child {
      margin-right: 0;
    }
  </style>
</head>

<body>
  <script>
    // 转换时分秒案例
    // 计算公式:
    // 小时:  h = parseInt(总秒数 / 60 / 60 % 24)
    // 分钟:  m = parseInt(总秒数 / 60 % 60)
    // 秒数:   s = parseInt(总秒数 % 60)

    // 1. 用户输入总的秒数
    let totalSeconds = +prompt('请您输入总的秒数:')

    // 2. 封装函数 getTime,把总的秒数转换为时分秒,注意记得数字补0 哦
    function getTime(t = 0) {
      // 转换小时
      let h = parseInt(t / 60 / 60 % 24)
      // 转换分钟
      let m = parseInt(t / 60 % 60)
      // 转换秒数
      let s = parseInt(t % 60)
      // 数字补0
      h = h < 10 ? '0' + h : h
      m = m < 10 ? '0' + m : m
      s = s < 10 ? '0' + s : s
      console.log(h, m, s)
      return `
        <span class="hour">${h}</span>
        <span class="minute">${m}</span>
        <span class="second">${s}</span>
      `
    }
    let str = getTime(totalSeconds) // 实参
    console.log(str)
    // 3. 页面打印输出
    document.write(`
       <div class="box">
        <div class="title">计算时间</div>
        <div>
          <div class="count">转换为时分秒</div>
          <div class="timer">
            ${str}
          </div>
        </div>
      </div>
    `)
  </script>
</body>

</html>

关键行与易错点

  • let totalSeconds = +prompt(...):prompt 返回的是字符串,前面加 + 做隐式类型转换,否则后面参与 / % 运算时容易得到意外结果。
  • function getTime(t = 0):形参给了默认值 0,用户取消输入(返回 null)或没传参时不会算出 NaN。
  • parseInt(t / 60 / 60 % 24):小时要先除以 60 得分钟、再除以 60 得小时,最后 %24 取一天内的 24 小时制;注意是从秒开始一路除,不能写成 t / 3600 % 24 之外的其他顺序。
  • h = h < 10 ? '0' + h : h:三元表达式补 0,注意补 0 后 h 变成了字符串,但后面只是拼接输出,不影响使用。
  • 函数 return 的是一段 HTML 字符串模板,最后统一 document.write 到页面里;实参 totalSeconds 传给形参 t,这是函数调用时值传递的基本过程。

代码二:函数表达式与立即执行函数 ​

html
<!DOCTYPE html>
<html lang="en">

<head>
  <meta charset="UTF-8">
  <title>函数表达式与立即执行函数</title>
</head>

<body>
  <script>
    // 1. 函数表达式:把匿名函数赋值给一个变量
    let fn = function () {
      console.log('我是函数表达式')
    }
    fn() // 变量名加小括号调用

    // 2. 函数表达式也可以传参、有返回值(和具名函数一样)
    let sum = function (x, y) {
      return x + y
    }
    let r1 = sum(1, 2) // 1、2 是实参
    console.log(r1) // 3

    // 3. 立即执行函数 写法一:两个小括号
    (function () {
      console.log('我是立即执行函数')
    })();

    // 4. 立即执行函数 写法二:一个小括号包住函数,后面再加一个小括号调用
    (function (x, y) {
      console.log(x + y) // 3
    }(1, 2));

    // 5. 使用场景:避免全局变量污染
    let number = 10
      ; (function () {
        let number = 1 // 变成了局部变量,不会和外面的冲突
        console.log(number) // 1
      })();
    console.log(number) // 10
  </script>
</body>

</html>

关键行与易错点

  • let fn = function () {}:匿名函数被当作一个"值"赋给变量,这种写法叫函数表达式;调用时用 fn(),本质是变量名加小括号。
  • 函数表达式必须先声明后使用:如果在 let fn = function(){} 之前就写 fn(),会报 Cannot access 'fn' before initialization,跟变量 let number = 10 之前不能使用是一个道理。
  • 立即执行函数的两个小括号含义不同:第一个小括号里的匿名函数是函数体,第二个小括号才是调用;写法一里外层小括号是为了把函数"包成一个整体",避免语法被误解析。
  • 传参位置:实参写在第二个小括号里(调用的小括号),形参写在第一个小括号里的函数小括号中。
  • 多个立即执行函数连写时必须用分号隔开,否则上一行结尾的 ) 会和下一行开头的 ( 粘在一起报错;分号写在后面或写在下一行开头都可以,如 ;(function(){})()。
  • 把全局的 let number = 10 放进立即执行函数里,就变成了局部变量,同一个作用域内不能重名的问题被"隔离"掉了 —— 这就是防止全局变量污染。

代码三:作用域分类与变量访问原则(就近原则) ​

html
<!DOCTYPE html>
<html lang="en">

<head>
  <meta charset="UTF-8">
  <title>作用域与变量访问原则</title>
</head>

<body>
  <script>
    // 1. 全局变量:在 script 标签(全局作用域)里声明,任何地方都能访问
    let number = 10
    console.log(number) // 10

    function fn() {
      console.log(number) // 10,局部作用域可以使用全局变量
    }
    fn()

    // 2. 函数作用域里的局部变量:外面不能访问
    function fun() {
      let number1 = 1
      console.log(number1) // 1
    }
    fun()
    // console.log(number1) // 报错:number1 is not defined

    // 3. 块级作用域(大括号内)的局部变量:外面不能访问
    {
      let x = 10
      console.log(x) // 10
    }
    // console.log(x) // 报错:x is not defined

    // 4. 变量的访问原则:就近原则,先找自己当前作用域,找不到再一层层往外找
    {
      let number = 456 // 全局(本块)里的 number
      function f1() {
        let number = 123 // f1 里的 number
        function f2() {
          console.log(number) // 123,f2 自己没有,往上找到 f1 的 123
        }
        f2()
      }
      f1()
    }
  </script>
</body>

</html>

关键行与易错点

  • 作用域就是变量的"有效范围":script 标签内是全局作用域,函数内是函数作用域,大括号内是块级作用域(局部作用域)。
  • 全局变量可以给其他作用域访问和修改;局部变量只能在自己的局部内使用,出了范围就报 xxx is not defined,这正是开头 for 循环里 let i 在外面打印会报错的原因。
  • 就近原则:访问变量时先看当前作用域有没有,有就直接用;没有才往上一层作用域找,一层层往外查,所以 f2 里打印的是离它最近的 123 而不是 456。
  • 不同作用域下变量名可以相同(number = 10 和 number = 20 不冲突),但同一个作用域内不能重名,否则报错。
  • 两个注意点:① 函数内部没有声明直接赋值的变量(如 y = 1)会被当成全局变量,不推荐这种写法;② 函数的形参相当于局部变量,只能在函数内部使用,函数外面访问会报错。
  • 代码第 4 段用了块级作用域把同名变量包起来,只是为了演示时避免与前面代码重名报错,实际开发中一般不会这样堆叠。

常见错误 ​

  • 秒数转换结果全是 NaN 或补 0 失效 现象:输入 3661,页面显示 NaN:NaN:NaN,或小时显示 010 之类的怪值。 原因:prompt() 返回的是字符串,h < 10 变成字符串比较('8' < 10 走的不是数值比较),'0' + h 之外的算术也会踩坑。 修法:入口就转成数字 let totalSeconds = +prompt('请输入总秒数'),或在函数里 t = Number(t) || 0。

  • 补 0 写成 h = h < 10 ? 0 + h : h 现象:9 秒显示成 9,不是 09。 原因:0 + 9 是数值加法得 9,不是字符串拼接。 修法:'0' + h(引号不能少),或统一用 String(h).padStart(2, '0')。

  • 函数表达式在声明前调用 现象:Uncaught ReferenceError: Cannot access 'fn' before initialization。 原因:let fn = function () {} 中 fn 是 let 声明的变量,存在暂时性死区,函数体不会像函数声明那样被提升。 修法:先声明后调用;确实需要"先调用后定义",就改用 function fn() {} 的具名函数声明。

  • 多个立即执行函数连写不加分号 现象:Uncaught TypeError: (intermediate value)(...) is not a function。 原因:ASI 没生效,上一段结尾的 () 被解析成"对上一行返回值的调用",两个 IIFE 粘成了一句。 修法:每个 IIFE 结尾补 ;,或统一写成 ;(function(){ ... })() 的前导分号风格。

  • 在函数/块里声明变量,跑到外面用 现象:Uncaught ReferenceError: num is not defined(for 循环外打印 i 也是这个报错)。 原因:let/const 是块级作用域,函数内声明的是局部变量,出了范围就不可见。 修法:需要外部拿到就用 return 返回;需要共享就提到外层声明;不要靠"函数内不声明直接赋值"来绕过——那会隐式变成全局变量,制造污染。

  • 以为全局和局部同名变量会报"重复声明" 现象:看到 let number = 10 和函数内 let number = 20 就判断"语法错误"。 原因:重复声明检查只在同一作用域内生效,嵌套作用域允许同名。 修法:理解成"就近原则"即可——当前作用域有就用当前的,没有才逐层向外找;同时提醒自己:不同作用域同名虽合法,但可读性差,正式代码应避免。

练习 ​

  1. 改造 getTime:写 getTime(t),返回 'HH:MM:SS' 字符串(不返回 HTML),要求用 padStart(2, '0') 补 0,并说明公式里 % 24 和 % 60 各自解决什么问题。再测 t = 0、t = 59、t = 86399、t = 90061 四组数据。

  2. 判断输出:给出下面代码,写出两次 console.log 的结果,并逐行说明查找过程。

    js
    let a = 1;
    function f1() {
      let a = 2;
      function f2() {
        console.log(a);      // ?
        console.log(b);      // ?
      }
      let b = 3;
      f2();
    }
    f1();
  3. IIFE 改造:把下面这段会因重名报错的代码,用立即执行函数改造为不报错的版本,并说出改造后这两个 number 分别属于什么作用域。

    js
    let number = 10;
    let number = 1;
  4. 调用顺序排错:以下三行分别能否正常执行?说明原因,并给出两种修改方案。

    js
    fn();                       // A
    let fn = function () { console.log('ok'); };
    fn();                       // B
  5. 综合实战:把"秒数转时分秒"案例改成——用 IIFE 包裹整个脚本,内部用函数表达式定义 getTime,页面不报任何全局变量,getTime 支持传入 t 且 t 为负数时返回 '00:00:00'(提示:入口处先做一次判断)。

原视频对照 ​

#视频时长
1作用域14:52
2变量的访问原则07:26
3匿名函数-函数表达式12:41
4匿名函数-立即执行函数12:09
5flexble.js源码中的立即执行函数03:41

小结 ​

  • 作用域的本质是"可见性边界":判断一段代码会不会报 is not defined,先问"这个变量声明在哪个花括号里",而不是问"我明明写了啊"。
  • 就近原则是查找规则,不是赋值规则:读变量时由内向外逐层找,找到即止;但赋值时若本层没声明,let 会报错、裸赋值会污染全局——两者不能混为一谈。
  • 函数表达式不提升,函数声明提升:这是"先声明后使用"这条铁律的唯一例外来源,记牢它就能一眼看出 Cannot access before initialization 的成因。
  • IIFE 的两个价值要分清:立即执行只是表象(本质仍是末尾那对括号在调用),真正价值是把变量关进函数作用域,做成模块级的"隔离舱"。
  • 分号在 IIFE 场景下不是风格问题而是语法问题:多个 IIFE 相邻时必须显式分隔,这一点不能交给 ASI。
  • 补 0、取整、取模这类细节决定案例成败:parseInt + % 的组合对正数没问题,但负数、超大值、字符串输入都要单独兜底,工程代码里优先用 padStart 统一格式化。
  • 案例的价值在于把零散语法串成链路:输入(类型转换)→ 计算(公式与取整)→ 格式化(补 0)→ 输出(模板字符串 + DOM),任何一环的类型没对齐,整条链就断。