技术背景

表单域能力逻辑需要一层安全层过滤能力,预防传入的表单协议含有敏感数据或者调用原生能力造成 XSS 攻击。

技术目标

轻量体积,符合 eval 同步运算结果,可定制规则、过滤关键词、运算符,剔除冗余能力(原型链、Symbol、继承),屏蔽原生能力,自成体系,定位是计算表达式且具备执行一定长度下符合脚本规则的能力。

方案

AST

有现成的社区开源库: espree、acorn、babylon 等,根据 Parse-API 规范实现,很快就否决,这几个库本身的定位就是为了遵循规范,定制规则不便 —— 比如+的优先级比-高,var 声明提高到全局作用域。

状态机

有限状态机(Finite-state machine)是一个模型,用来表示有限个状态以及在这些状态之间的转移和动作等行为的数学模型,因为它的状态是“有限的”,所以可以模拟世界上大部分事物。

例如TCP 协议状态机

看起来比较绕,但在代码中很常见。e.g. 某个字符串内存在 abc;

function hasFirstCode(str) {
if (str.length === 0) return false;
if (str.charAt(0) === "a") {
return hasSecondCode(str.substr(1));
}
return hasFirstCode(str.substr(1));
}
function hasSecondCode(str) {
if (str.charAt(0) === "b") {
return hasThirdCode(str.substr(1));
}
return hasFirstCode(str); // fix aabc
}
function hasThirdCode(str) {
if (str.charAt(0) === "c") {
return true;
}
return hasFirstCode(str);
}
hasFirstCode("fabc"); // true
hasFirstCode("aabc"); // true
hasFirstCode("cbc"); // false

其状态如下

状态机可归纳为 4 个要素,

  • 现态,即当前现有状态。
  • 条件,当满足某个条件,触发某个动作或执行一次状态的迁移。
  • 动作,条件满足后执行的动作,非必需
    • 可以不执行任何动作,直接迁移到新状态。
    • 当动作执行完毕后可以迁移到新的状态,也可以仍旧保持原状态。
  • 次态,条件满足后要迁往的新状态。“次态”是相对于“现态”而言的,“次态”一旦被激活,就转变成新的“现态”。

状态的阶段:

  • “现态”和“条件”是因。
  • “动作”和“次态”是果。

状态机一般分为三种类型:

  • Moore 型状态机:下一状态只由当前状态决定,即次态=f(现态,输入),与 Mealy 一致,不同的是输出=f(现态),输出只与当前的状态有关,输入只决定状态机内部的状态改变,不影响最终的输出。输出$\Delta$依赖于现态$\mathrm{Q}$

    $\lambda = \mathrm{Q} \to \Delta $

  • Mealy 型状态机:下一状态不但与当前状态有关,还与当前输入值有关,即次态=f(现态,输入) 与 Moore 一致,不同的是输出=f(现态,输入),输出$\Delta$依赖于现态$\mathrm{Q}$和输入 $\Sigma$

    $\lambda = \mathrm{Q} \times \Sigma \to \Delta $

因为 Moore 输出是固定故平常很少使用到,大多数使用的都是 Mealy 型状态机,本文就状态机实现。

准备工作

熟悉一些基本点

运算顺序

运算符的权重根据以下表进行:

运算符 描述
. 成员属性访问、数组下标、函数调用以及表达式分组
++ – - ~ ! delete new typeof void 一元运算符、返回数据类型、new 操作符、未定义值
* / % 乘法、除法、取模+ -加法、减法、字符串拼接
<< >> >>> 移位
< <= > >= instanceof 小于、小于等于、大于、大于等于、instanceof
== != === !== 等于、不等于、严格相等、非严格相等
& 按位与
^ 按位异或
&& 逻辑与
|| 逻辑或
?: 条件(三目运算符)
= OP = 赋值、连续赋值、运算赋值加等,减等,除等,幂等,模等,按位*等
, 多重求值

执行过程

Js 引擎执行过程分为三个阶段

  • 语法分析, 检查语法错误。
  • 预编译,简单理解就是在内存中开辟一些空间,存放一些变量与函数。
  • 执行阶段,也就是解释执行。

执行上下文

上下文也称作用域,执行上下文又分为:GO(Global Object)初始化执行和 AO(Activity Object)运行时执行。

GO 是全局上下文,全局环境初始化生成。

AO 则负责动态作用域,JavaScript 采用的词法作用域(lexical scoping),也就是静态作用域,与之相对应的便是在执行态的动态作用域。

引用类型

