Skip to content

回顾 数组与函数(Day6 2/3) ​

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

学习目标 ​

  • 能通过代码演示区分“函数名引用”与“函数名加小括号调用”,并说明不加小括号时函数体不执行。
  • 能使用 return 设置函数返回值,判断有返回值、无返回值及 return 提前结束函数的执行结果。
  • 能准确区分形参与实参,说明调用时实参按顺序为形参赋值,并分析实参数量多于或少于形参数量时的结果。
  • 能为形参设置默认值,分别使用 ES5 判断赋值和 ES6 默认参数语法,并说明带默认值的形参应放在参数列表后部的原因。
  • 能使用 arguments 获取调用时传入的全部实参,利用其 length 和索引遍历参数,并实现可变参数数量函数。
  • 能判断变量是全局变量还是局部变量,验证全局变量在函数内外及不同脚本中的可访问范围。

知识点 ​

本节笔记代码(保留) ​

text
1. 数组
   1.1 概念: 元素、索引
   1.2 创建数组(声明数组)
   1.3 读写数组中的元素
   1.4 稀疏数组
   1.5 遍历数组
   1.6 数组添加元素
   1.7 数组删除元素
   1.8 多维数组
   1.9 字符串的数组特性 / 类数组、伪数组
   
2. 函数
   2.1 函数是js中一种数据类型
   2.2 函数的组成
       函数名、函数体、参数、返回值
   2.3 创建函数
       ① function 关键字方式
       ② 表达式方式(直接量方式)
       ③ Function 函数  对应 Array、Number、String、Boolean
       ④ Function 构造函数

数组回顾:先抓住“元素、索引、创建与读写” ​

为什么需要:数组用来成批保存数据,后续函数参数、类数组、arguments 都要用到“索引 + length”的结构。怎么想:先分清元素和索引,索引从 0 开始;创建数组可用直接量,也可调用 Array()。转写稿里反复强调:Array 本身是变量,值是函数;Array() 才调用并返回新数组,Array 不加括号得到的是函数本身。怎么做:读写用 arr[索引];遍历用循环;增删可用 push、slice 等。易错点:Array 与 Array() 不同;稀疏数组有长度但部分位置没有值;类数组/伪数组“像数组”,但类型不一定是数组,后面 arguments 就是典型。

函数调用:函数名后加小括号才执行 ​

为什么需要:函数体只有被调用才执行,否则只是“把函数当数据放着”。怎么想:看到函数名,先问“后面有没有小括号”。怎么做:

js
function fn() {
  console.log('我是fn');
}
fn;   // 不调用,函数体不执行
fn(); // 调用,输出“我是fn”

转写稿调试过:res1 = Array() 得到空数组;res2 = Array 得到 Array 函数本身;res3 = Array(200, 300, 400) 才得到含元素的数组。易错点:把函数名赋给变量,不等于调用;fn06 里写了 console.log(100),但只写 res1 = fn06 没有输出,res1 是函数本身;写 res2 = fn06() 才会执行函数体,而它的返回值另算。

返回值:输出是输出,返回是返回 ​

为什么需要:函数常常要“算出结果交给调用者”,而不是自己直接输出。怎么想:return 右边的表达式的值,会成为函数调用表达式的值。怎么做:

js
function f01() {
  return 10 * 5;
}
console.log(f01()); // 50

转写稿还演示了 return 可结束函数:执行到 return 后,后面的语句不再执行。没有 return、或 return; 右边为空,都表示没有返回值;若强行取调用表达式的值,得到 undefined。易错点:console.log 是输出,不是返回;函数里输出 100,不代表返回 100。fn06() 的返回值仍是 undefined。

形参与实参:按顺序赋值 ​

为什么需要:函数要接收外部数据。怎么想:形参是创建函数时写的“没有赋值的变量”;实参是调用时传入的数据,按顺序给形参赋值。怎么做:

js
function func(name, age) {
  console.log(name, age);
}
func('高小乐', 78);

数量不一致时:实参多于形参,多出的没有对应形参,不起作用;实参少于形参,后面的形参没有被赋值,使用时得到 undefined。默认值要放在后面:ES5 可在函数体判断 if (address === undefined) address = '上海';,ES6 可直接写 function f(name, age = 10) {}。易错点:不能靠“空着第二个参数”跳过它,空字符串也是值;默认值放前面会失去意义。

arguments:函数内部收集所有实参 ​

