Appearance
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 不受块级限制
}块级作用域
let 和 const 具有块级作用域,只在 {} 包裹的代码块内有效:
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()); // 3createCounter 执行完毕后,其内部变量 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,或重新设计数据结构避免不必要的大对象捕获。