跳转到正文

值转换

每次跨越 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 中的数组必须使用连续的整数索引空间。

PHP
$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 中:

PHP
$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。

PHP
$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 内进行转换:

PHP
$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 或 [] 的类型都无法往返转换。