为什么需要:有些函数不规定参数个数,比如 push、Array、slice 的后续参数。怎么想:arguments 是系统创建、只在函数内部可用的变量,值是类数组,包含调用时传入的所有实参。怎么做:

js
function sum() {
  let res = 0;
  for (let i = 0; i < arguments.length; i++) {
    res += arguments[i];
  }
  return res;
}
console.log(sum(1, 2, 3, 4, 5)); // 15

易错点:arguments 不是真数组,但有索引和 length;函数外使用会报未定义;它适合“可变参数数量”的函数。

全局变量与作用域提醒 ​

为什么需要:变量不是哪里都能用。怎么想:在函数外创建的是全局变量,函数内创建的是局部变量;for、if 是语句不是函数,所以其中用 var 创建的变量仍按全局处理。函数名本身也是变量,也有作用域。易错点:转写稿里的“that/va”应理解为 let/var 的对比提醒;本节作用域规则按 var 讲,let 与 var 不一样,后续再学。

函数调用:函数名加小括号才是执行 ​

为什么需要:初学者最容易把“函数名”和“函数调用”混为一谈。函数体写了不代表会执行,必须通过调用触发。

怎么想:函数名本身是一个变量,它的值是函数;函数名后面加上小括号,才是“调用这个函数”,函数体里的语句才会执行。只写函数名,相当于把一个函数值放在那里,什么也不会发生。

怎么做:

js
function fn() {
  console.log("我是 fn");
}

fn;    // 只是取函数本身,不执行函数体
fn();  // 调用,输出“我是 fn”

易错点:不要看到 fn 就以为执行了。判断标准很简单:后面有没有小括号。没有小括号,函数体不执行。

返回值:输出是输出,返回是返回 ​

为什么需要:函数经常要“算出一个结果”交给调用者继续使用。直接 console.log 只是输出,不等于把结果返回给外部。

怎么想:return 用来设置返回值;函数调用表达式的值,就是这次调用得到的返回值。业务上需要计算结果时,优先把结果返回,至于要不要输出,由使用函数的人决定。

怎么做:

js
function f01() {
  return 10 * 5;
}

console.log(f01()); // 50

return 右边写表达式;表达式可以是直接量、变量或运算式。return 还有第二个作用:结束当前函数,return 后面的代码不再执行。

易错点:函数里写了 console.log(100),并不代表函数有返回值。没有 return,或 return 右边为空,函数就没有返回值;如果强行取调用表达式的值,浏览器会给 undefined。

形参与实参:调用时按顺序赋值 ​

为什么需要:函数需要接收外部数据,才能根据不同的输入做不同的事。

怎么想:创建函数时写的参数叫形参,形参本质上是“没有赋值的变量”,只能在函数内部使用;调用函数时传入的数据叫实参,实参用于按顺序给形参赋值。

怎么做:

js
function fun(name, age) {
  console.log("我叫" + name + ",今年" + age + "岁");
}

fun("高晓乐", 78); // 实参按顺序赋给 name、age

实参可以是变量、直接量或表达式,只要有值即可。形参不能写成直接量,因为它需要接收值。

易错点:形参只在函数内部有效,函数外部不能使用;函数外部即使有同名变量,也和函数内部的形参不是同一个变量。

数量不匹配与默认值:多给白给,少给得 undefined ​

为什么需要:实际开发中可能出现实参数量与形参数量不一致,需要知道 JS 的处理规则。

怎么想:实参按顺序赋值。实参多了,多出的没有对应形参,等于“白给”;实参少了,后面的形参没有被赋值,使用时得到 undefined。

怎么做:

js
function f(name, age, address) {
  console.log(name, age, address);
}

f("孙悟空", 800); // address 为 undefined

默认值可以用 ES5 方式判断,也可以用 ES6 方式直接写:

js
// ES5:函数体内部判断
function f1(name, age, address) {
  if (address === undefined) {
    address = "上海";
  }
  console.log(name, age, address);
}

// ES6:形参默认值
function f2(name, age, address = "上海") {
  console.log(name, age, address);
}

易错点:有默认值的形参应放在后面。因为实参按顺序赋值,如果默认值参数放前面,后面的实参仍然会按位置覆盖它,默认值就失去意义。

arguments:拿到全部实参 ​

为什么需要:有些函数不规定参数个数,比如求和、push、slice 等,需要接收“调用时传了多少就用多少”。

