第 11 章 函数基础

第 11 章 函数基础

函数是 JavaScript 的灵魂——没有函数的 JavaScript,就像没有调料的方便面,能吃,但总觉得少了点什么。

11.1 函数定义

函数声明:function 关键字,存在提升

函数声明是最传统的定义函数的方式,就像写简历一样正式——用 function 关键字,后面跟函数名(必须有):

1
2
3
4
5
6
// 函数声明
function greet(name) {
  return "你好," + name + "!";
}

console.log(greet("小明")); // "你好,小明!"

函数声明会被提升(hoisting)——这意味着你可以先调用函数,再声明函数,程序依然能正常运行:

1
2
3
4
5
6
// 可以在声明之前调用!
console.log(sayHello("world")); // "Hello, world!"

function sayHello(name) {
  return "Hello, " + name + "!";
}

这是因为 JavaScript 引擎在执行代码之前,会把所有函数声明"提升"到当前作用域的顶部。就像老师在你提问之前就知道答案一样,JavaScript 引擎在运行代码之前就已经"看到了"所有的函数声明。

💡 为什么需要提升?因为函数声明和变量声明一样,会被提升到顶部,这样我们就可以在代码中先使用后定义,符合人类"先想到要做什么,再实现"的思维方式。


函数表达式:赋值给变量,存在提升差异

函数表达式把函数当作一个值,赋值给变量。就像把一只猫放进盒子里,从此这只猫就叫"盒子里的猫":

1
2
3
4
5
6
// 函数表达式
const greet = function(name) {
  return "你好," + name + "!";
};

console.log(greet("小红")); // "你好,小红!"

关键区别:函数表达式不会被完全提升!

1
2
3
4
5
6
// ❌ 错误!用 const / let 声明时,变量会先进入「暂时性死区」
console.log(greet("小明")); // ReferenceError: Cannot access 'greet' before initialization

const greet = function(name) {
  return "你好," + name + "!";
};

为什么?因为 const / let 的声明会被提升到作用域顶部,但在「初始化」之前处于暂时性死区(TDZ),此时访问它会直接抛 ReferenceError,而不是拿到 undefined。

1
2
3
4
5
6
7
// 对比一下:如果用老式的 var,结果就不一样了
console.log(hello("小明")); // TypeError: hello is not a function
var hello = function(name) {
  return "你好," + name + "!";
};
// 这里 var 的声明被提升、并初始化为 undefined,
// 所以是「调用 undefined」的 TypeError,而不是 TDZ 的 ReferenceError

一句话区分:函数声明是「整个函数都提前」,可以随便先调用;函数表达式只是「变量提前」,赋值这一步不会提前,所以要赋值之后再调用。

所以函数表达式必须先赋值再调用!

1
2
3
4
5
6
// ✅ 正确做法
const greet = function(name) {
  return "你好," + name + "!";
};

console.log(greet("小明")); // "你好,小明!"

⚠️ 小心陷阱:函数表达式虽然变量声明会被提升,但初始值是 undefined,所以在赋值之前调用会报错。而函数声明是完全提升(声明和函数体都被提升),所以可以在任何位置调用。


匿名函数

顾名思义,匿名函数就是没有名字的函数。它通常出现在需要函数作为值的地方,比如回调函数:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
// 匿名函数作为回调
setTimeout(function() {
  console.log("三秒后打印这句话");
}, 3000);

// 匿名函数作为事件处理器
button.addEventListener("click", function(event) {
  console.log("按钮被点击了!");
});

// 匿名函数作为参数
const numbers = [1, 2, 3, 4, 5];
const doubled = numbers.map(function(n) {
  return n * 2;
});
console.log(doubled); // [2, 4, 6, 8, 10]

匿名函数的优点是简洁,缺点是调试时堆栈信息会显示为 “anonymous function”,不利于调试。所以如果一个函数需要复用,最好给它一个名字。


箭头函数:() =>

ES6 引入了箭头函数,这是 JavaScript 函数定义的"简化版",语法更加简洁:

1
2
3
4
5
6
7
8
9
// 传统函数
const add1 = function(a, b) {
  return a + b;
};

// 箭头函数
const add2 = (a, b) => {
  return a + b;
};

箭头函数的简写规则:

  1. 如果只有一个参数,可以省略括号(但参数为空时不能省略):
1
2
const double = x => x * 2;  // 只有一个参数,省略括号
const getRandom = () => Math.random();  // 没有参数,括号不能省
  1. 如果函数体只有一条语句,可以省略大括号,并且隐式返回:
