功能说明
该示例实现 JSON Pointer 的三个常用操作:按路径读取、在已有父级下写入、删除并返回被删值。指针支持空串表示整个文档,斜杠分隔层级,波浪号转义为 ~0、斜杠转义为 ~1。数组既接受规范十进制下标,也支持 set 时用减号追加。适用于配置覆盖、JSON Patch 基础设施与按路径更新嵌套数据。
关键代码
parseJsonPointer 先校验前导斜杠,再对每个 token 分别替换 ~1 与 ~0,顺序不能颠倒,否则 ~01 会被错误还原。get 逐层下探,遇到非对象、越界数组或非自有属性立即返回 undefined,不会顺着原型链取值。set 与 remove 在每一层调用 assertSegment,拒绝 proto、prototype 与 constructor,避免通过路径写入污染 Object.prototype。数组下标禁止前导零,set 允许等于长度以实现追加。
使用注意
set 不会自动创建中间对象,父级缺失会抛 RangeError,这能防止拼写错误悄悄生成新分支;需要自动建链时应在上层显式创建。删除数组元素使用 splice,后续下标会前移,批量删除应从后往前处理。读取不会触发原型污染防护,但只访问自有属性,因此 constructor 等继承字段不会被读到。该实现不支持 URI fragment 形式的前导井号。
验证方式
测试转义路径 /a1b 与 /m0n 能命中对应键;对数组执行读取、覆盖与减号追加;删除数组元素后断言长度减一;向 set 传入 proto 路径应抛 TypeError;读取不存在路径返回 undefined;空指针读取返回根文档,而对空指针执行 set 或 remove 应抛错。
代码示例
const BLOCKED = new Set(["__proto__", "prototype", "constructor"]);
export function parseJsonPointer(pointer) {
if (typeof pointer !== "string") throw new TypeError("pointer must be a string");
if (pointer === "") return [];
if (!pointer.startsWith("/")) throw new SyntaxError("pointer must start with /");
return pointer
.slice(1)
.split("/")
.map((token) => token.replace(/~1/g, "/").replace(/~0/g, "~"));
}
function assertSegment(token) {
if (BLOCKED.has(token)) throw new TypeError("unsafe pointer segment: " + token);
}
function arrayIndex(token, allowAppend, length) {
if (token === "-") {
if (!allowAppend) throw new RangeError("append is only allowed for set");
return length;
}
if (!/^(0|[1-9]\d*)$/.test(token)) throw new RangeError("invalid array index: " + token);
const index = Number(token);
const max = allowAppend ? length : length - 1;
if (index > max) throw new RangeError("array index out of range: " + token);
return index;
}
export function getJsonPointer(document, pointer) {
let current = document;
for (const token of parseJsonPointer(pointer)) {
if (current === null || typeof current !== "object") return undefined;
if (Array.isArray(current)) {
if (!/^(0|[1-9]\d*)$/.test(token)) return undefined;
const index = Number(token);
if (index >= current.length) return undefined;
current = current[index];
} else {
if (!Object.prototype.hasOwnProperty.call(current, token)) return undefined;
current = current[token];
}
}
return current;
}
export function setJsonPointer(document, pointer, value) {
const tokens = parseJsonPointer(pointer);
if (tokens.length === 0) throw new TypeError("cannot set the document root");
let current = document;
for (let index = 0; index < tokens.length - 1; index += 1) {
const token = tokens[index];
assertSegment(token);
if (current === null || typeof current !== "object") {
throw new TypeError("parent path does not exist");
}
if (Array.isArray(current)) {
current = current[arrayIndex(token, false, current.length)];
} else {
if (!Object.prototype.hasOwnProperty.call(current, token)) {
throw new RangeError("missing path segment: " + token);
}
current = current[token];
}
}
const last = tokens[tokens.length - 1];
assertSegment(last);
if (current === null || typeof current !== "object") {
throw new TypeError("parent path does not exist");
}
if (Array.isArray(current)) {
const index = arrayIndex(last, true, current.length);
if (index === current.length) current.push(value);
else current[index] = value;
} else {
current[last] = value;
}
return document;
}
export function removeJsonPointer(document, pointer) {
const tokens = parseJsonPointer(pointer);
if (tokens.length === 0) throw new TypeError("cannot remove the document root");
let current = document;
for (let index = 0; index < tokens.length - 1; index += 1) {
const token = tokens[index];
assertSegment(token);
if (current === null || typeof current !== "object") return undefined;
if (Array.isArray(current)) {
const position = arrayIndex(token, false, current.length);
current = current[position];
} else {
if (!Object.prototype.hasOwnProperty.call(current, token)) return undefined;
current = current[token];
}
}
const last = tokens[tokens.length - 1];
assertSegment(last);
if (current === null || typeof current !== "object") return undefined;
if (Array.isArray(current)) {
const position = arrayIndex(last, false, current.length);
return current.splice(position, 1)[0];
}
if (!Object.prototype.hasOwnProperty.call(current, last)) return undefined;
const removed = current[last];
delete current[last];
return removed;
}