主题
函数(Day6 3/3)
本页对应 Day06_JavaScript,共 6 段视频,约 1 小时 43 分。
学习目标
- 能够区分函数内部用
var声明的局部变量、函数外部声明的全局变量,以及函数内未用var创建变量时的全局影响,并说明其设计缺陷。 - 能够根据嵌套函数代码绘制作用域链,按“本作用域→上层作用域→全局”的顺序分析变量查找过程,并预测输出或定位
not defined报错。 - 能够判断一个变量的上层作用域只由函数声明位置决定,与函数调用位置无关,并能通过给定代码预测执行结果。
- 能够说明全局代码和函数代码执行前的预处理过程,解释
var变量“先创建、后赋值”的提升现象,并预测声明前后使用变量的输出。 - 能够区分
function声明与函数表达式在提升规则上的差异,并分析同一作用域中同名var变量与function声明在不同执行阶段的赋值结果和调用结果。
知识点
局部变量与全局变量
为什么需要:先弄清变量到底能在哪里用,否则很容易在函数外访问局部变量而报错,也会误把函数内漏写 var 的变量当成局部变量。
怎么想:变量是否可用,先看它在哪个作用域里创建。函数内部用 var 创建的是局部变量;函数外部创建的是全局变量。
怎么做:
html
<script>
function f() {
var address = '上海';
console.log(address); // 上海
}
f();
// console.log(address); // 报错:address is not defined
</script>如果确实需要全局变量,应在函数外先声明,代码更规范。
易错点:函数内不加 var 直接写 a = 10,会创建全局变量。转写稿中反复强调:这是设计缺陷,不是特点,不建议使用;严格模式下会报错。
js
function f() {
a = 10; // 不建议:a 变成全局变量
}
f();
console.log(a); // 10同名变量与作用域链
为什么需要:不同作用域里可能出现同名变量,必须能判断“这次用的到底是哪一个”。
怎么想:使用变量时,先从当前作用域找;找不到再沿上层作用域找,直到全局;全局也没有才报错。
怎么做:
js
var userName = '高小乐';
var userAge = 100;
function fn() {
var userName = '曹操';
userName = '孙悟空';
userAge = 200;
console.log(userName); // 孙悟空
console.log(userAge); // 200
}
fn();
console.log(userName); // 高小乐
console.log(userAge); // 200userName 改的是函数内局部变量;userAge 在本作用域没声明,于是找到并修改全局变量。
易错点:不同作用域可以有同名变量,它们不是同一个变量;同一作用域不能重复声明同名变量。
作用域链示例:
js
function func01() {
var address = '北京';
function func02() {
var age = 300;
function func03() {
var userName = '贾宝玉';
console.log(userName); // 贾宝玉
console.log(age); // 300
console.log(address); // 北京
// console.log(message); // 报错:not defined
}
func03();
}
func02();
}
func01();查找顺序是:本作用域 → 上层 → 更上层 → 全局;找到即停止,找不到才报错。
作用域与函数调用位置无关
为什么需要:很多初学者以为“函数在哪里调用,作用域就在哪里”,这是作用域链最常见的误区。
怎么想:函数的作用域由“函数声明的位置”决定,不由调用位置决定。调用只决定函数体是否执行。
怎么做:
js
var h = 100;
function fn() {
console.log(h);
}
function func() {
var h = 200;
fn(); // 输出 100,不是 200
}
func();fn 声明在全局,它的上层作用域就是全局;虽然它在 func 里被调用,但不会把 func 当成上层作用域。
易错点:不要根据调用位置推导作用域链,只看函数声明写在哪里。
变量提升
为什么需要:解释为什么在 var 声明之前访问变量不报“未定义变量”,而是得到 undefined。
怎么想:全局代码执行前会预处理,查找全局中的 var,提前创建变量但不赋值;正式执行到声明语句时才赋值。函数调用时,函数体执行前也会做类似预处理。
怎么做:
js
console.log(address); // undefined
var address = '上海';
console.log(address); // 上海
function f() {
console.log(number); // undefined
var number = 100;
console.log(number); // 100
}
f();易错点:提升的是“变量创建”,不是值;不带 var 不提升:
js
// console.log(age); // 报错:age is not defined
age = 18;
console.log(age); // 18函数提升
为什么需要:函数声明也会被提升,而且和 var 不一样:它在预处理阶段就“创建并赋值”。
怎么想:function fn01(){} 可视为创建变量 fn01,预处理时变量已指向函数本身,所以声明前也能调用。
怎么做:
js
console.log(fn01); // 输出函数本身
fn01(); // 可以调用
function fn01() {
console.log('fn01');
}函数表达式则按 var 规则处理:
js
console.log(fn02); // undefined
// fn02(); // 报错:fn02 is not a function
var fn02 = function () {
console.log('fn02');
};
fn02();易错点:只有 function 声明形式提升得“彻底”;var fn02 = function(){} 中 fn02 提升后仍是 undefined,声明前不能调用。
var 与 function 同名
为什么需要:同名变量和函数放在同一作用域时,需要按执行过程分析,而不是死记“谁优先级高”。
怎么想:同一作用域最终只有一个同名变量。预处理时 var address 先创建但不赋值;遇到 function address 会创建并赋值为函数。正式执行到 var address = '北京' 时再赋值。
怎么做:
js
console.log(address); // 函数本身
function address() {
console.log('我是函数 address');
}
var address = '北京';
console.log(address); // 北京
// address(); // 报错:address is not a function易错点:不要以为同时存在“一个变量 address”和“一个函数 address”;自始至终只有一个 address,只是不同时刻它的值不同。调用前要看它当时是不是函数类型。
作业一:从任意参数中取最大/最小值
为什么需要:实际开发里经常要封装通用小工具。参数个数不固定时,不能只写死几个形参,而要利用 arguments 或剩余参数收集全部实参。转写稿里反复强调:arguments 和形参一样,都是函数内部的局部变量,只在当前函数作用域内有效。
怎么想:先判断有没有参数;再把第一个参数当作临时最大值/最小值;然后从第二个参数开始逐个比较,遇到更大/更小就更新。也可以用 Math.max.apply(null, arguments),但初学阶段建议先写循环,能把“取第一个值作基准”的过程看清楚。
怎么做:
js
function getMax() {
if (arguments.length === 0) return undefined;
var max = arguments[0];
for (var i = 1; i < arguments.length; i++) {
if (arguments[i] > max) {
max = arguments[i];
}
}
return max;
}
function getMin() {
if (arguments.length === 0) return undefined;
var min = arguments[0];
for (var i = 1; i < arguments.length; i++) {
if (arguments[i] < min) {
min = arguments[i];
}
}
return min;
}
console.log(getMax(3, 9, 2, 7)); // 9
console.log(getMin(3, 9, 2, 7)); // 2易错点:arguments 是类数组,不是真正的数组,不能直接调用 map、sort;箭头函数没有自己的 arguments;空参数时 Math.max() 返回 -Infinity、Math.min() 返回 Infinity,业务里最好自己约定返回 undefined 或抛错;字符串和数字混在一起比较时,要先把类型规则说清楚。
作业二:作用域和变量提升面试题
为什么需要:这类题不是背结论,而是训练“代码到底怎么执行”的分析能力。转写稿里的核心结论是:使用变量时先找本作用域,找不到再逐层向上找,直到全局;函数的上层作用域只看函数声明的位置,与调用位置无关。
怎么想:先看函数内部有没有 var 声明。有,就在函数预处理阶段创建了局部变量,只是还没赋值;所以函数内第一次输出该变量时是 undefined,不会去全局找。再看有没有不加 var 的赋值:非严格模式下它会沿作用域链找旧变量,找不到就在全局创建;严格模式下直接报错。
怎么做:用下面这个简化案例逐步分析:
js
var count = 100;
function fn() {
console.log(count); // undefined
var count = 200;
console.log(count); // 200
message = 'where';
}
fn();
console.log(count); // 100
console.log(message); // where推导过程:打开页面先预处理全局代码,创建全局 count,再声明 fn。调用 fn 后,函数体预处理时发现 var count,于是在函数作用域内提前创建局部 count,但不赋值。执行第一句 console.log(count) 时,本作用域已经有 count,只是值为 undefined,所以不会用全局的 100。执行 var count = 200 时只做赋值,第二句输出 200。函数执行完后,全局 count 仍是 100。message = 'where' 没加 var,执行时沿作用域链找不到已有 message,非严格模式下在全局创建,所以函数外能输出 where。
易错点:把“变量创建”和“变量赋值”混为一谈;以为函数内输出 count 会先拿全局值;以为函数调用位置能改变作用域链。还要注意 function 声明提升比 var 更“彻底”:var 只创建不赋值,function 声明在预处理阶段创建并赋值。同一作用域里不要写同名 var 和 function,否则正式执行到 var 赋值时会把函数值改掉,后面再调用就会报“不是函数”。严格模式下,不加 var 创建变量会直接报 ReferenceError。
作业三:封装数组排序函数
为什么需要:sort() 默认按字符串字典序排序,数值数组会出问题,例如 [10, 2, 1] 可能排成 [1, 10, 2]。封装函数还能避免直接修改原数组,让调用者更安全。
怎么想:参数接收数组,先复制一份,再对副本排序并返回。数值排序要传比较函数 (a, b) => a - b;如果按字符串排,可用 localeCompare 或默认规则。
怎么做:
js
function sortArray(arr) {
var copy = arr.slice();
copy.sort(function (a, b) {
return a - b;
});
return copy;
}
var nums = [10, 2, 1, 30];
console.log(sortArray(nums)); // [1, 2, 10, 30]
console.log(nums); // [10, 2, 1, 30]易错点:直接 arr.sort() 会修改原数组;比较函数返回布尔值不行,必须返回正数、负数或 0;空数组、非数组参数要在业务层做校验;如果数组里是字符串,不要机械套 a - b,否则会得到 NaN。
调试时建议用 console.log 分步打印,打开浏览器 F12 看输出顺序;分析作用域题时先标出“本作用域有哪些声明”,再沿作用域链向上找,不要凭感觉猜。
课堂画面






