主题
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 在起作用,属于预期行为。函数内部的变量是局部的,调试时只能在函数里打印,这也是作用域带来的好处——出问题时范围清晰,不会牵一发动全身。
课件图示








代码示例
代码一:综合案例 —— 秒数转换为时分秒(完整页面)
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就判断"语法错误"。 原因:重复声明检查只在同一作用域内生效,嵌套作用域允许同名。 修法:理解成"就近原则"即可——当前作用域有就用当前的,没有才逐层向外找;同时提醒自己:不同作用域同名虽合法,但可读性差,正式代码应避免。
练习
改造 getTime:写
getTime(t),返回'HH:MM:SS'字符串(不返回 HTML),要求用padStart(2, '0')补 0,并说明公式里% 24和% 60各自解决什么问题。再测t = 0、t = 59、t = 86399、t = 90061四组数据。判断输出:给出下面代码,写出两次
console.log的结果,并逐行说明查找过程。jslet a = 1; function f1() { let a = 2; function f2() { console.log(a); // ? console.log(b); // ? } let b = 3; f2(); } f1();IIFE 改造:把下面这段会因重名报错的代码,用立即执行函数改造为不报错的版本,并说出改造后这两个
number分别属于什么作用域。jslet number = 10; let number = 1;调用顺序排错:以下三行分别能否正常执行?说明原因,并给出两种修改方案。
jsfn(); // A let fn = function () { console.log('ok'); }; fn(); // B综合实战:把"秒数转时分秒"案例改成——用 IIFE 包裹整个脚本,内部用函数表达式定义
getTime,页面不报任何全局变量,getTime支持传入t且t为负数时返回'00:00:00'(提示:入口处先做一次判断)。
原视频对照
| # | 视频 | 时长 |
|---|---|---|
| 1 | 作用域 | 14:52 |
| 2 | 变量的访问原则 | 07:26 |
| 3 | 匿名函数-函数表达式 | 12:41 |
| 4 | 匿名函数-立即执行函数 | 12:09 |
| 5 | flexble.js源码中的立即执行函数 | 03:41 |
小结
- 作用域的本质是"可见性边界":判断一段代码会不会报
is not defined,先问"这个变量声明在哪个花括号里",而不是问"我明明写了啊"。 - 就近原则是查找规则,不是赋值规则:读变量时由内向外逐层找,找到即止;但赋值时若本层没声明,
let会报错、裸赋值会污染全局——两者不能混为一谈。 - 函数表达式不提升,函数声明提升:这是"先声明后使用"这条铁律的唯一例外来源,记牢它就能一眼看出
Cannot access before initialization的成因。 - IIFE 的两个价值要分清:立即执行只是表象(本质仍是末尾那对括号在调用),真正价值是把变量关进函数作用域,做成模块级的"隔离舱"。
- 分号在 IIFE 场景下不是风格问题而是语法问题:多个 IIFE 相邻时必须显式分隔,这一点不能交给 ASI。
- 补 0、取整、取模这类细节决定案例成败:
parseInt+%的组合对正数没问题,但负数、超大值、字符串输入都要单独兜底,工程代码里优先用padStart统一格式化。 - 案例的价值在于把零散语法串成链路:输入(类型转换)→ 计算(公式与取整)→ 格式化(补 0)→ 输出(模板字符串 + DOM),任何一环的类型没对齐,整条链就断。