前言
ECMAScript标准是深入学习JavaScript原理最好的资料,没有其二。
通过增加对ECMAScript语言的理解,理解javascript现象后面的逻辑,提升个人编码能力。
欢迎关注和订阅专栏 重学前端-ECMAScript协议上篇
有趣的代码
蓝鸟的旧版本有一段代码非常有意思 util.js
javascript
function toFastProperties(obj) {
/*jshint -W027*/
function f() {}
f.prototype = obj; //
ASSERT("%HasFastProperties", true, obj);
return f;
eval(obj);
}
几个问题
- toFastProperties,函数名很有意思, 快属性? 其本意是想做啥
- return前又用eval又是何意
新版本已经修改了, github.com/petkaantono...。
解释:How does Bluebird's util.toFastProperties function make an object's properties "fast"?
简单说就是为了让访问对象的属性更快。
javascript
function toFastProperties(obj) {
/*jshint -W027*/ // 禁用语法检查
function f() {}
f.prototype = obj; // 设置原型
ASSERT("%HasFastProperties", true, obj);
return f;
eval(obj); // 防止引擎优化
}
设置属性的函数
设置属性的函数,后面会用到。 主要是给对象设置常规属性和排序属性,至于什么是常规属性,排序属性,请继续往下看。
javascript
function setProperties(obj, propertyCount, elementCount) {
//添加常规属性
for (let i = 0; i < propertyCount; i++) {
let ppt = `property${i}`;
obj[ppt] = ppt;
}
// 排序属性
for (let i = 0; i < elementCount; i++) {
obj[i] = `element${i}`;
}
}
function setProperty(obj, key, value) {
obj[key] = value;
}
排序属性 elements 和 普通属性 properties
V8官方的图片,来源 Fast properties in V8
普通属性: (普通字符串)左边的属于是普通属性,遍历输出顺序按照添加顺序。
排序属性: (数字字符串)右边的是排序属性, 遍历输出顺序按照索引大小。

示例:排序属性和普通属性
javascript
function MyObject() { }
const obj = new MyObject();
// 设置15个普通属性
// 15 = 10个内属性 + 5个properties
setProperties(obj, 15, 5);

示例: 遍历输出顺序
javascript
function MyObject() {}
const obj = new MyObject();
obj['a'] = 'a'
obj['c'] = 'c'
obj['b'] = 'b'
obj[2] = '2'
obj[1] = '1'
obj[0] = '0'
console.log(...Object.keys(obj));
// 0 1 2 a c b
隐藏类 HiddenClass
类似于面向对象编程语言中的类, 目的:提高属性访问速度, 在v8中,常说为map。
下面内容翻译自 iddenClasses and DescriptorArrays。
- 隐藏类存储了关于对象的元信息,包括对象上的属性信息和一个指向对象原型的引用。

