Skip to content

可变参数数量的函数-取参数中最大的和最小的与变量提升题目(Day7 1/3) ​

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

学习目标 ​

  • 能准确区分函数名与函数调用表达式,说明 return 对返回值及函数执行流程的影响,并判断无返回值函数的调用结果。
  • 能使用 arguments 编写求任意数量参数最大值和最小值的函数,并通过多组实参验证结果。
  • 能分析包含全局/局部作用域、变量提升、函数提升的代码,预测输出结果并说明原因。
  • 能口述冒泡排序“逐轮比较、相邻交换、每轮缩小比较范围”的思路,并手工推演给定数组的排序过程。
  • 能使用两层循环实现升序和降序冒泡排序,正确控制比较轮数与每轮比较次数。
  • 能封装数组排序函数,支持升序/降序参数及默认升序,并调用该函数验证不同数组的排序结果。

知识点 ​

函数回顾:用“变量”看函数 ​

为什么需要:函数是 JS 里最重要的数据类型之一,后面参数、作用域、提升、排序封装都建立在“函数也是变量”这个视角上。
怎么想:函数由函数名、参数、函数体、返回值组成;函数名本质是变量,只是它的值类型是函数。函数名加 () 才是调用;只写函数名是在使用这个变量的值。
怎么做:记住三条:return 右边表达式的值成为返回值;执行到 return 函数立即结束;返回不等于输出,输出是 console.log/alert,返回是给调用表达式一个结果。形参是函数内局部变量,实参按顺序赋值;带默认值的形参要放后面。arguments 是系统创建的伪数组,保存调用时传入的全部实参,可按索引取、可循环。
易错点:没有 return 或只写 return; 时,调用表达式得到 undefined;不要把“返回”说成“输出”;形参默认值放前面会失去意义;函数内不加 var 创建变量会变成全局,这是不规范写法。

可变参数:用 arguments 取最大最小 ​

为什么需要:实参个数不固定时,不能靠写死形参。
怎么想:先把第一个实参当最大值,再从第二个开始逐个比较,谁大就把 res 换成谁;最小值同理,只把 > 改成 <。
怎么做:

js
function max() {
  var res = arguments[0];
  for (var i = 1; i < arguments.length; i++) {
    if (arguments[i] > res) res = arguments[i];
  }
  return res;
}
function min() {
  var res = arguments[0];
  for (var i = 1; i < arguments.length; i++) {
    if (arguments[i] < res) res = arguments[i];
  }
  return res;
}

易错点:循环从 1 开始,因为 0 已经作为初始值;arguments 是伪数组,不是真数组;函数体里没有输出时,要自己写 console.log(max(3, 8, 2)) 才能看到结果。

变量提升与作用域链:从输出反推 ​

为什么需要:面试题常把 var、函数声明、作用域链混在一起考。
怎么想:浏览器执行前会预处理:var 只创建变量不赋值;function 声明会创建变量并赋值为函数。函数内也有提升,但要等函数调用时才预处理。查找变量时先找本作用域,再沿作用域链往上找,不会往下找。
怎么做:看下面代码:

js
console.log(a); // undefined
var a = 0;
console.log(a); // 0
function fn() {
  console.log(a); // undefined,局部 var a 提升
  var a = 10;
  console.log(a); // 10
}
fn();
console.log(a); // 0,全局 a 没被函数内局部 a 改掉

若函数声明写在调用前,函数提升会让变量已有函数值,所以能提前调用;若变量值是函数,加 () 才能调用。
易错点:没写 var 的变量不参与提升,可能直接报错;函数声明前可调用,但 var a = function(){} 这种表达式写法只是变量提升,提前调用会报 not a function;函数与数字相加时会先转数字,函数转数字得到 NaN。

冒泡排序:先推导,再写双层循环 ​

为什么需要:排序是算法入门,也能练循环嵌套和交换逻辑。
怎么想:升序时两两比较,前面比后面大就交换,让大的往后走。第一轮把最大值送到最后;第二轮把第二大送到倒数第二;第三轮送第三大……轮数比元素个数少 1,每轮比较次数逐轮减少。转写中调试过程是:第一轮后 101 到最后,第二轮后 89 到位,第三轮 78,第四轮 56,第五轮 45,继续逼近有序。
怎么做:

js
var nums = [23, 101, 12, 45, 29, 89, 56, 78, 18, 182];
for (var k = 1; k < nums.length; k++) {
  for (var i = 0; i < nums.length - k; i++) {
    if (nums[i] > nums[i + 1]) {
      var temp = nums[i];
      nums[i] = nums[i + 1];
      nums[i + 1] = temp;
    }
  }
}
console.log(nums);

易错点:内层不能写死 length - 1,应写 length - k;交换必须借第三方变量;升序判断是 >,降序改成 <;外层从 1 开始时,内层减 k 刚好对应“每轮少比一次”。

数组排序函数封装:把方向做成参数 ​

为什么需要:排序代码要复用,不能只排一个数组。
怎么想:外层控制轮数,内层控制该轮比较次数;第二个参数 isDown 决定降序还是升序。转写中用表达式变量 expre 保存比较结果,再统一交换,避免重复写交换代码。
怎么做:

js
function arraySort(arr, isDown = false) {
  for (var k = 1; k < arr.length; k++) {
    for (var i = 0; i < arr.length - k; i++) {
      var expre = isDown ? arr[i] < arr[i + 1] : arr[i] > arr[i + 1];
      if (expre) {
        var temp = arr[i];
        arr[i] = arr[i + 1];
        arr[i + 1] = temp;
      }
    }
  }
  return arr;
}
console.log(arraySort([23, 101, 12, 45])); // 升序
console.log(arraySort([23, 101, 12, 45], true)); // 降序

易错点:忘记 return arr,调用后拿不到排好的数组;不传第二参数时默认 false 表示升序;isDown 可直接放 if 判断,不必画蛇添足写 === true;调试时若函数名写错会报错,先检查函数名和调用名是否一致。

回顾提纲(保留原代码) ​

text
1. 函数的概念
2. 函数的组成  函数名 函数体 参数 返回值
3. 函数的调用和返回值
4. 参数
   形参和实参、形参的默认值、arguments
5. 变量的作用域
   作用域 全局变量 局部变量
   作用域链
6. 变量提升 
   变量提升
   函数提升

函数的概念 ​

为什么需要:先明确“函数在 JS 里到底是什么”,后面学调用、参数、作用域才不会混乱。
怎么想:把函数看成一种数据类型;函数名本质上是变量名,只是这个变量的值是函数类型的数据。
怎么做:声明函数后,函数名可以像变量一样被使用;只有“函数名加小括号”才是调用。
易错点:不要把“写函数名”当成调用;单写函数名是在使用这个变量的值,而函数调用表达式才有表达式的值。转写稿里反复强调:函数并不特殊,它和 number、string 一样是数据类型,只是多了“可被调用”的本事。

函数的组成 ​

为什么需要:知道函数由哪些部分组成,才能读懂和写出完整函数。
怎么想:一个函数通常包含函数名、函数体、参数、返回值;其中函数名就是变量名。
怎么做:函数体写具体逻辑;参数负责接收外部数据;返回值用 return 设置。
易错点:不要以为函数名只是“名字”,它其实是变量;也不要把“函数体里写了输出”误认为“函数有返回值”。返回和输出是两回事。

函数的调用和返回值 ​

为什么需要:调用决定函数何时执行,返回值决定函数把计算结果交给谁。
怎么想:函数名() 是调用;return 表达式 的值就是函数返回值。若只写 return 或函数体没有 return,函数没有返回值,调用表达式得到 undefined。
怎么做:需要结果时写 return 表达式;执行到 return 后,函数立即结束,后面的代码不再执行。
易错点:返回不等于输出;return 后面的代码不会执行;没有返回值时浏览器给出 undefined,不要理解成“函数没有值”。转写稿中的调试结论是:无返回值时调用表达式仍要有值,这个值就是 undefined。

参数:形参、实参、默认值与 arguments ​