怎么想:arguments 是系统在函数内部创建的变量,值是一个类数组,包含调用函数时传入的全部实参。它有索引和 length,可以遍历,但它不是真正的数组。

怎么做:

js
function sum() {
  let result = 0;
  for (let i = 0; i < arguments.length; i++) {
    result += arguments[i];
  }
  return result;
}

console.log(sum(1, 2, 3, 4, 5)); // 15

易错点:arguments 只能在函数内部使用,函数外部会报 arguments is not defined;不要把它当成数组,它只是类数组。形参只能接收固定数量,arguments 能拿到全部实参。

作用域:函数内部才有局部变量 ​

为什么需要:变量不是哪里都能用,必须知道它的可作用范围,避免在外部误用函数内部变量。

怎么想:在函数外创建的变量是全局变量,哪里都能用;在函数内创建的变量是局部变量,只在当前函数内可用。形参和 arguments 都属于函数内部使用的变量。

怎么做:

js
var username = "高晓乐"; // 全局变量

function fn() {
  var age = 18; // 局部变量
  console.log(username); // 可以访问全局变量
  console.log(age);
}

fn();
console.log(username); // 可以
// console.log(age);   // 报错:age 未定义

易错点:for、if 等是语句结构,不是函数;在它们里面用 var 创建的变量,按本节 var 规则仍属于全局变量。不要把 var 和 let 的作用域规则混为一谈,本节讲的是 var 的作用域规则。

课堂画面 ​

课堂画面 1

课堂画面 2

课堂画面 3

课堂画面 4

课堂画面 5

课堂画面 6

代码示例 ​

示例一:函数的调用与返回值(01.html)

html
<!DOCTYPE html>
<html lang="zh-CN">
<head>
  <meta charset="UTF-8">
  <title>函数的调用与返回值</title>
</head>
<body>
<script>
  // 1. 有返回值的函数:return 右边写表达式,表达式的值就是返回值
  function fn01() {
    return 10 * 5;
  }
  console.log(fn01()); // 50

  // 2. 返回值可以继续作为另一个函数的返回值
  function fn02() {
    return fn01(); // fn01() 的值是 50,又作为 fn02 的返回值
  }
  console.log(fn02()); // 50

  // 3. 没有 return / return 右边是空的 => 没有返回值
  function fn03() {
    var sum = 100 * 2 + 56;
    return;
  }
  function fn04() {}

  console.log(fn03()); // undefined
  console.log(fn04()); // undefined

  // 4. return 还能结束函数
  function fn05() {
    console.log('fn05 start');
    return;                 // 函数在这里就结束了
    console.log('fn05 end'); // 永远执行不到
  }
  fn05();

  // 5. 输出是输出,返回是返回
  function fn06() {
    console.log(100); // 这是输出,不是返回值
  }

  var res1 = fn06;   // 不加小括号:没有调用,res1 得到的是函数本身
  console.log(res1); // ƒ fn06() { console.log(100) }

  var res2 = fn06(); // 加了小括号:调用,res2 得到的是返回值
  console.log(res2); // undefined

  console.log(fn06()); // 先输出 100(函数体里的),再输出 undefined(返回值)
</script>
</body>
</html>
  • 关键行:fn01() 才是调用,fn01 只是取到这个变量(函数本身)的值。函数名后必须跟小括号,函数体的语句才会执行。
  • 关键行:return 10 * 5;——return 右边是表达式,表达式的值就是函数调用表达式的值,所以 fn01() 的值是 50。
  • 关键行:var res1 = fn06; 与 var res2 = fn06(); 的区别,是整个“调用 vs 赋值”最容易混的地方:不加括号把“函数自己”交出去,加括号把“返回值”交出去。
  • 易错点:return;(右边为空)和压根不写 return,都表示“没有返回值”,并不是返回 0。没有返回值时若非要取函数调用表达式的值,浏览器自动给 undefined。
  • 易错点:看到函数要先默念“输出是输出,返回是返回”。console.log(100) 只是输出,函数并没有返回值;console.log(fn06()) 会输出两个东西:先是 100,后是 undefined。
  • 易错点:return 后面的语句不会再执行,所以“写在 return 后面的代码”等于废代码;但如果 return 写在 if 分支里,不一定每次都执行到。

示例二:形参、实参与形参默认值(02.html)

html
<!DOCTYPE html>
<html lang="zh-CN">
<head>
  <meta charset="UTF-8">
  <title>形参和实参</title>
