JSON.stringify() 在 V8 中是确定性的吗?
Is JSON.stringify() deterministic in V8?
我还没有看到(还?)JSON.stringify
在 Node.JS 中是非确定性的。
不能保证它在规范级别上是确定性的。
但是V8呢?
它的实现是确定性的吗?
是否可以保证它对未来的 V8 版本保持确定性?
编辑:
对于确定性,我的意思是无论 json_str
的值是多少,以下断言都是正确的。 (鉴于该值是一个有效的 JSON 字符串。)
const obj = JSON.parse(json_str);
assert(JSON.stringify(obj)===JSON.stringify(obj)); // always true
编辑 2:
实际上,我也对以下断言为真感兴趣
if( deepEqual(obj1, obj2) ) {
assert(JSON.stringify(obj1)===JSON.stringify(obj2))
}
事实并非如此(查看答案)。
如果 "deterministic" 是指对象属性的枚举顺序: 是 实际指定的,并且 V8 遵循规范。参见 https://tc39.github.io/ecma262/#sec-ordinaryownpropertykeys。
[编辑:这是对您的明确定义的回答,所以是的,JSON.stringify 在这个意义上是确定性的。]
如果 "deterministic" 你的意思是 "always returns the same string for the same input object",那么,不是 :-)
> var o = { toJSON: function() { return Math.random(); } }
> JSON.stringify(o);
< "0.37377773963616434"
> JSON.stringify(o);
< "0.8877065604993732"
Proxy
对象和 JSON.stringify
的 replacer
参数也可用于创建任意行为(即使 JSON.stringify
本身总是做同样的事情)。
如果 "deterministic" 是其他意思,请说明。
用你的话来说,决定论归结为:
- 顺序 => 对象数据会以相同的顺序编组吗?
是的,遍历对象数据总是在同一个'route'中发生。
- 内容 => 对象数据是否会以相同的内容编组?
是的,除非像@jmrk 上面解释的那样通过 toJSON 引入的任意性覆盖。
- 并发 => 是否会在检查之间修改对象数据?
不,V8 脚本运行程序是单线程的,因此不会发生混乱的访问。
- 规范 => 规范中是否有违反确定性的条款?
不,除了上下文替换器/覆盖之外,解析器和字符串化应该每次都产生相同的数据。
- 兼容性 => 所有的 stringify 方法都会产生兼容的数据吗?
不,规范在列出对象字段的顺序上不明确,因此实现可以自由地遍历对象,这意味着数据可能与 'purpose' 和 'spirit' 相同,不可比较字节到字节。
希望对您有所帮助!
澄清 jmrk 的回答;
根据规范,整数键按数字顺序序列化,非整数键按 属性 创建的时间顺序序列化,例如;
var o = {};
o[2] = 2;
o.a = 3;
o.b = 4;
o["1"] = 1;
assert(JSON.stringify(o)==='{"1":1,"2":2,"a":3,"b":4}');
因此以下断言保证为真
if( obj1 === obj2 ) {
assert(JSON.stringify(obj1) === JSON.stringify(obj2));
}
但是两个"deep equal"对象可能被序列化为不同的字符串;
var obj1 = {};
obj1["a"] = true;
obj1["b"] = true;
assert(JSON.stringify(obj1)==='{"a":true,"b":true}');
var obj2 = {};
obj2["b"] = true;
obj2["a"] = true;
assert(JSON.stringify(obj2)==='{"b":true,"a":true}');
规格报价;
- Let keys be a new empty List.
For each own property key P of O that is an integer index, in ascending numeric index order, do
a. Add P as the last element of keys.
For each own property key P of O that is a String but is not an integer index, in ascending chronological order of property creation, do
a. Add P as the last element of keys.
For each own property key P of O that is a Symbol, in ascending chronological order of property creation, do
a. Add P as the last element of keys.
- Return keys.
来自https://tc39.github.io/ecma262/#sec-ordinaryownpropertykeys
万一有人想找一个可以预测 JSON 转储的函数,我写了一个:
const sortObj = (obj) => (
obj === null || typeof obj !== 'object'
? obj
: Array.isArray(obj)
? obj.map(sortObj)
: Object.assign({},
...Object.entries(obj)
.sort(([keyA], [keyB]) => keyA.localeCompare(keyB))
.map(([k, v]) => ({ [k]: sortObj(v) }),
))
);
这是一个组合的确定性 JSON 转储:
const deterministicStrigify = obj => JSON.stringify(deterministic(sortObj))
它适用于上面的示例:
> obj1 = {};
> obj1.b = 5;
> obj1.a = 15;
> obj2 = {};
> obj2.a = 15;
> obj2.b = 5;
> deterministicStrigify(obj1)
'{"a":15,"b":5}'
> deterministicStrigify(obj2)
'{"a":15,"b":5}'
> JSON.stringify(obj1)
'{"b":5,"a":15}'
> JSON.stringify(obj2)
'{"a":15,"b":5}'
@fodma1 的方法可以使用 JSON.stringify
的第二个参数更简单地实现,'replacer' 函数:
const deterministicReplacer = (_, v) =>
typeof v !== 'object' || v === null || Array.isArray(v) ? v :
Object.fromEntries(Object.entries(v).sort(([ka], [kb]) =>
ka < kb ? -1 : ka > kb ? 1 : 0));
然后:
JSON.stringify({b: 1, a: 0, c: {e: [], d: null, f: 1}}, deterministicReplacer, 2);
JSON.stringify({c: {f: 1, e: [], d: null}, b: 1, a: 0}, deterministicReplacer, 2);
两者都给出:
{"a":0,"b":1,"c":{"d":null,"e":[],"f":1}}
出于速度原因,我也切换到简单的字符串比较,假设我们只关心排序顺序的可重复性。
我还没有看到(还?)JSON.stringify
在 Node.JS 中是非确定性的。
不能保证它在规范级别上是确定性的。
但是V8呢? 它的实现是确定性的吗? 是否可以保证它对未来的 V8 版本保持确定性?
编辑:
对于确定性,我的意思是无论 json_str
的值是多少,以下断言都是正确的。 (鉴于该值是一个有效的 JSON 字符串。)
const obj = JSON.parse(json_str);
assert(JSON.stringify(obj)===JSON.stringify(obj)); // always true
编辑 2:
实际上,我也对以下断言为真感兴趣
if( deepEqual(obj1, obj2) ) {
assert(JSON.stringify(obj1)===JSON.stringify(obj2))
}
事实并非如此(查看答案)。
如果 "deterministic" 是指对象属性的枚举顺序: 是 实际指定的,并且 V8 遵循规范。参见 https://tc39.github.io/ecma262/#sec-ordinaryownpropertykeys。 [编辑:这是对您的明确定义的回答,所以是的,JSON.stringify 在这个意义上是确定性的。]
如果 "deterministic" 你的意思是 "always returns the same string for the same input object",那么,不是 :-)
> var o = { toJSON: function() { return Math.random(); } }
> JSON.stringify(o);
< "0.37377773963616434"
> JSON.stringify(o);
< "0.8877065604993732"
Proxy
对象和 JSON.stringify
的 replacer
参数也可用于创建任意行为(即使 JSON.stringify
本身总是做同样的事情)。
如果 "deterministic" 是其他意思,请说明。
用你的话来说,决定论归结为:
- 顺序 => 对象数据会以相同的顺序编组吗?
是的,遍历对象数据总是在同一个'route'中发生。
- 内容 => 对象数据是否会以相同的内容编组?
是的,除非像@jmrk 上面解释的那样通过 toJSON 引入的任意性覆盖。
- 并发 => 是否会在检查之间修改对象数据?
不,V8 脚本运行程序是单线程的,因此不会发生混乱的访问。
- 规范 => 规范中是否有违反确定性的条款?
不,除了上下文替换器/覆盖之外,解析器和字符串化应该每次都产生相同的数据。
- 兼容性 => 所有的 stringify 方法都会产生兼容的数据吗?
不,规范在列出对象字段的顺序上不明确,因此实现可以自由地遍历对象,这意味着数据可能与 'purpose' 和 'spirit' 相同,不可比较字节到字节。
希望对您有所帮助!
澄清 jmrk 的回答;
根据规范,整数键按数字顺序序列化,非整数键按 属性 创建的时间顺序序列化,例如;
var o = {};
o[2] = 2;
o.a = 3;
o.b = 4;
o["1"] = 1;
assert(JSON.stringify(o)==='{"1":1,"2":2,"a":3,"b":4}');
因此以下断言保证为真
if( obj1 === obj2 ) {
assert(JSON.stringify(obj1) === JSON.stringify(obj2));
}
但是两个"deep equal"对象可能被序列化为不同的字符串;
var obj1 = {};
obj1["a"] = true;
obj1["b"] = true;
assert(JSON.stringify(obj1)==='{"a":true,"b":true}');
var obj2 = {};
obj2["b"] = true;
obj2["a"] = true;
assert(JSON.stringify(obj2)==='{"b":true,"a":true}');
规格报价;
- Let keys be a new empty List.
For each own property key P of O that is an integer index, in ascending numeric index order, do
a. Add P as the last element of keys.
For each own property key P of O that is a String but is not an integer index, in ascending chronological order of property creation, do
a. Add P as the last element of keys.
For each own property key P of O that is a Symbol, in ascending chronological order of property creation, do
a. Add P as the last element of keys.
- Return keys.
来自https://tc39.github.io/ecma262/#sec-ordinaryownpropertykeys
万一有人想找一个可以预测 JSON 转储的函数,我写了一个:
const sortObj = (obj) => (
obj === null || typeof obj !== 'object'
? obj
: Array.isArray(obj)
? obj.map(sortObj)
: Object.assign({},
...Object.entries(obj)
.sort(([keyA], [keyB]) => keyA.localeCompare(keyB))
.map(([k, v]) => ({ [k]: sortObj(v) }),
))
);
这是一个组合的确定性 JSON 转储:
const deterministicStrigify = obj => JSON.stringify(deterministic(sortObj))
它适用于上面的示例:
> obj1 = {};
> obj1.b = 5;
> obj1.a = 15;
> obj2 = {};
> obj2.a = 15;
> obj2.b = 5;
> deterministicStrigify(obj1)
'{"a":15,"b":5}'
> deterministicStrigify(obj2)
'{"a":15,"b":5}'
> JSON.stringify(obj1)
'{"b":5,"a":15}'
> JSON.stringify(obj2)
'{"a":15,"b":5}'
@fodma1 的方法可以使用 JSON.stringify
的第二个参数更简单地实现,'replacer' 函数:
const deterministicReplacer = (_, v) =>
typeof v !== 'object' || v === null || Array.isArray(v) ? v :
Object.fromEntries(Object.entries(v).sort(([ka], [kb]) =>
ka < kb ? -1 : ka > kb ? 1 : 0));
然后:
JSON.stringify({b: 1, a: 0, c: {e: [], d: null, f: 1}}, deterministicReplacer, 2);
JSON.stringify({c: {f: 1, e: [], d: null}, b: 1, a: 0}, deterministicReplacer, 2);
两者都给出:
{"a":0,"b":1,"c":{"d":null,"e":[],"f":1}}
出于速度原因,我也切换到简单的字符串比较,假设我们只关心排序顺序的可重复性。