JavaScript 基础类型不不做解释了,引用类型也就是复杂类型是读写是根据栈[stack]内存中保存了指向堆[Heap]内存中的引用指针,真实的值是存在堆中的。
例如var foo = { bar: 1}转化以下伪代码:

heap.0x0012ffad = { bar: 1};
stack[Symbol(foo)] = 0x0012ffad;

解析var n = 1 在 JavaScript 是怎样执行。

V8 是将声明和赋值分开处理的,其解析成var n; n = 1;, n = 1还是在原处,相当于拆散了,为了处理声明提升,在每层作用域都会维护一个独立的声明作用域(variables_ 表),这样运行时就可以从声明作用域中递归查找变量。

首先是初始化 GO,var n,查看当前 scope 是否存在变量n,由于Parser::Declare 提升放到该层的作用域内。存在则跳过该声明,继续解析。

然后进行的是 LHS(Left-Hand-Side)查询。将右侧 RHS 查找源值(读操作)赋值(写操作)给左侧变量。从宽泛的定义上可以理解成 LHS 是引用查询,RHS 是根据引用或值查找源值 —— 例如函数传参fn(n) 和 运算a+b;

在 JavaScript 里,我们无法通过正常办法拿到引用地址,但是可以通过一些取巧的方式取到。

实现

既然使用状态机,词法解析与“编译”保持同步。

概览

不需要一次性解析全部的代码,故分为解析阶段和运算阶段。

  1. 解析阶段包括词法解析(生成 token-stream)、语法校验 (甄别符合规范预期) 和语义分析 (根据关系生成指令供解算),语法校验稍前于词法解析,作为词法解析的前置行为,功能耦合的同时也将部分词法解析器的压力分担掉;两者具有各自的类型规范,词法解析得到 Token 与 Instruction 指令,用于第二步运算。
  2. 运算阶段代替的是解释执行,结合传入的定义变量与配置进行运算,得到结果,在每层运算周期中含有 scope 和 global 两个作用域环境,这样即可以模拟作用域。

Token,词法分析是将字符序列转换为 Token 序列的过程生成的过程,Token 概括了序列内每个标记的类型、值和其他信息。

Instruction,根据当前 Token 的“现态”去进行“条件”的匹配完成“动作”得到“次态”生成的产物,说白了就是语义分析后得到的指令。

首先必须拿到 token,例如 var a = 1 + 1 的解析代码

const token = [];
const Instruction = [];
const expression = `var a = 1 + 1`;
let p = 0;
// -------Token Type
const TOKEN_VAR = "token_VAR"; // 赋值操作符
const TOKEN_NAME = "token_name"; // 名称
const TOKEN_NUMBER = "token_number"; // 数字
const TOKEN_OPERATOR = "token_operator"; // 操作符
// -------Token
function newToken(type, value, pos) {
token.push(
Object.assign(Object.create(null), {
type,
value,
pos,
})
);
}

function next() {
if (p === expression.length - 1) return;
if (isWhiteSpace()) {
return next();
} else if (isVariable() || isOperator() || isNumber() || isName()) {
return next();
} else {
console.error(`Unexpected identifier`);
}
}

function isWhiteSpace() {
const matchWS = /^(\t|\n|\r|\s)/.exec(expression.substr(p).charAt(0));
if (matchWS && matchWS[1]) {
p++;
return true;
}
return false;
}

function isVariable() {
// match variable ident
const word = expression.substr(p).match(/\b\w*\b/)[0];
if (["const", "var", "let"].includes(word)) {
newToken(TOKEN_VAR, word, p);
p += word.length;
return true;
}
return false;
}
function isName() {
const first = expression.charAt(0);
let result;
if (first === "_" || first === "$" || /^[a-zA-Z]/.test(first)) {
result = /^((_|$)?[0-9a-zA-Z|$|_]{1,})/.exec(expression.substr(p));
}

if (
result === undefined ||
result === null ||
typeof result[1] !== "string"
) {
return false;
}
[, result] = result;
p += result.length;
newToken(TOKEN_NAME, result, p);
return true;
}

function isNumber() {
const result = /(^([1-9]\d*(\.\d+)|(\d*(\.\d+)?)))/.exec(
expression.substr(p)
);
if (result && result[1]) {
newToken(TOKEN_NUMBER, result[1], p);
p += result[1].length;
return true;
}
return false;
}

function isOperator() {
const result = /^((\+\+)|(\-\-)|(\|\|)|(\&\&)|(\=\=)|(\!\=)|(\>\=)|(\<\=)|(\+)|(\-)|(\!)|(\~)|(\*)|(\/)|(\=)|(\;))/.exec(
expression.substr(p)
);
if (result && result[1]) {
newToken(TOKEN_OPERATOR, result[1], p);
p += result[1].length;
return true;
}
return false;
}
next();


