JavaScript 函数总览

JS 函数的三层身份(可调用代码块、对象、作用域边界)、定义方式、参数系统、this、闭包、高阶函数、特殊函数。

#type / synthesis #status / evergreen #resource / javascript #resource / ecmascript

[!info] related notes

JavaScript 函数总览

函数的三层身份

JS 函数不是”特殊语法糖”,它同时是:

  1. 可执行的代码块 — 能被调用
  2. 一种对象 — 能赋值、传参、返回、挂属性
  3. 一种作用域边界 — 形成局部变量、闭包、词法环境
function fn(a, b) { return a + b }

typeof fn      // "function"
fn(1, 2)       // 3
fn.x = 100     // 可以挂属性
fn.x           // 100

一、定义方式

函数声明

function add(x, y) { return x + y }
  • 有函数名
  • 会提升(hoist):声明前可调用
  • typeof add === "function"

函数表达式

const add = function(x, y) { return x + y }
  • 变量受 var / let / const 规则控制
  • 函数本身不整体提升到可调用状态
  • 可匿名,也可具名

匿名函数表达式

const f = function() {}

具名函数表达式

const c = function d() {
  console.log(typeof d)  // "function"(内部可见)
}
console.log(typeof c)    // "function"
console.log(typeof d)    // "undefined"(外部不可见)

用途:

  • 递归更稳定(内部名不依赖外部变量)
  • 调试堆栈信息更清楚

箭头函数

const add = (a, b) => a + b
const square = x => x * x
const getObj = () => ({ x: 1 })

详见下方「箭头函数」章节。

构造器方式

const add = new Function('x', 'y', 'return x + y')
  • 运行时动态拼代码
  • 性能和安全性差,默认全局作用域
  • 工程中极少使用

二、提升(Hoisting)

