Skip to content

12|JavaScript 函数与作用域

把重复逻辑封装成函数,可以减少代码量、提高可维护性。JavaScript 的函数不只是"一段可复用代码",它还涉及作用域、闭包、this 指向等概念——这些在运维脚本和前端交互代码中都会遇到。

一、函数声明与表达式

函数声明

js
function greet(name) {
  return `你好,${name}`;
}

console.log(greet("运维"));   // "你好,运维"

函数声明会被提升(hoisting)——可以在声明之前调用:

js
console.log(add(1, 2));   // 3,正常执行

function add(a, b) {
  return a + b;
}

函数表达式

把函数赋值给变量:

js
const multiply = function (a, b) {
  return a * b;
};

console.log(multiply(3, 4));   // 12

函数表达式不会被提升,必须在定义之后调用:

js
console.log(multiply(3, 4));   // ReferenceError: Cannot access 'multiply' before initialization

const multiply = function (a, b) {
  return a * b;
};

箭头函数

更简洁的函数写法:

js
// 完整写法
const add = (a, b) => {
  return a + b;
};

// 省略 return(函数体只有一行表达式)
const add = (a, b) => a + b;

// 只有一个参数,可省略括号
const square = (x) => x * x;

// 返回对象字面量(需要括号包裹)
const makeUser = (name) => ({ name: name, active: true });

箭头函数没有自己的 this,会继承外层作用域的 this。这个特性在处理回调时非常有用,也是箭头函数和普通函数最重要的区别。

二、参数

默认参数

js
function connect(host, port = 8080) {
  console.log(`连接 ${host}:${port}`);
}

connect("localhost");        // "连接 localhost:8080"
connect("localhost", 9090);  // "连接 localhost:9090"

剩余参数

js
function sum(...numbers) {
  return numbers.reduce((a, b) => a + b, 0);
}

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

...numbers 把所有传入的参数收集成一个数组。比旧的 arguments 对象更直观,且是真正的数组(arguments 是类数组对象,没有 map/filter 等方法)。

解构参数

js
function printServer({ hostname, ip }) {
  console.log(`${hostname} (${ip})`);
}

printServer({ hostname: "web-01", ip: "192.168.1.10" });
// "web-01 (192.168.1.10)"

三、作用域

作用域决定变量在哪些地方可用。JavaScript 有三级作用域:

全局作用域

在函数和代码块之外声明的变量属于全局作用域:

js
const appName = "ops-console";   // 全局变量

function init() {
  console.log(appName);   // 可以访问
}

全局变量在任何地方都能访问,但也意味着任何地方都能修改——容易导致命名冲突和意外覆盖。现代开发中尽量用模块系统(ESM)管理变量,减少全局污染。

函数作用域

var 声明的变量具有函数作用域,在函数内任何地方都可用:

js
function test() {
  if (true) {
    var x = 10;
  }
  console.log(x);   // 10,var 不受块级限制
}

块级作用域

letconst 具有块级作用域,只在 {} 包裹的代码块内有效:

js
function test() {
  if (true) {
    let y = 10;
    const z = 20;
  }
  // console.log(y);   // ReferenceError: y is not defined
  // console.log(z);   // ReferenceError: z is not defined
}

块级作用域让变量生命周期更清晰,是现代 JavaScript 推荐的做法。

四、闭包

函数可以记住并访问它被创建时的作用域,即使这个函数在当前作用域之外执行:

js
function createCounter() {
  let count = 0;

  return function () {
    count++;
    return count;
  };
}

const counter = createCounter();
console.log(counter());   // 1
console.log(counter());   // 2
console.log(counter());   // 3

createCounter 执行完毕后,其内部变量 count 本应被销毁。但返回的匿名函数仍然持有对 count 的引用,所以 count 被保留在内存中。这种现象称为闭包。

闭包的实用场景——为事件处理生成带参数的回调:

js
function makeHandler(serverId) {
  return function () {
    console.log(`重启服务器 ${serverId}`);
  };
}

const servers = ["web-01", "web-02", "web-03"];
servers.forEach((id) => {
  const btn = document.getElementById(`btn-${id}`);
  btn.addEventListener("click", makeHandler(id));
});

如果没有闭包,直接在循环中绑定事件会得到错误结果:

js
// 错误:所有按钮都输出 "web-03"
servers.forEach((id) => {
  const btn = document.getElementById(`btn-${id}`);
  btn.addEventListener("click", () => {
    console.log(`重启服务器 ${id}`);   // 箭头函数本身也能形成闭包
  });
});

实际上上面这段箭头函数的写法是正确的,因为它每次迭代都创建了一个新的箭头函数,各自捕获了当时的 id。真正的错误写法是:

js
// 错误(假设用 var)
for (var i = 0; i < servers.length; i++) {
  const btn = document.getElementById(`btn-${servers[i]}`);
  btn.addEventListener("click", function () {
    console.log(`重启服务器 ${servers[i]}`);   // i 最终等于 3
  });
}

五、this 的指向

this 是 JavaScript 中最容易混淆的概念之一。它的值取决于函数的调用方式,而不是定义位置:

方法调用

js
const server = {
  name: "web-01",
  showName: function () {
    console.log(this.name);
  },
};

server.showName();   // "web-01",this 指向 server 对象

普通函数调用

js
function showName() {
  console.log(this.name);
}

showName();   // this 指向全局对象(浏览器中是 window),通常不是期望的结果

箭头函数

箭头函数没有自己的 this,继承外层作用域:

js
const server = {
  name: "web-01",
  showName: () => {
    console.log(this.name);   // this 指向定义时的外层作用域
  },
};

server.showName();   // undefined(外层是全局作用域)

对象方法不要用箭头函数定义,除非确实需要继承外层 this

事件回调中的 this

js
const btn = document.getElementById("restart");

// 普通函数:this 指向触发事件的元素(btn)
btn.addEventListener("click", function () {
  console.log(this.id);   // "restart"
});

// 箭头函数:this 继承外层作用域
btn.addEventListener("click", () => {
  console.log(this.id);   // undefined(外层不是 btn)
});

六、常见错误

函数声明和表达式混用导致提升问题

js
// 错误
console.log(sum(1, 2));   // ReferenceError

const sum = function (a, b) {
  return a + b;
};

// 正确:用函数声明
console.log(sum(1, 2));   // 3
function sum(a, b) {
  return a + b;
}

箭头函数误用于对象方法

js
// 错误
const server = {
  name: "web-01",
  restart: () => {
    console.log(`重启 ${this.name}`);   // this.name 是 undefined
  },
};

// 正确:用普通函数
const server = {
  name: "web-01",
  restart() {
    console.log(`重启 ${this.name}`);
  },
};

闭包导致内存泄漏

js
// 闭包持有对大对象的引用,导致对象无法被垃圾回收
function createProcessor() {
  const hugeData = new Array(1000000).fill("x");

  return function () {
    console.log("处理中...");
    // hugeData 没有被使用,但仍然被闭包引用
  };
}

const processor = createProcessor();   // hugeData 一直驻留内存

如果闭包引用的变量不再需要,在返回的函数中将其设为 null,或重新设计数据结构避免不必要的大对象捕获。