此时的状态取到的 tokenize

[
{type: "token_var", value: "var", pos: 3}
{type: "token_name", value: "a", pos: 5}
{type: "token_operator", value: "=", pos: 6}
{type: "token_number", value: "1", pos: 9}
{type: "token_operator", value: "+", pos: 10}
{type: "token_number", value: "1", pos: 13}
]

光取到 tokenize 没用,还必须进行语义分析,分析 tokenize 序列之间的关系,分析它们到底要做什么。

比如

{type: "token_var", value: "var", pos: 3}
{type: "token_name", value: "a", pos: 5}

var 标识针对性较强,除了声明没其它用途;

{type: "token_operator", value: "=", pos: 6}

运算符的针对性相对于弱一些,但难点是几十种运算符的优先级和状态管理。

{type: "token_number", value: "1", pos: 9}
{type: "token_operator", value: "+", pos: 10}
{type: "token_number", value: "1", pos: 13}

只看这三条的确能得到结果,但并不是最终结果,因为可能是这样的

+{type: "token_operator", value: "-", pos: 8}
{type: "token_number", value: "1", pos: 9}
{type: "token_operator", value: "+", pos: 10}
{type: "token_number", value: "1", pos: 13}
+{type: "token_operator", value: "*", pos: 14}
+{type: "token_number", value: "2", pos: 15}

在这种情况下,就需要管理状态。

管理状态

怎么去管理状态?将这个问题分解成若干个问题。

  • 如何将状态简化。
  • 如何梳理简化后的状态。
  • 怎样的状态利于逻辑运算的。

状态简化

例如将~1+2-3*4%5转化成

1 + 2 - ((3 * 4) % 5);

结果没有改变,但结构更明确化,但怎么得到预期结构?根据运算顺序的权重图可以推演出下图:


把它当做一个俯视圆锥体这么看不是很明确,换种方式表达可能更清晰


与函数调用栈行为相同,秉承先进后出的原则。每层级运算符作为一个函数,每个函数也作为一个状态。把运算符优先级当做栈,权重越大的运算符越后加载。

梳理简化后的状态

例如(0 || undefined) && !false换成伪代码表示:

// 标识集
const TOKEN_STRING = "TOKEN_STRING";
const TOKEN_NUMBER = "TOKEN_NUMBER";
const TOKEN_NAME = "TOKEN_NUMBER"; // 变量名称
const TOKEN_PAREN = "TOKEN_PAREN"; // 表达式分组括号
const TOKEN_OPERATOR = "TOKEN_OPERATOR"; // 运算符
// 指令集
const INSTR_STRING = "INSTR_STRING";
const INSTR_NUMBER = "INSTR_NUMBER";
const INSTR_NAME = "INSTR_NAME";
const INSTR_EXPRE = "INSTR_EXPR"; // 表达式,某些情况表达式是懒执行的 比如 false ? b() : c
const INSTR_OPERATOR1 = "INSTR_OPERATOR1"; // 一元运算符
const INSTR_OPERATOR2 = "INSTR_OPERATOR2"; // 二元运算符

class Token {
constructor(public type: string, public value: any, public index: number) {}
}
class Instruction {
constructor(public type: string, public value: any) {}
}

