JavaScript 函数与作用域

AI 辅助生成本文由 AI 起草并经人工核查,示例均标明软件版本;发现错误请以官方文档为准。

Node.js 22 LTS

函数是 JavaScript 的一等公民:可以赋值给变量、作为参数传递、作为返回值返回。理解函数,核心是理解作用域链与闭包。

三种定义方式

// 1. 函数声明:会被提升,可在定义前调用
function add(a, b) {
  return a + b;
}

// 2. 函数表达式:赋值给变量,不提升
const multiply = function (a, b) {
  return a * b;
};

// 3. 箭头函数:更短,且没有自己的 this
const divide = (a, b) => a / b;

区别要点:

  • 函数声明有提升,可以先调用后定义;函数表达式和箭头函数必须先定义再使用。
  • 箭头函数的 this 来自外层作用域(词法绑定),普通函数的 this 由调用方式决定。
  • 需要递归命名、需要 this、或者要作为构造函数时,用 function;回调、数组方法里的短函数用箭头函数。

参数:默认值、剩余参数、解构

function greet(name = "匿名", punctuation = "!") {
  return `你好,${name}${punctuation}`;
}

greet();          // "你好,匿名!"
greet("ann");     // "你好,ann!"

默认值只在参数为 undefined 时生效,传 null 不会触发:

greet(null);      // "你好,null!"

参数个数不确定用剩余参数(收集成真数组):

function sum(...nums) {
  return nums.reduce((total, n) => total + n, 0);
}

sum(1, 2, 3);    // 6

注意剩余参数必须是最后一个形参。函数内部还有 arguments 对象,但它是类数组、没有数组方法,新代码一律用 ...。

参数较多时用一个对象,配合解构:

function createUser({ name, role = "user", active = true }) {
  return { name, role, active };
}

createUser({ name: "ann", role: "admin" });
// { name: "ann", role: "admin", active: true }

这样调用方不必记住参数顺序。

返回值

没有 return 或 return; 的函数返回 undefined:

function log(msg) {
  console.log(msg);
}
log("hi");   // 控制台输出 hi,表达式的值是 undefined

箭头函数返回对象字面量时要加括号,否则 {} 会被当成函数体:

const makeUser = (name) => ({ name, createdAt: Date.now() });

作用域与作用域链

作用域分三层:全局作用域、函数作用域、块级作用域(let / const + {})。

查找变量时从内向外逐层找,这条链路叫作用域链:

const global = "外层";

function outer() {
  const mid = "中层";

  function inner() {
    const local = "内层";
    console.log(local, mid, global);   // "内层 中层 外层"
  }

  inner();
}

内层能读外层,外层不能读内层:

function outer() {
  const secret = "不可见";
}
console.log(secret);   // ReferenceError

闭包

函数记住了定义时的作用域,即使在外层函数执行结束后,依然能访问其中的变量——这就是闭包。

function makeCounter() {
  let count = 0;
  return function () {
    count += 1;
    return count;
  };
}

const next = makeCounter();
next();   // 1
next();   // 2
next();   // 3

count 没有随 makeCounter() 返回而销毁,因为返回的函数还持有它。闭包的典型用途:保存私有状态。

function requireMin(n) {
  return (value) => value.length >= n;
}

const longEnough = requireMin(8);
["abc", "abcdefghij"].filter(longEnough);   // ["abcdefghij"]

var 在循环里的经典陷阱

// 问题代码
const fns = [];
for (var i = 0; i < 3; i++) {
  fns.push(() => i);
}
fns.forEach((fn) => console.log(fn()));   // 3 3 3

var i 是函数作用域,三个闭包共享同一个 i,循环结束时 i 已经是 3。

改成 let,每次迭代生成独立的块级绑定:

const fns = [];
for (let i = 0; i < 3; i++) {
  fns.push(() => i);
}
fns.forEach((fn) => console.log(fn()));   // 0 1 2

这也是「新代码不用 var」最直接的理由之一。

this 的一句话说明

普通函数的 this 取决于怎么被调用:

const obj = {
  value: 42,
  getValue() {
    return this.value;
  },
};

obj.getValue();          // 42,this 指向 obj

const detached = obj.getValue;
detached();              // undefined(严格模式下),this 丢失

箭头函数没有自己的 this,会沿用外层,因此适合作为回调:

const timer = {
  seconds: 0,
  start() {
    setInterval(() => {
      this.seconds += 1;    // this 仍是 timer
    }, 1000);
  },
};

常见错误与注意事项

  1. 在定义前调用箭头函数:不提升,会抛 ReferenceError。
  2. 箭头函数里用 this 期望指向调用者:箭头函数继承外层 this,做对象方法时用普通函数写法。
  3. 箭头函数直接返回对象忘了括号:() => { name } 返回 undefined,正确写法是 () => ({ name })。
  4. 闭包意外持有大对象:变量被闭包引用就不会被回收,长生命周期的闭包要注意别抓住整个大数组。
  5. 循环里用 var + 异步回调:全部拿到最后一次的值,改用 let。

小结

  • 定义方式:需要提升或 this 用 function,短回调用箭头函数。
  • 参数支持默认值、剩余参数、对象解构。
  • 内层可读外层,外层读不到内层;闭包就是「函数带着定义时的作用域一起走」。
  • 循环里声明计数器一律用 let。

下一步