异步编程三剑客:回调、闭包与Promise组合实战
很好,这个问题其实考察的是 异步编程模型的过渡与组合应用:
从 回调函数 → 闭包保存状态 → Promise/async,它们往往不是孤立使用,而是结合起来解决实际业务场景。
📌 回调函数、闭包和 Promise 常见组合应用
1. 回调 + 闭包
应用场景:保存上下文(状态),避免参数层层传递
闭包能“记住”调用时的环境,把参数存储起来,延迟到回调中使用。
function createTask(taskName) {return function(callback) {setTimeout(() => {callback(null, `任务 ${taskName} 完成`);}, 1000);};
}const taskA = createTask("A");
taskA((err, result) => {console.log(result); // 任务 A 完成
});
➡️ 闭包保存了 taskName
,回调只关注结果。
2. 回调 + Promise(Promise 化)
应用场景:把传统回调 API 转换为 Promise,方便链式调用或 async/await
常用的就是 util.promisify
或自己封装。
const fs = require("fs");function readFilePromise(path) {return new Promise((resolve, reject) => {fs.readFile(path, "utf8", (err, data) => {if (err) return reject(err);resolve(data);});});
}readFilePromise("./test.txt").then(data => console.log("读取成功:", data)).catch(err => console.error("读取失败:", err));
➡️ 这里 回调风格 API → Promise,就是组合应用。
3. 闭包 + Promise
应用场景:保存执行环境,同时返回异步结果
比如我们想“记住 URL”,但请求时再触发。
function createFetcher(url) {return function() {return new Promise((resolve) => {setTimeout(() => {resolve(`已获取数据: ${url}`);}, 1000);});};
}const fetchUser = createFetcher("https://api/user/1");
fetchUser().then(console.log); // 已获取数据: https://api/user/1
➡️ 闭包保存 url
,Promise 负责异步返回。
4. 回调 + 闭包 + Promise
应用场景:复杂业务逻辑封装,比如“延迟队列任务执行 + Promise 封装”
下面演示一个队列:任务函数内部用闭包保存状态,对外暴露 Promise 接口,同时兼容回调。
function createAsyncTask(taskName) {return function(callback) {return new Promise((resolve, reject) => {setTimeout(() => {const result = `${taskName} 完成`;if (callback) callback(null, result); // 回调resolve(result); // Promise}, 1000);});};
}// 使用回调
const task1 = createAsyncTask("下载");
task1((err, result) => console.log("回调:", result));// 使用 Promise
task1().then(result => console.log("Promise:", result));
➡️ 一个函数同时支持 回调 & Promise,闭包又能保存状态。
✅ 总结
- 回调 + 闭包:保存状态,延迟执行时还能访问上下文。
- 回调 + Promise:对旧 API 进行 Promise 化,兼容异步链式调用。
- 闭包 + Promise:保存参数/环境,返回一个延迟执行的异步操作。
- 三者结合:既能兼容传统回调,又能支持 Promise/async,常用于工具库封装。
要不要我给你写一个 支持回调 & Promise & async/await 三种调用方式的通用封装函数?这样可以完整演示三者组合的最佳实践。
判断字符串是否为合法 JSON(不依赖 JSON.parse
) — 原理 + 代码
下面提供一个 逐字符解析(recursive-descent) 的实现,并在开头解释关键判断点和处理细节。目标不是构建整个对象,而是严格按 JSON 语法检查每个标记:花括号 {}
、中括号 []
、字符串引号 "
、冒号 :
、逗号 ,
、转义、数字格式、布尔/空字面量、以及空白允许的位置等。
该实现会返回是否合法,并在不合法时给出一个尽可能精确的位置和信息,方便定位问题。
为什么要自己实现(关键规则回顾)
- 对象的 key 必须是双引号字符串(
"name"
),单引号不允许。 - 字符串必须用双引号
"
包围,字符串内部允许转义:\" \\ \/ \b \f \n \r \t \uXXXX
。未转义的控制字符(U+0000 ~ U+001F)不允许出现在字符串中。 - 数字的语法严格:可选负号,整数部分要么是
0
要么以非零数字开头并跟随数字;小数部分(若有)必须有.
后至少一位数字;指数部分(若有)为e
/E
,可选+
/-
,后面至少一位数字。 - 数组的元素用逗号分隔,不能有尾随逗号(
[1,2,]
不合法)。 - 对象的键值对用逗号分隔,
key:value
之间必须有冒号:
,不能有尾随逗号。 - true / false / null 是三个固定字面量(不能大小写改变)。
- 任意位置可有空白字符(空格、制表、换行、回车)——它们在语法上无意义,应被跳过。
算法思路(摘要)
- 使用索引
i
从头到尾读取字符串。 - 提供
parseValue()
作为入口,根据当前字符分派到parseObject
/parseArray
/parseString
/parseNumber
/parseLiteral
。 parseObject
/parseArray
负责检查逗号、尾逗号、冒号、以及键类型(对象键必须是字符串)。parseString
会严格验证转义序列(包括\uXXXX
)并拒绝未闭合或包含未经转义的控制字符的字符串。parseNumber
按照 JSON 数字的正规式逐步校验(防止01
、1.
、1e
等非法形式)。- 解析结束后跳过尾部空白,索引需恰好到字符串末尾(否则有额外字符,非法)。
JS 实现代码
/*** validateJSON(text)* 返回 { valid: true } 或 { valid: false, error: { message, pos } }** 严格按照 JSON 语法验证,不调用 JSON.parse。*/
function validateJSON(text) {const s = String(text);const len = s.length;let i = 0;function error(msg) {return { valid: false, error: { message: msg, pos: i } };}function isWhitespace(ch) {return ch === ' ' || ch === '\t' || ch === '\n' || ch === '\r';}function skipWhitespace() {while (i < len && isWhitespace(s[i])) i++;}function isDigit(ch) {return ch >= '0' && ch <= '9';}function isDigit1to9(ch) {return ch >= '1' && ch <= '9';}function isHex(ch) {return /[0-9a-fA-F]/.test(ch);}// parseValue: dispatchfunction parseValue() {skipWhitespace();if (i >= len) return { ok: false, msg: 'Unexpected end while expecting a value' };const ch = s[i];if (ch === '{') return parseObject();if (ch === '[') return parseArray();if (ch === '"') {const ok = parseString();if (!ok.ok) return ok;return { ok: true };}if (ch === '-' || isDigit(ch)) {const ok = parseNumber();if (!ok.ok) return ok;return { ok: true };}// literals true/false/nullif (s.startsWith('true', i)) { i += 4; return { ok: true }; }if (s.startsWith('false', i)) { i += 5; return { ok: true }; }if (s.startsWith('null', i)) { i += 4; return { ok: true }; }return { ok: false, msg: `Unexpected token '${ch}' while expecting a value` };}// parseString: assumes s[i] === '"', moves i to char after closing quotefunction parseString() {if (s[i] !== '"') return { ok: false, msg: 'String must start with "' };i++; // consume opening "while (i < len) {const ch = s[i];if (ch === '"') { i++; return { ok: true }; } // closing quoteif (ch === '\\') {i++;if (i >= len) return { ok: false, msg: 'Unterminated escape sequence in string' };const esc = s[i];if (esc === 'u') {// need 4 hex digitsif (i + 4 >= len) return { ok: false, msg: 'Incomplete \\uXXXX escape (need 4 hex digits)' };for (let k = i + 1; k <= i + 4; k++) {if (!isHex(s[k])) return { ok: false, msg: 'Invalid hex digit in \\u escape' };}i += 5; // skip 'u' + 4 hex digits -> now i points to char after the 4 hex digitscontinue;} else if ('"\\/bfnrt'.includes(esc)) {i++; // valid simple escape, move past itcontinue;} else {return { ok: false, msg: `Invalid string escape '\\${esc}'` };}} else {// control chars (U+0000 through U+001F) are not allowed unescapedif (s.charCodeAt(i) <= 0x1F) return { ok: false, msg: 'Unescaped control character in string' };i++;}}return { ok: false, msg: 'Unterminated string (missing closing ")' };}// parseNumber: strict JSON number rulesfunction parseNumber() {const start = i;if (s[i] === '-') i++;if (i >= len) return { ok: false, msg: 'Invalid number - reached end prematurely' };if (s[i] === '0') {i++;// a leading zero must not be followed by a digitif (i < len && isDigit(s[i])) return { ok: false, msg: 'Leading zeros are not allowed in numbers' };} else if (isDigit1to9(s[i])) {while (i < len && isDigit(s[i])) i++;} else {return { ok: false, msg: 'Invalid number: expected digit' };}// fractionif (i < len && s[i] === '.') {i++;if (i >= len || !isDigit(s[i])) return { ok: false, msg: 'Fraction part requires at least one digit after dot' };while (i < len && isDigit(s[i])) i++;}// exponentif (i < len && (s[i] === 'e' || s[i] === 'E')) {i++;if (i < len && (s[i] === '+' || s[i] === '-')) i++;if (i >= len || !isDigit(s[i])) return { ok: false, msg: 'Exponent requires at least one digit' };while (i < len && isDigit(s[i])) i++;}// optionally validate numeric range by trying Number(), but not strictly necessary for syntax checkconst numStr = s.slice(start, i);if (!isFinite(Number(numStr))) return { ok: false, msg: 'Numeric value out of range' };return { ok: true };}// parseArray: expects s[i] === '['function parseArray() {if (s[i] !== '[') return { ok: false, msg: 'Array must start with [' };i++; // consume '['skipWhitespace();if (i < len && s[i] === ']') { i++; return { ok: true }; } // empty arraywhile (i < len) {const v = parseValue();if (!v.ok) return v;skipWhitespace();if (i >= len) return { ok: false, msg: 'Unterminated array (missing ])' };if (s[i] === ']') { i++; return { ok: true }; }if (s[i] === ',') {i++; // consume comma and expect another valueskipWhitespace();// disallow trailing comma: if next is ']' -> error (we'll catch when loop repeats)if (i < len && s[i] === ']') return { ok: false, msg: 'Trailing comma in array is not allowed' };continue;}return { ok: false, msg: `Expected ',' or ']' in array but found '${s[i]}'` };}return { ok: false, msg: 'Unterminated array (missing ])' };}// parseObject: expects s[i] === '{'function parseObject() {if (s[i] !== '{') return { ok: false, msg: 'Object must start with {' };i++; // consume '{'skipWhitespace();if (i < len && s[i] === '}') { i++; return { ok: true }; } // empty objectwhile (i < len) {skipWhitespace();if (i >= len) return { ok: false, msg: 'Unterminated object (missing })' };// key must be a stringif (s[i] !== '"') return { ok: false, msg: `Object keys must be strings enclosed in double quotes` };const keyOk = parseString();if (!keyOk.ok) return keyOk;skipWhitespace();if (i >= len || s[i] !== ':') return { ok: false, msg: `Missing ':' after object key` };i++; // consume ':'// parse valueconst valOk = parseValue();if (!valOk.ok) return valOk;skipWhitespace();if (i >= len) return { ok: false, msg: 'Unterminated object (missing })' };if (s[i] === '}') { i++; return { ok: true }; }if (s[i] === ',') {i++; // consume comma and continue to next key:valueskipWhitespace();// disallow trailing comma before closing braceif (i < len && s[i] === '}') return { ok: false, msg: 'Trailing comma in object is not allowed' };continue;}return { ok: false, msg: `Expected ',' or '}' in object but found '${s[i]}'` };}return { ok: false, msg: 'Unterminated object (missing })' };}// mainskipWhitespace();if (i >= len) return error('Empty input (no JSON value)');const root = parseValue();if (!root.ok) return error(root.msg);skipWhitespace();if (i !== len) return error('Unexpected extra characters after valid JSON value');return { valid: true };
}
使用示例(及预期结果)
const tests = ['{"a":1, "b":[true,false,null, "str", {"x": -12.34e+10}]}', // valid' [ 1, 2, 3 ] ', // valid'"a string with \\u0041"', // valid'123', // valid (JSON allows a top-level number)'null', // valid"{'a':1}", // invalid: single quotes'{"a":1,}', // invalid: trailing comma'["a",]', // invalid: trailing comma'{"a": "b}', // invalid: unterminated string'01', // invalid: leading zero'1e', // invalid: incomplete exponent'{ "a": true false }', // invalid: missing comma
];for (const t of tests) {const r = validateJSON(t);console.log(JSON.stringify({ input: t, result: r }));
}
预期(示例):
- 前 5 个返回
{ valid: true }
。 - 后面几项会返回
{ valid: false, error: { message: '...', pos: <index> } }
,message
会指明是单引号、尾随逗号、未闭合字符串、前导零、缺失指数数字、缺少逗号等具体原因,pos
给出发生错误时的字符位置,便于调试。
扩展建议
- 当前实现只做语法验证。如果你还想要 同时把字符串解析成 JS 对象,可以把
parseObject
/parseArray
/parseString
返回实际构造的值(但会消耗更多内存)。 - 可改进的点:返回更细粒度的错误信息(期望项、发现项、行列号),例如把
pos
转为line:col
,对大文件可加上最大递归深度限制以防栈溢出。 - 若你仅需在 Node/浏览器里使用,
JSON.parse
已足够且更快速。但当你需要给用户更好的报错定位或在受限环境验证输入时,上面的实现更有用。
如果你需要,我可以:
- 把上面的验证器改成 同时返回解析好的 JS 值(即实现一个完整的 JSON 解析器),或者
- 增加行列信息(line/column)以便在编辑器中高亮错误位置,或者
- 把它改写为 生成更友好的错误消息(例如建议修复方法)。要哪个我直接给出代码即可。
加粗样式