class Parser {
current: Token = null; // 当前Token
nextToken: Token = null; // 下个Token

constructor(public exporession: string) {
if (typeof exporession !== "string") {
throw new Error("Expected string but actual is" + typeof exporession);
}
this.next();
}

getNextToken = (): Token => {
return; /* 获取下一个 Token */
};

next = () => {
this.current = this.nextToken;
return (this.nextToken = this.getNextToken());
};

accpet = (type: string, value?: string | number, next = true): boolean => {
// 语义解析命中预期,准备收集指令并下一步 Token 的收集
if (
this.nextToken &&
this.nextToken.type === type &&
(value ? this.nextToken.value === value : true)
) {
return next && this.next(), true; // next表示只需要检查
}
return false;
};

assert = (type: string, value: string | number) => {
// 断言
if (!this.accpet(type, value)) {
throw new Error("parse Error");
}
};

parseExpression = (instr: Array<Instruction>) => {
// 解析表达式
const exprInstr = [];
this.parseOrExpression(exprInstr);

for (var i = 0, len = exprInstr.length; i < len; i++) {
instr.push(exprInstr[i]);
}
return instr;
};

parseOrExpression = (instr: Array<Instruction>) => {
// 解析 ||
this.parseAndExpression(instr);
while (this.accpet(TOKEN_OPERATOR, "||")) {
var falseBranch = [];
this.parseAndExpression(falseBranch);
instr.push(new Instruction(INSTR_EXPRE, falseBranch));
instr.push(new Instruction(INSTR_OPERATOR2, "||"));
}
};

parseAndExpression = (instr: Array<Instruction>) => {
// 解析 &&
this.parseField(instr);
while (this.accpet(TOKEN_OPERATOR, "&&")) {
var trueBranch: Array<Instruction> = [];
this.parseField(trueBranch);
instr.push(new Instruction(INSTR_EXPRE, trueBranch));
instr.push(new Instruction(INSTR_OPERATOR2, "&&"));
}
};

parseUnaryExpression = (instr: Array<Instruction>) => {
// ... 暂存当前指针
if (this.accept(TOKEN_OPERATOR, isUnaryOpeator)) {
// 内置函数调用
if (unarySymbolMapReg.test(this.current.value)) {
// +, ++, +, -, !, ~,
const op = this.current;
this.parseUnaryExpression(exprInstr); // ++-1
exprInstr.push(new Instruction(INSTR_OPERATOR1, op.value));
} else {
// 回溯, 还原指针
this.parseField(instr);
}
} else {
this.parseField(instr); // 外置函数 || 内声明函数
}
};

parseField = (instr: Array<Instruction>) => {
// 解析 字段
if (this.accpet(TOKEN_NAME)) {
instr.push(new Instruction(INSTR_NAME, this.current.value));
} else if (this.accpet(TOKEN_NUMBER)) {
instr.push(new Instruction(INSTR_NUMBER, this.current.value));
} else if (this.accpet(TOKEN_STRING)) {
instr.push(new Instruction(INSTR_STRING, this.current.value));
} else if (this.accpet(TOKEN_PAREN, "(")) {
// (a=1)
this.parseExpression(instr);
this.assert(TOKEN_PAREN, ")");
} else {
throw new Error("unexpected " + this.nextToken);
}
};
}

const instr = [];
const parseInstance = new Parser("(0 || undefined) && !false");
parseInstance.parseExpression(instr);

注意上例在一元运算符语义分析时进行了回溯处理,比如 typeofreturn 它可能是typeof (xxx) 调用,也可能是 typeof xxx方式调用。这种情况预判nextToken 不够用,需要根据其之后两三个的情况处理,其他场景诸如function fn(){xxx}const fn = function () {}同理。通过梳理,可以得到以下 tokenize 和 Instruction

token = [
{ type: "TOKEN_PAREN", value: "(", index: 2},
{ type: "TOKEN_NUMBER", value: 0, index: 3},
{ type: "TOKEN_OPERATOR", value: "||", index: 6},
{ type: "TOKEN_NAME", value: "undefined", index: 16},
{ type: "TOKEN_PAREN", value: ")", index: 17},
{ type: "TOKEN_PAREN", value: ")", index: 17},
{ type: "TOKEN_OPERATOR", value: "&&", index: 20},
{ type: "TOKEN_OPERATOR", value: "!", index: 22},
{ type: "TOKEN_NAME", value: "false", index: 27}
]

Instruction = [
{ type: "INSTR_NUMBER", value: 0 }
{ type: "INSTR_EXPRE", value: Array(1) }
{ type: "INSTR_OPERATOR2", value: "||" }
{ type: "INSTR_EXPRE", value: Array(2) }
{ type: "INSTR_OPERATOR2", value: "&&" }
]

利于运算

循序渐进,以下是简单场景下 1 + 1 的运算逻辑

const instr = [
{type: "INSTR_NUMBER", value: "1"},
{type: "INSTR_NUMBER", value: "1"},
{type: "INSTR_OPERATOR2", value: "+"},
]

let len = instr.length;
let n1, n2
let stack = []; // 作为运算数据池,其作用类似于原生JS的作用域结构。目的是解决递归计算。

for(let i = 0; i < len; i++) {
const {value, type} = instr[i].value
switch(type){
case INSTR_NUMBER: {
stack.push(value);
break;
}
case INSTR_OPERATOR: {
n1 = stack.pop();
n2 = stack.pop();
const f = binaryMap[value];
stack.push(f(n1,n2));
break;
}
default: {
...
break
}
}
}

稍微复杂一点的场景 (0 || undefined) && !false 的运算逻辑。

