JavaScript 函数总览
JS 函数的三层身份(可调用代码块、对象、作用域边界)、定义方式、参数系统、this、闭包、高阶函数、特殊函数。
[!info] related notes
- 所属 MOC: ecmascript-moc
- 原子概念: this-keyword, call-apply-bind, arguments-object, ecmascript-closures, currying, function-length, 箭头函数, rest 参数和 spread 运算符, 默认参数
- 相关概念: ecmascript-execution-context, ecmascript-prototypes, Promise, array-prototype-map, higher-order-function-callback-mismatch
- ES6 入口: ES6 新特性 MOC
JavaScript 函数总览
函数的三层身份
JS 函数不是”特殊语法糖”,它同时是:
- 可执行的代码块 — 能被调用
- 一种对象 — 能赋值、传参、返回、挂属性
- 一种作用域边界 — 形成局部变量、闭包、词法环境
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 |
箭头函数(const) | 同 let / 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;严格:undefined | fn() |
方法调用 obj.fn() | 调用它的对象 | obj.fn() |
构造调用 new Fn() | 新创建的对象 | new Person() |
| 显式绑定 | 指定的 thisArg | fn.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) |
prototype | 有 | undefined |
| 写法 | 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
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
七、闭包
函数能够记住并访问它定义时所在词法作用域中的变量,即使这个函数在外部执行。
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
常见高阶函数:map、filter、reduce、sort、forEach。
详见 array-prototype-map、higher-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 返回新函数 |
| 高阶函数回调参数 | 参数错位陷阱 |
学习顺序建议
- 函数声明 / 表达式 / 箭头函数
- 作用域 / 词法环境
- 闭包
- this
- call / apply / bind
- new / prototype / 原型链
- Promise / async(本质也离不开函数)
面试要点
来自 ts-utility-types-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 工具类型本质上是基于泛型、条件类型和映射类型的类型转换器。常用工具包括 Partial、Pick、Omit、Record、ReturnType、Parameters、Awaited、Readonly,它们帮助我们在类型层面做裁剪、映射和推导。
常用工具分类
裁剪类
Partial<T>:把所有属性变可选,做局部更新参数Required<T>:把所有属性变必选Pick<T, K>:从 T 中挑选指定键 KOmit<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 + 映射类型语法