为什么需要:参数让函数能接收不同数据,完成可复用的计算。
怎么想:形参是函数内部变量,作用域只在函数内,所以是局部变量;实参是调用时按顺序传给形参的值。默认值让没有对应实参的形参也有值;arguments 是系统自动创建的局部变量,值是伪数组,包含调用时传入的所有实参。
怎么做:形参有默认值时,通常把带默认值的参数放后面;需要获取全部实参时用 arguments,可通过索引取成员,也可循环遍历。
易错点:实参按顺序赋值,默认值参数放前面容易失去意义;arguments 不是真正数组,是伪数组;不要手动乱改它。转写稿中“取参数最大/最小”的练习,就是利用 arguments 遍历所有实参。

变量的作用域与作用域链 ​

为什么需要:变量不是哪里都能用,必须先确定作用范围。
怎么想:作用域是变量的可作用范围;按范围分全局变量和局部变量。判断局部变量的关键只看“是否在函数内创建”,循环、分支内部创建并不自动变成局部变量。作用域链用于查找变量:先从当前作用域找,找不到再往上层找,直到全局;不会往下层找。
怎么做:函数内需要局部变量就用 var 等关键字正常声明;需要全局变量就在函数外提前声明。函数作用域由“函数声明语句所在位置”决定,不由调用位置决定。
易错点:函数内不写 var 直接赋值会变成全局变量,这是不规范写法,严格模式下会报错;使用变量时只可能向当前作用域和上层作用域找,不会到下级作用域找。

变量提升与函数提升 ​

为什么需要:理解提升才能解释“代码还没执行到声明,为什么变量能访问、函数能调用”。
怎么想:全局代码执行前会预处理;函数体内的局部提升要等函数调用时才进行。var 提升只创建变量、不赋值;function 声明提升会创建变量并赋值为函数。函数表达式按 var 处理,只创建不赋值。
怎么做:调试时先标出声明位置,再按“预处理创建/赋值”和“正式执行赋值”两步分析。转写稿中的题目可推出:function 声明前调用可成功;var 声明前访问得到 undefined;a++ 时若 a 是函数,函数转 number 得到 NaN,再执行后面的赋值才变成字符串。
易错点:不是所有“函数”都提升成可调用函数,函数表达式在赋值前调用会报 not a function;变量提升不是“把值提前”,只是提前创建;函数内局部提升发生在调用时,不调用就不处理函数体。

课堂画面 ​

课堂画面 1

课堂画面 2

课堂画面 3

课堂画面 4

课堂画面 5

课堂画面 6

代码示例 ​

示例一:可变参数数量的函数(arguments 取最大值 / 最小值) ​

html
<!DOCTYPE html>
<html lang="zh-CN">
<head>
  <meta charset="UTF-8">
  <title>可变参数数量的函数</title>
</head>
<body>
<script>
  // 取所有实参中的最大值
  function max() {
    var res = arguments[0];              // 先默认第一个参数是最大的
    for (var i = 1; i < arguments.length; i++) {
      if (arguments[i] > res) {          // 当前这个比 res 还大
        res = arguments[i];              // 就把 res 换成它
      }
    }
    return res;
  }

  // 取所有实参中的最小值:只需把 > 改成 <
  function min() {
    var res = arguments[0];
    for (var i = 1; i < arguments.length; i++) {
      if (arguments[i] < res) {
        res = arguments[i];
      }
    }
    return res;
  }

  // 函数声明写在前面调用也没问题(函数提升)
  console.log(max(23, 101, 12, 45, 29, 89, 56, 78, 18)); // 101
  console.log(min(23, 101, 12, 45, 29, 89, 56, 78, 18)); // 12
  console.log(max(3, 7, 8));                             // 8
  console.log(min(9));                                   // 9
</script>
</body>
</html>

要点解释 / 易错点

  • max() / min() 不写形参,靠 arguments 拿到全部实参;arguments 是系统自动创建的局部变量,是一个伪数组(有 length、有索引,但不是真数组)。
  • res 的初值取 arguments[0],循环从 i = 1 开始,避免自己和自己比;如果从 0 开始也不影响结果,但多比一次。
  • 易错:把 arguments.length 写成 arguments.lenght,或用 arguments[0] 之前没传参 → res 是 undefined,比较结果永远是 undefined。
  • 求最小值时只改一个比较符号(> 改 <),其他不动,这是基于已有代码精准修改的能力。

