技术知识文章集合TECHNICAL ARCHIVE · 457 DOCUMENTS

显示模式

登录
ARCHIVE DOCUMENTJS

JavaScript 深入浅出第 2 课:函数是一等公民是什么意思?

所属馆藏
JavaScript
文件格式
Markdown
原始路径
JavaScript/34-JavaScript深入浅出第2课:函数是一等公民是什么意思呢?
本文目录12 个章节
  1. 《JavaScript 深入浅出》系列
  2. 一、什么是“一等公民”
  3. 二、函数作为参数:回调与高阶函数
  4. 三、把函数赋值给变量
  5. 四、函数保存到对象和数组
  6. 五、函数作为返回值:闭包与函数工厂
  7. 六、函数式编程与高阶函数
  8. 七、第二部分:不要无故增加包装函数
  9. 八、this、回调与方法借用
  10. 九、异步函数也可以是一等值
  11. 十、总结
  12. 参考资料与原文

JavaScript 深入浅出第 2 课:函数是一等公民是什么意思?

Category(分类): JavaScript Status: 已整理(2026)

本文保留原文关于“函数是一等公民”、回调、函数作为值、返回函数和闭包的讲解,也保留第二部分对无意义包装函数的讨论。原文中的 var、普通函数和页面转载格式属于历史背景;代码已恢复为可运行形式,并补充高阶函数、this、异步回调和现代数组 API 的边界。

《JavaScript 深入浅出》系列

一、什么是“一等公民”

“一等公民”(first-class citizen)不是 ECMAScript 规范中的正式术语,而是程序设计语言中的能力描述:如果某种值可以像其他普通值一样被赋给变量、作为参数传递、作为返回值,通常就说它具有一等地位。许多资料还会把“可以放进数据结构”列为一等值的表现。

因此,JavaScript 中的函数可以:

  1. 赋值给变量;
  2. 作为对象属性或数组元素保存;
  3. 作为参数传给另一个函数;
  4. 由函数返回;
  5. 在满足调用条件时被执行。
function greet(name) {
  return `Hello, ${name}`
}

const anotherGreet = greet
const functions = [greet, anotherGreet]

console.log(anotherGreet('Ada')) // Hello, Ada
console.log(functions[0] === greet) // true

这里的“函数是一等公民”并不意味着函数是 JavaScript 中唯一重要的值,也不意味着 JavaScript 是纯函数式语言。它只是说明函数可以作为数据流经程序。

历史背景

原文提到 JavaScript 借鉴 Scheme,并将函数提升到一等公民的地位。这个说法适合作为语言历史背景:Scheme 等 Lisp 方言对词法作用域、闭包和函数式编程影响很大。但“一等函数”不是 JavaScript 专属特性,也不是由某一条 ECMAScript 条款命名的概念。

二、函数作为参数:回调与高阶函数

把函数传给另一个函数时,接收函数的函数就是一个高阶函数(higher-order function)。更完整的定义是:高阶函数接收一个或多个函数作为参数,或者返回一个函数,或者同时具备两者。

function runTwice(action) {
  action()
  action()
}

runTwice(() => {
  console.log('run')
})

作为参数传入的函数通常叫回调函数(callback)。回调不必然意味着异步:Array.prototype.map()sort() 的比较函数也是同步回调。

2.1 setTimeout 中的回调

setTimeout(() => {
  console.log('稍后执行')
}, 1000)

setTimeout 是浏览器、Node.js 等宿主环境提供的计时器 API,不是 ECMAScript 核心语言本身。它的第一个参数是函数,宿主在计时器到期后安排一次调用;1000 毫秒是最小等待请求,不是精确的执行时间。

回调可能同步执行,也可能异步执行,必须以具体 API 的契约为准:

function mapSynchronously(values, callback) {
  return values.map(callback)
}

function later(callback) {
  setTimeout(callback, 0)
}

console.log(mapSynchronously([1, 2], value => value * 2)) // 立即得到 [2, 4]
later(() => console.log('下一次任务机会'))

2.2 sort 的比较函数

sort 可以接收自定义比较函数,因此同一个排序 API 可以处理不同的业务规则:

const employees = [
  { name: 'Liu', age: 21 },
  { name: 'Zhang', age: 37 },
  { name: 'Wang', age: 45 },
  { name: 'Li', age: 30 },
  { name: 'Zan', age: 55 },
  { name: 'Xi', age: 37 }
]

employees.sort((a, b) => a.age - b.age)
console.log(employees.map(employee => employee.age)) // [21, 30, 37, 37, 45, 55]

现代 JavaScript 的排序有几个容易忽略的点:

  • sort()原地修改数组,并返回同一个数组;
  • 从 ES2019 开始,规范要求排序稳定,即比较结果相等的元素保持原有相对顺序;
  • 比较函数应尽量满足反对称性、传递性和一致性,否则结果取决于引擎实现和输入;
  • 不想修改原数组时,可以使用 ES2023 的 toSorted(),或先复制再排序。
