这份超全JavaScript函数指南让你从小白变大神
你是不是曾经看着JavaScript里各种函数写法一头雾水?是不是经常被作用域搞得晕头转向?别担心,今天这篇文章就是要帮你彻底搞懂JavaScript函数!
读完本文,你将收获:
- 函数的各种写法和使用场景
- 参数传递的底层逻辑
- 作用域和闭包的彻底理解
- 箭头函数的正确使用姿势
准备好了吗?让我们开始这场函数探险之旅!
函数基础:从“Hello World”开始
先来看最基础的函数声明方式:
// 最传统的函数声明
function sayHello(name) {
return "Hello, " + name + "!";
}
// 调用函数
console.log(sayHello("小明")); // 输出:Hello, 小明!
这里有几个关键点要记住:function是关键字,sayHello是函数名,name是参数,花括号里面是函数体。
但JavaScript的函数写法可不止这一种,还有函数表达式:
// 函数表达式
const sayHello = function(name) {
return "Hello, " + name + "!";
};
console.log(sayHello("小红")); // 输出:Hello, 小红!
这两种写法看起来差不多,但在底层处理上有些细微差别。函数声明会被提升到作用域顶部,而函数表达式不会。
函数参数:比你想的更灵活
JavaScript的函数参数处理真的很贴心,不像其他语言那么死板:
function introduce(name, age, city) {
console.log("我叫" + name + ",今年" + age + "岁,来自" + city);
}
// 正常调用
introduce("张三", 25, "北京"); // 输出:我叫张三,今年25岁,来自北京
// 参数不够 - 缺失的参数会是undefined
introduce("李四", 30); // 输出:我叫李四,今年30岁,来自undefined
// 参数太多 - 多余的参数会被忽略
introduce("王五", 28, "上海", "多余参数1", "多余参数2"); // 输出:我叫王五,今年28岁,来自上海
看到没?JavaScript不会因为参数个数不匹配就报错,这既是优点也是坑点。
为了解决参数不确定的情况,我们可以用arguments对象或者更现代的rest参数:
// 使用arguments对象(较老的方式)
function sum() {
let total = 0;
for (let i = 0; i < arguments.length; i++) {
total += arguments[i];
}
return total;
}
console.log(sum(1, 2, 3, 4)); // 输出:10
// 使用rest参数(ES6新特性,推荐!)
function sum2(...numbers) {
return numbers.reduce((total, num) => total + num, 0);
}
console.log(sum2(1, 2, 3, 4)); // 输出:10
rest参数的写法更清晰,而且它是个真正的数组,能用所有数组方法。
作用域深度探秘:变量在哪生效?
作用域可能是JavaScript里最让人困惑的概念之一,但理解它至关重要。
先看个简单例子:
let globalVar = "我是全局变量"; // 全局变量,在任何地方都能访问
function testScope() {
let localVar = "我是局部变量"; // 局部变量,只在函数内部能访问
console.log(globalVar); // 可以访问全局变量
console.log(localVar); // 可以访问局部变量
}
testScope();
console.log(globalVar); // 可以访问
// console.log(localVar); // 报错!localVar在函数外部不存在
但事情没那么简单,看看这个经典的var和let区别:
// var的怪癖
function varTest() {
if (true) {
var x = 10; // var没有块级作用域
let y = 20; // let有块级作用域
}
console.log(x); // 输出:10 - var声明的变量在整个函数都可用
// console.log(y); // 报错!y只在if块内可用
}
varTest();
这就是为什么现在大家都推荐用let和const,避免var的奇怪行为。
闭包:JavaScript的超级力量
闭包听起来高大上,其实理解起来并不难:
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
看到神奇之处了吗?count变量本来应该在createCounter执行完就消失的,但因为返回的函数还在引用它,所以它一直存在。
闭包在实际开发中超级有用,比如创建私有变量:
function createBankAccount(initialBalance) {
let balance = initialBalance; // 私有变量,外部无法直接访问
return {
deposit: function(amount) {
balance += amount;
return balance;
},
withdraw: function(amount) {
if (amount <= balance) {
balance -= amount;
return balance;
} else {
return "余额不足";
}
},
getBalance: function() {
return balance;
}
};
}
const myAccount = createBankAccount(1000);
console.log(myAccount.getBalance()); // 输出:1000
console.log(myAccount.deposit(500)); // 输出:1500
console.log(myAccount.withdraw(200)); // 输出:1300
// console.log(balance); // 报错!balance是私有变量,无法直接访问
这样我们就实现了数据的封装和保护。
箭头函数:现代JavaScript的利器
ES6引入的箭头函数让代码更简洁:
// 传统函数
const add = function(a, b) {
return a + b;
};
// 箭头函数
const addArrow = (a, b) => {
return a + b;
};
// 更简洁的箭头函数(只有一条return语句时)
const addShort = (a, b) => a + b;
console.log(add(1, 2)); // 输出:3
console.log(addArrow(1, 2)); // 输出:3
console.log(addShort(1, 2)); // 输出:3
但箭头函数不只是语法糖,它没有自己的this绑定:
const obj = {
name: "JavaScript",
regularFunction: function() {
console.log("普通函数this:", this.name);
},
arrowFunction: () => {
console.log("箭头函数this:", this.name); // 这里的this不是obj
}
};
obj.regularFunction(); // 输出:普通函数this: JavaScript
obj.arrowFunction(); // 输出:箭头函数this: undefined(在严格模式下)
这就是为什么在对象方法里通常不用箭头函数。
立即执行函数:一次性的工具
有时候我们需要一个函数只执行一次:
// 立即执行函数表达式 (IIFE)
(function() {
const secret = "这个变量不会污染全局作用域";
console.log("这个函数立即执行了!");
})();
// 带参数的IIFE
(function(name) {
console.log("Hello, " + name);
})("世界");
// 用箭头函数写的IIFE
(() => {
console.log("箭头函数版本的IIFE");
})();
在模块化规范出现之前,IIFE是防止变量污染全局的主要手段。
高阶函数:把函数当参数传递
在JavaScript中,函数是一等公民,可以像变量一样传递:
// 高阶函数 - 接收函数作为参数
function processArray(arr, processor) {
const result = [];
for (let i = 0; i < arr.length; i++) {
result.push(processor(arr[i]));
}
return result;
}
const numbers = [1, 2, 3, 4, 5];
// 传递不同的处理函数
const doubled = processArray(numbers, function(num) {
return num * 2;
});
const squared = processArray(numbers, function(num) {
return num * num;
});
console.log(doubled); // 输出:[2, 4, 6, 8, 10]
console.log(squared); // 输出:[1, 4, 9, 16, 25]
这就是函数式编程的基础,也是数组方法map、filter、reduce的工作原理。
实战演练:构建一个简单的事件系统
让我们用今天学的知识构建一个实用的小工具:
function createEventEmitter() {
const events = {}; // 存储所有事件和对应的监听器
return {
// 监听事件
on: function(eventName, listener) {
if (!events[eventName]) {
events[eventName] = [];
}
events[eventName].push(listener);
},
// 触发事件
emit: function(eventName, data) {
if (events[eventName]) {
events[eventName].forEach(listener => {
listener(data);
});
}
},
// 移除监听器
off: function(eventName, listenerToRemove) {
if (events[eventName]) {
events[eventName] = events[eventName].filter(
listener => listener !== listenerToRemove
);
}
}
};
}
// 使用示例
const emitter = createEventEmitter();
// 定义监听器函数
function logData(data) {
console.log("收到数据:", data);
}
// 监听事件
emitter.on("message", logData);
// 触发事件
emitter.emit("message", "你好世界!"); // 输出:收到数据: 你好世界!
emitter.emit("message", "这是第二条消息"); // 输出:收到数据: 这是第二条消息
// 移除监听器
emitter.off("message", logData);
emitter.emit("message", "这条消息不会被接收"); // 不会有输出
这个例子用到了我们今天学的几乎所有概念:函数返回函数、闭包、高阶函数等。
常见坑点与最佳实践
学到这里,你已经是函数小能手了!但还要注意这些常见坑点:
// 坑点1:循环中的闭包
console.log("=== 循环闭包问题 ===");
for (var i = 0; i < 3; i++) {
setTimeout(function() {
console.log(i); // 输出:3, 3, 3 而不是 0, 1, 2
}, 100);
}
// 解决方案1:使用let
for (let i = 0; i < 3; i++) {
setTimeout(function() {
console.log(i); // 输出:0, 1, 2
}, 100);
}
// 解决方案2:使用IIFE
for (var i = 0; i < 3; i++) {
(function(j) {
setTimeout(function() {
console.log(j); // 输出:0, 1, 2
}, 100);
})(i);
}
最佳实践总结:
- 优先使用const,其次是let,避免var
- 简单的函数用箭头函数,方法定义用普通函数
- 注意this的指向问题
- 合理使用闭包,但要注意内存泄漏
总结
恭喜你!现在已经对JavaScript函数有了全面的理解。从基础声明到高级概念,从作用域到闭包,这些都是JavaScript编程的核心基础。
记住,理解函数的关键在于多写代码、多思考。每个概念都要亲手试一试,看看不同的写法会产生什么效果。