1
2
const add = (a, b) => a + b;
console.log(add(1, 2)); // 3
  1. 如果想直接返回一个对象,需要把对象用括号包起来,否则 {} 会被解释为函数体:
1
2
3
4
5
6
7
8
// ❌ 错误
const createUser = (name, age) => { name: name, age: age };

// ✅ 正确
const createUser = (name, age) => ({ name: name, age: age });

// 或者更简洁
const createUser = (name, age) => ({ name, age }); // ES6 对象属性简写

箭头函数还有几个重要特性:

  • 没有 arguments 对象
  • this 词法绑定(继承外层作用域的 this)
  • 不能用作构造函数(不能 new)
  • 没有 prototype 属性

函数提升规则:函数声明完整提升,函数表达式只提升变量声明

写法提升情况提前调用会怎样
函数声明 function f(){}整个函数(含函数体)提升正常工作
函数表达式 const f = function(){}只提升变量声明,且进入 TDZReferenceError
函数表达式 var f = function(){}提升变量声明并初始化为 undefinedTypeError
箭头函数 const f = () => {}同 const,进入 TDZReferenceError
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
// 函数声明
console.log(declared()); // "函数声明被调用" — 可以调用
function declared() {
  return "函数声明被调用";
}

// 函数表达式(const)
console.log(expressed()); // ReferenceError: Cannot access 'expressed' before initialization
const expressed = function() {
  return "函数表达式被调用";
};

🎯 最佳实践:

  • 如果需要函数提升(先调用后定义),用函数声明
  • 如果想避免意外调用,用函数表达式
  • 如果用箭头函数或函数表达式给变量赋值,注意在赋值之后再调用

11.2 参数

形参与实参

这是两个非常重要的概念:

  • 形参(Parameters):函数定义时的参数,就像留学中介告诉你"需要准备这些材料"——只是列出名字,并不实际占用东西。
  • 实参(Arguments):函数调用时的参数,就像你真正去递交签证时带的那些文件——实际的材料。
1
2
3
4
5
6
7
// name 和 age 是形参(参数列表)
function introduce(name, age) {
  console.log(`我是${name},今年${age}岁`);
}

// "张三" 和 25 是实参(实际传递的值)
introduce("张三", 25); // "我是张三,今年25岁"

默认参数(ES6+)

ES6 之前,如果你想让参数有默认值,得这么写:

1
2
3
4
5
6
7
8
// 传统写法:手动检查并赋值
function greet(name) {
  name = name || "陌生人";
  return "你好," + name + "!";
}

console.log(greet());        // "你好,陌生人!"
console.log(greet("小明"));  // "你好,小明!"

这种写法有个问题——如果我传入 0、""、false 这些"假值",默认值也会生效(因为 0 || "陌生人" 返回 "陌生人"),这可能不是我们想要的。

ES6 引入了默认参数:

1
2
3
4
5
6
7
function greet(name = "陌生人") {
  return "你好," + name + "!";
}

console.log(greet());        // "你好,陌生人!"
console.log(greet("小明"));  // "你好,小明!"
console.log(greet(""));      // "你好,!"(空字符串是合法的实参,不会触发默认值)

💡 默认参数只在实参为 undefined 时才会使用,0、""、false 这些假值都是合法的实参!

多个默认参数:

1
2
3
4
5
6
7
function createUser(name, age = 18, city = "北京") {
  return { name, age, city };
}

console.log(createUser("小明"));           // { name: "小明", age: 18, city: "北京" }
console.log(createUser("小红", 20));      // { name: "小红", age: 20, city: "北京" }
console.log(createUser("小李", 25, "上海")); // { name: "小李", age: 25, city: "上海" }

使用函数调用作为默认值:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
function getDefaultName() {
  console.log("生成默认名字..."); // 只有在需要默认值时才执行
  return "匿名用户";
}

function greet(name = getDefaultName()) {
  return "你好," + name + "!";
}

console.log(greet());        // 打印"生成默认名字...",返回"你好,匿名用户!"
console.log(greet("小明"));   // "你好,小明!"(不触发默认函数)

arguments 对象:类数组对象

在 ES6 之前,arguments 对象是访问函数参数的唯一方式。它是一个类数组对象(Array-like Object),长得像数组但不是数组:

1
2
3
4
5
6
7
8
function sum() {
  console.log(arguments); // [Arguments] { '0': 1, '1': 2, '2': 3, '3': 4 }
  console.log(arguments.length); // 4
  console.log(arguments[0]); // 1
  console.log(arguments[1]); // 2
}

