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