知序
返回
保存草稿
发布
H1
H2
H3
1
2
3
Excalidraw 图
AI 写作
AI 优化
目录
输入内容后显示目录
Markdown
# 04 - Value 类型系统 > 这篇解决什么问题:上一篇你发现 `eval` 返回的永远是一个叫 `Value` 的东西——`asInt()` 能取出数字,但它到底是什么、还能转成什么?跨语言的 Java 值和 JS 值到底怎么互相转换?如果你不管类型直接 `asInt()`,会不会炸?这篇把 `Value` 这套类型系统讲透:为什么跨语言边界的一切值都必须是 `Value`、怎么用 `isXxx`/`hasXxx`/`canXxx` 判断类型、怎么用 `asXxx` 安全转换、数组/对象/函数三类复合值怎么操作。 > 读完你会知道:`Value` 作为"跨语言值句柄"的本质、从基本类型到数组/对象/函数的完整判断与转换方法、为什么链式操作如此常见、以及"先判断后转换"这条安全铁律能帮你避开哪些坑。 --- ## 1. Value 是什么:跨语言的"值句柄" ### 1.1 一句话定义 **`Value` 是 GraalVM Polyglot API 里,Java 和 JS 之间传递任何值的统一包装**。无论是数字、字符串、布尔、null、数组、对象还是函数,只要它**跨过了语言边界**,在 Java 侧拿到手的一定是一个 `Value`,然后再按需转换成 Java 类型。  *图:Value 类型系统:判断与转换* ``` JS 侧 Java 侧 42 ──eval──▶ Value(42) ──asInt()──▶ 42 'hello' ──eval──▶ Value('hello') ──asString()──▶ "hello" [1,2,3] ──eval──▶ Value(Array) ──getArrayElement(0)──▶ Value(1) ({name:'A'}) ──eval──▶ Value(Object) ──getMember("name")──▶ Value("A") (x)=>x+1 ──eval──▶ Value(Function) ──execute(3)──▶ Value(4) ``` **为什么会这样设计?** 因为两条语言不能直接"握手"——Java 不能直接持有一个 JS 对象,JS 也不能直接持有 Java 对象。跨语言边界需要一个共同的、能被两边识别的使者,`Value` 就是这个"护照"或"句柄":**值本身可能活在另一边,你手上这个 `Value` 是对它的描述/代理**。 > 一句话:**只要值穿过语言边界,落到 Java 侧就是 `Value`;具体是什么、怎么用,靠一套判断和转换方法去问它。** ### 1.2 最核心的安全铁律:先判断,后转换 `asInt()` 本质是一种**强转**:如果这个 `Value` 实际上不是数字,直接 `asInt()` 会抛异常(比如 `ClassCastException`)。所以标准、安全的用法永远是两步走: ``` 先 isXxx() 判断它是什么 → 再 asXxx() 转成目标 Java 类型 ``` ```text 危险:asInt() 裸转 → Value('hello').asInt() → 抛异常 安全:isNumber() 先判断 → isNumber ? asInt() : 走兜底 ``` 这条铁律会贯穿你在 GraalJS 里读值的所有代码,第 3 节每个复合类型的操作都以它为底色。 > ⚠️ `asInt()` 是**强转**:如果 Value 实际不是数字,会抛异常(如 `ClassCastException`)。所以标准用法是先用 `isXxx()` 判断,再 `asXxx()` 转换。 > 一句话:**跨语言读值的铁律 = 先 `isXxx`/`hasXxx`/`canXxx` 判断,再 `asXxx` 转换,二者成对使用才安全。** --- ## 2. 判断与转换速查表 这套 API 分两个维度:**判断**(问它是什么)和**转换**(把它变成 Java 类型)。 ### 2.1 判断类方法(is / has / can 三族) | Java 侧方法 | 判断/操作 | JS 侧类型 | |---|---|---| | `isNull()` | 判断是否为 null/undefined | `null` / `undefined` | | `isNumber()` | 判断是否为数字 | `number` | | `isString()` | 判断是否为字符串 | `string` | | `isBoolean()` | 判断是否为布尔 | `boolean` | | `hasArrayElements()` | 判断是否为数组 | `Array` | | `hasMembers()` | 判断是否为对象(有成员) | `Object` | | `canExecute()` | 判断是否为函数(可调用) | `Function` | | `canInstantiate()` | 判断是否为构造函数 | `class` / 构造函数 | 注意命名的规律:基本类型用 `isXxx()`,数组用 `hasArrayElements()`(因为要"有下标元素"才算)、对象用 `hasMembers()`(因为要"有成员")、函数用 `canExecute()`(因为重点是"可调用")。**判断方法的名字,其实就是在描述该类型最关键的操作能力**。 ### 2.2 转换类方法(asXxx + 复合值操作) | Java 侧方法 | 用途 | |---|---| | `asInt()` / `asDouble()` / `asLong()` | 数字 → Java 数值类型 | | `asString()` | 字符串 → Java `String` | | `asBoolean()` | 布尔 → Java `boolean` | | `getArraySize()` / `getArrayElement(i)` | 数组 → 长度 / 取元素 | | `getMember(name)` / `putMember(name, v)` | 对象 → 取/设成员 | | `execute(args...)` | 函数 → 调用 | ```text 类型判断矩阵: isNumber() ◀── Value(42) isString() ◀── Value('hello') isBoolean() ◀── Value(true) isNull() ◀── Value(null) hasArrayElements() ◀── Value([1,2,3]) hasMembers() ◀── Value({name:'Alice'}) canExecute() ◀── Value((x,y)=>x+y) ``` > 一句话:**判断靠 `isXxx`/`hasXxx`/`canXxx` 三族问"它是什么",转换靠 `asXxx` + 数组/对象/函数专用方法把它变成 Java 能用的形态。** --- ## 3. 逐段跑起来:六类值怎么判断与转换 ### 3.1 基本类型:数字可以多角度转换 ```java Value intVal = context.eval("js", "42"); System.out.println("42 -> asInt()=" + intVal.asInt() + " asDouble()=" + intVal.asDouble() + " isNumber=" + intVal.isNumber()); // 42 -> asInt()=42 asDouble()=42.0 isNumber=true ``` 同一个 `Value(42)` 既能 `asInt()` 得到 `42`,也能 `asDouble()` 得到 `42.0`,且 `isNumber()` 为 `true`。**这背后的语义是**:JS 里的 `number` 并不区分整数和浮点(不像 Java 有 `int`/`long`/`double` 之分),跨过来后它是一个"数字值",具体以哪种 Java 数值类型取出来,**由你决定**。这一点是跨语言类型系统最需要适应的差异。 ``` 42 -> asInt()=42 asDouble()=42.0 isNumber=true ``` ### 3.2 字符串与布尔 ```java Value strVal = context.eval("js", "'hello'"); System.out.println("'hello' -> asString()=" + strVal.asString() + " isString=" + strVal.isString()); // 'hello' -> asString()=hello isString=true Value boolVal = context.eval("js", "true"); System.out.println("true -> asBoolean()=" + boolVal.asBoolean()); // true -> asBoolean()=true ``` `asString()` / `asBoolean()` 与 `isString()` / `isBoolean()` 一一对应,最朴素的一对。 ``` 'hello' -> asString()=hello isString=true true -> asBoolean()=true ``` ### 3.3 null 的判断 ```java Value nullVal = context.eval("js", "null"); System.out.println("null -> isNull()=" + nullVal.isNull()); // null -> isNull()=true ``` JS 的 `null`(以及 `undefined`)在 Java 侧用 `isNull()` 判断。注意:这里的转换没有对应方法——**null 不需要"转换"**,判断它就够了。所以 `isNull()` 是"判断后无 asXxx 可转"的典型代表。 ``` null -> isNull()=true ``` > 一句话:**基本类型里,数字是"一个数字值可多角度取",字符串/布尔一一对应转,null 只需判断无需转换。** ### 3.4 数组:getArraySize + getArrayElement ```java Value arr = context.eval("js", "[1, 2, 3]"); System.out.println("数组 -> size=" + arr.getArraySize() + " [0]=" + arr.getArrayElement(0).asInt() + " hasArrayElements=" + arr.hasArrayElements()); // 数组 -> size=3 [0]=1 hasArrayElements=true ``` JS 数组在 Java 侧的完整操作:`hasArrayElements()` 判断"是不是数组",`getArraySize()` 取长度,`getArrayElement(i)` 按下标取第 i 个元素。**注意取回来的元素仍然是 `Value`**——所以示例里 `getArrayElement(0)` 还要再补一个 `.asInt()` 才能当数字用。 ``` 数组 -> size=3 [0]=1 hasArrayElements=true ``` ### 3.5 对象:getMember 取成员 ```java Value obj = context.eval("js", "({name: 'Alice', age: 30})"); System.out.println("对象 -> name=" + obj.getMember("name").asString() + " age=" + obj.getMember("age").asInt() + " hasMembers=" + obj.hasMembers()); // 对象 -> name=Alice age=30 hasMembers=true ``` JS 对象用 `hasMembers()` 判断"是不是对象",`getMember("name")` 按成员名取值——**等价于 JS 里的 `obj.name`**。同样,取回来的成员是 `Value`,`asString()` / `asInt()` 再收口。 ``` 对象 -> name=Alice age=30 hasMembers=true ``` ### 3.6 函数:canExecute + execute ```java Value fn = context.eval("js", "(x, y) => x + y"); System.out.println("函数 -> canExecute=" + fn.canExecute() + " execute(3,4)=" + fn.execute(3, 4).asInt()); // 函数 -> canExecute=true execute(3,4)=7 ``` 函数用 `canExecute()` 判断"是否可调用",`execute(3, 4)` 传入参数调用,返回的**仍是 `Value`**,再 `asInt()`。 ``` 函数 -> canExecute=true execute(3,4)=7 ``` > 一句话:**数组用 size+element、对象用 member、函数用 execute,三类复合值取出的东西永远是 `Value`,要点收尾再做 asXxx。** --- ## 4. 两个贯穿始终的设计,值得停下来想 ### 4.1 为什么 getArrayElement / getMember / execute 返回的还是 Value 这是 Polyglot API 的一条铁律:**只要跨语言边界,返回的就一定是 `Value`**。原因是第 1.1 节说的——Java 不能直接持有 JS 对象,所以每一个跨语言操作的结果,都仍然是个"对 JS 侧值的句柄",而不是原生 Java 对象。 这条规则带来一个非常常见、也很舒服的写代码方式:**链式操作**。因为每一步返回的都是 `Value`,你可以一路点下去直达目标: ```java context.eval("js", "({list: [10, 20, 30]})") .getMember("list") // Value(Array) .getArrayElement(1) // Value(20) .asInt(); // 20 ``` `eval` 出对象 → 取 `list` 成员 → 取下标 1 的元素 → 转成 `int`,一路到底,中间没有类型断裂。 > 💬 **Q:为什么 getArrayElement / getMember / execute 返回的还是 `Value`?** > > 因为**只要跨语言边界,就一定是 `Value`**——这是 Polyglot API 的规则:Java 不能直接持有 JS 对象,JS 也不能直接持有 Java 对象。`Value` 是唯一的"护照"。这也正是"链式操作非常常见"的根因:每一步都返回 `Value`,可以一路点下去。 ### 4.2 asInt() 的"有损转换"边界:哪些能转、哪些会炸 `asInt()` 并不是只有"完全匹配"才能转,它做的是**按数值语义转换**: | 情况 | 行为 | |---|---| | `Value(42).asInt()` | OK,得 42 | | `Value(42.5).asInt()` | **有损但允许**,取整得 42 | | `Value('hello').asInt()` | **抛异常**(根本不是数字) | 所以分清楚两件事:**数值内的精度损失是允许的**(42.5 → 42),**类型不对则是报错**(字符串不能当数字)。生产代码要避开后者,靠第 1.2 节的铁律——先 `isNumber()` 判断,再 `asInt()`。 > 💬 **Q:`asInt()` 把数字 42 取成 `int`,如果值是 42.5 会怎样?** > > 数字转换是**按数值语义**进行的:`Value(42.5).asInt()` 会取整得到 42(有损但允许);如果 JS 值根本不是数字(如 `'hello'`),`asInt()` 会抛异常。所以生产代码要遵循「先 `isNumber()` 判断、再 `asInt()` 转换」。 > 一句话:**每步返回 `Value` 让链式调用成为常态;`asInt` 对数字有损取整但允许、对非数字直接抛错,所以先判断再转换。** --- ## 5. 再深一层:Value 的两个容易被忽略的语义 把 `Value` 用熟之后,还有两处"看不见的设计"值得点破——它们决定了你不会在细节上翻车。 ### 5.1 用 hasMembers / getMember 读对象,绕不开"JS 属性名语义" `getMember("name")` 读的成员名,走的是**JS 的属性语义**。这带来一个和 Java 的差异:读不存在的成员时,JS 会返回 `undefined`(对应 `isNull()` 判 `true`),而不是抛异常。所以要判断"这个对象到底有没有某个 key",别靠 try-catch,要用"先 `hasMembers()` 再 `getMember(name)` 再 `isNull()` 判断"的组合——这是 JS 语义在跨语言侧的忠实映射。 > ⏳ 顺带提醒:原型链上的属性是否被 `getMember` 读到,涉及"只看自有属性还是连继承的一起算"的语义差异,这个留给第 07 篇判断题去深究。这里记住"JS 语义决定返回值是 `undefined` 而非抛错"就够立住心智。 ### 5.2 基本值的 `Value` 是"立即算出来"的,复合值是"引用式"的 同样是 `Value`,内部有本质差别: | 值类型 | Java 侧持有的 `Value` 背后是什么 | 含义 | |---|---|---| | 基本值(数字/字符串/布尔/null) | 一个**拷贝出来的标量** | 取值即取回,和 JS 侧再无瓜葛 | | 复合值(数组/对象/函数) | 一个**指向 JS 侧对象的句柄(引用)** | 你改的是"同一份对象",会反映回 JS 侧 | 这意味着:基本值你随便取、随便改,不影响 JS;但复合值你通过 `putMember` 往里写,JS 那边能看到——这正是第 07 篇"数据共享"能成立的根本原因。**提前意识到"复合值 Value 是引用"这一点,能让你在遇到"Java 改了一处、JS 跟着变"时毫不意外**。 > 一句话:**读对象成员用 JS 属性语义(缺员返回 undefined 而非抛错);基本值 Value 是拷贝、复合值 Value 是引用——后者是跨语言数据共享成立的前提。** --- ## 6. 本篇小结 ```text ┌──────────────────────────────────────────────────────┐ │ 04 Value 类型系统 要带走的话 │ ├──────────────────────────────────────────────────────┤ │ 1. 跨语言边界一切值都是 Value(唯一护照/句柄) │ │ 2. 铁律 = 先 isXxx/hasXxx/canXxx 判断再 asXxx 转换 │ │ 3. 数字 = 一个数字值可多角度取(int/double) │ │ 4. 数组 size/element、对象 member、函数 execute │ │ 5. 每步返回 Value → 链式调用是常态 │ │ 6. asInt 有损取整允许、类型不对抛错 │ └──────────────────────────────────────────────────────┘ │ v 已经能"安全读回任何 JS 值",下一步把 Java 数据送进 JS 算 ──→ 下一篇:05 - Java 调用 JS ``` ## 思考题(自测) 1. `Value(42).asInt()` 与 `Value(42.5).asInt()` 行为有何不同?JS 的 `number` 在 Java 侧为什么"可整数可浮点"? 2. 如果 `asInt()` 失败抛异常,除了 try-catch,有没有更优雅的判断方式?(提示:先 isXxx 再 asXxx) 3. `getMember` 能拿到"JS 原型链上的属性"吗?这和 JS 的 `obj.name` 语义一致吗?如果不一致,跨语言读对象有哪些要注意?
预览
发布文章
添加摘要让读者更快了解文章内容
取消
确认发布
AI 写作助手
输入一个主题,AI 将为你生成一篇 Markdown 博客文章
AI 正在写作中...
取消
开始生成
AI 优化助手
描述你的修改需求,AI 将在原文基础上进行补充或微调
AI 正在优化中...
取消
开始优化
画布
从已有图里选
✕
图名
保存后生成 /images/excalidraw/<图名>.svg(只能用字母、数字、短横线、下划线、点)
取消
保存并插入