示例二:作用域链 + 变量提升 / 函数提升 经典题 ​

html
<!DOCTYPE html>
<html lang="zh-CN">
<head>
  <meta charset="UTF-8">
  <title>变量提升与作用域链</title>
</head>
<body>
<script>
  /* ---------- 题 1:作用域链 ---------- */
  function fun() {
    var name = 10;   // 局部变量,只属于 fun
    fun2();          // 调用 fun2
  }
  function fun2() {
    // fun2 的作用域链:fun2 自身 -> 全局(不含 fun!)
    console.log(name); // ReferenceError: name is not defined
  }
  // fun();  // 打开这行会报错

  /* ---------- 题 2:变量提升(只创建不赋值) ---------- */
  console.log(a1);       // undefined
  var a1 = 0;
  console.log(a1);       // 0

  /* ---------- 题 3:函数提升比变量提升更“彻底” ---------- */
  console.log(a2);       // 输出函数本身(变量已创建且赋值为函数)
  console.log(a2++);     // NaN:函数转 number 得 NaN,NaN + 1 还是 NaN
  var a2 = '我是变量';
  function a2() { console.log('我是函数'); }
  console.log(a2);       // '我是变量'

  /* ---------- 题 4:内外同名变量,不会“往下找” ---------- */
  var a3 = 0;
  function fn() {
    console.log(a3);     // undefined:函数内部的 var a3 提升了
    var a3 = 1;
    console.log(a3);     // 1
  }
  fn();
  console.log(a3);       // 0:全局的 a3 没有被影响
</script>
</body>
</html>

要点解释 / 易错点

  • 函数的作用域只看函数声明语句写在哪里,与调用位置无关。fun2 声明在全局,所以它的上层作用域是全局,不是 fun,因此取不到 fun 里的 name。
  • var 提升:预处理阶段只创建变量、不赋值,所以声明语句之前使用得到 undefined(不报错);不带 var 直接赋值则是全局变量、且不提升,声明前使用直接报错。
  • function 声明提升:创建变量并且赋值为函数,所以可以在函数声明语句之前调用;而函数表达式(var f = function(){})只按变量提升处理,前面调用会 not a function。
  • a2++ 的坑:此时 a2 的值是函数,++ 要求 number,函数转 number 是 NaN,NaN + 1 还是 NaN。
  • 使用变量时只在自身作用域和上层作用域找,绝不会往下找,所以函数里的 a3 和全局的 a3 互不影响。

示例三:冒泡排序 + 数组排序函数封装(升序 / 降序) ​

html
<!DOCTYPE html>
<html lang="zh-CN">
<head>
  <meta charset="UTF-8">
  <title>冒泡排序与函数封装</title>
</head>
<body>
<script>
  /**
   * 数组排序(冒泡排序)
   * @param  {Array}   arr    要排序的数组
   * @param  {Boolean} isDown 是否降序,false(默认)= 升序
   * @return {Array}          排好序的数组
   */
  function arySort(arr, isDown = false) {
    // 外层循环:控制比较的轮数,length - 1 轮
    for (var k = 1; k < arr.length; k++) {
      // 内层循环:控制该轮比较的次数,越往后次数越少
      for (var i = 0; i < arr.length - k; i++) {
        // 把“比较表达式”提取成一个变量
        var expr = isDown
          ? arr[i] < arr[i + 1]   // 降序:前面小就交换
          : arr[i] > arr[i + 1];  // 升序:前面大就交换
        if (expr) {
          var temp = arr[i];      // 引入第三方变量交换
          arr[i] = arr[i + 1];
          arr[i + 1] = temp;
        }
      }
    }
    return arr;                   // 必须返回,否则拿到 undefined
  }

  var nums = [23, 101, 12, 45, 29, 89, 56, 78, 18];

  console.log(arySort(nums));            // 升序(第二个参数不传,默认 false)
  console.log(arySort(nums, true));      // 降序
  console.log(arySort([3, 1, 9, 5]));    // 也可以直接传数组直接量
</script>
</body>
</html>