const byName = (a, b) => a.name.localeCompare(b.name, 'zh-CN')
const sortedEmployees = employees.toSorted(byName)

console.log(sortedEmployees === employees) // false
console.log(employees !== sortedEmployees) // true

如果需要兼容没有 toSorted() 的旧环境,可以使用:

const sorted = [...employees].sort(byName)

三、把函数赋值给变量

函数声明和函数表达式都能得到函数值:

function helloByDeclaration() {
  return 'Hello'
}

const helloByExpression = function () {
  return 'Hello'
}

const helloByArrow = () => 'Hello'

console.log(typeof helloByDeclaration) // function
console.log(typeof helloByExpression) // function
console.log(typeof helloByArrow) // function

typeof 对函数返回 "function",这是 typeof 为可调用对象保留的特殊结果;函数在更广义上仍然是对象,可以有属性、原型和方法:

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

add.description = '加法函数'
console.log(add.description) // 加法函数
console.log(add.length) // 2:形参列表中第一个默认参数之前的参数数量

Function.length 只适合描述简单的形参数量,不等于函数真正必须接收的参数数量:

function example(a, b = 1, ...rest) {}
console.log(example.length) // 1

四、函数保存到对象和数组

函数可以作为对象属性保存:

const person = {
  name: 'Wang Lei',
  age: 40,
  greeting() {
    return `Hello! My name is ${this.name}.`
  }
}

console.log(person.age) // 40
console.log(person.greeting()) // Hello! My name is Wang Lei.

“函数作为对象的方法调用时,this 是该对象”需要加上调用形式这个前提。person.greeting() 有一个明确的接收者 person,但取出函数后再调用就不再自动保留它:

const greeting = person.greeting
console.log(greeting()) // 非严格函数/模块环境下的结果不同,不要依赖它是 person

const boundGreeting = person.greeting.bind(person)
console.log(boundGreeting()) // Hello! My name is Wang Lei.

箭头函数没有自己的 this,不能用作需要动态接收者的方法:

const counter = {
  value: 1,
  read: () => this.value
}

console.log(counter.read()) // 通常不是 1:箭头函数捕获定义处的 this

如果方法需要动态使用调用者,应使用普通函数方法;如果需要固定外层上下文,可以明确使用箭头函数或 bind。不要把“函数是一等值”和“函数自动携带调用对象”混为一谈。

五、函数作为返回值:闭包与函数工厂

函数也可以返回另一个函数:

function sayHello(message) {
  return function () {
    return `Hello, ${message}`
  }
}

const sayHelloToFundebug = sayHello('Fundebug!')
const sayHelloToGoogle = sayHello('Google!')

console.log(sayHelloToFundebug()) // Hello, Fundebug!
console.log(sayHelloToGoogle()) // Hello, Google!

返回的函数保留了对外层 message 绑定的访问,这就是闭包的常见表现。每次调用 sayHello 都会形成独立的环境:

function createCounter(initial = 0) {
  let value = initial

  return {
    increment(step = 1) {
      value += step
    },
    getValue() {
      return value
    }
  }
}

const first = createCounter(100)
const second = createCounter(200)

first.increment(5)
console.log(first.getValue()) // 105
console.log(second.getValue()) // 200

闭包常用于:

  • 创建私有状态;
  • 配置函数或函数工厂;
  • 生成事件处理器;
  • 实现记忆化、节流和防抖;
  • 封装模块内部实现。

但闭包不是自动的内存泄漏。只有当定时器、事件监听器、缓存或其他可达对象仍然引用闭包,且闭包又引用了较大的对象时,才可能造成不必要的长期保留。

六、函数式编程与高阶函数

一等函数是函数式风格的重要基础,但 JavaScript 仍然是一门多范式语言。函数式编程通常还会强调纯函数、不可变数据、组合和避免隐式副作用;一等函数本身并不能保证这些性质。

6.1 mapfilterreduce

const numbers = [1, 2, 3, 4]

const doubled = numbers.map(value => value * 2)
const even = numbers.filter(value => value % 2 === 0)
const total = numbers.reduce((sum, value) => sum + value, 0)

console.log(doubled) // [2, 4, 6, 8]
console.log(even) // [2, 4]
console.log(total) // 10

这些 API 把遍历流程交给数组,把每个元素如何转换或筛选交给回调。回调仍可能有副作用,因此不能仅因为使用了 map 就称为纯函数式代码。

6.2 组合函数

const trim = value => value.trim()
const toLowerCase = value => value.toLowerCase()
const addPrefix = value => `user:${value}`

const pipe = (...functions) => value =>
  functions.reduce((current, currentFunction) => currentFunction(current), value)

const normalizeUserId = pipe(trim, toLowerCase, addPrefix)
console.log(normalizeUserId('  Ada  ')) // user:ada