const binaryMap = {
'||': (a, b) => a || b,
'&&': (a, b) => n1 ? n1 : n2,
'-': (a, b) => a - b
}
const unaryMap = {
'!': (n) => !n
}
function calc(instr, values) {
const len = instr.length;
let n1, n2
let stack = []; // 作为运算数据池,其作用类似于原生JS的作用域结构。目的是解决递归计算。

for(let i = 0; i < len; i++) {
const {type, value} = instr[i];
switch(type){
case INSTR_NUMBER: {
stack.push(value);
break;
}
case TOKEN_OPERATOR2: { // 抽象描述,待细化,N元操作符 需要N个操作数
[n1, n2] = stack.splice(-2, 2);
const f = binaryMap[value];
if(value === '&&') { // false && fn() fn不会执行
stack.push(f(n1, calc([n2], values)));
}else {
stack.push(f(n1,n2));
}
break;
}
case INSTR_OPERATOR1: { // 一元操作符
n1 = stack.pop();
stack.push(unaryMap[value](n1));
break;
}
case INSTR_NAME: {
stack.push(values[value]);
}
case INSTR_VAR: { // values属于全局上下文,let const的赋值加到当前scope里,下面说
const v = stack.pop()
values[value] = v;
}
default: {
...
break
}
}
}
return stack[0]
}

经得出最终结果符合预期,其他的运算符根据先后顺序可以推算出来这里就不做赘述。

复杂数据声明

声明var foo = { bar: 1 } 对于目前的就是一个解析格式与解析时读取的问题。首先从 tokenize 阶段,添加相应的标识符{ } : , [ ] ,数组同理。

  • token-stream 语法分析,生成 token 的处理逻辑;
  • parser 进行语义分析
  • calc 解释执行
// token-stream.ts
export default class TokenStream {
pos = 0;

// 当前解析character
current: null | TypeToken = null;

constructor(public expression: string) {}

//....other

getSomeCode = (len = 1, offset = 0): string => {
const start = offset + this.pos;
const { length } = this.expression;
return this.expression.substr(
start,
start + len > length ? length - start : len
);
};

/**
* 判断是否操作符
* @desc 操作符 + - * / || % ^ ? : . > < = >= <= | == === != !== in
*/
isOperator = (): boolean => {
const str = this.getSomeCode(Infinity);
let result: string | undefined;
if (operatorReg.test(str)) {
result = execFactoryReg(operatorReg, str); // 匹配标识符 不匹配typeof return
} else if (unaryMapReg.test(str)) {
result = execFactoryReg(unaryMapReg, str); // 匹配typeof return
}
if (!result) return false;
result = result.replace(/\s/g, "");
this.pos += result.length;
this.current = this.newToken(TOKEN_OPERATOR, result);
return true;
};

/**
* 圆、方括号
*/
isParenthesis = (): boolean => {
var first = this.getSomeCode();
if (contains(["(", ")"], first)) {
this.current = this.newToken(TOKEN_PAREN, first);
} else if (contains(["[", "]"], first)) {
this.current = this.newToken(TOKEN_SQUARE, first);
} else if (contains(["{", "}"], first)) {
this.current = this.newToken(TOKEN_CURLY, first);
} else {
return false;
}
this.pos++;
return true;
};

isComma = (): boolean => {
var first = this.getSomeCode();
if (first === ",") {
this.current = this.newToken(TOKEN_COMMA, ",");
this.pos++;
return true;
}
return false;
};
}

在 parser 解析器里添加相应的语义分析逻辑

