Async/Await
在function前面加上Async关键字,会让这个函数的返回结果变成一个Promise对象。如代码所示。
await关键字不能像async一样可以单独存在,只能放在async函数中,如test3所示。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
| async function test() { return "Hello World"; }
async function test2() { return Promise.resolve("Hello World"); }
async function test3() { let result = await test2(); console.log(result); } let result = test(); let result2 = test2(); test3();
console.log(result); console.log(result2);
result2.then((result) => { console.log(result); });
|
运行结果:
1 2 3 4 5
| (base) leonlee@Mac ES6 % node async-await-demo.mjs Promise { 'Hello World' } Promise { <pending> } Hello World Hello World
|
在Promise函数内部发生了错误该怎么捕获?如下代码使用 then 会让 catch 的代码显得非常臃肿冗余。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28
| const getJSON = () => { const jsonString = "{invalid JSON data}";
return new Promise((resolve, reject) => { setTimeout(() => { reject("网络请求发生错误"); }, 2000); }); }
const makeRequest = async () => { try { getJSON() .then((data) => { console.log(data); }) .catch((error) => { console.log(error); console.log("第一个catch"); }); } catch (error) { console.log(error); console.log("第二个catch"); } }
makeRequest();
|
运行结果:
1 2 3
| (base) leonlee@Mac ES6 % node async-await-demo.mjs 网络请求发生错误 第一个catch
|
这个时候如果用async await,代码就清晰了很多。而且还可以捕获到Promise内部的错误。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
| const getJSON = () => { const jsonString = "{invalid JSON data}";
return new Promise((resolve, reject) => { setTimeout(() => { reject("网络请求发生错误"); }, 2000); }); }
const makeRequest = async () => { try { let data = await getJSON(); console.log(data); } catch (error) { console.log(error); console.log("第二个catch"); } }
makeRequest();
|
1 2 3
| (base) leonlee@Mac ES6 % node async-await-demo.mjs 网络请求发生错误 第二个catch
|
面试官如果问:“async/await 相比 Promise 的优势在哪里?”你该做何回答🤔。
async/await异步代码在形式上同步化,通过生成器(Generator)和自动执行器实现了协程(Coroutine)的效果。
这里出现了两个新的术语生成器(Generator)和自动执行器及协程(Coroutine)。
Generator和function*
生成器(Generator)和迭代器是async/await的底层实现原理。在 ES6 早期,还没有 async/await 时,大牛们是用 Generator(生成器)来模拟它的。
生成器 (function*): 它是一个可以“中途停止”的函数。看到 yield 关键字,函数就会交出执行权,停在那。
迭代器 (Iterators): 一个专门负责调用生成器 next() 方法的函数。它发现 yield 后面是一个 Promise,就等这个 Promise 成功后,自动把结果塞回给生成器,让它继续跑。
底层逻辑: async 函数其实就是一个被包装过的 Generator,而浏览器引擎(如 V8)在后台充当了那个迭代器。
Coroutine
协程是一个深层的概念。一个程序可以有多个执行流,它们可以主动让出控制权,稍后再恢复。
- 当代码运行到 await 时,该函数会暂停。
- 它把 JS 主线程的控制权“交还”给事件循环(Event Loop)。
- 主线程可以去处理点击事件、渲染页面。
- 等异步任务(如网络请求)回来了,事件循环再把之前的函数推回栈中,从刚才暂停的地方恢复执行。
这种“暂停 -> 恢复”的能力,就是协程的核心特征。
来段代码就懂了
通过这个实验,你会发现 async/await 其实就是手动控制 Generator 的自动化版本。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29
| function* myGenerator() { console.log("开始请求..."); const res1 = yield new Promise(r => setTimeout(() => r("数据A"), 1000)); console.log("收到:", res1); const res2 = yield new Promise(r => setTimeout(() => r("数据B"), 1000)); console.log("收到:", res2); }
function run(genFn) { const g = genFn();
function next(data) { const result = g.next(data); if (result.done) return; result.value.then(val => { next(val); }); }
next(); }
run(myGenerator);
|
运行结果:
1 2 3 4
| (base) leonlee@Mac ES6 % node async-await-demo.js 开始请求... 收到: 数据A 收到: 数据B
|
这时面试官又问:“既然 await 会暂停函数,那它会阻塞主线程吗?”
答:“不会。await 只会暂停当前函数(协程)的执行,它会立即释放主线程的控制权,让主线程去处理其他微任务或宏任务。这正是 JS 处理高并发异步任务的核心机理。”
切记:
永远不要在循环里直接 await。如果你需要同时请求 10 个接口,用 Promise.all
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28
| const fetchData = (id) => new Promise(res => setTimeout(() => res(`Data ${id}`), 1000));
async function series() { console.time('series'); const d1 = await fetchData(1); const d2 = await fetchData(2); console.log(d1, d2); console.timeEnd('series'); }
async function parallel() { console.time('parallel'); const p1 = fetchData(1); const p2 = fetchData(2); const results = await Promise.all([p1, p2]); console.log(results); console.timeEnd('parallel'); }
series().then(() => { console.log('--------------------------------'); parallel(); }).catch(err => { console.error(err); });
|
代码结果:
1 2 3 4 5 6
| (base) leonlee@Mac ES6 % node async-await-demo.js Data 1 Data 2 series: 2.020s -------------------------------- [ 'Data 1', 'Data 2' ] parallel: 1.005s
|
详谈Promise
面试官问:“如果你有 3 个接口,其中一个挂了,你还想要剩下两个的结果,怎么办?”
| API |
特点 |
适用场景 |
| Promise.all |
全部成功才成功,一个失败即整体失败 |
强依赖的一组请求 |
| Promise.allSettled |
无论成败,返回所有结果的数组 |
互不干扰的独立任务 |
| Promise.any |
只要有一个成功就返回,全部失败才报错 |
取最快、最可靠的源 |
| Promise.withResolvers |
(2024新特性) 将 resolve 暴露到外部 |
延迟构建、流式处理 |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29
| let promise1 = new Promise((resolve, reject) => { setTimeout(() => { const user = { id: 1, name: "Tom1" }; reject(user); }, 1000); });
let promise2 = new Promise((resolve, reject) => { setTimeout(() => { const user = { id: 2, name: "Tom2" }; resolve(user); }, 2000); });
let promise3 = new Promise((resolve, reject) => { setTimeout(() => { const user = { id: 3, name: "Tom3" }; reject(user); }, 3000); });
Promise.race([promise1, promise2, promise3]).then((results) => { console.log(results); }).catch((error) => { console.log("error caught"); console.log(error); }).finally(() => { console.log("finally"); });
|
for await…of
普通的 for…of 循环是处理内存中已经存在的数据(比如一个普通的数组)。而 for await…of 是处理随时间流逝才逐个产生的数据。
想象你正在刷短视频:
- 同步数组: 服务器一次性把 100 个视频全传给你。你用 for…of 循环播放。
- 异步流: 你看完第 1 个,服务器才传第 2 个。你不知道第 3 个什么时候到。这时候你需要 for await…of。
底层原理:异步迭代器(Async Iterator)
普通的数组实现了 Symbol.iterator 接口,而异步迭代源实现了 Symbol.asyncIterator 接口。 它的 next() 方法返回的不是 { value, done },而是一个 Promise,这个 Promise 最终会 resolve 成 { value, done }。
这段代码你会发现每一轮循环都会自动暂停并等待Promise完成。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
|
async function* asyncRandomGenerator() { for (let i = 0; i < 5; i++) { await new Promise(resolve => setTimeout(resolve, 1000)); const num = Math.floor(Math.random() * 5) + 1; yield num; } }
async function test() { console.log("--- 开始获取 ---"); for await (const num of asyncRandomGenerator()) { console.log(`[${new Date().toLocaleTimeString()}] 拿到:`, num); } }
test();
|
那么这个时候面试官问你:“for await…of和Promise.all有什么区别?”
答:
- Promise.all: 它是“并发”的。如果你有 5 个 Promise,它会同时启动它们,等最慢的那个回来后,一次性给你所有结果。
- for await…of: 它是“串行”的。它拿完第 1 个,才去拿第 2 个。它保证了异步任务的顺序执行。
面试官接着问:“如果我有一个很大的文件流,或者需要按顺序处理一批 API 请求,我该用哪个?”
答:“应该用 for await…of。因为它不会像 Promise.all 那样瞬间挤爆内存或触发服务器并发限制,它提供了一种背压(Backpressure)机制,处理完一个再处理下一个。”
例如如下代码:
1 2 3 4 5 6 7 8 9 10
| import fs from 'fs';
async function readFileLineByLine() { const readable = fs.createReadStream('huge_log_file.txt', { encoding: 'utf8' }); for await (const chunk of readable) { console.log('读取到了一块数据:', chunk); } }
|
Top-level Await (ES2022)
在 ES2022 之前,await 只能在 async 函数内部使用。如果你想在模块顶层获取异步数据,你不得不写这种丑陋的 IIFE(立即调用函数表达式):
1 2 3 4 5
| (async () => { const data = await fetchConfig(); export const config = data; })();
|
什么是 Top-level Await?
它允许你在 异步模块(Async Modules) 的顶层直接使用 await。这意味着整个模块的加载会等待这个异步操作完成后,才宣告“加载完毕”。
例如,有文件A db.mjs
1 2 3 4 5
| console.log("1. 开始连接数据库..."); await new Promise(res => setTimeout(res, 2000)); export const connection = { status: "Connected", id: 9527 }; console.log("2. 数据库连接成功!");
|
和文件B app.mjs
1 2 3 4
| import { connection } from './db.mjs';
console.log("3. 正在启动应用,检查连接:", connection.status);
|
运行 node app.mjs,会观察到:
- 控制台先停顿 2 秒。
- 严格按照 1 -> 2 -> 3 的顺序打印。
- app.mjs 会阻塞等待 db.mjs 中的 await 执行完毕。
底层原理:模块执行图 (Module Graph)
在底层,JS 引擎(如 V8)会将模块依赖关系看作一张图。
- 普通模块: 一边链接一边执行。
- 异步模块: 当引擎遇到 await 时,它会暂停当前模块及其父模块的执行,但不会阻塞主线程(事件循环依然可以处理其他任务)。
- 并行加载: 如果 app.mjs 同时引入了 db.mjs 和 config.mjs,这两个异步模块的 await 是并行启动的,类似于 Promise.all 的逻辑。
面试官问:“既然 await 会阻塞模块执行,那如果网络很慢,页面不就白屏了吗?”
答:
- 作用域局限: 它只阻塞依赖该模块的链路,不会阻塞不相关的代码块或主线程的渲染。
- 确定性: 它解决了“竞态条件(Race Condition)”。以前如果异步初始化没完成,导出的变量可能是 undefined;现在它保证了 import 拿到的值一定是初始化完成后的。
- 错误处理: 如果顶层 await 失败,该模块及其依赖它的模块都不会执行,这比带着错误强行运行要安全得多。
| 特性 |
ES2017 async/await |
ES2022 Top-level await |
| 使用位置 |
必须在 async 函数内部 |
可以在 ESM 模块最顶层 |
| 导出能力 |
很难在异步完成后导出变量 |
轻松导出异步获取的变量 |
| 阻塞范围 |
仅阻塞当前函数内部执行流 |
阻塞当前模块及其父模块的加载 |
| 环境要求 |
任何 JS 环境 |
必须是 ES Modules 环境 |
解构与 Rest/Spread
… 运算符在 ES2018 扩展到了对象上,它极大地简化了状态管理(如 React/Redux)。
面试官问:“const b = { …a } 是深拷贝还是浅拷贝?”
答:是浅拷贝。它只克隆了对象的第一层基本类型值,对于引用类型(对象、数组),它只拷贝了内存地址。
1 2 3 4 5 6 7 8
| const original = { a: 1, nested: { b: 2 } }; const copy = { ...original };
copy.a = 100; copy.nested.b = 999;
console.log(original.a); console.log(original.nested.b);
|
可选链 ?.
在 ?. 出现之前,我们要读取 user.profile.address.city 需要写一长串 &&。
?. 是一个短路操作符。如果 . 左侧的值是 null 或 undefined,它会立即停止运算并返回 undefined,而不会抛出 TypeError。
1 2 3 4 5 6 7 8 9 10 11
| const user = null;
console.log(user?.name);
const data = { getAge: null }; console.log(data.getAge?.());
|
空值合并 ??
细节对比:?? vs ||
- || (逻辑或): 只要左侧是 Falsy(false, 0, “”, NaN, null, undefined),就返回右侧。
- ?? (空值合并): 只有左侧是 Nullish(null, undefined),才返回右侧。
1 2 3 4 5 6 7
| const userCount = 0;
const count1 = userCount || 10;
const count2 = userCount ?? 10;
|
逻辑赋值运算符
这是 ??、||、&& 与 = 的结合,旨在减少重复代码。
- a ||= b:等同于 a || (a = b) (如果 a 没值/为假,赋新值)
- a ??= b:等同于 a ?? (a = b) (如果 a 彻底不存在,赋新值)
1 2 3 4 5 6
| const config = { timeout: 0, title: "" };
config.timeout ??= 3000; config.title ||= "Default Title";
console.log(config);
|
Event Loop
在这一节中,我们会搞清楚三个概念:执行栈(Call Stack)、微任务(Microtask)、宏任务(Macrotask)。核心规则:执行优先级。
宏任务: 计时器,ajax,读取文件
微任务:promise.then()
执行顺序:
- 同步程序
- process.nextTick
- 微任务
- 宏任务
- setImmediate
看这段令人头疼的代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25
| console.log('1');
setTimeout(() => { console.log('2'); Promise.resolve().then(() => { console.log('3'); }); }, 0);
new Promise((resolve) => { console.log('4'); resolve(); }).then(() => { console.log('5'); });
async function test() { console.log('6'); await Promise.resolve(); console.log('7'); }
test();
console.log('8');
|
没关系,让我们来一步一步分解!
第一轮:执行同步代码(清空执行栈)
1.console.log(‘1’): 直接打印 1。
2.setTimeout: 这是一个宏任务。JS 引擎把它扔进宏任务队列(挂起),暂不执行。
3.new Promise:
- 注意!Promise 构造函数里的代码是同步执行的。所以打印 4。
- resolve() 被调用,.then() 里的回调被扔进微任务队列。
4.test():
- 调用函数,打印 6。
- 遇到 await Promise.resolve()。await 之后的所有代码(即 console.log(‘7’))会被理解为 .then() 里的内容,扔进微任务队列。此时 test 函数暂停,主线程跳出函数继续往下走。
5.console.log(‘8’): 打印 8。
第一轮结束,目前打印了:1, 4, 6, 8
第二轮:清空微任务
此时执行栈空了,JS 引擎去检查微任务队列。队列里现在有两个:
- Promise.then (来自第4步) -> 打印 5。
- await 后的残余 (来自第6步) -> 打印 7。
微任务全部清空。此时打印了:1, 4, 6, 8, 5, 7
第三轮:执行宏任务
微任务清空后,JS 引擎去看宏任务队列。
- setTimeout 的回调开始执行
- 打印 2。
- 里面又发现一个 Promise.resolve().then()。重点: 这是一个新的微任务,执行完当前这个宏任务(setTimeout 回调)后,引擎会发现它,并立即执行它。
- 打印 3。
全剧终。打印顺序:1, 4, 6, 8, 5, 7, 2, 3
面试题
面试题1
1 2 3 4 5 6 7 8 9 10 11
| console.log("A");
setTimeout(() => { console.log("B"); }, 0);
Promise.resolve().then(() => { console.log("C"); });
console.log("D");
|
面试题2
1 2 3 4 5 6 7 8 9 10 11
| console.log("A");
process.nextTick(() => { console.log("B"); });
Promise.resolve().then(() => { console.log("C"); });
console.log("D");
|
面试题3
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| console.log("A");
process.nextTick(() => { console.log("B"); process.nextTick(() => { console.log("C"); }); });
Promise.resolve().then(() => { console.log("D"); });
console.log("E");
|
面试题4
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| console.log("A");
setTimeout(() => { console.log("B");
Promise.resolve().then(() => { console.log("C"); });
}, 0);
Promise.resolve().then(() => { console.log("D"); });
console.log("E");
|
面试题5
1 2 3 4 5 6 7
| setTimeout(() => { console.log("timeout"); }, 0);
setImmediate(() => { console.log("immediate"); });
|
面试题6
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
| console.log("1");
setTimeout(() => { console.log("2"); process.nextTick(() => { console.log("3"); }); Promise.resolve().then(() => { console.log("4"); }); }, 0);
Promise.resolve().then(() => { console.log("5"); });
process.nextTick(() => { console.log("6"); });
console.log("7");
|
答案
1 2 3 4 5 6
| 1: A D C B 2: A D B C 3: A E B C D 4: A E D B C 5: 不一定,可能 timeout immediate,也可能 immediate timeout 6. 1 7 6 5 2 3 4
|
const对象的属性为什么可以被修改?
因为类型是引用类型,变量person仅仅保存的是对象的地址(指针),意味着const仅保证地址(指针)不发生改变,修改对象的属性不会改变对象的地址(指针),所以是被允许的。也就是说const定义的引用类型只要指针不发生改变,其他的不论怎么变都是可以的。
基本类型存放在栈内存;引用类型放在堆地址,栈中只保存引用类型指向的地址。
1 2 3 4 5 6 7
| const person = { name: "John", age: 20 }
person.name = "Tom"; console.log(person.name);
|
箭头函数
箭头函数的this永远指向上一层作用域的this,且箭头函数的this在定义时就被确定了,所以在使用时无法改变箭头函数的this。
1 2 3 4 5 6 7 8 9 10
| var id = "Global"
const func = () => { console.log(this.id); }
func(); func.call({id: "Obj"}); func.apply({id: "Obj"}); func.bind({id: "Obj"})();
|
那么,利用这个特性,很利于封装回调函数。例如在DOM事件的回调函数封装在一个对象里面。如果把下面代码中的箭头函数改为普通function函数,则会报错,因为this的指向会在调用时变成调用者,在这个例子中也就是document而非我们想要的handler。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| var handler = { id: "123456",
init: function() { document.addEventListener("click", (event) => { this.doSomething(event); }); },
doSomething: function(e) { console.log("Handling", e.type, "for", this.id); } }
handler.init();
|
优点:简化代码。可以把如下代码
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| function insert(value) { return { into: function(arr) { return { after: function(afterValue) { arr.splice(arr.indexOf(afterValue) + 1, 0, value); return arr; } } } } }
console.log(insert(5).into([1, 2, 3, 4]).after(3));
|
简化为:
1 2 3 4 5 6 7 8 9 10
| let insert = (value) => ({ into: (arr) => ({ after: (afterValue) => { arr.splice(arr.indexOf(afterValue) + 1, 0, value); return arr; }, }) })
console.log(insert(5).into([1, 2, 3, 4]).after(3));
|
缺点1:this的作用域问题
1 2 3 4 5 6 7 8 9 10
| const person = { age: 18, add: () => { console.log("this is:", this); this.age++; }, };
person.add(); console.log(person.age);
|
改回function就没有这个问题了。
1 2 3 4 5 6 7 8 9 10
| const person = { age: 18, add: function() { console.log("this is:", this); this.age++; }, };
person.add(); console.log(person.age);
|
缺点2: 动态绑定的时候就不能使用箭头函数
1 2 3 4 5
| var button = document.getElementById("button");
button.addEventListener("click", () => { this.classList.add("on"); });
|
1 2 3 4 5
| var button = document.getElementById("button");
button.addEventListener("click", function() { this.classList.add("on"); });
|
面试题
问输出是什么?
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| function foo() { return () => { return () => { return () => { console.log("id: ", this.id); } } } }
var f = foo.call({id: 1}); var t1 = f.call({id: 2})()(); var t2 = f().call({id: 3})(); var t3 = f()().call({id: 4});
|
核心点是:箭头函数的 this 在定义时就被固定,不会随调用方式改变。
具体过程
- f = foo.call({id: 1})
- foo 里的 this 被设为 {id: 1}
- 所有嵌套的箭头函数都从 foo 的作用域里继承这个 this,即 this 一直是 {id: 1}
- .call({id: 2})、.call({id: 3})、.call({id: 4}) 不起作用
- 箭头函数忽略 .call() 传入的 this
- 它们始终使用定义时的 this(即 {id: 1})
- 三层箭头函数的 this 都一样
- 无论通过哪种方式(
f.call(...)()()、f().call(...)()、f()().call(...))调用
- 打印出的都是同一个 this.id,也就是 1
小结
普通函数:this 由调用方式决定(谁调用、.call / .apply 等)
箭头函数:this 在定义时确定,不能通过 .call / .apply 或 bind 修改
因此,无论怎么调用,这里 console.log(“id: “, this.id) 都会输出 id: 1。
class和构造函数
在ES6之前,没有class时候是这样创建类的:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
| function Person(name, age) { this.name = name; this.age = age; }
Person.staticMethod = function () { console.log("static method"); }
Person.prototype.sayName = function() { console.log(this.name); };
Person.prototype.sayAge = function() { console.log(this.age); };
let person1 = new Person("John", 20); person1.sayName(); person1.sayAge(); Person.staticMethod();
|
有了class之后,可以用class来写
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
| class Person { constructor(name, age) { this.name = name; this.age = age; }
sayName() { console.log(this.name); }
sayAge() { console.log(this.age); }
static staticMethod() { console.log("static method"); } }
let person1 = new Person("John", 20); person1.sayName(); person1.sayAge(); Person.staticMethod();
|
那这俩的区别是什么呢?
ES6中,class的原型方法不可以枚举。
ES5中,构造函数上的原型方法可以枚举。
此话怎么讲?看代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
| function Person(name, age) { this.name = name; this.age = age; }
Person.prototype.sayName = function() { console.log(this.name); };
Person.prototype.sayAge = function() { console.log(this.age); };
Person.staticMethod = function() { console.log("static method"); };
let person1 = new Person("John", 20); for (let prop in person1) { console.log(prop); }
|
运行结果:
而class的话:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
| class Person { constructor(name, age) { this.name = name; this.age = age; }
sayName() { console.log(this.name); }
sayAge() { console.log(this.age); }
static staticMethod() { console.log("static method"); } }
let person1 = new Person("John", 20); for (let prop in person1) { console.log(prop); }
|
运行结果:
内部方法[[Construct]]
[[Construct]]是Javascript引擎的一个内部方法,主要用户创建和初始化对象。我们不能直接访问它,而是JavaScriopt引擎在背后用来处理通过new关键字创建新对象的机制。
不是所有函数都可以作为构造函数,只有具有[[Construct]]的函数才能作为构造函数。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| function isConstructor(func) { try { Reflect.construct(Object, [], func); return true; } catch (error) { return false; } }
function test1() {}
const test2 = () => {};
console.log(isConstructor(test1)); console.log(isConstructor(test2));
|
#手写实现Promise.all()方法
它接受Promise对象的数组作为输入,并返回一个新的Promise对象。只有当数组中的所有Promimse都成功完成时才会执行then里面的回调函数,如果任何一个Promise失败,都会执行catch函数。