组合函数把多个小函数连接成新的函数。实际项目中应注意错误处理、异步 Promise、类型约束和调试可读性,不要为了“函数式”而制造过深的调用链。

七、第二部分:不要无故增加包装函数

原文第二部分用下面的例子说明:如果包装函数只是把相同的参数原样传给另一个函数,可以直接传递后者:

const hi = name => `Hi ${name}`
const unnecessaryGreeting = name => hi(name)

const greeting = hi

console.log(unnecessaryGreeting('Jonas')) // Hi Jonas
console.log(greeting('Jonas')) // Hi Jonas

同理,下面两种写法在 ajaxCall 的参数签名、返回值和 this 语义完全一致时等价。为使示例可独立阅读,这里提供一个最小的 ajaxCall 占位实现:

function ajaxCall(callback) {
  return callback({ ok: true })
}

const getServerStuff = callback => ajaxCall(json => callback(json))
const getServerStuffDirectly = ajaxCall

console.log(getServerStuffDirectly(json => json.ok)) // true

但不能把“包装函数总是多余”当成绝对规则。包装函数在这些场景中是有意义的:

  • 调整参数顺序或参数结构;
  • 绑定或转换 this
  • 增加日志、重试、鉴权和错误处理;
  • 将回调结果转换为 Promise;
  • 适配不同 API 的签名;
  • 限制暴露给调用者的数据和能力。
function requestWithLogging(request, logger) {
  return function wrappedRequest(...args) {
    logger.log('request started')
    try {
      const result = Reflect.apply(request, this, args)
      logger.log('request finished')
      return result
    } catch (error) {
      logger.log('request failed')
      throw error
    }
  }
}

因此更准确的建议是:删除没有改变语义的机械包装,但保留具有适配、约束或副作用边界意义的包装。

7.1 控制器的直接委托

原文用控制器举例:

const Views = {
  index(posts) {
    return `index: ${posts.length}`
  },
  show(post) {
    return `show: ${post.title}`
  }
}

const BlogController = {
  index: Views.index,
  show: Views.show
}

console.log(BlogController.index([{}, {}])) // index: 2

这个直接赋值只有在 Views.index 不依赖 this 且签名兼容时才安全。如果原方法需要 Views 作为 this,应使用绑定或有意编写适配器:

const viewController = {
  index: Views.index.bind(Views)
}

八、this、回调与方法借用

原文提醒把对象方法作为一等函数传递时要小心 this,这个提醒仍然有效:

const database = {
  prefix: 'db',
  save(value) {
    return `${this.prefix}:${value}`
  }
}

function invoke(callback, value) {
  return callback(value)
}

// invoke(database.save, 'item'):调用形式已经不再是 database.save()
console.log(invoke(database.save.bind(database), 'item')) // db:item

在现代代码中,可以通过以下方式明确接收者:

  • database.save.bind(database)
  • value => database.save(value)
  • 将依赖作为参数传入的纯函数;
  • 使用类实例方法时由调用者保持方法引用。

“尽量少用 this”是某些函数式编程风格的偏好,不是 JavaScript 的规范要求。面向对象 API、DOM 事件、Web API 和类方法仍然经常使用 this;关键是让绑定规则显式、稳定。

九、异步函数也可以是一等值

Promise 和 async 函数把函数值继续用于异步控制流:

function fetchJson(url, fetchImpl = fetch) {
  return fetchImpl(url).then(response => {
    if (!response.ok) {
      throw new Error(`HTTP ${response.status}`)
    }
    return response.json()
  })
}

async function loadUser(id) {
  return fetchJson(`/api/users/${id}`)
}

thencatchfinally 都接收回调;async 函数本身也是可传递的函数值,但它返回 Promise,而不是直接返回最终数据。把函数作为回调传递时,应明确异常是同步抛出、Promise rejection,还是由宿主事件机制报告。

十、总结

  • 一等函数是语言能力描述,不是 ECMAScript 的正式类型分类;
  • JavaScript 函数可以赋值、传参、返回、存储和调用;
  • 高阶函数是接收函数和/或返回函数的函数;
  • 回调可以同步也可以异步,不能仅凭名称判断执行时机;
  • 函数作为方法调用时的 this 取决于调用形式,函数脱离对象后可能丢失接收者;
  • 返回函数常常形成闭包,闭包可以保存私有状态,但不等于内存泄漏;
  • 直接传递函数可以减少无意义的胶水代码,但适配、日志、错误处理和绑定 this 都是合理的包装场景;
  • 一等函数是函数式编程的重要基础,但不自动使 JavaScript 成为纯函数式语言。

参考资料与原文

457 DOCUMENTS · 10 COLLECTIONS
ARCHIVE SEARCH457 篇文章

SEARCH GUIDE

输入关键词开始搜索

支持搜索文章标题、所属分类和原始文档路径。

按分类浏览

10 COLLECTIONS