在上一篇文章中,我们了解了 JavaScript 函数的基本概念——如何定义函数、传递参数、返回值,以及箭头函数的用法。
但会写函数、会调用函数,只是入门的第一步。
你有没有想过:为什么一个函数可以访问在它外面声明的变量?为什么一个函数在创建它的外层函数已经执行完之后,还能访问那个变量?为什么一个函数可以被当作参数传给另一个函数?
这些行为背后,涉及 JavaScript 三个核心概念:作用域、闭包和高阶函数。本文带你一步步理解它们,并用实例说明它们之间的联系。如果你在手机上调试网页,也可以先了解一下如何让 iPhone Safari 在后台打开新标签页,方便同时查看代码和文档。
一、理解作用域
作用域决定了变量在代码中的哪些位置可以被访问。
看这个例子:
const name = "Dini"; function greet() { console.log(name); } greet();greet()函数可以访问name,尽管name并不是在函数内部声明的。这是因为name声明在外部作用域中。
再看另一个例子:
function greet() { const message = "Hello, Dini!"; console.log(message); // "Hello, Dini!" } greet(); console.log(message); // ReferenceError: message is not defined第一个console.log()正常运行,因为message是在greet()内部声明的。但第二个会报错,因为message只存在于函数的作用域里。
全局作用域 └── greet() └── messagemessage在greet()内部可以访问,但从外部访问不到。这就是作用域的核心:一个变量只能在它被定义的作用域以及嵌套在它内部的作用域中被访问。
JavaScript 有以下几种作用域:
- 全局作用域:脚本模式下所有代码的默认作用域。
- 模块作用域:模块模式下代码的作用域。
- 函数作用域:由函数创建的作用域。
- 块级作用域:由一对花括号(代码块)创建的作用域。
注意:代码块只对let和const声明生效,对var不生效。
{ var x = 10; } console.log(x); // 10{ const x = 10; } console.log(x); // ReferenceError: x is not defined二、词法作用域与作用域链
JavaScript 使用词法作用域,也就是说,变量的可访问性由代码书写的位置决定,而不是由函数调用的位置决定。
const name = "Dini"; function outer() { const greeting = "Hello"; function inner() { console.log(greeting); console.log(name); } inner(); } outer();inner()可以同时访问greeting和name。为什么?因为 JavaScript 通过作用域链来查找变量。
inner() ↓ outer() ↓ 全局作用域当 JavaScript 执行console.log(greeting)时:
- 先在
inner()内部找greeting,没找到; - 再到外层
outer()找,找到了,查找停止。
而执行console.log(name)时,inner()和outer()里都没有name,于是继续往全局作用域找,最终在那里找到。
这就是作用域链。要记住:这个关系只能从内层往外层走。内层函数可以访问外层作用域的变量,反过来不行。
这个概念非常重要,因为它是理解闭包的基础。
三、什么是闭包
理解了作用域和作用域链之后,来看一个初看可能觉得奇怪的现象:
function createGreeting() { const message = "Hello, Dini!"; return function () { console.log(message); }; } const greet = createGreeting(); greet();输出是:
Hello, Dini!你可能会疑惑:createGreeting()已经执行完了,greet()为什么还能访问message?按常理,外层函数执行完后,它内部的变量应该被回收才对。
这就是闭包在起作用。
函数会记住它诞生时的作用域
当 JavaScript 创建createGreeting()返回的那个内部函数时,这个函数会保持对其创建时所处作用域中所有变量的访问权。
greet ↓ function () { console.log(message); } ↓ 记住了对 ↓ message = "Hello, Dini!" 的访问也就是说,即使createGreeting()已经执行完毕,返回的函数仍然能访问它创建时的环境。这种行为就叫闭包。
简单定义:闭包是指函数在其外层函数执行完毕后,仍然保持对外层词法作用域中变量的访问能力。
关键点在于:函数记住的不是变量的值的拷贝,而是对变量本身的引用。
闭包可以保存状态
看一个计数器的例子:
function createCounter() { let count = 0; return function () { count++; return count; }; } const counter = createCounter(); console.log(counter()); // 1 console.log(counter()); // 2 console.log(counter()); // 3count声明在createCounter()内部。执行完const counter = createCounter()后,createCounter()已经结束了。但每次调用counter(),count的值仍然存在,并且在之前的基础上累加。
这是因为返回的函数形成了对count的闭包,在多次调用之间维持了这个变量的状态。
闭包创建独立的状态
更有意思的是,如果创建两个计数器:
function createCounter() { let count = 0; return function () { count++; return count; }; } const counterA = createCounter(); const counterB = createCounter(); console.log(counterA()); // 1 console.log(counterA()); // 2 console.log(counterB()); // 1 console.log(counterB()); // 2虽然两个计数器来自同一个函数,但它们各自维护独立的count,互不影响。每次调用createCounter()都会创建一个新的词法环境,返回的函数闭包在各自的环境上。
这就是闭包的强大之处:它可以用来创建独立、持久的状态。
闭包有什么用
闭包不只是 JavaScript 的一个有趣特性,它在实际开发中用途广泛:
- 在多次函数调用之间保持状态
- 创建函数工厂
- 封装数据
- 创建需要访问外部变量的回调函数
- 处理异步操作
举个函数工厂的例子:
function createMultiplier(multiplier) { return function (number) { return number * multiplier; }; } const double = createMultiplier(2); const triple = createMultiplier(3); console.log(double(5)); // 10 console.log(triple(5)); // 15double记住了multiplier = 2,triple记住了multiplier = 3。同一个工厂函数可以生成不同的函数,各自保留自己的状态。
这就是闭包的核心思想:一个函数可以携带它创建时所处环境的访问权。
四、函数是值
JavaScript 函数还有一个重要特性:函数就是值。
这意味着函数可以像其他值一样对待——可以赋值给变量、可以传给另一个函数、也可以从函数中返回。
把函数赋值给变量
const greet = function () { console.log("Hello!"); }; greet();这里greet存储的不是函数的执行结果,而是对函数本身的引用。所以要加括号greet()才会执行。
还可以把同一个函数赋值给另一个变量:
const greet = function () { console.log("Hello!"); }; const sayHello = greet; sayHello();把函数作为参数传递
function greet(name) { console.log(`Hello, ${name}!`); } function processUser(name, callback) { callback(name); } processUser("Dini", greet);注意我们传的是greet,不是greet()。传greet是把函数本身传过去;传greet()会立刻执行函数,传过去的是它的返回值。
这种被传给另一个函数、稍后再被执行的函数,通常叫做回调函数。
从函数中返回另一个函数
function createGreeting(name) { return function () { console.log(`Hello, ${name}!`); }; } const greetDini = createGreeting("Dini"); greetDini();这个例子和闭包那一节很像。返回的函数不仅是一个函数值,它还闭包在了name变量上。
const greetDini = createGreeting("Dini"); const greetNaufal = createGreeting("Naufal"); greetDini(); // Hello, Dini! greetNaufal(); // Hello, Naufal!每个返回的函数都能访问自己那份name。
函数可以被存储、传递和返回——这就是所谓的一等函数。JavaScript 把函数当作一等公民来对待,这也是函数如此强大的原因。
五、什么是高阶函数
既然函数可以接收另一个函数作为参数,或者返回一个函数,那么"操作其他函数的函数"叫什么?这就是高阶函数。
定义:高阶函数是满足以下至少一个条件的函数:
- 接收另一个函数作为参数;
- 返回一个函数作为结果;
- 两者都做。
看一个简单例子:
function processNumber(number, operation) { return operation(number); } function double(number) { return number * 2; } const result = processNumber(5, double); console.log(result); // 10processNumber()接收double作为参数,然后执行它。它不需要知道operation具体做什么,只需要接收函数并调用它。这让函数更加灵活和可复用。
高阶函数 vs 回调函数
这两个概念紧密相关,但描述的是不同角色:
processNumber()是高阶函数(它接收另一个函数);double()是回调函数(它被传给高阶函数,稍后被调用)。
回调函数是"被传过去稍后执行的函数",高阶函数是"接收或返回其他函数的函数"。
高阶函数也可以返回函数
回到之前createMultiplier的例子:
function createMultiplier(multiplier) { return function (number) { return number * multiplier; }; } const double = createMultiplier(2); const triple = createMultiplier(3);createMultiplier()返回了一个新函数,所以它也是高阶函数。返回的函数同时闭包在了multiplier上。这就是高阶函数和闭包协同工作的典型例子。
高阶函数有什么用
高阶函数让代码更灵活、可复用、可组合。我们不需要为每种操作写一个独立的函数,而是写一个通用函数,在调用时传入具体的行为。
function calculate(number, operation) { return operation(number); } const double = calculate(10, number => number * 2); const square = calculate(10, number => number ** 2); console.log(double); // 20 console.log(square); // 100实际上,你大概率已经用过很多次高阶函数了。比如数组的map():
const numbers = [1, 2, 3, 4, 5]; const doubled = numbers.map(number => number * 2);map()接收一个函数作为参数,所以它是高阶函数。类似的还有filter()、forEach()、reduce()、find()、some()、every()等。
六、数组方法中的高阶函数
用同一个数据集来演示:
const users = [ { name: "Dini", age: 25, active: true }, { name: "Naufal", age: 30, active: false }, { name: "Nabil", age: 28, active: true }, ];map()
map()通过对原数组的每一项进行变换,返回一个新数组。
const names = users.map(user => user.name); console.log(names); // ["Dini", "Naufal", "Nabil"]用法规则:当你想把每一项都变换成另一种形式时,用 map()。
filter()
filter()筛选出满足条件的项,返回新数组。
const activeUsers = users.filter(user => user.active); console.log(activeUsers);回调返回true的项被保留,返回false的被剔除。
用法规则:当你想从数组中挑选出符合条件的若干项时,用 filter()。
forEach()
forEach()对每一项执行一次函数,但不返回新数组。
users.forEach(user => { console.log(user.name); }); // Dini // Naufal // Nabil注意forEach()返回undefined。如果你需要得到一个新数组,应该用map()。
用法规则:当你只想对每一项执行某个操作、不需要返回新数组时,用 forEach()。
reduce()
reduce()把数组的多项合并成一个最终结果。
const totalAge = users.reduce((total, user) => { return total + user.age; }, 0); console.log(totalAge); // 83第二个参数0是累加器的初始值。回调接收两个参数:total(当前累加值)和user(当前项)。
reduce()的结果不一定是数字,也可以是对象:
const usersByName = users.reduce((result, user) => { result[user.name] = user; return result; }, {}); console.log(usersByName);用法规则:当你想把整个数组归约成一个值或一个对象时,用 reduce()。
总结对比
| 方法 | 作用 | 返回值 |
|---|---|---|
map() | 变换每一项 | 新数组 |
filter() | 筛选符合条件的项 | 新数组 |
forEach() | 对每一项执行操作 | undefined |
reduce() | 归约为一个结果 | 单个累加结果 |
这些方法虽然用途不同,但模式是一样的:数组方法负责"怎么遍历",回调函数负责"对每一项做什么"。这就是高阶函数的威力——我们不需要自己写循环逻辑,只需要描述想要的行为。
比如,不使用filter()时我们要这样写:
const activeUsers = []; for (const user of users) { if (user.active) { activeUsers.push(user); } }使用filter()后:
const activeUsers = users.filter(user => user.active);第二版更关注"我们要什么"(活跃用户),而不是"怎么遍历"。这就是现代 JavaScript 中高阶函数如此常用的原因。
七、总结
本文梳理了 JavaScript 函数相关的几个核心概念:
- 作用域:决定变量在哪里可以被访问。
- 词法作用域与作用域链:决定 JavaScript 从哪里开始查找变量。
- 闭包:让函数在外部作用域结束后仍能访问其中的变量。
- 函数是值:函数可以被存储、传递和返回。
- 高阶函数:接收或返回其他函数的函数,如
map()、filter()、reduce()。
理解这些概念之间的联系,是写出清晰、优雅 JavaScript 代码的基础。写代码时遇到环境问题,可以参考Windows 网络相关设置以及其他实用教程。
相关阅读:
- 如何手动和自动把 iPhone 备份到 iCloud
- 如何在 Safari 浏览器中允许或拦截弹窗
- 如何关闭查找我的 iPhone