// parser.ts
export default Parser {
/**
* 解析表达式整个句柄
*/
parseExpression = (instr: TypeInstruction[]): void => {
const exprInstr: TypeInstruction[] = []

this.parseMultipleEvaluation(exprInstr)
exprInstr.forEach(exp => (instr.push(exp)))
}

/**
* 解析连续求值 例如 数组字面量 [1, 2, [3, 4, 5]] (1, 2, 3)
*/
parseMultipleEvaluation = (exprInstr: TypeInstruction[]): void => {
this.parseAssignmentExpression(exprInstr)
while (this.accept(TOKEN_COMMA, ',')) {
this.parseConditionalExpression(exprInstr)
}
}

//...other

parseField = (exprInstr: TypeInstruction[]): void => {
if (this.accept(TOKEN_OPERATOR, isUnaryOpeator)) {
// 内置一元运算符
exprInstr.push(new Instruction(INSTR_OPERA1, this.current.value));
} else if (this.accept(TOKEN_NUMBER)) {
// 数字类型
exprInstr.push(new Instruction(INSTR_NUMBER, this.current.value));
} else if (this.accept(TOKEN_STRING)) {
// 字符串类型 \"name\"
exprInstr.push(new Instruction(INSTR_PLAIN, this.current.value));
} else if (this.accept(TOKEN_PAREN, '(')) {
// 圆括号,调用 或 表达式(a=1)
this.parseExpression(exprInstr);
this.assert(TOKEN_PAREN, ')');
} else if (this.accept(TOKEN_SQUARE, '[')) {
// 数组字面量
this.parseArrayLiteralDeclaration(exprInstr)
} else if (this.accept(TOKEN_CURLY, '{', false)) {
// Object字面量声明
this.parseObjectLiteralDeclaration(exprInstr)
} else if (this.accept(TOKEN_VAR, ['const', 'var', 'let'])) {
// 赋值表达式 收集 ident,避免 variableName 识别成 Name 引发error
const identifier = this.current
this.assert(TOKEN_NAME)
exprInstr.push(new Instruction(INSTR_VARNAME, this.current.value));
exprInstr.push(new Instruction(INSTR_VAR, identifier.value))
} else {
throw new Error('unexpected ' + this.nextToken);
}
}

/**
* 解析对象字面量
*/
parseArrayLiteralDeclaration = (exprInstr: TypeInstruction[]) => {
// Array字面量声明 TODO: 需要和 obj['a'] 做区分
const instr = []
if (this.accept(TOKEN_SQUARE, ']')) { // []
exprInstr.push(new Instruction(INSTR_ARRAY, instr))
return
}
this.parseExpression(instr)
this.assert(TOKEN_SQUARE, ']')
exprInstr.push(new Instruction(INSTR_ARRAY, instr))
}

/**
* 解析对象字面量
*/
parseObjectLiteralDeclaration = (exprInstr: TypeInstruction[]) => {
while (this.accept(TOKEN_CURLY, '{')) {
const instr = {}
if (this.accept(TOKEN_CURLY, '}')) { // {}
exprInstr.push(new Instruction(INSTR_OBJECT, instr))
return
}
while (this.accept(TOKEN_NAME) || this.accept(TOKEN_NUMBER) || this.accept(TOKEN_STRING)) { // key
const key = this.current.value
this.assert(TOKEN_OPERATOR, ':');
instr[key] = []; // 以表达式作为每个key的value递归处理
if (this.accept(TOKEN_CURLY, '{', false)) { //
this.parseObjectLiteralDeclaration(instr[key])
} else {
this.parseExpression(instr[key]);
}
this.accept(TOKEN_COMMA, ','); //{ a: 1,} 可能是有的,但不可以用assert
}
this.assert(TOKEN_CURLY, '}'); // 断言结尾
exprInstr.push(new Instruction(INSTR_OBJECT, instr))
}
}
}

var foo = { bar: { arr: ['a','b','c']}}; 的指令集解析如下

[
{
"type": "INSTR_VARNAME",
"value": "foo"
},
{
"type": "INSTR_EXPRE",
"value": [
{
"type": "INSTR_OBJECT",
"value": {
"bar": [
{
"type": "INSTR_OBJECT",
"value": {
"arr": [
{
"type": "INSTR_ARRAY",
"value": [
{"type": "INSTR_PLAIN", "value": "a" },
{"type": "INSTR_PLAIN", "value": "b" },
{"type": "INSTR_PLAIN", "value": "c" }
]
}
]
}
}
]
}
}
]
},
{
"type": "INSTR_VAR",
"value": "var"
}
]

// calc.ts
function calc(instr, value, statis = false) { // 默认return stack[0], statis = true 表示全量return
// ...other
case INSTR_ARRAY: { // 数组字面量
stack.push(calc(value, values, true))
break
}
case INSTR_OBJECT: { // 对象字面量
const instr = Object.create(null)
Object.keys(value).forEach(key => {
instr[key] = calc(value[key], values, statis)
})
stack.push(instr)
break
}
//...
}

作用域

ES6 之前,正常情况下(抛开 catch/with)只有 function 才能创建新的作用域 —— function Scope,而 ES6 引入了 Block Scope,后者不在需求内就不做说明了,照葫芦画瓢。

所以实现作用域也顺带把内置 function 也做了,function abs(a,b){ return(a-b || b-a)}

// token-stream.ts
isFunctionDefined = (): boolean => {
const word = this.expression.substr(this.pos).match(/\b\w*\b/);
if(word === 'function') {
this.current = this.newToken(TOKEN_FUNC, undefined);
this.pos += word.length
const nextToken = this.checkNextAccessGrammar(); // 语法检查下个token 是否符合 FunctionName 约束声明规则
if(nextToken.type !== TOKEN_NAME) {
this.parseError('function definition should have function name')
return false
}
return true;
}
return false
}

