值转换
每次跨越 PHP↔JS 边界的调用——setGlobal、call,以及从 JS 调用的宿主函数——都会经过转换器:
- PHP → JS:宿主值流入引擎时(
setGlobal()、call()参数、宿主函数返回值)。 - JS → PHP:JS 值流出时。这里有两种规则略有不同的路径:
eval()和call()的结果,以及 JS 传给宿主函数的参数。
转换会复制数据。基本类型映射为基本类型,数组和对象会复制到新的结构中,PHP 可调用对象会包装成 JS 函数。除可调用对象本身外,不会通过引用共享任何内容。
PHP → JS
| PHP 值 | JS 结果 |
|---|---|
null |
null |
true / false |
true / false |
int |
number(IEEE 754 双精度;超过 2^53 的整数会丢失精度) |
float |
number(保留 NaN、±Infinity、±0) |
string |
string(将 UTF-8 PHP 字节解释为 UTF-16 代码单元),即使它命名了一个 PHP 函数 |
列表 array(键按顺序为 0、1、2、…) |
新的 Array,元素递归转换 |
其他任何 array |
新的普通 Object,值递归转换 |
Closure、可调用对象、可调用数组(如 [$object, 'method']) |
调用 PHP 可调用对象的 function |
其他对象、Generator、资源 |
undefined |
混合键数组(PHP 的 [0 => 'a', 'name' => 'b'])会变成普通对象,因为 JS 中的数组必须使用连续的整数索引空间。
$engine->setGlobal('list', [1, 2, 3]); // → JS Array
$engine->setGlobal('map', ['a' => 1, 'b' => 2]); // → JS Object
$engine->setGlobal('mixed', [0 => 'x', 'k' => 'v']); // → JS Object
由于数组会被复制,JS 中的更改会留在 JS 中:
$list = [1, 2, 3];
$engine->setGlobal('list', $list);
$engine->eval('list.push(4)');
count($list); // 3
$engine->eval('list.length'); // 4
不可调用的 PHP 对象无法转换;它们会以 undefined 的形式到达(问题 #18)。请先将其转换为数组,例如使用 get_object_vars(),或将其方法公开为闭包。参见 共享对象。
JS → PHP:eval() 和 call() 结果
| JS 值 | PHP 结果 |
|---|---|
undefined、null |
null |
boolean |
bool |
number(整数且在 int 范围内) |
int(-0 变为 0) |
NaN、Infinity、-Infinity |
NAN、INF、-INF |
number(其他) |
float |
string |
string(UTF-8) |
bigint、symbol |
null |
function |
null |
Array |
列表 array,元素递归转换 |
TypedArray |
数字列表 array |
其他对象(普通对象、Error、Date、Map、Set、Promise、ArrayBuffer、…) |
由其自身数据属性组成的关联式 array,跳过访问器和函数值属性 |
最后一行表示,状态存储在内部槽中的对象会转换为空数组:new Date(0)、new Map([["a", 1]]) 和 Promise.resolve(5) 都会返回 []。Error 会返回其自身的 message 和 stack。自引用对象会将其内部引用转换为 null。
$engine->eval('[1, 2, 3]'); // [1, 2, 3]
$engine->eval('({a: 1, b: 2})'); // ['a' => 1, 'b' => 2]
$engine->eval('new Uint8Array([1, 2, 3])'); // [1, 2, 3]
$engine->eval('1234567890123456789n'); // null
$engine->eval('new Date(0)'); // []
需要在 PHP 中使用上述值时,请在 JS 内进行转换:
$engine->eval('(1234567890123456789n).toString()'); // "1234567890123456789"
$engine->eval('new Date(0).toISOString()'); // "1970-01-01T00:00:00.000Z"
$engine->eval('Object.fromEntries(new Map([["a", 1]]))'); // ['a' => 1]
JS → PHP:宿主函数参数
JS 传给 PHP 宿主函数的参数,对基本类型、数组和普通对象遵循相同规则,但有以下区别:
| JS 值 | PHP 参数 |
|---|---|
TypedArray、ArrayBuffer、DataView |
引擎自身的 JsValue 对象,不转换 |
| 对象 | 包含其所有自有属性的关联式 array,包括访问器(会运行其 getter)和函数值属性 |
| 箭头函数 | 一个 array(通常为空) |
使用 function 关键字声明的 function,或任何引用自身的对象 |
不转换:复制过程永远不会结束,PHP 会耗尽内存(问题 #21) |
目前无法在宿主函数中接收 JS 回调。跨越此边界时请传递数据,而不是函数。
数字与精度
JavaScript 数字是 IEEE 754 双精度数。PHP 整数使用平台原生格式(所有现代构建均为 64 位)。转换可精确保留 ±2^53 范围内的整数;更大的 PHP 整数会变为 number,超过 16 位十进制数字后会丢失精度。如果需要对更大的整数进行精确计算,请在 JS 中使用 BigInt,并将结果作为字符串传出。
字符串
PHP 字符串是字节数组;JS 字符串是 UTF-16 代码单元序列。引擎将 PHP 字节解释为 UTF-8,再在内部重新编码为 UTF-16,以此完成转换。对于有效的 UTF-8 输入,这一过程不会丢失数据。无效 UTF-8 字节会原样传递,不会替换为 U+FFFD。
String.length、索引访问(s[0])和切片都按照 ECMAScript 规范以 UTF-16 代码单元计数,而不是码点或字节。因此,包含 '🍕' 这类扩展平面字符的字符串会有 .length === 2(即 U+1F355 的两个代理项)。
往返稳定性
普通数据经过往返转换后会保持不变:由标量、列表和关联数组组成的 PHP 值经由 setGlobal() 传入,再通过 eval() 或 call() 返回,会得到相同的结构;但整数浮点数会以 int 的形式返回(1.0 返回 1)。
上表中其他映射到 null、undefined 或 [] 的类型都无法往返转换。