- 其是优化编译器和内联缓存的关键 :隐藏类对于V8的优化编译器和内联缓存来说至关重要。以下是原因:
- 优化编译器:如果优化编译器可以通过隐藏类确保对象结构的一致性,它可以直接内联属性访问。这意味着属性访问可以转化为直接内存访问操作,从而大幅提高执行效率。
- 内联缓存:内联缓存是一种优化技术,它记录了属性在对象中的位置。如果对象的隐藏类没有变化,内联缓存可以快速定位属性,无需进行属性查找。
隐藏类的(上图的 descriptptors)描述符数组包含了有关命名属性的信息,比如属性名称以及存储值的位置。
在访问对象属性时,V8引擎使用隐藏类来确定属性在内存中的位置。由于隐藏类记录了属性的偏移量,V8可以直接通过偏移量访问属性,而不需要遍历对象的属性列表,这样可以显著提高属性访问的速度。
更多细节可以 参考 极客时间 李兵大佬的 03 | 快属性和慢属性:V8是怎样提升对象属性访问速度的?
查看隐藏类数据。
javascript
let point = {x:100,y:200};
%DebugPrint(point);
node --allow-natives-syntax "hideclass.js"
javascript
node --allow-natives-syntax "hideclass.js"
DebugPrint: 000000D0908C2309: [JS_OBJECT_TYPE]
- map: 0x01e7c71f2259 <Map(HOLEY_ELEMENTS)> [FastProperties]
- prototype: 0x03fe6512a321 <Object map = 000001E7C71C1239>
- elements: 0x03698a741309 <FixedArray[0]> [HOLEY_ELEMENTS]
- properties: 0x03698a741309 <FixedArray[0]>
- All own properties (excluding elements): {
00000285D42D5B29: [String] in OldSpace: #x: 100 (const data field 0), location: in-object
00000285D42D5B41: [String] in OldSpace: #y: 200 (const data field 1), location: in-object
}
000001E7C71F2259: [Map]
- type: JS_OBJECT_TYPE
- instance size: 40
- inobject properties: 2
- elements kind: HOLEY_ELEMENTS
- unused property fields: 0
- enum length: invalid
- stable_map
- back pointer: 0x01e7c71f2211 <Map(HOLEY_ELEMENTS)>
- prototype_validity cell: 0x0322c6dc15e9 <Cell value= 1>
- instance descriptors (own) #2: 0x00d0908c2361 <DescriptorArray[2]>
- prototype: 0x03fe6512a321 <Object map = 000001E7C71C1239>
- constructor: 0x03fe65112791 <JSFunction Object (sfi = 00000285D42DCF31)>
- dependent code: 0x03698a741239 <Other heap object (WEAK_FIXED_ARRAY_TYPE)>
- construction counter: 0
具有相同结构的对象(相同顺序的相同属性)具有相同的 隐藏类
html
<script>
function MyObject() { }
const obj = new MyObject();
setProperty(obj, 'p1', 'v1');
const obj2 = new MyObject();
setProperty(obj2, 'p1', 'v1');
</script>

默认情况下,添加的每个新命名属性都会导致创建一个新的 隐藏类
注意 back_pointer 属性,表示的是由谁变化而来。

添加数组索引属性不会创建新的 隐藏类
html
<script>
function MyObject() {
}
const myObj = new MyObject();
myObj[2] = 2;
myObj[3] = 3;
myObj[4] = '4';
// myObj["gaga"] = "gaga"
</script>
如下看不到 back_pointer

如果你取消注释 // myObj["gaga"] = "gaga"
, 再试试。
内属性 In-object properties
查找普通属性的时候,V8 会先查找出 properties 属性所指向的对象 properties,然后再在 properties 对象中查找 B 属性,这种方式在查找过程中增加了一步操作。
V8将部分常规属性直接存储到对象本身,成为内属性。
当常规属性少于一定数量时(默认是10),V8 就会将这些常规属性直接写进对象中,
这就是 V8 支持所谓的对象内属性,这些属性直接存储在对象本身上。这些是 V8 中最快的属性,可以直接访问。

内属性是有数量限制的,默认是10个

超过11个,多余的普通属性就会放到 properties下面

至于数量问题, V8 10.7.116 js-objects.h 855 行有提到计算公式。

至于各个参数,在 time.geekbang.org/column/arti... 也有详细的罗列。 这个数值嘛,随着v8的版本变更,也可能会变化。

快速与慢速属性 (Fast vs. slow properties)
快速属性
通常我们将存储在线性属性存储中的属性定义为"快速"。快速属性只需通过属性存储中的索引进行访问。
线性结构进行添加或者删除操作的时候,执行效率低。
慢速属性
慢属性的对象内部会有独立的非线性数据结构 (hash表) 作为属性存储容器。所有的属性元信息不再是线性存储的,而是直接保存在属性字典中。
普通属性一定是慢属性吗?
不是的。如下, 20个中的10个在properties中,其依旧是线性的。 判断是不是快属性,v8有暴露方法 %HasFastProperties