sum(1, 2, 3, 4);

arguments 的特点:

  1. 它是类数组对象,有 length 属性,可以用下标访问
  2. 它不是真正的数组,没有数组的方法(如 forEach、map)
  3. 它只在普通函数中有效,箭头函数没有 arguments
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
// 类数组转数组
function demo() {
  // 方法1:Array.from
  const args1 = Array.from(arguments);

  // 方法2:展开运算符
  const args2 = [...arguments];

  // 方法3:slice
  const args3 = Array.prototype.slice.call(arguments);

  console.log(args1, args2, args3); // 都是真正的数组
}

⚠️ 注意:arguments 已经被 ES6 的剩余参数取代了,尽量使用剩余参数 ...args 代替!


剩余参数 …args(ES6+):真正的数组

ES6 引入了剩余参数(Rest Parameters),用 ... 开头,可以接收不定数量的参数。它比 arguments 更好用,因为它是真正的数组:

1
2
3
4
5
6
7
8
function sum(...numbers) {
  console.log(numbers); // [1, 2, 3, 4] — 是真正的数组!
  return numbers.reduce((a, b) => a + b, 0);
}

console.log(sum(1, 2, 3, 4)); // 10
console.log(sum(1, 2));        // 3
console.log(sum());            // 0

剩余参数 vs arguments:

特性剩余参数 ...argsarguments
类型真正的数组类数组对象
能否用于箭头函数✅ 可以❌ 不可以(箭头函数没有 arguments)
是否包含所有实参只包含「剩余」的那些包含本次调用的全部实参
能否与默认参数/解构混用✅ 可以❌ 不能
是否有数组方法(map/reduce 等)✅ 有❌ 没有,得先转换
与形参是否联动不联动非严格模式下会联动(历史包袱)
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
// 「联动」是什么意思?看看这个非严格模式下的老行为
function legacy(a) {
  arguments[0] = 99;  // 改 arguments
  console.log(a);     // 99!形参 a 也跟着变了
}
legacy(1);

// 用严格模式或者剩余参数就没有这个问题
function modern(a, ...rest) {
  rest[0] = 99;
  console.log(a, rest[0]); // 1 99(a 不受影响)
}
modern(1, 2);

// 另外,剩余参数必须是最后一个形参,下面这种写法是语法错误
// function bad(...args, last) {}
1
2
3
4
5
6
7
// 剩余参数可以和其他参数混用
function multiply(factor, ...numbers) {
  return numbers.map(n => n * factor);
}

console.log(multiply(2, 1, 2, 3, 4)); // [2, 4, 6, 8]
// factor = 2, ...numbers = [1, 2, 3, 4]

解构剩余参数:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
function parseCommand(action, ...options) {
  const [verb, target, value] = options;
  console.log(action, verb, target, value);
}

parseCommand("send", "email", "user@example.com", "Hello!");
// action = "send"
// options = ["email", "user@example.com", "Hello!"]
// verb = "email"
// target = "user@example.com"
// value = "Hello!"

参数是怎么传的:值传递与「引用共享」

JavaScript 里只有值传递,但对象的「值」是引用,所以会产生「函数内部改对象、外面也变」的效果:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
// 1. 基本类型:传进去的是副本,改不到外面
function changeNumber(n) {
  n = 100; // 只是改了函数内部的副本
}
let count = 1;
changeNumber(count);
console.log(count); // 1(不受影响)

// 2. 对象:传进去的是「引用的副本」,指向同一个对象
function changeProperty(obj) {
  obj.name = "被改了"; // 改的是同一个对象
}
const user = { name: "原值" };
changeProperty(user);
console.log(user.name); // "被改了"

// 3. 但重新赋值形参,不会影响外面的变量
function replaceObject(obj) {
  obj = { name: "新对象" }; // 只是让局部变量指向了新对象
}
replaceObject(user);
console.log(user.name); // "被改了"(还是上一次改过的那个对象)

// 4. 想彻底不受影响,就在函数里先拷贝一份
function changeCopy(obj) {
  const copy = { ...obj };
  copy.name = "副本改了";
  return copy;
}
console.log(changeCopy(user).name); // "副本改了"
console.log(user.name);             // "被改了"(原对象安全)

记住这句口诀:基本类型传副本,对象传「指向同一对象的引用副本」。所以「函数会不会修改我传进去的对象」取决于函数内部做了什么,而不是参数本身。