// parser.ts
parseField = (exprInstr: TypeInstruction[]): void => {
//...
} else if (this.accept(TOKEN_FUNC, undefined, false)) {
this.parseFunctionDefinedDeclaration(exprInstr);
}
}
/**
* 解析函数声明
*/
parseFunctionDefinedDeclaration = (expreInstr: TypeInstruction[]) => {
while (this.accept(TOKEN_FUNC)) {
if (this.assert(TOKEN_NAME)) { // function fn(){}
const funcName = this.current.value;
const instr = []; // 参数
if (this.accept(TOKEN_PAREN, '(') && !this.accept(TOKEN_PAREN, ')')) { // fn()
do {
this.parseField(instr); // 收集参数
} while (this.accept(TOKEN_COMMA))
this.assert(TOKEN_PAREN, ')')
}
this.parseFunctionBodyExpression(instr) // 收集完参数只剩下 { 函数体 };
expreInstr.push(new Instruction(INSTR_FUNCDEF, instr))
expreInstr.push(new Instruction(INSTR_FUNCDEF, funcName))
}
}
}

/**
* 解析函数体
*/
parseFunctionBodyExpression = (exprInstr: TypeInstruction[]) => {
if (this.accept(TOKEN_CURLY, '{')) {
const instr = [];
do {
this.parseExpression(instr)
} while (this.accept(TOKEN_SEMICOLON, ';') && !this.accept(TOKEN_CURLY, '}', false)) // 每行必须是;结尾
this.assert(TOKEN_CURLY, '}')
this.accept(TOKEN_SEMICOLON, ';')
exprInstr.push(new Instruction(INSTR_EXPRE, instr))
}
}

作用域的形态与作用域链几乎一致,每个函数体除要有一个作用域之外,还需要具有更新其词法作用域的能力,创建 CustomFunc 类,_scope 为当前作用域,当发生更新作用域时,通过传入的属性,把形参赋值实参,达到更新作用域的目的。另外一方面,使用let const 新声明的变量都会挂载到该级作用域_scope 上面。

// calc.ts
function calc(
tokens: Instruction<any>[],
values = Object.create(null),
statis = false,
scope = Object.create(null) // 当前作用域
) {
// ... other
case INSTR_FUNCDEF: { // 函数定义
if (typeof value !== 'string') {
stack.push(value); // 函数体
continue
}
if (stack.length === 0) return
const _scope = Object.setPrototypeOf(Object.create(null), scope); // 创建一个新的作用域,并将
_scope[value] = new Instruction<CustomFunc>(INSTR_EXECUTBODY, new CustomFunc(stack.pop(), values, _scope, ceval))
// Object.assign(scope, _scope)
break
}
}
// 内声明函数体
class CustomFunc {
args: string[];

constructor(public func: Instruction<any>[], public values: Record<string, any>, public _scope: Record<string, any>, public ceval: Ceval) {
const args = this.func.splice(0, func.length - 1)
this.args = calc(args, mapToObject(args, (k) => k), true); //mapToObject: Array to Object e.g. ['a', 'b'] => { a: undefined, b: undefined }
this._scope = merge(mapToObject(args), _scope);

Object.setPrototypeOf(this, new.target.prototype); // restore
}

updateScope = (scope: any[]) => { // 更新作用域,假设 Object attribute 是不保证顺序的。
this.args.forEach((key, index) => {
if(hasAttribute(this._scope, key)) {
this._scope[key] = scope[index]
}
});
}

invokeBody = () => {
return calc(this.func, this.values, false, this._scope)
}
}

调用函数的解析较为简单

// token-stream.ts
// 解析外置函数调用
parseOuterFunctionCallExpression = (exprInstr: TypeInstruction[]) => {
this.parseMemberAccessExpression(exprInstr) // 成员访问, 后面引用说
if (this.current.type === TOKEN_NAME && this.accept(TOKEN_PAREN, '(', false)) {// fn( 形态
this.parseArguments(exprInstr)
}
}

// 解析参数
parseArguments = (exprInstr: TypeInstruction[]): void => {
while (this.accept(TOKEN_PAREN, '(')) {
if (this.accept(TOKEN_PAREN, ')')) {
// 立即调用
exprInstr.push(new Instruction(INSTR_FUNCALL, 0)) // 参数长度
} else {
let count = 0
while (!this.accept(TOKEN_PAREN, ')')) {
do {
this.parseConditionalExpression(exprInstr); // 兼容 fn(false ? 1 : 2, 3)
count++;
} while (this.accept(TOKEN_COMMA))
}
exprInstr.push(new Instruction(INSTR_FUNCALL, count))
}
}
}