快慢属性的转换
快转慢
- 使用delete
- 太多普通属性, 大于25个(node 16.10.0)
- ......
delete
如何证明呢?
设置了15个普通属性,使用 node --allow-natives-syntax x.js
执行, 会发现执行delete之前%HasFastProperties的结果是true
,执行之后是false
javascript
function MyObject() {}
const obj = new MyObject();
function setProperties(obj, propertyCount, elementCount) {
//添加常规属性
for (let i = 0; i < propertyCount; i++) {
let ppt = `property${i}`;
obj[ppt] = ppt;
}
// 排序属性
for (let i = 0; i < elementCount; i++) {
obj[i] = `element${i}`;
}
}
setProperties(obj, 15, 0);
// node --allow-natives-syntax "2. Fast Properties\2.3 Fast_Slow\index.js"
console.log(%HasFastProperties(obj)) //
delete obj.property10;
console.log(%HasFastProperties(obj))
运行结果:
javascript
true
false
delete 一定 快 转 慢 吗?
答案是否定的, 隐藏类,已经提到了,有个back_pointer 属性,可以得知,当前的隐藏类从谁演变而来。
反向而行, 按照添加顺序的逆序删除,是不是就不会改变呢?
示例: 输出的结果都是true
javascript
const utils = require('../util');
function MyObject() {}
const obj = new MyObject();
utils.setProperties(obj, 15, 0);
// node --allow-natives-syntax "2. Fast Properties\2.4 Fast_Slow\1.1.2 fast delete.js"
console.log(%HasFastProperties(obj)) // true
delete obj.property14;
console.log(%HasFastProperties(obj)) // true
delete obj.property13;
console.log(%HasFastProperties(obj)) // true
delete obj.property12;
console.log(%HasFastProperties(obj)) // true
delete obj.property11;
console.log(%HasFastProperties(obj)) // true
一定数量的普通属性 也是慢属性
javascript
const utils = require('../util');
function MyObject() {}
const obj = new MyObject();
const obj2 = new MyObject();
utils.setProperties(obj, 25, 0);
utils.setProperties(obj2, 26, 0);
// node --allow-natives-syntax "2. Fast Properties\2.4 Fast_Slow\1.2 toSlow many properties.js"
console.log('obj:', %HasFastProperties(obj)) // obj: true
console.log('obj2:', %HasFastProperties(obj2)) // obj2: false
慢转快
- 调用私有 %ToFastProperties 。
- 设置对象成为一个函数(或对象)的原型。
%ToFastProperties
javascript
const utils = require('../util');
function MyObject() {}
const obj = new MyObject();
utils.setProperties(obj, 26, 10);
console.log(%HasFastProperties(obj)); // false
%ToFastProperties(obj);
console.log(%HasFastProperties(obj) // true
设置对象成为一个函数的原型
原型通常存储多实例共享的属性,并且很少动态的更改 。 因此,他们设置为快速模式,可以提升访问速度。
- bluebird toFastProperties 文章开头
- to-fast-propertis 库
- MagicFunc
最近的版本,比如我的是 v16.10.0
, 使用刚开始的有趣的代码段 bluebird toFastProperties
,并不能把慢转快。
换成 7.7.0也未能成功

一个很知名的库 to-fast-properties就是利用这个原理把对象转为快属性,这里直接把源码加入项目,因为源码一共就没多少行。
javascript
const toFastproperties = require('./to-fast-properties');
function MyObject() {}
const obj = new MyObject();
function setProperties(obj, propertyCount, elementCount) {
//添加常规属性
for (let i = 0; i < propertyCount; i++) {
let ppt = `property${i}`;
obj[ppt] = ppt;
}
// 排序属性
for (let i = 0; i < elementCount; i++) {
obj[i] = `element${i}`;
}
}
setProperties(obj, 15, 0);
// node --allow-natives-syntax "toFast.js"
console.log(%HasFastProperties(obj))
delete obj.property10;
console.log(%HasFastProperties(obj))
toFastproperties(obj);
console.log(%HasFastProperties(obj))
javascript
true
false
true
MagicFun
奇技淫巧学 V8 之四,迁移对象至快速模式 其有详细介绍,一山更比一山高。 这还涉及到了 多态内联缓存 PICs
javascript
function MagicFunc(obj) {
function FakeConstructor() {
this.x = 0; // StoreIC(x)
}
// 拷贝 <Map> 作为 prototype, 不再与其它对象共享
FakeConstructor.prototype = obj;
// StoreIC(x) state == UNINITIALIZED
new FakeConstructor();
// StoreIC(x) state == PREMONOMORPHIC
new FakeConstructor();
// StoreIC(x) state == MONOMORPHIC
// 隐式调用 MakePrototypesFast, 由 slow 到 fast 迁移
};
V8的操作
V8的7.0本,比如 github.com/v8/v8/blob/...
javascript
void JSObject::OptimizeAsPrototype(Handle<JSObject> object,
bool enable_setup_mode) {
if (object->IsJSGlobalObject()) return;
if (enable_setup_mode && PrototypeBenefitsFromNormalization(object)) {
// First normalize to ensure all JSFunctions are DATA_CONSTANT.
JSObject::NormalizeProperties(object, KEEP_INOBJECT_PROPERTIES, 0,
"NormalizeAsPrototype");
}
if (object->map()->is_prototype_map()) {
if (object->map()->should_be_fast_prototype_map() &&
!object->HasFastProperties()) {
JSObject::MigrateSlowToFast(object, 0, "OptimizeAsPrototype");
}
} else {
Handle<Map> new_map = Map::Copy(object->GetIsolate(),
handle(object->map(), object->GetIsolate()),
"CopyAsPrototype");
JSObject::MigrateToMap(object, new_map);
object->map()->set_is_prototype_map(true);
// Replace the pointer to the exact constructor with the Object function
// from the same context if undetectable from JS. This is to avoid keeping
// memory alive unnecessarily.
Object* maybe_constructor = object->map()->GetConstructor();
if (maybe_constructor->IsJSFunction()) {
JSFunction* constructor = JSFunction::cast(maybe_constructor);
if (!constructor->shared()->IsApiFunction()) {
Context* context = constructor->context()->native_context();
JSFunction* object_function = context->object_function();
object->map()->SetConstructor(object_function);
}
}
}
}
主要看这几行代码,如果应该是should_be_fast_prototype_map() 并且不HasFastProperties(), 就调用JSObject::MigrateSlowToFast。
javascript
if (object->map()->should_be_fast_prototype_map() &&
!object->HasFastProperties()) {
JSObject::MigrateSlowToFast(object, 0, "OptimizeAsPrototype");
}
其他
to-fast-properties 库简介
周下载量千万级的库

javascript
let fastProto = null;
// Creates an object with permanently fast properties in V8. See Toon Verwaest's
// post https://medium.com/@tverwaes/setting-up-prototypes-in-v8-ec9c9491dfe2#5f62
// for more details. Use %HasFastProperties(object) and the Node.js flag
// --allow-natives-syntax to check whether an object has fast properties.
function FastObject(object) {
// A prototype object will have "fast properties" enabled once it is checked
// against the inline property cache of a function, e.g. fastProto.property:
// https://github.com/v8/v8/blob/6.0.122/test/mjsunit/fast-prototype.js#L48-L63
// 无object参数时,
if (fastProto !== null && typeof fastProto.property) {
// 重置 result 为传入的object
const result = fastProto;
// 置空 fastProto和FastObject.prototype
fastProto = FastObject.prototype = null;
return result;
}
// 有object参数,设置fastProto以及FastObject函数的原型
// fastProto一般情况等于 object
fastProto = FastObject.prototype = object == null ? Object.create(null) : object;
// new 不传参数 走上面的分支
return new FastObject();
}
const inlineCacheCutoff = 10;
// Initialize the inline property cache of FastObject.
for (let index = 0; index <= inlineCacheCutoff; index++) {
FastObject();
}
export default function toFastproperties(object) {
return FastObject(object);
}
可能大家注意到了, 初始化执行了10次? 为什么要10次? 基于node 16.10.0 测试6次以上可以,6次都不行。
--allow-natives-syntax
node启动的时候,可以带上这个参数,这其实是 v8 的一个选项,开启后,可以使用一些私有的 API。
你可以使用 node --v8-options
命令来查询v8的启动选项。
在选项中,你可以查找到 这个--allow-natives-syntax
选项。

javascript
--allow-natives-syntax (allow natives syntax)
type: bool default: false
小结
普通属性:10 和 25
chrome目前情况(以后可能会变):
一般情况小于10的普通属性属于内属性,访问速度杠杠的。
25个以上的普通属性,将以非线程的结构存储,会影响访问速度。
快属性和慢属性 可以转换
快转慢:
- delete
- 普通属性数量
慢转快
- %ToFastProperties
- 设置对象成为一个函数的原型
引用
Fast properties in V8
图解 Google V8 | 03 | 快属性和慢属性:V8是怎样提升对象属性访问速度的?
fast-prototype.js
JSObject::MigrateSlowToFast
What's up with monomorphism?
V8 官方网站 的中文翻译 )
Optimization killers