11.3 返回值

return 语句

return 语句用于指定函数的返回值。当函数遇到 return 时,会立即返回指定的值,并停止执行函数体内后面的代码:

1
2
3
4
5
6
function add(a, b) {
  return a + b;
}

const result = add(1, 2);
console.log(result); // 3

return 立即退出函数:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
function process(value) {
  console.log("1. 开始处理");

  if (value < 0) {
    console.log("2. 负数处理");
    return; // 提前退出,不执行后面的代码
  }

  console.log("2. 正数处理");
  console.log("3. 结束处理");
}

process(-5);
// 输出:
// 1. 开始处理
// 2. 负数处理

process(10);
// 输出:
// 1. 开始处理
// 2. 正数处理
// 3. 结束处理

return 可以返回任何值:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
// 返回数字
function double(x) {
  return x * 2;
}

// 返回字符串
function greet(name) {
  return `Hello, ${name}!`;
}

// 返回布尔值
function isEven(n) {
  return n % 2 === 0;
}

// 返回数组
function range(start, end) {
  return Array.from({ length: end - start + 1 }, (_, i) => start + i);
}

// 返回对象
function createPoint(x, y) {
  return { x, y };
}

// 返回函数(高阶函数)
function createMultiplier(factor) {
  return function(number) {
    return number * factor;
  };
}

未指定返回值时返回 undefined

如果函数没有 return 语句,或者 return 后面没有值,那么函数的返回值是 undefined:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
function sayHello(name) {
  console.log("你好," + name + "!");
}

const result = sayHello("小明");
console.log(result); // undefined

// 显式返回 undefined
function sayBye(name) {
  console.log("再见," + name + "!");
  return undefined;
}

console.log(sayBye("小红")); // undefined

💡 很多初学者会搞混 console.log() 和 return:

  • console.log() 只是打印值到控制台,不影响函数的返回值
  • return 是返回值给调用者,函数外部可以拿到这个值
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
function getGreeting(name) {
  console.log("计算中..."); // 打印到控制台
  return "你好," + name + "!"; // 返回值
}

const greeting = getGreeting("小明");
// 计算中...
// greeting 变量的值是 "你好,小明!"

console.log(greeting); // "你好,小明!"

11.4 箭头函数

简写语法:单参数省略括号 / 单行省略 return

箭头函数有几种简写形式:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// 完整写法
const add = (a, b) => {
  return a + b;
};

// 省略括号(单参数)
const double = x => {
  return x * 2;
};

// 省略大括号和 return(单行函数体)
const add = (a, b) => a + b;
const double = x => x * 2;

// 没有任何参数
const getRandom = () => Math.random();

// 多行函数体不能省略大括号
const calculate = (a, b) => {
  const sum = a + b;
  const product = a * b;
  return sum + product;
};

返回对象需加括号

箭头函数返回对象时,必须用括号包起来,否则 {} 会被解释为函数体:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
// ❌ 错误:JS 引擎会认为 {} 是函数体
const createUser = (name, age) => { name: name, age: age };

// ✅ 正确:用括号包起来
const createUser = (name, age) => ({ name: name, age: age });

// 利用 ES6 对象属性简写
const createUser = (name, age) => ({ name, age });

console.log(createUser("小明", 18)); // { name: "小明", age: 18 }

无 arguments 对象

箭头函数没有 arguments 对象,如果你需要访问所有参数,应该使用剩余参数:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
// 普通函数:可以使用 arguments
function sum() {
  return Array.from(arguments).reduce((a, b) => a + b, 0);
}

console.log(sum(1, 2, 3)); // 6

// 箭头函数:使用剩余参数
const sumArrow = (...numbers) => {
  return numbers.reduce((a, b) => a + b, 0);
};

console.log(sumArrow(1, 2, 3)); // 6

this 词法绑定:继承外层函数的 this

这是箭头函数最重要的特性之一!箭头函数没有自己的 this,它会继承外层作用域的 this:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
// 普通函数:this 指向调用者
const person = {
  name: "小明",
  sayHi: function() {
    console.log("你好,我是" + this.name);
  },
  waitAndSayHi: function() {
    setTimeout(function() {
      console.log("你好,我是" + this.name); // this 指向 window/undefined!
    }, 1000);
  }
};

person.sayHi();         // "你好,我是小明"
person.waitAndSayHi();  // 1 秒后打印,但内容取决于运行环境(见下)

