Skip to content

函数(Day6 3/3) ​

本页对应 Day06_JavaScript,共 6 段视频,约 1 小时 43 分。

学习目标 ​

  1. 能够区分函数内部用 var 声明的局部变量、函数外部声明的全局变量,以及函数内未用 var 创建变量时的全局影响,并说明其设计缺陷。
  2. 能够根据嵌套函数代码绘制作用域链,按“本作用域→上层作用域→全局”的顺序分析变量查找过程,并预测输出或定位 not defined 报错。
  3. 能够判断一个变量的上层作用域只由函数声明位置决定,与函数调用位置无关,并能通过给定代码预测执行结果。
  4. 能够说明全局代码和函数代码执行前的预处理过程,解释 var 变量“先创建、后赋值”的提升现象,并预测声明前后使用变量的输出。
  5. 能够区分 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);  // 200

userName 改的是函数内局部变量;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 看输出顺序;分析作用域题时先标出“本作用域有哪些声明”,再沿作用域链向上找,不要凭感觉猜。

课堂画面 ​

课堂画面 1

课堂画面 2

课堂画面 3

课堂画面 4

课堂画面 5

课堂画面 6

代码示例 ​

示例一:局部变量、同名变量与作用域链 ​

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。
  • 易错点:不要以为存在两个同名变量;自始至终只有一个,只是不同时刻值不同。

常见错误 ​

  1. 现象:函数内写 a = 10,函数外还能访问 a。
    原因:未加声明关键字,变成隐式全局变量。
    修法:函数内声明变量必须加 var/let/const;需要全局变量就在函数外显式声明。

  2. 现象:函数内外同名变量,误以为改一个另一个也一定变。
    原因:没有按作用域链判断,忽略了“本作用域优先”。
    修法:先画作用域链;同名变量尽量避免;修改前确认当前作用域是否存在该绑定。

  3. 现象:认为函数在哪个位置调用,就用哪个位置的作用域。
    原因:把“调用位置”误当成“声明位置”。
    修法:作用域只看函数声明位置;调用位置只决定是否执行。

  4. 现象:console.log(a); var a = 1; 得到 undefined,却以为应该报错。
    原因:var 提升只创建变量、不赋值。
    修法:先声明后使用;不要依赖提升;必要时开启严格模式。

  5. 现象:表达式函数在定义前调用,报 fn is not a function。
    原因:var fn = function(){} 按 var 规则提升,调用时值还是 undefined。
    修法:先定义再调用;需要前置调用时改用 function 声明,并统一团队规范。

  6. 现象:同名 var 与 function 混用后,调用结果和预期不一致。
    原因:同一作用域只有一个同名绑定,后续赋值会覆盖当前值。
    修法:同一作用域禁止重名;按执行顺序分析赋值;不要用“优先级”死记。

  7. 现象:封装排序函数后原数组被意外改动,或函数返回 undefined。
    原因:直接原地排序且忘记 return,输入输出约定不清。
    修法:明确“原地排序”还是“返回新数组”;需要新数组先复制;确保返回排好序的数组。

练习 ​

  1. 封装两个函数:maxOf(...args) 和 minOf(...args),从所有参数中取出最大值、最小值;要求处理“没有参数”的情况,并说明你的返回值策略。

  2. 写出下列代码的输出,并按执行顺序说明依据:

js
var x = 100;
function a() {
  console.log(x);
}
function b() {
  var x = 200;
  a();
}
b();
  1. 封装 sortArray(arr, compareFn):参数是数组,返回排好序的数组;要求不修改原数组。再写一个原地排序版本,并说明二者差异。

  2. 判断下列代码每一步的输出或报错,并说明原因:

js
console.log(fn);
function fn() {
  console.log('fn');
}
var fn = 1;
console.log(fn);
fn();
  1. 分析下面代码的 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
6var和function08:25

小结 ​

  1. 函数内声明变量必须带关键字,隐式全局是设计缺陷,不是可炫耀的技巧。
  2. 作用域链查找只向上、不向下;同名变量在不同作用域中各自独立。
  3. 函数作用域由声明位置决定,与调用位置无关。
  4. var 提升只创建不赋值;function 声明提升会创建并赋值;表达式函数按 var 规则处理。
  5. 同一作用域内同名绑定只有一个,后续赋值会覆盖当前值,不要想象成“两个变量”。
  6. 最值、排序这类封装题,先约定参数、返回值、边界情况和是否修改原数组。
  7. 调试作用域与提升问题,先画作用域链,再按代码执行顺序标出创建、赋值、使用三个时刻。