case INSTR_FUNCALL: { // 内声明函数调用,也可以添加外部函数
const args = stack.splice(-value, value); // INSTR_FUNCALL的value表示传入参数长度
fn = stack.pop();
if(fn.value instanceof CustomFunc) {
fn.value.updateScope(args) // 先更新作用域
stack.push(fn.value.invokeBody()) // 再执行函数体
} else if (typeof fn === 'function') { // 外置函数,即在consts内声明的
stack.push(fn.apply(null, args))
continue
}
break
}

这样就兼容了自定义函数,也方便后续添加其他规则。

this

内置函数是不允许解析到 this 的,this 并不属于动态语言的一部分,由于运行时产生 AO 的缘故让它的行为比较像动态语言,其次这个特性个人觉得也不太需要。fn.apply(null, args)是暂时方案实现,只需要以一些判断依据处理,由于不是重点就暂搁一边。

引用

Object、Array 的修改是根据引用来写入的。

var foo = { bar: [1, 2, 3] };
var b = foo.bar;

在实现中是以读取上下文中的属性拿到值,再逐个拿到

//parser.ts
parseMemberAccessExpression = (exprInstr: TypeInstruction[]): void => {
this.parseField(exprInstr);
while (this.accept(TOKEN_OPERATOR, ".") || this.accept(TOKEN_SQUARE, "[")) {
if (this.current.value === ".") {
this.assert(TOKEN_NAME); // a.1×
exprInstr.push(new Instruction(INSTR_MEMBER, this.current.value));
} else if (this.current.value === "[") {
this.parseExpression(exprInstr);
this.assert(TOKEN_SQUARE, "]");
exprInstr.push(new Instruction(INSTR_MEMBER));
}
}
};

instr = [
{ type: "INSTR_VARNAME", value: "b"},
{ type: "INSTR_EXPRE",
value: [
{ type: "INSTR_MEMBER", value: "bar" },
{ type: "INSTR_NAME", value: "foo" },
},
{ type: "INSTR_VAR", value: "var"}
];

// calc.ts
case INSTR_MEMBER: { // 成员访问
if (!value) { // a["b"]
[n1, n2] = stack.splice(-2, 2);
stack.push(n1[n2]);
break
}
n1 = stack.pop(); // a.b
stack.push(n1[value])
break
}

读是没有问题的,问题出在写,LeftHandSide 的左侧只能是一个引用。

obj.arr[1] = 1
// 会被转化成
[1, 2, 3][1] = 1;

正常情况在原生拿不到引用地址,换个思路转成以下写法

[Reference, "bar"][1] = 1;

上代码

// calc.ts

//...
case INSTR_OPERA2: { // 二元运算
[n1, n2] = stack.splice(-2, 2)
if (value === '=') {
if (n1 instanceof Reference) {
n1.setValue(n2);
n1.destory();
n1 = null;
} else {
fn(n1, n2, hasAttribute(scope, n1) ? scope : values)
}
}
break
}
case INSTR_MEMBER: { // 成员访问
const nextItem = tokens[i+2]; // 解析顺序 [INSTR_MEMBER] => [INSTR_EXPRE] => [INSTR_OP2 =]
const keys = calc(value, values, true, scope); // ['foo', bar']
const ref = getReference(keys, scope, values) // 创建引用实例

if(nextItem && nextItem.type === INSTR_OPERA2 && nextItem.value === '=') { // 下一个指令是赋值操作
stack.push(ref);
} else {
stack.push(ref.getValue())
ref.destory();
ref = null
}
break
}

// 根据paths尝试获取引用
function getReference(keyQueue: string[], scope: Record<string, any>, values: Record<string, any>): Reference {
let path
let target = hasAttribute(scope, path) ? scope : values;
const lastKey = keyQueue.pop();
while (path = keyQueue.shift()) {
if (hasAttribute(target, path)) {
target = target[path];
} else if (!target) {
throw new TypeError(`Uncaught TypeError: Cannot read property '${path}' of ${target}`)
} else {
target = undefined
}
}
return new Reference(target, lastKey)
}

class Reference {
constructor(public target: any, public path: string) {
if (!target) {
throw new ReferenceError(`${path}:${String(target)} Invalid parent reference`)
}
}

setValue = (value) => (this.target[this.path] = value)
getValue = () => this.target[this.path]
destory = () => this.target = null;
}

最后要接入TC39/test262的 testcase。目前Ceval已经完成部分测试用例,压缩后20+k。

移步Repo获得更好的阅读体验.

参考文献:
深入浅出理解有限状态机/云峰小罗;