看看上面 waitAndSayHi 的问题——setTimeout 的回调是普通函数,它被当作普通调用执行,因此 this 不再指向 person:

  • 在浏览器的非严格模式脚本里,this 是 window。而 window.name 的默认值是空字符串 "",所以会打印出「你好,我是」。
  • 在 Node 或者任何严格模式(包括 ES 模块、"use strict")下,this 是 undefined,访问 this.name 会直接抛 TypeError。

不管哪种情况,这都不是你想要的结果 —— 这正是箭头函数要解决的问题。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
// 箭头函数:继承外层的 this
const person = {
  name: "小明",
  waitAndSayHi: function() {
    setTimeout(() => {
      console.log("你好,我是" + this.name); // this 指向 person!
    }, 1000);
  }
};

person.waitAndSayHi(); // "你好,我是小明"(1秒后)

不能用作构造函数

箭头函数没有 prototype 属性,也不能用 new 调用:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
const Person = (name, age) => {
  this.name = name;
  this.age = age;
};

// ❌ TypeError: Person is not a constructor
const p = new Person("小明", 18);

// ✅ 普通函数可以用 new
const PersonNormal = function(name, age) {
  this.name = name;
  this.age = age;
};
const p2 = new PersonNormal("小红", 20); // 正常工作

不能用作对象方法

虽然技术上可以在对象中定义箭头函数作为方法,但它的 this 不会绑定到对象:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
const calculator = {
  value: 10,
  // ❌ 箭头函数:this 不会指向 calculator
  add: (n) => this.value + n,
  // ✅ 普通函数:this 会指向 calculator
  subtract: function(n) {
    return this.value - n;
  }
};

console.log(calculator.subtract(3)); // 7
console.log(calculator.add(3));      // NaN(this.value 是 undefined)

说明:对象字面量里的箭头函数,this 取的是定义它时外层作用域的 this,跟调用者 calculator 毫无关系。上面的 NaN 是浏览器/CommonJS 等「外层 this 是全局对象」环境下的结果;在严格模式或 ES 模块里,外层 this 是 undefined,这里会直接抛 TypeError。

1
2
3
4
5
6
// 如果确实想在对象里用箭头函数,可以让它还额外接收一个参数
const calculator2 = {
  value: 10,
  add: ({ value }, n) => value + n // 不依赖 this,而是显式传入
};
console.log(calculator2.add(calculator2, 3)); // 13

普通函数 vs 箭头函数对比

特性普通函数箭头函数
this动态绑定,取决于调用方式词法绑定,继承外层
arguments有没有(可用 ...args)
prototype有没有
new可以用作构造函数不能
作为对象方法this 指向对象this 不指向对象
提升函数声明完整提升变量声明提升,赋值不提升
简写语法无单参数省略括号、单行省略 return
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
// 什么时候用箭头函数?
const numbers = [1, 2, 3, 4, 5];

// ✅ 适合:回调函数,不需要自己的 this
numbers.filter(n => n > 2);        // [3, 4, 5]
numbers.map(n => n * 2);           // [2, 4, 6, 8, 10]
numbers.reduce((a, b) => a + b);  // 15

// ❌ 不适合:需要动态 this 的场景
document.addEventListener("click", () => {
  // 这里的 this 不指向 document!
  console.log(this);
});

document.addEventListener("click", function() {
  // 这里的 this 指向 document
  console.log(this);
});

💡 总结:大多数情况下,箭头函数让代码更简洁。但要记住它的限制——没有自己的 this、arguments、prototype,不能用作构造函数。如果需要这些特性,就用普通函数。


11.5 函数的其他要点

函数也是对象:name / length / toString

函数是「可调用的对象」,所以它也有属性:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
function greet(name, age = 18, ...rest) {
  return `你好,${name}`;
}

console.log(greet.name);   // "greet"(函数名,匿名函数是 "",赋值给变量时可能取变量名)
console.log(greet.length); // 1(注意:只数「第一个默认值之前」的形参个数)

const f = (a, b, ...c) => {};
console.log(f.name);   // "f"
console.log(f.length); // 2

console.log(typeof greet);        // "function"
console.log(greet instanceof Function); // true

// 函数可以挂自定义属性,也能作为参数、作为返回值(这就是「高阶函数」)
greet.version = "1.0";
console.log(greet.version); // "1.0"

// toString 能看到源码(实现相关,别依赖它的具体格式)
console.log(((a) => a * 2).toString()); // Node 里是 "(a)=>a*2",浏览器可能带空格

立即执行函数表达式(IIFE)