定义方式提升行为
函数声明整体提升,声明前可调用
var 函数表达式只提升变量声明(var foo),值为 undefined
let / const 函数表达式暂时性死区(TDZ),声明前访问报 ReferenceError
箭头函数(constlet / const,有 TDZ
foo()          // ok
function foo() {}

// bar()       // TypeError: bar is not a function
var bar = function() {}

// baz()       // ReferenceError: Cannot access 'baz' before initialization
const baz = function() {}

三、参数系统

详见 arguments-object

命名参数

function test(a, b) { console.log(a, b) }
test(1)        // 1, undefined(少传)
test(1, 2, 3)  // 1, 2(多传忽略)

JS 不强制参数个数严格一致。

默认参数

function greet(name = 'world') { return 'hello ' + name }

greet()          // "hello world"
greet(undefined) // "hello world"(undefined 触发默认值)
greet(null)      // "hello null"(null 不触发)

只有 undefined 触发默认值,不是所有假值。

剩余参数 rest

function sum(...nums) { return nums.reduce((a, b) => a + b, 0) }

...nums 收集剩余实参成为真正的数组

arguments

普通函数内自动可用的类数组对象。箭头函数没有 arguments

function log(a, b, c, d) {
  console.log(a, b, c, d);  // 1, 2, 3, undefined
  arguments[0] = 'bfe';
  arguments[3] = 'dev';
  console.log(a, b, c, d);  // "bfe", 2, 3, undefined
}
log(1, 2, 3);

关键行为:非严格模式下 arguments[n] 和命名参数双向同步——但仅限实际传递的参数。超出范围的赋值只作用于 arguments 对象本身。

四、this

详见 this-keyword

普通函数的 this 不是定义时决定的,而是调用时决定的。

调用方式与 this

调用方式this 指向示例
普通调用 fn()非严格:window / global;严格:undefinedfn()
方法调用 obj.fn()调用它的对象obj.fn()
构造调用 new Fn()新创建的对象new Person()
显式绑定指定的 thisArgfn.call(obj), fn.apply(obj)

call / apply / bind

详见 call-apply-bind

function foo(a, b) { console.log(this.x, a, b) }
const obj = { x: 100 }

foo.call(obj, 1, 2)     // 立即调用,参数逐个传
foo.apply(obj, [1, 2])  // 立即调用,参数数组传
const bar = foo.bind(obj, 1)  // 不立即调用,返回新函数
bar(2)

容易踩坑:方法被拿出来调用

const obj = {
  x: 10,
  foo() { console.log(this.x) }
}
const f = obj.foo
f() // undefined(已经不是 obj.foo(),是普通函数调用)

五、箭头函数

与普通函数的区别

特性普通函数箭头函数
this调用时决定继承外层作用域,不可被 call/apply/bind 改变
arguments有自己的没有,继承外层
new 构造可以不可以(TypeError)
prototypeundefined
写法function() {}() => {}

箭头函数 this 的经典用法

对象方法不要用箭头函数(this 不指向对象):

const obj = {
  x: 10,
  foo: () => { console.log(this.x) }  // 不会输出 10
}

回调函数常用箭头函数保留外层 this

function Timer() {
  this.count = 0
  setInterval(() => {
    this.count++  // 继承 Timer 的 this
    console.log(this.count)
  }, 1000)
}

六、构造函数与 new

详见 ecmascript-prototypes

new 做了什么

const p = new Person('Tom')

等价于:

const obj = {}
obj.__proto__ = Person.prototype
Person.call(obj, 'Tom')
return obj  // 如果 Person 没返回对象的话

构造函数返回值规则

function A() { this.x = 1; return 100 }    // 返回基本类型 → 忽略,返回新对象
function B() { this.x = 1; return { y: 2 } } // 返回对象 → 替代默认新对象

new A().x  // 1
new B().x  // undefined
new B().y  // 2

七、闭包

详见 ecmascript-closures

函数能够记住并访问它定义时所在词法作用域中的变量,即使这个函数在外部执行。

function outer() {
  let count = 0
  return function() {
    count++
    console.log(count)
  }
}
const fn = outer()
fn() // 1
fn() // 2

私有变量

function createCounter() {
  let count = 0
  return {
    inc() { count++ },
    get() { return count }
  }
}

经典坑:循环闭包

for (var i = 0; i < 3; i++) {
  setTimeout(function() { console.log(i) }, 0)
}
// 输出 3, 3, 3(共享同一个 i)

for (let i = 0; i < 3; i++) {
  setTimeout(function() { console.log(i) }, 0)
}
// 输出 0, 1, 2(let 每轮创建新绑定)

八、作用域

JS 采用词法作用域:变量查找由函数定义的位置决定,不是调用的位置。

let x = 1
function outer() {
  let x = 2
  function inner() { console.log(x) }
  return inner
}
const f = outer()
f() // 2(inner 定义时在 outer 内部,查找 outer 的 x)

九、高阶函数

接收函数作为参数,或返回函数。

// 接收函数
function map(arr, fn) {
  const res = []
  for (const item of arr) res.push(fn(item))
  return res
}

// 返回函数
function multiply(x) {
  return function(y) { return x * y }
}
const double = multiply(2)
double(5) // 10

常见高阶函数:mapfilterreducesortforEach。 详见 array-prototype-maphigher-order-function-callback-mismatch

十、柯里化

详见 currying

把多参数函数变成一连串单参数函数,依赖闭包:

function add(a) {
  return function(b) {
    return function(c) {
      return a + b + c
    }
  }
}
add(1)(2)(3) // 6

十一、IIFE(立即执行函数)

(function() {
  console.log('run')
})()

用途:创建独立作用域、避免污染全局变量、旧时代模拟模块。

十二、递归

function factorial(n) {
  if (n <= 1) return 1
  return n * factorial(n - 1)
}

用具名函数表达式更稳定:

const factorial = function fact(n) {
  if (n <= 1) return 1
  return n * fact(n - 1)  // 不依赖外部变量名
}

十三、特殊函数类型

getter / setter

const obj = {
  _x: 1,
  get x() { return this._x },
  set x(v) { this._x = v }
}

本质也是函数,通过属性访问语法触发。

generator

function* gen() {
  yield 1
  yield 2
}
const it = gen()
it.next() // { value: 1, done: false }

调用后不直接执行,返回迭代器。详见 ecmascript-iterators-and-generators

async

async function foo() {
  const x = await Promise.resolve(10)
  return x
}
foo().then(console.log) // 10

返回 Promise,await 等待 Promise 解析。详见 async / await

严格模式下的函数行为

'use strict'
function foo() { console.log(this) }
foo() // undefined(非严格模式是 window)

十四、函数对象的成员

属性含义
name函数名(调试用,具名表达式返回内部名)
length形参数量(不含 rest)
prototype构造函数实例共享方法的落点,箭头函数无
caller历史属性,现代代码尽量不用

详见 function-length

十五、最小必会清单

题型核心考点
函数声明 vs 表达式提升差异
具名函数表达式内部名可见性
闭包词法作用域捕获
循环闭包var 共享 vs let 隔离
this调用时决定
箭头函数 this继承外层,不可绑定
new 做了什么创建对象 + 原型绑定 + this 绑定 + 返回
call / apply / bind立即调用 vs 返回新函数
高阶函数回调参数参数错位陷阱

学习顺序建议

  1. 函数声明 / 表达式 / 箭头函数
  2. 作用域 / 词法环境
  3. 闭包
  4. this
  5. call / apply / bind
  6. new / prototype / 原型链
  7. Promise / async(本质也离不开函数)

面试要点

来自 ts-utility-types-interview-question 的面试视角整理。

补充来自 type-vs-interface-interview-question

一句话回答

interface 更适合描述对象结构,支持声明合并和 extends 继承;type 更适合联合类型、交叉类型、类型别名和复杂类型表达。工程里通常按”对外契约用 interface,类型组合用 type”来分工。

核心区别

声明合并

  • interface:支持声明合并,同名 interface 会自动合并成员
  • type:不支持,同名 type 会报错
interface User {
  name: string;
}
interface User {
  age: number;
}
// 合并后: { name: string; age: number; }

表达能力

  • interface:只能描述对象/函数签名结构
  • type:可以表示联合类型、交叉类型、元组、映射类型、条件类型等
type Status = 'pending' | 'success' | 'error';
type Result<T> = { ok: true; data: T } | { ok: false; error: string };

继承方式

  • interface:用 extends 继承其他 interface
  • type:用交叉类型 & 组合,或条件类型表达

implements

  • 两者都可以被 class implements,行为基本一致

面试回答主线

工程习惯怎么选

  • 对外暴露、稳定的对象契约优先 interface(如组件 props、API 响应结构)
  • 联合类型、映射类型、条件类型等复杂表达用 type
  • 团队统一风格比”选哪个”更重要

如果只能选一个

现代 TS 项目里 type 的表达能力更强,很多团队倾向统一用 type。但如果项目涉及第三方库的类型扩展,interface 的声明合并仍然有独特价值。

和亿格云项目的关联

在我项目里,contracts 层对外暴露的 DTO 和 API 协议多用 interface,因为结构稳定、可能被多方扩展;而内部的类型组合、联合状态、工具类型推导多用 type

常见误区

说”interface 只能用于对象”

interface 也可以描述函数签名、索引签名、构造签名,不只是普通对象。

说”type 完全不能继承”

type 可以通过交叉类型 & 实现组合效果,只是语法不同。

最短记忆方式

  • interface = 对象契约 + 声明合并 + extends 继承
  • type = 类型别名 + 联合/交叉 + 条件/映射
  • 习惯 = 对外 interface,对内 type 组合

一句话回答

TS 工具类型本质上是基于泛型、条件类型和映射类型的类型转换器。常用工具包括 PartialPickOmitRecordReturnTypeParametersAwaitedReadonly,它们帮助我们在类型层面做裁剪、映射和推导。

常用工具分类

裁剪类

  • Partial<T>:把所有属性变可选,做局部更新参数
  • Required<T>:把所有属性变必选
  • Pick<T, K>:从 T 中挑选指定键 K
  • Omit<T, K>:从 T 中排除指定键 K
interface User { id: number; name: string; email: string; }

type UpdateUserDto = Partial<Pick<User, 'name' | 'email'>>;
// { name?: string; email?: string }

映射类

  • Record<K, V>:从键集合映射到值类型
  • Readonly<T>:把所有属性变只读
type Role = 'admin' | 'editor' | 'viewer';
type RoleLabels = Record<Role, string>;
// { admin: string; editor: string; viewer: string }

推导类

  • ReturnType<T>:从函数类型推导返回值
  • Parameters<T>:从函数类型推导参数元组
  • Awaited<T>:展开 Promise 嵌套
type ApiResult = ReturnType<typeof fetchUser>;
type FirstParam = Parameters<typeof fetchUser>[0];
type Unwrapped = Awaited<Promise<Promise<string>>>; // string

面试回答主线

在项目里怎么用

  • 接口层裁剪:后端返回的完整 DTO 和前端表单需要的字段往往不同,用 Pick/Omit 做边界收敛
  • 配置映射:枚举到标签/颜色的映射用 Record,比随便写对象更安全
  • 通用封装:写通用 hooks 或 service 时,用 ReturnType/Parameters 从已有函数推导类型,减少重复定义

如果让手写 Partial 或 Pick

// Partial 实现
type MyPartial<T> = {
  [K in keyof T]?: T[K];
};

// Pick 实现
type MyPick<T, K extends keyof T> = {
  [P in K]: T[P];
};

关键点:keyof 取键、in 遍历、映射类型语法、可选修饰符 ?

和工程的关系

工具类型用得好,可以减少类型不一致和重复定义。但也不要过度使用,导致类型难以读懂。

最短记忆方式

  • 裁剪 = Partial / Pick / Omit
  • 映射 = Record / Readonly
  • 推导 = ReturnType / Parameters / Awaited
  • 核心 = keyof + in + 映射类型语法
创建于 2025/1/1 更新于 2026/5/27