</head>
<body>
<script>
  // 形参:创建函数时写的“没有赋值的变量”;实参:调用函数时传的数据
  // ES5 方式设置默认值:在函数体里判断 undefined 再赋值
  function func(name, age, address) {
    if (address === undefined) {
      address = '上海';
    }
    console.log('我叫' + name + ',年龄' + age + ',住在' + address);
  }

  func('高小乐', 78, '山洞'); // 三个都传:默认值不生效
  func('孙悟空', 897);        // 少传 address:走默认值

  // 实参数量与形参数量不一致,也不会报错
  function func2(name1, name2) {
    console.log(name1 + '和' + name2);
  }
  func2('高小乐', '曹操');             // 正常
  func2('贾宝玉', '高小乐', '林黛玉'); // 多出来的实参白给,被忽略
  func2('孙悟空');                     // 第二个形参没被赋值 => undefined

  // ES6 方式设置默认值:直接在形参后面写等号
  function func3(name, age = 10, address = '上海') {
    console.log('我叫' + name + ',年龄' + age + ',住在' + address);
  }
  func3('曹操');          // 两个默认值都生效
  func3('孙悟空', 800);   // age 用实参,address 用默认值
  func3('高小乐', 78, '山洞'); // 默认值全不生效
</script>
</body>
</html>
  • 关键行:if (address === undefined) { address = '上海'; }——ES5 没有默认值语法,只能靠判断“这个形参是不是 undefined”来临时补值。
  • 关键行:function func3(name, age = 10, address = '上海')——ES6 直接在形参位置写默认值,函数体里啥也不用干。
  • 易错点:实参给形参赋值是严格按顺序的。func2('贾宝玉','高小乐','林黛玉') 里,第三个实参根本没有对应形参,白给;func2('孙悟空') 只给第一个形参赋值,第二个形参存在但没值,用的时候自动是 undefined。
  • 易错点:带默认值的形参必须放在形参列表后面。因为顺序赋值,前面的位置没法跳过;把带默认值的放前面,调用时想“跳过”它只能传个空字符串 '',而空字符串是数据,照样会赋上值,默认值就失效了。
  • 易错点:形参本质就是变量,但只能在函数内部使用;函数外面即使有个同名的 name(系统自带的),也和函数里的形参毫无关系。
  • 易错点:设了默认值的形参也叫“可选参数”——调用时这个实参可给可不给,不给就用默认值。

示例三:arguments 与变量作用域(03.html)

html
<!DOCTYPE html>
<html lang="zh-CN">
<head>
  <meta charset="UTF-8">
  <title>arguments 与作用域</title>
</head>
<body>
<script>
  // arguments:系统在函数内部创建的变量,值是调用时传入的所有实参组成的类数组
  function sum() {
    var res = 0; // 记录和
    for (var i = 0; i < arguments.length; i++) {
      res += arguments[i];
    }
    return res;
  }
  console.log(sum(1, 2, 3, 4, 5, 6, 7)); // 28
  console.log(sum(1, 5));                 // 6

  // arguments 只能在函数里用,外面用会报 arguments is not defined
  // console.log(arguments);

  // 全局变量:函数外面创建的变量,哪里都能用
  var username = '高小乐';
  function fn() {
    console.log(username); // 函数内部也能用
  }
  fn();
  console.log(username);   // 函数外部也能用

  // for、if 是语法结构,不是函数,里面的 var 也是全局变量
  for (var i = 0; i <= 10; i++) {
    // 空循环
  }
  console.log(i); // 11

  var age;
  for (var j = 0; j <= 10; j++) {
    age = j + 10;
  }
  console.log(age); // 20(最后一次循环时 j 是 10)

  if (true) {
    var address = '上海';
  }
  console.log(address); // 上海,if 里创建的也是全局变量

  // 函数名本身也是一个变量(全局的)
  console.log(fn); // 输出的是函数本身
</script>
</body>
</html>
  • 关键行:arguments.length 和 arguments[i]——arguments 是类数组(伪数组),有索引、有 length,可以像数组一样遍历,但它的类型不是 Array。
  • 关键行:res += arguments[i];——写形参只能接到固定个数的实参,多的实参会被丢掉;用 arguments 则无论传多少个都能全部拿到,这就是实现“可变参数数量函数”的方式(push、slice、Array 内部也是这么干的)。
  • 易错点:arguments 只能在函数内部使用,在函数外面写 console.log(arguments) 会报 arguments is not defined;并且不同函数里的 arguments 值各不相同。
  • 易错点:判断全局/局部只看一点——有没有写在函数里。for、if、switch 都是语句不是函数,所以 for (var i = ...) 里的 i、if 里的 address 都是全局变量,出了大括号照样能用。
  • 易错点:console.log(age) 得到 20 而不是 11,因为循环体最后一次执行时 j 是 10,执行完循环体之后才 j++ 变成 11。
  • 易错点:本节的作用域规则是基于 var 讲的,不能直接套到 let/const 上,它们的作用域判定规则不同,会得出不一样的结果。