在 ES6 模块出现之前,IIFE 是最常用的「创建独立作用域」的手段:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
// 经典写法:函数表达式外面套一层括号,让它变成表达式,然后立刻调用
(function () {
  const secret = "只有这个作用域内能看见";
  console.log("IIFE 执行了");
})();
// console.log(secret); // ReferenceError:外面拿不到

// 箭头函数版本更清爽
(() => {
  console.log("箭头版 IIFE");
})();

// 带参数、带返回值的写法
const result = (function (a, b) {
  return a + b;
})(1, 2);
console.log(result); // 3

// 常见用途:模块模式 —— 只把想暴露的部分挂到全局
const counterModule = (function () {
  let count = 0; // 外部访问不到
  return {
    increment: () => ++count,
    getCount: () => count
  };
})();
counterModule.increment();
console.log(counterModule.getCount()); // 1
// 现在的项目里,这个角色基本被 ES 模块(import / export)取代了

块级作用域中的函数声明

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
// 在块(if / for / {})里用函数声明,ES6 起它的作用域被限制在这个块内
{
  function inner() {
    return "只在块内可见";
  }
  console.log(inner()); // "只在块内可见"
}
// console.log(inner()); // ReferenceError

// 严格模式(含 ES 模块)下行为一致;老的非严格模式脚本里它会被「泄漏」到外面
// 结论:想稳妥就写成 const inner = () => {...},语义最清晰

默认参数的几个细节

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
// 1. 默认值表达式在「每次调用」时才求值,不是定义时
function append(item, list = []) {
  list.push(item);
  return list;
}
console.log(append(1)); // [1]
console.log(append(2)); // [2](每次都拿到全新的数组,不会互相污染)

// 对比:如果默认值写在函数外面,就会共享同一个数组
const shared = [];
function appendShared(item, list = shared) {
  list.push(item);
  return list;
}
appendShared(1);
console.log(appendShared(2)); // [1, 2] —— 这就是共享带来的坑

// 2. 默认值可以使用前面的形参,但不能引用后面的(TDZ)
function area(width, height = width) {
  return width * height;
}
console.log(area(5)); // 25
// function bad(a = b, b = 2) {} // ReferenceError: Cannot access 'b' before initialization

// 3. 传 null 不会触发默认值,只有 undefined 会
console.log(area(5, null)); // 0(null * 5 = 0)
console.log(area(5, undefined)); // 25

本章小结

本章我们深入学习了 JavaScript 函数的定义和调用:

  1. 函数定义方式:函数声明(整体提升,随便先调用)、函数表达式(只提升变量声明,const/let 会进 TDZ 抛 ReferenceError,var 则是 TypeError)、箭头函数(简洁语法)。

  2. 参数:形参与实参的区别;默认参数只在实参为 undefined 时生效,每次调用都会重新求值;arguments 是类数组、箭头函数里没有、非严格模式下还会和形参联动;剩余参数 ...args 是真正的数组,必须放在形参最后。

  3. 参数传递:JavaScript 只有值传递。基本类型传的是副本,改不到外面;对象传的是「引用的副本」,改属性会影响外部对象,但给形参重新赋值不影响外部变量。

  4. 返回值:return 语句、提前退出、未指定返回值时返回 undefined。

  5. 箭头函数特性:

    • 简写语法:单参数省略括号、单行省略 return
    • 返回对象需加括号
    • 无 arguments(用剩余参数代替)
    • this 词法绑定(继承外层)
    • 不能用作构造函数
    • 不能用作对象方法
  6. 其他要点:函数也是对象(有 name、length、可挂自定义属性);IIFE 用来造独立作用域,如今多被 ES 模块取代;块级作用域里的函数声明只在该块内可见;默认参数有求值时机、TDZ 和「null 不触发默认值」三个细节。

📊 图示:函数定义方式对比

graph TD
    A[函数定义] --> B[函数声明]
    A --> C[函数表达式]
    A --> D[箭头函数]

    B --> B1[function 关键字]
    B --> B2[完整提升]
    B --> B3[有 arguments]

    C --> C1[赋值给变量]
    C --> C2[只提升变量声明]
    C --> C3[可匿名]

    D --> D1["() => {} 语法"]
    D --> D2[this 词法绑定]
    D --> D3[无 arguments]
    D --> D4[不能 new]

下章预告:下一章我们将探索作用域与闭包——JavaScript 最神秘又最强大的特性之一!准备好了吗? 🔮

最后修改 September 19, 2026: 更新 (3489033b1)