手搓JSVM第 10 篇:对象、数组与属性访问

文章来源声明: 原文作者:泯泷; 来源站点:掘金; 原文链接:https://juejin.cn/post/7684212531368591386; 本文基于上述来源整理/加工,觅优补充点评,仅供技术学习交流。版权归原作者所有。
觅优短评

以文件柜抽屉比喻讲透属性访问的运行时本质,opcode清单与执行过程拆解清晰,适合跟着系列自研VM的开发者对照实现。

![10-cover.png](https://p6-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/98133129cbd0448c9b12eca185b18dd0~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5rOv5rO3:q75.awebp?rk3s=f64ab15b&x-expires=1789808408&x-signature=mlyKAY8UEG4n7PAQl%2BdVJJ%2F2kL4%3D)
  1. 本文目标

本篇继续扩展 VM 的运行时能力,让它支持对象和数组:

let obj = { x: 1 };
obj.x;
​
let arr = [1, 2];
arr[0];

为此需要新增以下 opcode:

  1. OBJECT_NEW

  2. OBJECT_SET

  3. ARRAY_NEW

  4. ARRAY_PUSH

  5. GET_PROP

  6. SET_PROP

  7. NEW

  8. 为什么对象需要运行时能力


数字和字符串可以直接作为值处理,而对象更像一个需要在运行时维护属性的容器。

因此,遇到对象属性访问:

obj.x

它并不是简单读取变量 x,而是要在运行时完成三个步骤:

先得到 obj
再得到属性名 'x'
再执行 obj['x']

形象化比喻:文件柜和抽屉标签

对象像文件柜,属性名像抽屉标签:

obj 文件柜
  x 抽屉 -> 1
  y 抽屉 -> 2

obj.x 就是去 obj 文件柜里按 x 这个标签找到对应抽屉,再取出里面的值。

10-object-cabinet.png

  1. 前置知识

概念说明
Object属性键值表
Property Key字符串或 symbol,这里先简化为字符串
MemberExpression`obj.x` 或 `obj[x]`
Array有序元素集合
Constructor可被 `new` 调用的函数
  1. 源码中的对应位置

概念源码位置说明
`GET_PROP` / `SET_PROP``src/runtime/opcodes.ts`属性读写 opcode
`OBJECT_NEW` / `OBJECT_SET``src/runtime/opcodes.ts`对象字面量
`ARRAY_NEW` / `ARRAY_PUSH``src/runtime/opcodes.ts`数组字面量
`NEW``src/runtime/opcodes.ts`构造调用
runtime cases`src/compiler/runtime-gen.ts`实际执行属性读写和构造
lowering object/array/member`src/compiler/lowering.ts`AST 到 IR
  1. 核心数据结构

Object Value

它解决什么问题

它负责保存对象的属性键值对,后续的属性读写都落到这个容器上。

教学版简化实现

const obj = {};
obj['x'] = 1;

Property Access

它解决什么问题

它把 obj.x 这种语法表示为运行时的属性读取操作。

教学版简化实现

regs[dst] = regs[objectReg][regs[propertyReg]];

10-property-access.png

Array Value

它解决什么问题

数组同样是运行时容器,只是元素按顺序保存。

教学版简化实现

const arr = [];
arr.push(1);

10-array-runtime.png

  1. Mermaid 图解

flowchart TD
    A[Object Literal] --> B[OBJECT_NEW r0]
    B --> C[LOAD_CONST key x]
    C --> D[LOAD_CONST value 1]
    D --> E[OBJECT_SET r0 r1 r2]

10-object-opcode-pipeline.png

flowchart TD
    A[MemberExpression obj.x] --> B[compile obj]
    A --> C[compile property key]
    B --> D[GET_PROP]
    C --> D

  1. 伪代码

compileObjectExpression(obj):
    dst = allocReg()
    emit object_new dst
    for each property:
        key = compile key
        value = compile value
        emit object_set dst, key, value
    return dst

compileMemberExpression(expr):
    object = compile expr.object
    property = compile property key
    dst = allocReg()
    emit get_prop dst, object, property
    return dst

  1. 教学版实现代码

const OPCODES = {
  OBJECT_NEW: 1,
  OBJECT_SET: 2,
  ARRAY_NEW: 3,
  ARRAY_PUSH: 4,
  GET_PROP: 5,
  SET_PROP: 6,
};

function executeObjectOpcode(opcode, code, state) {
  const { regs } = state;

  switch (opcode) {
    case OPCODES.OBJECT_NEW: {
      const dst = code[state.pc++];
      regs[dst] = {};
      break;
    }

    case OPCODES.OBJECT_SET: {
      const object = regs[code[state.pc++]];
      const key = regs[code[state.pc++]];
      const value = regs[code[state.pc++]];
      object[key] = value;
      break;
    }

    case OPCODES.GET_PROP: {
      const dst = code[state.pc++];
      const object = regs[code[state.pc++]];
      const key = regs[code[state.pc++]];
      regs[dst] = object[key];
      break;
    }
  }
}

  1. 示例输入与输出

let obj = { x: 1 };
obj.x;

简化 IR:

object_new r0
load_const r1, 'x'
load_const r2, 1
object_set r0, r1, r2
init_slot slot0(obj), r0
load_slot r3, slot0(obj)
load_const r4, 'x'
get_prop r5, r3, r4
return r5

输出:

1

  1. 执行过程拆解

StepInstructionObject State
1`object_new r0``{}`
2`load_const 'x'``{}`
3`load_const 1``{}`
4`object_set r0, 'x', 1``{ x: 1 }`
5`get_prop r5, r0, 'x'`返回 1
  1. 与原始源码的差异

主题教学版正式源码
属性 key字符串支持 computed key
对象字面量普通属性还支持方法、getter/setter 转换
数组简单 push还处理 holes、spread 脱糖
new本篇只说明正式 runtime 用 `Reflect.construct`
  1. 常见问题

Q1:obj.x 和变量 x 一样吗?

不一样。变量 x 从 environment 读取;obj.x 则先得到对象,再从这个对象上读取属性 x

Q2:为什么属性名也要放寄存器?

因为属性名不一定是固定字符串,它也可能是表达式,例如 obj[key]。只有先把 key 求值出来,运行时才能完成属性访问。

  1. 本文小结

到这里,VM 已经能在运行时创建对象和数组,并通过属性 key 完成对象属性访问。

下一篇将继续讲:

第 11 篇:打包、模块与 CLI