常见错误 ​

  1. 现象:写了 fn 却以为函数已执行 → 原因:函数名不加小括号只是取函数值,不是调用 → 修法:确认执行必须写 fn();需要传参时写 fn(实参)。
  2. 现象:函数里 console.log 了结果,却以为调用表达式有返回值 → 原因:把“输出”当成“返回” → 修法:需要外部拿到结果时用 return 表达式,调用处用变量接收或直接参与运算。
  3. 现象:return 后面的代码“莫名不执行” → 原因:return 会结束当前函数 → 修法:把必须执行的语句放到 return 前;分支中检查 return 是否提前命中。
  4. 现象:给形参设默认值后,传参结果和预期错位 → 原因:实参按顺序赋给形参,默认值参数放在前面会破坏可选性 → 修法:把带默认值的形参统一放到参数列表尾部。
  5. 现象:实参少于形参时,函数内部拿到 undefined 却以为会报错 → 原因:JS 不因参数数量不匹配直接报错,未赋值的形参就是无值变量 → 修法:调用时补齐实参,或在函数内/参数列表中设置默认值。
  6. 现象:对 arguments 调 push、map 等数组方法报错 → 原因:arguments 是类数组,不是真数组 → 修法:按索引和 length 遍历;确需数组方法时先转换为数组。
  7. 现象:在函数外访问形参或 arguments 报未定义 → 原因:它们只在函数内部有效 → 修法:需要外部使用的数据通过 return 返回,或提升为外部变量再赋值。
  8. 现象:用 let 验证本课 var 的作用域结论,结果与课堂示例不一致 → 原因:let 与 var 的作用域规则不同 → 修法:复现本课作用域案例时先用 var;不要把 var 结论直接套到 let 上。

练习 ​

  1. 阅读代码并写出输出结果,说明每次输出的是“函数本身”“返回值”还是“函数内部输出”:
    js
    function fn() &#123;
      console.log('A');
    &#125;
    console.log(fn);
    console.log(fn());
  2. 改错:要求函数返回两数之和,但当前代码只输出不返回。请修改,使 const r = add(2, 3); 后 r 为 5。
    js
    function add(a, b) &#123;
      console.log(a + b);
    &#125;
  3. 编写 sum():不固定参数个数,使用 arguments 计算所有实参之和并返回;分别测试传 2 个、5 个、0 个参数的情况。
  4. 编写 createUser(name, age, address),要求 age 默认 18、address 默认 '上海';分别用 1 个、2 个、3 个实参调用,并解释为什么默认值参数要放后面。
  5. 判断下列变量分别在哪些位置可访问,并说明原因:
    js
    var a = 1;
    function outer() &#123;
      var b = 2;
      if (true) &#123;
        var c = 3;
      &#125;
      for (var i = 0; i < 1; i++) &#123;&#125;
      console.log(a, b, c, i);
    &#125;
    console.log(a);

原视频对照 ​

#视频时长
1函数的调用09:14
2函数的返回值17:34
3形参和实参09:19
4形参和实参的数量问题05:01
5形参的默认值12:48
6arguments12:53
7全局变量12:52

小结 ​

  1. 是否执行看小括号:fn 是函数值,fn() 才是调用。
  2. 是否拿到结果看 return:输出不等于返回,调用表达式的值来自返回值。
  3. return 既设置返回值,也会立即结束当前函数。
  4. 形参是函数内部未赋值的变量;实参按顺序给形参赋值。
  5. 参数数量不匹配通常不报错:多出的实参不发挥作用,缺少的形参得到 undefined。
  6. 默认值参数应放在参数列表尾部,否则按顺序传参时默认值容易失去意义。
  7. arguments 是函数内可用的类数组,适合实现可变参数数量的函数,但不是真数组。
  8. 作用域以函数为边界;本课 var 示例中,for、if 等语句块不会单独产生局部作用域。