
# 值转换

每次跨越 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](https://github.com/inline0/phasis/issues/18)）。请先将其转换为数组，例如使用 `get_object_vars()`，或将其方法公开为闭包。参见 [共享对象](https://phasis.inline0.dev/zh/docs/interop/shared-objects)。

## 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](https://github.com/inline0/phasis/issues/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` 或 `[]` 的类型都无法往返转换。