要点解释 / 易错点

  • 两层循环分工:外层 k 控制轮数(length - 1 轮),内层 i 控制每轮比较次数(length - k 次),因为每轮结束最大的元素已经“冒”到最后,后面不用再比。
  • k 从 1 开始,内层写 i < arr.length - k,刚好让第 1 轮减 1、第 2 轮减 2……省去了写 length - 1 - (k - 1) 的换算。
  • 交换两个元素必须借第三方变量 temp:先存 arr[i],再覆盖,最后回填,顺序写错会丢数据。
  • 易错:忘了 return arr,调用处得到 undefined;函数名写错(ARAY / arySort)也会报 not a function。
  • 默认值写成 isDown = false,不传参时 isDown 是 undefined,在 if 判断里同样等价于 false,这就是系统函数常把布尔参数默认设为 false 的原因。
  • 把 isDown ? ... : ... 提取为 expr 变量,是为了让交换逻辑只写一遍,避免升序、降序两段代码重复。

常见错误 ​

  1. 现象:函数里写了 return,但页面/控制台没有结果。
    原因: 把“返回”当成“输出”,return 只把值交给调用处。
    修法: 需要展示时在调用处写 console.log(fn()),把返回值和输出分开。

  2. 现象:只写 fn 不写 fn(),得到函数体或后续调用报错。
    原因: 单写函数名是在取变量值,加小括号才是调用。
    修法: 调用统一写 fn(...);只有需要传递函数本身时才写函数名。

  3. 现象:未写 var/let 的变量在声明前使用报错,误以为“都会提升”。
    原因: 提升只处理 var/function 等声明,裸赋值不提升且不规范。
    修法: 先声明后使用;开启严格模式检查,避免隐式全局变量。

  4. 现象:冒泡排序内层比较到 arr[i+1] 为 undefined,或排序结果不对。
    原因: 内层边界没有随轮数收缩,i 取到了 length-1。
    修法: 外层 k=1..length-1,内层 i < arr.length-k,只比较 arr[i] 与 arr[i+1]。

  5. 现象:交换两个元素后两个值变成一样。
    原因: 先覆盖再赋值,原值丢失。
    修法: 用临时变量 temp=arr[i]; arr[i]=arr[i+1]; arr[i+1]=temp;,或使用解构交换。

  6. 现象:封装排序函数后,调用得到 undefined。
    原因: 函数内修改了数组,但忘记 return。
    修法: 明确接口,最后 return arr;调用处接收返回值。

练习 ​

  1. 阅读代码,写出输出并说明原因:

    js
    var a = 1;
    function f() &#123;
      console.log(a);
      var a = 2;
      function g() &#123;
        console.log(a);
      &#125;
      g();
    &#125;
    f();
    console.log(a);
  2. 编写 maxArgs() 和 minArgs():不使用固定形参,通过 arguments 返回所有实参中的最大值/最小值;没有传参时返回 null。

  3. 对数组 [23, 101, 12, 45, 29, 89, 56, 78, 18] 做升序冒泡排序,手工写出第一轮每次交换后的数组,并统计第一轮交换次数。

  4. 封装 sortArray(arr, isDown = false):默认升序,传 true 降序,返回排序后的数组;要求交换逻辑只写一次,用条件表达式变量控制比较方向,并测试空数组、单元素、已排序数组。

原视频对照 ​

#视频时长
1回顾18:29
2可变参数数量的函数-取参数中最大的和最小的04:47
3变量提升题目10:16
4冒泡排序-思路分析11:27
5冒泡排序-代码实现22:29
6数组排序函数封装15:54

小结 ​

  1. 写函数前先分清“调用、取值、返回、输出”四件事,能避免大量低级错误。
  2. 作用域题不要凭感觉,先标出使用点,再沿作用域链向上找声明。
  3. 提升题按“预处理创建、执行赋值”两步推,不要跳步。
  4. 冒泡排序先手工跑一轮,再抽象外层轮数和内层边界;边界决定正确性。
  5. 封装函数先定参数、默认值、返回值,再写实现;调用处严格按接口使用。
  6. 排序和参数类函数必须测边界:空、单元素、已排序、全逆序、多参数/少参数。
  7. 代码简化要建立在可读性上:条件表达式变量可减少重复,但不能牺牲可理解性。