代码示例
示例一:局部变量、同名变量与作用域链
html
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<title>作用域与作用域链</title>
</head>
<body>
<script>
// ===== 1. 局部变量:函数外无法访问 =====
function f1() {
var address = '上海';
console.log('函数内:', address); // 上海
}
f1();
// console.log(address); // 报错:address is not defined
// ===== 2. 同名变量:函数内优先用自己作用域的 =====
var userName = '高小乐';
var userAge = 100;
function fn() {
var userName = '曹操';
userName = '孙悟空'; // 改的是局部 userName
userAge = 200; // 本作用域没有 userAge,改的是全局
console.log('函数内:', userName, userAge); // 孙悟空 200
}
fn();
console.log('函数外:', userName, userAge); // 高小乐 200
// ===== 3. 形参也是局部变量 =====
var name01 = 100;
var name02 = 200;
function fun(name01, name02) {
name01 = name01 * 2;
name02 = name02 * 2;
console.log('函数内:', name01, name02); // 200 400
}
fun(name01, name02);
console.log('函数外:', name01, name02); // 100 200
// ===== 4. 作用域链:从本作用域逐层向上找 =====
function func01() {
var address = '北京';
var job = '四处游荡';
function func02() {
var age = 300;
function func03() {
var userName = '贾宝玉';
console.log(userName); // 贾宝玉(本层)
console.log(age); // 300(上一层 func02)
console.log(address); // 北京(上上层 func01)
console.log(job); // 四处游荡(全局)
// console.log(notExist); // 报错:找不到
}
func03();
}
func02();
}
func01();
</script>
</body>
</html>要点解释
var写在函数内部就是局部变量,函数外访问会报is not defined。- 函数内
var userName会“遮蔽”全局同名变量;userAge在本作用域没声明,沿作用域链找到全局并修改。 - 形参是局部变量,和全局同名互不影响;调用时只是把全局值复制给形参。
- 作用域链查找规则:先本作用域,找不到再逐层向上,直到全局;找到就停,找不到才报错。
- 易错点:不要以为函数内改了
userAge,外面的userName也会变;只有本作用域没有声明时才会改到全局。
示例二:作业——求最大值、最小值与数组排序
html
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<title>作业:最值与排序</title>
</head>
<body>
<script>
// ===== 1. 从所有参数中取出最大值 =====
function getMax() {
if (arguments.length === 0) return undefined;
var max = arguments[0];
for (var i = 1; i < arguments.length; i++) {
if (arguments[i] > max) {
max = arguments[i];
}
}
return max;
}
// ===== 2. 从所有参数中取出最小值 =====
function getMin() {
if (arguments.length === 0) return undefined;
var min = arguments[0];
for (var i = 1; i < arguments.length; i++) {
if (arguments[i] < min) {
min = arguments[i];
}
}
return min;
}
console.log(getMax(3, 9, 1, 20, 7)); // 20
console.log(getMin(3, 9, 1, 20, 7)); // 1
// ===== 3. 封装数组排序函数 =====
function sortArray(arr) {
// 复制一份,避免修改原数组
var copy = arr.slice();
copy.sort(function (a, b) {
return a - b;
});
return copy;
}
var nums = [5, 2, 9, 1, 7];
var sorted = sortArray(nums);
console.log('原数组:', nums); // [5, 2, 9, 1, 7]
console.log('排序后:', sorted); // [1, 2, 5, 7, 9]
</script>
</body>
</html>要点解释
arguments是函数内部的局部变量,保存所有实参,是类数组,有length和下标。- 求最值时先取
arguments[0]作为初始值,循环从索引1开始比较。 sort()默认按字符串比较,数字排序必须传比较函数(a, b) => a - b,否则[10, 2, 1]会排成[1, 10, 2]。arr.slice()复制数组,避免sort()直接修改原数组。- 易错点:不要用箭头函数写
getMax,箭头函数没有自己的arguments。
示例三:变量提升、函数提升与 var/function 同名
html
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<title>变量提升与函数提升</title>
</head>
<body>
<script>
// ===== 1. var 变量提升:只创建,不赋值 =====
console.log('提升后的 address:', address); // undefined
var address = '上海';
console.log('赋值后的 address:', address); // 上海
// ===== 2. function 声明提升:创建并赋值 =====
console.log('提升后的 fn01:', fn01); // 函数本身
fn01(); // 可以提前调用
function fn01() {
console.log('我是 fn01');
}
// ===== 3. 函数表达式:走 var 的规则 =====
console.log('提升后的 fn02:', fn02); // undefined
// fn02(); // 报错:fn02 is not a function
var fn02 = function () {
console.log('我是 fn02');
};
fn02(); // 赋值后才能调用
// ===== 4. 局部变量提升(面试题)=====
var count = 100;
var message = 'what';
function fn() {
console.log('函数内第一次 count:', count); // undefined
var count = 200;
console.log('函数内第二次 count:', count); // 200
message = 'where'; // 不加 var,改的是全局
console.log('函数内 message:', message); // where
}
fn();
console.log('函数外 count:', count); // 100
console.log('函数外 message:', message); // where
// ===== 5. var 与 function 同名:自始至终只有一个变量 =====
(function () {
console.log('同名 address(预处理后):', address); // 函数本身
function address() {
console.log('我是函数 address');
}
console.log('同名 address(函数声明后):', address); // 还是函数本身
var address = '北京';
console.log('同名 address(var 赋值后):', address); // 北京
// address(); // 报错:address is not a function
})();
</script>
</body>
</html>要点解释
var提升:预处理阶段只创建变量、不赋值,所以前面访问得到undefined。function声明提升:预处理阶段创建并赋值,所以可以在声明之前调用。- 函数表达式
var fn02 = function () {}走var的规则,声明前调用会报fn02 is not a function。 - 函数内
var count提升到函数作用域顶部,所以第一次输出undefined,不会去用全局的100。 - 不加
var的message = 'where'是给全局变量赋值,这是设计缺陷,严格模式下会报错。 var与function同名时,同一个作用域只有一个变量;function提升时赋值为函数,之后var赋值会覆盖成字符串,再调用就报is not a function。- 易错点:不要以为存在两个同名变量;自始至终只有一个,只是不同时刻值不同。
常见错误
现象:函数内写
a = 10,函数外还能访问a。
原因:未加声明关键字,变成隐式全局变量。
修法:函数内声明变量必须加var/let/const;需要全局变量就在函数外显式声明。现象:函数内外同名变量,误以为改一个另一个也一定变。
原因:没有按作用域链判断,忽略了“本作用域优先”。
修法:先画作用域链;同名变量尽量避免;修改前确认当前作用域是否存在该绑定。现象:认为函数在哪个位置调用,就用哪个位置的作用域。
原因:把“调用位置”误当成“声明位置”。
修法:作用域只看函数声明位置;调用位置只决定是否执行。现象:
console.log(a); var a = 1;得到undefined,却以为应该报错。
原因:var提升只创建变量、不赋值。
修法:先声明后使用;不要依赖提升;必要时开启严格模式。现象:表达式函数在定义前调用,报
fn is not a function。
原因:var fn = function(){}按var规则提升,调用时值还是undefined。
修法:先定义再调用;需要前置调用时改用function声明,并统一团队规范。现象:同名
var与function混用后,调用结果和预期不一致。
原因:同一作用域只有一个同名绑定,后续赋值会覆盖当前值。
修法:同一作用域禁止重名;按执行顺序分析赋值;不要用“优先级”死记。现象:封装排序函数后原数组被意外改动,或函数返回
undefined。
原因:直接原地排序且忘记return,输入输出约定不清。
修法:明确“原地排序”还是“返回新数组”;需要新数组先复制;确保返回排好序的数组。
练习
封装两个函数:
maxOf(...args)和minOf(...args),从所有参数中取出最大值、最小值;要求处理“没有参数”的情况,并说明你的返回值策略。写出下列代码的输出,并按执行顺序说明依据:
js
var x = 100;
function a() {
console.log(x);
}
function b() {
var x = 200;
a();
}
b();封装
sortArray(arr, compareFn):参数是数组,返回排好序的数组;要求不修改原数组。再写一个原地排序版本,并说明二者差异。判断下列代码每一步的输出或报错,并说明原因:
js
console.log(fn);
function fn() {
console.log('fn');
}
var fn = 1;
console.log(fn);
fn();- 分析下面代码的 4 个输出,并指出哪些变量被修改:
js
var userName = '高小乐';
var userAge = 100;
function test() {
var userName = '曹操';
userName = '孙悟空';
userAge = 200;
console.log(userName, userAge);
}
test();
console.log(userName, userAge);原视频对照
| # | 视频 | 时长 |
|---|---|---|
| 1 | 局部变量 | 17:59 |
| 2 | 作用域链 | 18:27 |
| 3 | 作用域与函数调用位置无关 | 23:41 |
| 4 | 变量提升 | 23:14 |
| 5 | 函数提升 | 12:08 |
| 6 | var和function | 08:25 |
小结
- 函数内声明变量必须带关键字,隐式全局是设计缺陷,不是可炫耀的技巧。
- 作用域链查找只向上、不向下;同名变量在不同作用域中各自独立。
- 函数作用域由声明位置决定,与调用位置无关。
var提升只创建不赋值;function声明提升会创建并赋值;表达式函数按var规则处理。- 同一作用域内同名绑定只有一个,后续赋值会覆盖当前值,不要想象成“两个变量”。
- 最值、排序这类封装题,先约定参数、返回值、边界情况和是否修改原数组。
- 调试作用域与提升问题,先画作用域链,再按代码执行顺序标出创建、赋值、使用三个时刻。