技术背景 表单域能力逻辑需要一层安全层过滤能力,预防传入的表单协议含有敏感数据或者调用原生能力造成 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); } function hasThirdCode (str ) { if (str.charAt(0 ) === "c" ) { return true ; } return hasFirstCode(str); } hasFirstCode("fabc" ); hasFirstCode("aabc" ); hasFirstCode("cbc" );
其状态如下
状态机可归纳为 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.0 x0012ffad = { 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 里,我们无法通过正常办法拿到引用地址,但是可以通过一些取巧的方式取到。
实现 既然使用状态机,词法解析与“编译”保持同步。
概览 不需要一次性解析全部的代码,故分为解析阶段和运算阶段。
解析阶段包括词法解析(生成 token-stream)、语法校验 (甄别符合规范预期) 和语义分析 (根据关系生成指令供解算),语法校验稍前于词法解析,作为词法解析的前置行为,功能耦合的同时也将部分词法解析器的压力分担掉;两者具有各自的类型规范,词法解析得到 Token 与 Instruction 指令,用于第二步运算。
运算阶段代替的是解释执行,结合传入的定义变量与配置进行运算,得到结果,在每层运算周期中含有 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 ;const TOKEN_VAR = "token_VAR" ; const TOKEN_NAME = "token_name" ; const TOKEN_NUMBER = "token_number" ; const TOKEN_OPERATOR = "token_operator" ; 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 ( ) { 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
转化成
结果没有改变,但结构更明确化,但怎么得到预期结构?根据运算顺序的权重图可以推演出下图:
把它当做一个俯视圆锥体这么看不是很明确,换种方式表达可能更清晰
与函数调用栈行为相同,秉承先进后出的原则。每层级运算符作为一个函数,每个函数也作为一个状态。把运算符优先级当做栈,权重越大的运算符越后加载。
梳理简化后的状态 例如(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" ; 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 ; nextToken: Token = null ; constructor (public exporession: string ) { if (typeof exporession !== "string" ) { throw new Error ("Expected string but actual is" + typeof exporession); } this .next(); } getNextToken = (): Token => { return ; }; next = () => { this .current = this .nextToken; return (this .nextToken = this .getNextToken()); }; accpet = (type : string , value?: string | number , next = true ): boolean => { if ( this .nextToken && this .nextToken.type === type && (value ? this .nextToken.value === value : true ) ) { return next && this .next(), true ; } 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); 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, "(" )) { 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);
注意上例在一元运算符语义分析时进行了回溯处理,比如 typeof
和 return
它可能是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, n2let stack = []; 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 = []; for (let i = 0 ; i < len; i++) { const {type , value} = instr[i]; switch (type ){ case INSTR_NUMBER: { stack.push(value); break ; } case TOKEN_OPERATOR2: { [n1, n2] = stack.splice(-2 , 2 ); const f = binaryMap[value]; if (value === '&&' ) { 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: { const v = stack.pop() values[value] = v; } default : { ... break } } } return stack[0 ] }
经得出最终结果符合预期,其他的运算符根据先后顺序可以推算出来这里就不做赘述。
复杂数据声明 声明var foo = { bar: 1 }
对于目前的就是一个解析格式与解析时读取的问题。首先从 tokenize 阶段,添加相应的标识符{
}
:
,
[
]
,数组同理。
token-stream 语法分析,生成 token 的处理逻辑;
parser 进行语义分析
calc 解释执行
export default class TokenStream { pos = 0 ; current: null | TypeToken = null ; constructor (public expression: string ) {} 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 ); }; isOperator = (): boolean => { const str = this .getSomeCode(Infinity ); let result: string | undefined ; if (operatorReg.test(str)) { result = execFactoryReg(operatorReg, str); } else if (unaryMapReg.test(str)) { result = execFactoryReg(unaryMapReg, str); } 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 解析器里添加相应的语义分析逻辑
export default Parser { parseExpression = (instr: TypeInstruction[]): void => { const exprInstr: TypeInstruction[] = [] this .parseMultipleEvaluation(exprInstr) exprInstr.forEach(exp => (instr.push(exp))) } parseMultipleEvaluation = (exprInstr: TypeInstruction[]): void => { this .parseAssignmentExpression(exprInstr) while (this .accept(TOKEN_COMMA, ',' )) { this .parseConditionalExpression(exprInstr) } } 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)) { exprInstr.push(new Instruction(INSTR_PLAIN, this .current.value)); } else if (this .accept(TOKEN_PAREN, '(' )) { this .parseExpression(exprInstr); this .assert(TOKEN_PAREN, ')' ); } else if (this .accept(TOKEN_SQUARE, '[' )) { this .parseArrayLiteralDeclaration(exprInstr) } else if (this .accept(TOKEN_CURLY, '{' , false )) { this .parseObjectLiteralDeclaration(exprInstr) } else if (this .accept(TOKEN_VAR, ['const' , 'var' , 'let' ])) { 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[] ) => { 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)) { const key = this .current.value this .assert(TOKEN_OPERATOR, ':' ); instr[key] = []; if (this .accept(TOKEN_CURLY, '{' , false )) { this .parseObjectLiteralDeclaration(instr[key]) } else { this .parseExpression(instr[key]); } this .accept(TOKEN_COMMA, ',' ); } 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" } ] function calc (instr, value, statis = false ) { 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)}
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(); if (nextToken.type !== TOKEN_NAME) { this .parseError('function definition should have function name' ) return false } return true ; } return false } 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)) { const funcName = this .current.value; const instr = []; if (this .accept(TOKEN_PAREN, '(' ) && !this .accept(TOKEN_PAREN, ')' )) { 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
上面。
function calc ( tokens: Instruction<any>[], values = Object.create(null ), statis = false , scope = Object .create (null ) // 当前作用域 ) { 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)) 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 ); this ._scope = merge(mapToObject(args), _scope); Object .setPrototypeOf(this , new .target.prototype); } updateScope = (scope: any[] ) => { 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) } }
调用函数的解析较为简单
parseOuterFunctionCallExpression = (exprInstr: TypeInstruction[] ) => { this .parseMemberAccessExpression(exprInstr) if (this .current.type === TOKEN_NAME && this .accept(TOKEN_PAREN, '(' , false )) { 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); count++; } while (this .accept(TOKEN_COMMA)) } exprInstr.push(new Instruction(INSTR_FUNCALL, count)) } } } case INSTR_FUNCALL: { const args = stack.splice(-value, value); fn = stack.pop(); if (fn.value instanceof CustomFunc) { fn.value.updateScope(args) stack.push(fn.value.invokeBody()) } else if (typeof fn === 'function' ) { 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;
在实现中是以读取上下文中的属性拿到值,再逐个拿到
parseMemberAccessExpression = (exprInstr: TypeInstruction[]): void => { this .parseField(exprInstr); while (this .accept(TOKEN_OPERATOR, "." ) || this .accept(TOKEN_SQUARE, "[" )) { if (this .current.value === "." ) { this .assert(TOKEN_NAME); 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" } ]; case INSTR_MEMBER: { if (!value) { [n1, n2] = stack.splice(-2 , 2 ); stack.push(n1[n2]); break } n1 = stack.pop(); stack.push(n1[value]) break }
读是没有问题的,问题出在写,LeftHandSide 的左侧只能是一个引用。
obj.arr[1 ] = 1 [1 , 2 , 3 ][1 ] = 1 ;
正常情况在原生拿不到引用地址,换个思路转成以下写法
[Reference, "bar" ][1 ] = 1 ;
上代码
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 ]; const keys = calc(value, values, true , scope); 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 } 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 获得更好的阅读体验.
参考文献:深入浅出理解有限状态机/云峰小罗 ;