普通视图

发现新文章,点击刷新页面。
昨天 — 2026年4月1日首页

我把 Claude Code 里的隐藏彩蛋提取出来了——零依赖的 ASCII 虚拟宠物系统

作者 Novlan1
2026年4月1日 22:49

前言

最近泄漏的 Claude Code(一款 AI 编程工具)源码中有 Buddy 系统,一个虚拟宠物伙伴(Buddy)系统(愚人节彩蛋)。每个用户都会根据自己的 ID 确定性地生成一只独一无二的 ASCII 小宠物,它会在终端里陪你写代码,你还可以摸它的头。

原始实现和 React/Ink、Bun 运行时深度耦合,没法单独拿出来用。于是我把核心逻辑提取出来,重新实现为一个零运行时依赖三端通用(终端 / 浏览器 / Node.js)的独立包——buddy-sprite

    __
  <(· )___
   (  ._>
    `--´
  Quackers ★★★

在线体验:novlan1.github.io/buddy-sprit…

转存失败,建议直接上传图片文件

它能做什么?

一句话概括:给你的终端或网页加一只 ASCII 宠物

核心特性:

  • 🎲 确定性生成:相同的 userId 永远生成相同的宠物,就像区块链地址生成头像一样
  • 🐣 18 种物种:鸭子、猫、龙、幽灵、六角恐龙、机器人、水豚、仙人掌……
  • 🎨 丰富的外观:6 种眼睛样式 × 8 种帽子(皇冠、巫师帽、螺旋桨帽……)
  • 稀有度系统:普通(60%) → 罕见(25%) → 稀有(10%) → 史诗(4%) → 传说(1%)
  • 📊 5 项属性:调试力、耐心、混乱、智慧、毒舌
  • 闪光变体:1% 概率获得闪光宠物
  • 💬 对话气泡:带自动淡出动画
  • 🤚 摸头互动:浮动爱心动画
  • 😴 待机动画:眨眼、小动作、呼吸
  • 📦 零依赖:纯 TypeScript,打包后约 14KB

技术实现

1. 确定性生成:从字符串到宠物

整个生成系统的核心思想是:一个字符串输入,确定性地输出一只完整的宠物

userId + salt → FNV-1a 哈希 → Mulberry32 PRNG 种子 → 宠物属性

FNV-1a 哈希

首先用 FNV-1a 算法把字符串转成一个 32 位整数:

function hashString(input: string): number {
  let hash = 2166136261
  for (let i = 0; i < input.length; i++) {
    hash ^= input.charCodeAt(i)
    hash = Math.imul(hash, 16777619)
  }
  return hash >>> 0
}

FNV-1a 的优点是实现简单、分布均匀、碰撞率低,非常适合这种场景。

Mulberry32 伪随机数生成器

有了种子之后,用 Mulberry32 算法生成伪随机数序列:

function mulberry32(seed: number): () => number {
  let state = seed >>> 0
  return function nextRandom() {
    state |= 0
    state = (state + 0x6d2b79f5) | 0
    let temp = Math.imul(state ^ (state >>> 15), 1 | state)
    temp = (temp + Math.imul(temp ^ (temp >>> 7), 61 | temp)) ^ temp
    return ((temp ^ (temp >>> 14)) >>> 0) / 4294967296
  }
}

Mulberry32 是一个周期为 2³² 的 PRNG,质量远超简单的 LCG,但实现只有几行代码。关键是它是确定性的——相同的种子永远产生相同的随机数序列。

加权稀有度抽取

稀有度的抽取使用加权随机:

const RARITY_WEIGHTS = { common: 60, uncommon: 25, rare: 10, epic: 4, legendary: 1 }

function rollRarity(rng: () => number): Rarity {
  const totalWeight = Object.values(RARITY_WEIGHTS).reduce((sum, w) => sum + w, 0)
  let roll = rng() * totalWeight
  for (const rarity of RARITIES) {
    roll -= RARITY_WEIGHTS[rarity]
    if (roll < 0) return rarity
  }
  return 'common'
}

传说级宠物只有 1% 的概率,但如果你的 userId 命中了,那它就是永远的传说。

2. ASCII 精灵帧动画

每种物种都有 3 帧 ASCII 动画,通过模板中的 {E} 占位符来替换不同的眼睛样式:

const BODIES = {
  cat: [
    ['            ', '   /\\_/\\    ', '  ( {E}   {E})  ', '  (  ω  )   ', '  (")_(")   '],
    ['            ', '   /\\_/\\    ', '  ( {E}   {E})  ', '  (  ω  )   ', '  (")_(")~  '],
    ['            ', '   /\\-/\\    ', '  ( {E}   {E})  ', '  (  ω  )   ', '  (")_(")   '],
  ],
  // ... 18 种物种
}

帽子作为独立的装饰行叠加在精灵顶部:

const HAT_LINES = {
  crown: '   \\^^^/    ',
  wizard: '    /^\\     ',
  halo: '   (   )    ',
  // ...
}

渲染时只需要简单的字符串替换和行拼接,没有任何 Canvas 或 DOM 操作。

3. 动画状态机:BuddyEngine

BuddyEngine 是整个系统的核心,它是一个基于 tick 的动画状态机:

┌─────────────┐
│  Idle State  │ ← 默认状态,播放待机序列
│  [0,0,0,0,  ││   1,0,0,0,  │   ← -1 = 眨眼, 1/2 = 小动作帧│  -1,0,0,2,  ││   0,0,0]    │
└──────┬──────┘
       │ speak() / pet()
       ▼
┌─────────────┐
│ Active State │ ← 快速循环所有帧
│  tick % 3   │
└──────┬──────┘
       │ 超时
       ▼
┌─────────────┐
│  Fade State  │ ← 气泡淡出
└──────┬──────┘
       │
       ▼
     回到 Idle

每个 tick(500ms)引擎会计算当前帧数据,然后通过回调发出一个 BuddyFrame 对象:

interface BuddyFrame {
  spriteLines: string[]        // ASCII 精灵行
  name: string                 // 宠物名字
  face: string                 // 单行脸部表情
  bubbleLines: string[] | null // 对话气泡文字
  isBubbleFading: boolean      // 气泡是否正在淡出
  isPetting: boolean           // 是否正在显示爱心
  colorKey: ColorKey           // 稀有度颜色键名
}

这个设计的精妙之处在于:引擎只负责计算状态,不负责渲染。渲染完全交给适配器层。

4. 适配器模式:一套引擎,多端渲染

                    ┌──────────────────┐
                    │   BuddyEngine    │
                    │  (动画状态机)     │
                    └────────┬─────────┘
                             │ BuddyFrame
              ┌──────────────┼──────────────┐
              ▼              ▼              ▼
     ┌────────────┐  ┌────────────┐  ┌────────────┐
     │  Terminal   │  │  Browser   │  │  Custom    │
     │  Adapter    │  │  Adapter   │  │  Adapter   │
     │ (ANSI码)    │  │ (DOM/CSS)  │  │ (自定义)    │
     └────────────┘  └────────────┘  └────────────┘
  • 终端适配器:使用 ANSI 转义码实现颜色和光标控制,原地刷新避免闪烁
  • 浏览器适配器:创建 DOM 元素,用 CSS transition 实现淡入淡出
  • 自定义适配器:直接消费 BuddyFrame,想怎么渲染都行

快速上手

安装

npm install buddy-sprite

终端使用

import { assembleCompanion } from 'buddy-sprite'
import { createTerminalBuddy } from 'buddy-sprite/terminal'

const companion = assembleCompanion('your-user-id', {
  name: 'Quackers',
  personality: '一个热爱调试的快乐小伙伴',
  hatchedAt: Date.now(),
})

const buddy = createTerminalBuddy(companion)
buddy.start()
buddy.speak('嘎嘎!你好呀!')
buddy.pet()  // 摸头,会出现爱心动画

// 不用时记得销毁
// buddy.destroy()

浏览器使用

<div id="buddy-container"></div>
<script type="module">
  import { assembleCompanion } from 'buddy-sprite'
  import { createBrowserBuddy } from 'buddy-sprite/browser'

  const companion = assembleCompanion('your-user-id', {
    name: 'Quackers',
    personality: '快乐',
    hatchedAt: Date.now(),
  })

  const buddy = createBrowserBuddy(
    document.getElementById('buddy-container'),
    companion,
  )
  buddy.start()
  buddy.speak('来自浏览器的问候!')
</script>

自定义渲染

如果你想在其他环境(比如 Electron、游戏引擎、甚至硬件终端)中使用,可以直接消费引擎输出:

import { BuddyEngine, assembleCompanion } from 'buddy-sprite'

const companion = assembleCompanion('your-user-id', {
  name: 'Quackers',
  personality: '快乐',
  hatchedAt: Date.now(),
})

const engine = new BuddyEngine(companion, (frame) => {
  // frame.spriteLines — ASCII 精灵行
  // frame.bubbleLines — 对话气泡文字
  // frame.isPetting   — 是否正在显示爱心
  // 你可以用任何方式渲染这些数据
  console.clear()
  console.log(frame.spriteLines.join('\n'))
})
engine.start()

架构总览

buddy-sprite/
├── src/
│   ├── index.ts          # 统一导出
│   ├── types.ts          # 类型定义(稀有度、物种、眼睛、帽子、属性)
│   ├── sprites.ts        # 18 种物种的 ASCII 精灵帧数据 + 渲染函数
│   ├── companion.ts      # 宠物生成(PRNG、哈希、稀有度抽取、属性生成)
│   ├── engine.ts         # BuddyEngine — 动画状态机(核心)
│   └── adapters/
│       ├── terminal.ts   # 终端适配器(ANSI 转义码)
│       └── browser.ts    # 浏览器适配器(DOM 渲染)
└── examples/
    ├── terminal-demo.ts  # 终端演示脚本
    └── browser-demo.html # 浏览器演示页面(直接打开,无需构建)

整个项目的设计遵循几个原则:

  1. 零依赖:不依赖任何第三方包,纯 TypeScript 实现
  2. 确定性:相同输入永远产生相同输出,没有任何随机性
  3. 关注点分离:引擎只管状态计算,适配器只管渲染
  4. 三端通用:ESM + CJS + DTS,终端/浏览器/Node.js 都能用

一些有趣的设计细节

为什么用 FNV-1a + Mulberry32?

在浏览器和 Node.js 中,Math.random() 不支持种子。我们需要一个确定性的随机数生成器,而且要足够轻量。FNV-1a 哈希 + Mulberry32 PRNG 的组合总共不到 20 行代码,但质量足够好,分布均匀,周期为 2³²。

为什么属性有"峰值"和"低谷"?

每只宠物会随机选一个"峰值属性"和一个"低谷属性",其余属性随机分布。这让每只宠物都有自己的"性格"——比如一只高混乱低耐心的猫,或者一只高智慧低毒舌的猫头鹰。比起所有属性都差不多的平庸分布,这种设计更有趣。

为什么帽子只给非普通稀有度?

普通宠物(60% 概率)没有帽子,这让帽子成为一种"稀有标志"。当你看到一只戴着皇冠的传说级龙,那种感觉是不一样的。

总结

buddy-sprite 是一个有趣的小项目,它展示了几个值得关注的技术点:

  • 确定性随机生成:FNV-1a + Mulberry32 的组合在前端场景中非常实用
  • ASCII 帧动画:用最简单的字符串操作实现流畅的动画效果
  • 引擎-适配器分离:一套核心逻辑,多端渲染,这种架构模式值得借鉴
  • 零依赖设计:在依赖爆炸的前端生态中,保持克制是一种美德

如果你觉得有趣,欢迎 Star ⭐ 和 PR:


本文项目灵感来源于 Claude Code 泄漏源码中隐藏的 Buddy 系统。原始实现使用 React + Ink,buddy-sprite 是一个全新的独立实现。

前端转后端基础- 变量和类型

2026年4月1日 20:38

PHP、Go、JavaScript三种语言在变量和类型相关的核心语法对照。

一、变量声明和赋值

PHP 变量语法

<?php
// PHP变量以$符号开头
$name = "张三";
$age = 25;
$height = 175.5;
$isStudent = true;
$score = null;

// 变量命名规则
$firstName = "李";           // 驼峰命名
$last_name = "四";           // 下划线命名
$UserName = "admin";         // 大驼峰命名
$_privateVar = "secret";     // 私有变量
$MAX_VALUE = 100;            // 常量风格

// 动态类型 - 变量类型可以改变
$var = 10;                   // 整数
$var = "hello";              // 字符串
$var = 3.14;                 // 浮点数
$var = [1, 2, 3];            // 数组
$var = new stdClass();       // 对象

// 引用赋值
$a = 5;
$b = &$a;                    // $b是$a的引用
$b = 10;
echo $a;                     // 输出10,$a的值也被改变了

// 可变变量
$varName = "name";
$$varName = "王五";          // 相当于 $name = "王五"
echo $name;                  // 输出"王五"

// 变量解析
$greeting = "Hello";
$message = "$greeting World";    // 双引号中变量会被解析
$message2 = '$greeting World';   // 单引号中变量不会被解析

// 变量变量
$prefix = "user";
${$prefix . "_name"} = "赵六";   // 相当于 $user_name = "赵六"
echo $user_name;                 // 输出"赵六"

// 变量作用域
$globalVar = "全局变量";

function testScope() {
    global $globalVar;           // 使用global关键字访问全局变量
    $localVar = "局部变量";
    echo $globalVar;
}

// 静态变量
function counter() {
    static $count = 0;
    $count++;
    return $count;
}

echo counter();  // 1
echo counter();  // 2
echo counter();  // 3

// 变量销毁
$var = "test";
unset($var);                     // 销毁变量
// echo $var;                    // 会报错:未定义变量

// 变量存在性检查
$var = "exists";
if (isset($var)) {
    echo "变量存在";
}

// 变量类型检查
$var = 123;
if (is_int($var)) {
    echo "是整数";
}

// 变量输出
$var = "test";
echo $var;                       // 直接输出
print $var;                      // print函数输出
var_dump($var);                  // 输出变量类型和值
print_r($var);                   // 打印变量信息

// 变量插值
$name = "张三";
$age = 25;
echo "姓名:$name,年龄:$age";
echo "姓名:{$name},年龄:{$age}";

// 变量赋值运算符
$a = 10;
$a += 5;     // $a = $a + 5
$a -= 3;     // $a = $a - 3
$a *= 2;     // $a = $a * 2
$a /= 4;     // $a = $a / 4
$a %= 3;     // $a = $a % 3
$a .= "test"; // $a = $a . "test" (字符串连接)

// 递增递减运算符
$i = 0;
$i++;        // 后置递增
++$i;        // 前置递增
$i--;        // 后置递减
--$i;        // 前置递减

// 三元运算符
$age = 20;
$status = ($age >= 18) ? "成人" : "未成年";

// 空合并运算符
$username = $_GET['user'] ?? 'guest';

// 变量类型声明(PHP 7+)
function add(int $a, int $b): int {
    return $a + $b;
}

// 严格类型模式
declare(strict_types=1);

// 变量变量的高级用法
$var1 = "value1";
$var2 = "var1";
echo $$var2;  // 输出"value1"

// 变量引用的高级用法
function modifyByRef(&$param) {
    $param = "modified";
}

$value = "original";
modifyByRef($value);
echo $value;  // 输出"modified"
?>

Go 变量语法

package main

import (
    "fmt"
    "reflect"
)

func main() {
    // 基本变量声明
    var name string = "张三"
    var age int = 25
    var height float64 = 175.5
    var isStudent bool = true
    var score *int = nil
    
    // 简短声明
    firstName := "李"
    lastName := "四"
    userName := "admin"
    
    // 多变量声明
    var (
        city string = "北京"
        population int = 21540000
        area float64 = 16410.54
    )
    
    // 多变量同时赋值
    a, b, c := 1, 2, 3
    x, y := "hello", 3.14
    
    // 变量类型推断
    var inferred = "自动推断类型"  // 推断为string
    inferred2 := 123               // 推断为int
    
    // 常量声明
    const (
        PI = 3.14159
        MAX_SIZE = 100
        DEFAULT_NAME = "guest"
    )
    
    // 枚举常量
    const (
        Sunday = iota
        Monday
        Tuesday
        Wednesday
        Thursday
        Friday
        Saturday
    )
    
    // 类型别名
    type Celsius float64
    type Fahrenheit float64
    
    var temperature Celsius = 25.5
    
    // 指针变量
    var ptr *int
    num := 10
    ptr = &num
    fmt.Println(*ptr)  // 解引用
    
    // 数组变量
    var arr [3]int = [3]int{1, 2, 3}
    arr2 := [5]string{"a", "b", "c", "d", "e"}
    
    // 切片变量
    slice := []int{1, 2, 3, 4, 5}
    slice2 := make([]string, 3)
    
    // 映射变量
    m := make(map[string]int)
    m["age"] = 25
    m["score"] = 90
    
    // 结构体变量
    type Person struct {
        Name string
        Age  int
    }
    
    var p Person
    p.Name = "张三"
    p.Age = 25
    
    p2 := Person{Name: "李四", Age: 30}
    
    // 接口变量
    var i interface{}
    i = "hello"
    i = 123
    i = 3.14
    
    // 通道变量
    ch := make(chan int)
    
    // 函数变量
    var add func(int, int) int
    add = func(a, b int) int {
        return a + b
    }
    
    // 变量作用域
    globalVar := "全局作用域"
    
    {
        localVar := "块级作用域"
        fmt.Println(localVar)
    }
    
    // 变量遮蔽
    outer := "outer"
    {
        outer := "inner"  // 遮蔽外层变量
        fmt.Println(outer)  // 输出"inner"
    }
    fmt.Println(outer)  // 输出"outer"
    
    // 变量零值
    var zeroInt int      // 0
    var zeroString string // ""
    var zeroBool bool    // false
    var zeroPtr *int     // nil
    
    // 变量类型转换
    var i1 int = 10
    var f1 float64 = float64(i1)
    var i2 int = int(f1)
    
    // 变量地址和指针
    value := 42
    ptrValue := &value
    fmt.Printf("值: %d, 地址: %p\n", value, ptrValue)
    
    // 变量比较
    str1 := "hello"
    str2 := "hello"
    fmt.Println(str1 == str2)  // true
    
    // 变量打印
    fmt.Println(name, age, height)
    fmt.Printf("姓名: %s, 年龄: %d\n", name, age)
    fmt.Printf("类型: %T, 值: %v\n", name, name)
    
    // 反射获取变量信息
    varName := "test"
    fmt.Println("类型:", reflect.TypeOf(varName))
    fmt.Println("值:", reflect.ValueOf(varName))
    
    // 变量交换
    x1, y1 := 1, 2
    x1, y1 = y1, x1  // 交换值
    
    // 变量初始化
    var initialized string
    initialized = "已初始化"
    
    // 多返回值
    result1, result2 := multipleReturn()
    fmt.Println(result1, result2)
    
    // 匿名变量
    _, nameOnly := getNameAndAge()
    fmt.Println(nameOnly)
    
    // 变量可见性
    // 首字母大写:包外可见
    // 首字母小写:包内可见
    
    // 变量命名规范
    // 使用驼峰命名法
    // 首字母大写表示导出
    // 首字母小写表示私有
    
    // 变量注释
    // 单行注释
    /* 
       多行注释
    */
    
    // 变量文档注释
    // ExportedVar 是一个导出的变量
    var ExportedVar = "exported"
}

// 多返回值函数
func multipleReturn() (int, string) {
    return 1, "hello"
}

// 返回多个值的函数
func getNameAndAge() (string, int) {
    return "张三", 25
}

JavaScript 变量语法

// var 声明(函数作用域)
var name = "张三";
var age = 25;
var height = 175.5;
var isStudent = true;
var score = null;
var undefinedVar;

// let 声明(块级作用域)
let firstName = "李";
let lastName = "四";
let userName = "admin";

// const 声明(常量)
const PI = 3.14159;
const MAX_SIZE = 100;
const DEFAULT_NAME = "guest";

// 变量提升
console.log(hoistedVar);  // undefined
var hoistedVar = "提升";

// 暂时性死区
// console.log(tempDead);  // ReferenceError
let tempDead = "暂时性死区";

// 重复声明
var x = 1;
var x = 2;  // 允许
// let y = 1;
// let y = 2;  // SyntaxError: Identifier 'y' has already been declared

// 变量作用域
var globalVar = "全局变量";

function testScope() {
    var functionVar = "函数作用域";
    let blockVar = "块级作用域";
    
    if (true) {
        var ifVar = "if块中的var";
        let ifLetVar = "if块中的let";
    }
    
    console.log(ifVar);     // 可访问
    // console.log(ifLetVar); // ReferenceError
}

// 块级作用域
{
    let blockScoped = "块级作用域变量";
    const blockConst = "块级常量";
}
// console.log(blockScoped);  // ReferenceError

// 变量解构赋值
// 数组解构
let [a, b, c] = [1, 2, 3];
let [first, , third] = [1, 2, 3];  // 跳过第二个元素
let [head, ...tail] = [1, 2, 3, 4, 5];  // rest操作符

// 对象解构
let {name: n, age: a} = {name: "张三", age: 25};
let {city = "北京"} = {};  // 默认值

// 嵌套解构
let {user: {name: userName}} = {user: {name: "李四"}};

// 交换变量
let x1 = 1, y1 = 2;
[x1, y1] = [y1, x1];

// 多返回值解构
function multipleReturn() {
    return [1, "hello", true];
}
let [num, str, bool] = multipleReturn();

// 变量类型
let str = "字符串";
let num = 123;
let float = 3.14;
let bool = true;
let n = null;
let undef = undefined;
let sym = Symbol("unique");
let bigInt = 123n;

// 动态类型
let dynamic = 10;
dynamic = "hello";
dynamic = true;
dynamic = [1, 2, 3];
dynamic = {key: "value"};

// typeof 运算符
console.log(typeof str);      // "string"
console.log(typeof num);      // "number"
console.log(typeof bool);     // "boolean"
console.log(typeof n);        // "object"
console.log(typeof undef);    // "undefined"
console.log(typeof sym);      // "symbol"
console.log(typeof bigInt);   // "bigint"

// 类型转换
// 字符串转换
let strNum = String(123);
let strBool = String(true);
let strNull = String(null);

// 数字转换
let numStr = Number("123");
let numBool = Number(true);
let numParse = parseInt("123");
let floatParse = parseFloat("3.14");

// 布尔转换
let boolStr = Boolean("hello");
let boolNum = Boolean(0);
let boolArr = Boolean([]);

// 隐式类型转换
let result = "5" + 3;      // "53"
let result2 = "5" - 3;     // 2
let result3 = "5" * 3;     // 15

// 变量比较
console.log(5 == "5");      // true (宽松相等)
console.log(5 === "5");     // false (严格相等)
console.log(null == undefined);  // true
console.log(null === undefined); // false

// 变量运算符
let count = 0;
count++;      // 后置递增
++count;      // 前置递增
count--;      // 后置递减
--count;      // 前置递减

count += 5;   // count = count + 5
count -= 3;   // count = count - 3
count *= 2;   // count = count * 2
count /= 4;   // count = count / 4
count %= 3;   // count = count % 3

// 三元运算符
let age = 20;
let status = (age >= 18) ? "成人" : "未成年";

// 逻辑运算符
let andResult = true && "hello";  // "hello"
let orResult = false || "default"; // "default"
let nullish = null ?? "default";  // "default"

// 可选链操作符
let user = {name: "张三"};
let city = user?.address?.city;  // undefined,不会报错

// 空值合并运算符
let username = null ?? "guest";  // "guest"

// 模板字符串
let name = "张三";
let age = 25;
let message = `姓名:${name},年龄:${age}`;
let multiLine = `
    这是
    多行
    字符串
`;

// 变量属性访问
let obj = {key: "value"};
let keyName = "key";
console.log(obj.key);        // 点号访问
console.log(obj["key"]);     // 方括号访问
console.log(obj[keyName]);   // 动态属性名

// 变量方法调用
let str = "hello";
console.log(str.toUpperCase());  // "HELLO"
console.log(str.length);         // 5

// 变量作为函数参数
function greet(name) {
    console.log(`Hello, ${name}!`);
}
greet("张三");

// 变量作为函数返回值
function createPerson(name, age) {
    return {name, age};
}
let person = createPerson("李四", 30);

// 闭包中的变量
function outer() {
    let outerVar = "outer";
    return function inner() {
        console.log(outerVar);
    };
}
let closure = outer();
closure();  // "outer"

// 立即执行函数表达式中的变量
(function() {
    let iifeVar = "IIFE";
    console.log(iifeVar);
})();

// 变量垃圾回收
let largeData = new Array(1000000).fill(0);
largeData = null;  // 允许垃圾回收

// 变量冻结
const obj = {key: "value"};
Object.freeze(obj);
// obj.key = "new";  // 严格模式下会报错

// 变量密封
const sealed = {key: "value"};
Object.seal(sealed);
// sealed.newKey = "new";  // 严格模式下会报错

// 变量属性描述符
let descriptorObj = {};
Object.defineProperty(descriptorObj, 'readOnly', {
    value: "只读",
    writable: false,
    enumerable: true,
    configurable: false
});

// 变量代理
let target = {key: "value"};
let proxy = new Proxy(target, {
    get: function(obj, prop) {
        return prop in obj ? obj[prop] : "默认值";
    }
});

// 变量符号
let sym1 = Symbol("description");
let sym2 = Symbol("description");
console.log(sym1 === sym2);  // false

// 全局符号注册表
let globalSym = Symbol.for("global");
let sameSym = Symbol.for("global");
console.log(globalSym === sameSym);  // true

// 变量迭代
let iterable = [1, 2, 3];
for (let item of iterable) {
    console.log(item);
}

// 变量展开运算符
let arr1 = [1, 2, 3];
let arr2 = [...arr1, 4, 5];
let obj1 = {a: 1, b: 2};
let obj2 = {...obj1, c: 3};

// 变量剩余参数
function sum(...numbers) {
    return numbers.reduce((a, b) => a + b, 0);
}
console.log(sum(1, 2, 3, 4, 5));  // 15

// 变量标签模板
function tag(strings, ...values) {
    console.log(strings);
    console.log(values);
}
let name = "张三";
tag`Hello ${name}!`;

// 变量私有字段(ES2022+)
class MyClass {
    #privateField = "私有字段";
    getPrivate() {
        return this.#privateField;
    }
}

// 变量静态字段
class MyClass2 {
    static staticField = "静态字段";
}
console.log(MyClass2.staticField);

// 变量装饰器(实验性)
// @decorator
// class DecoratedClass {
//     @readonly
//     method() {}
// }

二、数据类型详解

PHP 数据类型

<?php
// 标量类型
// 整数类型
$int1 = 123;              // 十进制
$int2 = -123;             // 负数
$int3 = 0123;             // 八进制(123)
$int4 = 0x1A;             // 十六进制(26)
$int5 = 0b11111111;       // 二进制(255)

// 浮点数类型
$float1 = 1.234;
$float2 = 1.2e3;          // 1200
$float3 = 7E-10;          // 0.0000000007

// 字符串类型
$string1 = '单引号字符串';
$string2 = "双引号字符串,可以包含变量 $name";
$string3 = "转义字符:\n 换行,\t 制表符";
$string4 = <<<EOT
    多行字符串
    Heredoc语法
EOT;

// 布尔类型
$bool1 = true;
$bool2 = false;
$bool3 = (bool)1;         // true
$bool4 = (bool)0;         // false
$bool5 = (bool)"0";       // false
$bool6 = (bool)"";        // false
$bool7 = (bool)null;      // false

// 复合类型
// 数组类型
$array1 = array(1, 2, 3);          // 索引数组
$array2 = [1, 2, 3];               // 短数组语法
$array3 = ["a" => 1, "b" => 2];    // 关联数组
$array4 = [1, "a" => 2, 3];        // 混合数组

// 对象类型
class Person {
    public $name;
    public $age;
    
    function __construct($name, $age) {
        $this->name = $name;
        $this->age = $age;
    }
}

$obj = new Person("张三", 25);

// 特殊类型
// NULL类型
$null1 = null;
$null2 = NULL;
$null3;  // 未初始化的变量

// 资源类型
$file = fopen("test.txt", "r");
// $file 是一个资源类型

// 回调类型
function callback($param) {
    echo $param;
}
$cb = 'callback';
$cb("test");

// 类型声明
// 标量类型声明(PHP 7+)
function add(int $a, int $b): int {
    return $a + $b;
}

function divide(float $a, float $b): float {
    return $a / $b;
}

function greet(string $name): string {
    return "Hello, $name";
}

function isActive(bool $status): bool {
    return $status;
}

// 返回类型声明
function getArray(): array {
    return [1, 2, 3];
}

function getObject(): Person {
    return new Person("李四", 30);
}

function getCallable(): callable {
    return function() {
        return "callable";
    };
}

// 可空类型
function nullable(?string $name): ?string {
    return $name;
}

// 联合类型(PHP 8+)
function union(int|string $param): int|string {
    return $param;
}

// 交集类型(PHP 8.1+)
function intersection(Traversable&Countable $param) {
    // ...
}

// 类型检查函数
$var = 123;
var_dump(is_int($var));        // true
var_dump(is_float($var));      // false
var_dump(is_string($var));     // false
var_dump(is_bool($var));       // false
var_dump(is_array($var));      // false
var_dump(is_object($var));     // false
var_dump(is_null($var));       // false
var_dump(is_numeric($var));    // true
var_dump(is_scalar($var));     // true

// 类型转换
// 显式转换
$int = (int)"123";
$float = (float)"3.14";
$string = (string)123;
$bool = (bool)"true";
$array = (array)$obj;
$object = (object)$array;

// settype函数
$var = "123";
settype($var, "integer");
var_dump($var);  // int(123)

// 类型比较
var_dump(1 == "1");      // true (宽松比较)
var_dump(1 === "1");     // false (严格比较)
var_dump(0 == false);    // true
var_dump(0 === false);   // false
var_dump(null == 0);     // true
var_dump(null === 0);    // false

// 类型提示
class Container {
    private array $items = [];
    
    public function addItem(string $item): void {
        $this->items[] = $item;
    }
    
    public function getItems(): array {
        return $this->items;
    }
}

// 枚举类型(PHP 8.1+)
enum Status: string {
    case DRAFT = 'draft';
    case PUBLISHED = 'published';
    case ARCHIVED = 'archived';
}

$status = Status::PUBLISHED;
echo $status->value;  // "published"

// 只读属性(PHP 8.1+)
class User {
    public readonly string $name;
    
    public function __construct(string $name) {
        $this->name = $name;
    }
}

// 新的初始化器(PHP 8.1+)
class Product {
    public function __construct(
        private string $name,
        private float $price = 0.0,
        private array $tags = []
    ) {}
}

// 属性类型
class Article {
    public string $title;
    public ?string $content = null;
    public array $metadata = [];
    public DateTime $createdAt;
}

// 泛型注释(虽然PHP没有原生泛型)
/**
 * @template T
 * @param T $value
 * @return T
 */
function identity($value) {
    return $value;
}
?>

Go 数据类型

package main

import (
    "fmt"
    "math"
    "math/big"
    "unsafe"
)

func main() {
    // 布尔类型
    var b1 bool = true
    var b2 bool = false
    fmt.Println(b1, b2)
    
    // 数值类型
    // 整数类型
    var int8Var int8 = 127           // -128 to 127
    var int16Var int16 = 32767       // -32768 to 32767
    var int32Var int32 = 2147483647  // -2147483648 to 2147483647
    var int64Var int64 = 9223372036854775807 // -9223372036854775808 to 9223372036854775807
    var intVar int = 42              // 平台相关,32或64位
    
    // 无符号整数
    var uint8Var uint8 = 255         // 0 to 255
    var uint16Var uint16 = 65535     // 0 to 65535
    var uint32Var uint32 = 4294967295 // 0 to 4294967295
    var uint64Var uint64 = 18446744073709551615 // 0 to 18446744073709551615
    var uintVar uint = 42            // 平台相关
    
    // 浮点数类型
    var float32Var float32 = 3.14    // IEEE-754 32位浮点数
    var float64Var float64 = 3.141592653589793 // IEEE-754 64位浮点数
    
    // 复数类型
    var complex64Var complex64 = 1 + 2i
    var complex128Var complex128 = 3 + 4i
    
    // 字符串类型
    var str1 string = "Hello"
    var str2 string = `Raw string
    多行字符串
    不转义`
    
    // 字节和符文
    var byteVar byte = 'A'           // uint8的别名
    var runeVar rune = '中'          // int32的别名,表示Unicode码点
    
    // 派生类型
    // 指针类型
    var ptr *int
    num := 42
    ptr = &num
    fmt.Println(*ptr)
    
    // 数组类型
    var arr1 [3]int = [3]int{1, 2, 3}
    var arr2 = [5]string{"a", "b", "c", "d", "e"}
    var arr3 = [...]int{1, 2, 3, 4, 5}  // 编译器推断长度
    
    // 切片类型
    var slice1 []int = []int{1, 2, 3}
    var slice2 = make([]string, 3)
    var slice3 = make([]float64, 5, 10) // 长度5,容量10
    
    // 映射类型
    var map1 map[string]int = make(map[string]int)
    var map2 = map[string]string{
        "name": "张三",
        "city": "北京",
    }
    
    // 结构体类型
    type Person struct {
        Name string
        Age  int
    }
    var p1 Person
    var p2 = Person{Name: "李四", Age: 30}
    
    // 接口类型
    var i1 interface{}
    i1 = "hello"
    i1 = 123
    i1 = 3.14
    
    // 函数类型
    var add func(int, int) int
    add = func(a, b int) int {
        return a + b
    }
    
    // 通道类型
    var ch1 chan int = make(chan int)
    var ch2 chan string = make(chan string, 10) // 带缓冲的通道
    
    // 类型别名
    type Celsius float64
    type Fahrenheit float64
    
    var temp Celsius = 25.5
    
    // 类型定义
    type MyInt int
    var myInt MyInt = 42
    
    // 枚举类型(使用iota)
    type Weekday int
    
    const (
        Sunday Weekday = iota
        Monday
        Tuesday
        Wednesday
        Thursday
        Friday
        Saturday
    )
    
    // 类型转换
    var i int = 42
    var f float64 = float64(i)
    var j int = int(f)
    
    var b byte = byte(i)
    var r rune = rune(i)
    
    // 类型断言
    var val interface{} = "hello"
    str, ok := val.(string)
    if ok {
        fmt.Println(str)
    }
    
    // 类型开关
    switch v := val.(type) {
    case string:
        fmt.Println("字符串:", v)
    case int:
        fmt.Println("整数:", v)
    default:
        fmt.Println("未知类型")
    }
    
    // 类型大小
    fmt.Println("int8大小:", unsafe.Sizeof(int8Var))
    fmt.Println("int64大小:", unsafe.Sizeof(int64Var))
    fmt.Println("float64大小:", unsafe.Sizeof(float64Var))
    
    // 类型零值
    var zeroInt int      // 0
    var zeroFloat float64 // 0.0
    var zeroBool bool    // false
    var zeroString string // ""
    var zeroPtr *int     // nil
    var zeroSlice []int  // nil
    var zeroMap map[string]int // nil
    var zeroFunc func()  // nil
    var zeroChan chan int // nil
    
    // 常量
    const Pi = 3.14159
    const MaxSize = 100
    
    const (
        A = iota  // 0
        B         // 1
        C         // 2
    )
    
    // 无类型常量
    const untyped = 42  // 无类型整数常量
    const untypedFloat = 3.14  // 无类型浮点数常量
    
    // 类型检查
    var x interface{} = 42
    fmt.Println("类型:", fmt.Sprintf("%T", x))
    fmt.Println("值:", x)
    
    // 大数类型
    bigInt := big.NewInt(12345678901234567890)
    fmt.Println("大整数:", bigInt)
    
    // 位运算
    a := 5  // 101
    b := 3  // 011
    fmt.Println("与:", a & b)   // 001 = 1
    fmt.Println("或:", a | b)   // 111 = 7
    fmt.Println("异或:", a ^ b) // 110 = 6
    fmt.Println("左移:", a << 1) // 1010 = 10
    fmt.Println("右移:", a >> 1) // 10 = 2
    
    // 数学常量
    fmt.Println("最大int8:", math.MaxInt8)
    fmt.Println("最小int8:", math.MinInt8)
    fmt.Println("最大float64:", math.MaxFloat64)
    
    // 类型方法
    type MyType int
    
    func (m MyType) Double() MyType {
        return m * 2
    }
    
    var mt MyType = 5
    fmt.Println("Double:", mt.Double())
    
    // 匿名结构体
    anon := struct {
        Name string
        Age  int
    }{
        Name: "匿名",
        Age:  25,
    }
    fmt.Println(anon)
    
    // 嵌入类型
    type Address struct {
        City string
    }
    
    type Employee struct {
        Name    string
        Address // 嵌入
    }
    
    emp := Employee{
        Name: "张三",
        Address: Address{City: "北京"},
    }
    fmt.Println(emp.City)  // 通过嵌入访问
    
    // 标签(struct tags)
    type User struct {
        Name  string `json:"name" validate:"required"`
        Email string `json:"email" validate:"email"`
    }
    
    // 泛型(Go 1.18+)
    type Number interface {
        int | int64 | float64
    }
    
    func Sum[T Number](nums []T) T {
        var sum T
        for _, num := range nums {
            sum += num
        }
        return sum
    }
    
    result := Sum([]int{1, 2, 3, 4, 5})
    fmt.Println("Sum:", result)
}

JavaScript 数据类型

// 原始类型(Primitive Types)
// 字符串类型
let str1 = "双引号字符串";
let str2 = '单引号字符串';
let str3 = `模板字符串,可以包含变量 ${name}`;
let str4 = String(123);  // 类型转换
let str5 = new String("对象字符串");  // 不推荐

// 数值类型
let num1 = 123;           // 整数
let num2 = -456;          // 负数
let num3 = 3.14;          // 浮点数
let num4 = 1.23e4;        // 科学计数法 12300
let num5 = 0xFF;          // 十六进制 255
let num6 = 0o777;         // 八进制 511
let num7 = 0b1111;        // 二进制 15
let num8 = Infinity;      // 无穷大
let num9 = -Infinity;     // 负无穷大
let num10 = NaN;          // 非数字
let num11 = Number.MAX_VALUE;  // 最大数值
let num12 = Number.MIN_VALUE;  // 最小数值

// 大整数类型(ES2020+)
let bigInt1 = 123n;
let bigInt2 = BigInt(123);
let bigInt3 = BigInt("123456789012345678901234567890");

// 布尔类型
let bool1 = true;
let bool2 = false;
let bool3 = Boolean(1);       // true
let bool4 = Boolean(0);       // false
let bool5 = Boolean("hello"); // true
let bool6 = Boolean("");      // false
let bool7 = !!value;          // 双重否定转换

// Undefined类型
let undef1;
let undef2 = undefined;
let undef3 = void 0;          // 另一种写法

// Null类型
let null1 = null;

// Symbol类型(ES2015+)
let sym1 = Symbol("description");
let sym2 = Symbol("description");
console.log(sym1 === sym2);  // false,唯一性

// 全局Symbol注册表
let globalSym1 = Symbol.for("global");
let globalSym2 = Symbol.for("global");
console.log(globalSym1 === globalSym2);  // true

// Symbol作为对象属性键
let obj = {
    [Symbol("key")]: "value",
    [Symbol.iterator]: function*() {
        yield 1;
        yield 2;
    }
};

// 对象类型(Object Types)
// 普通对象
let obj1 = {key: "value"};
let obj2 = new Object();
let obj3 = Object.create(null);  // 无原型对象

// 数组
let arr1 = [1, 2, 3];
let arr2 = new Array(1, 2, 3);
let arr3 = Array.of(1, 2, 3);
let arr4 = Array.from("hello");  // ['h', 'e', 'l', 'l', 'o']

// 函数
function func1() {}
let func2 = function() {};
let func3 = () => {};
let func4 = new Function('a', 'b', 'return a + b');

// 日期对象
let date1 = new Date();
let date2 = new Date(2024, 0, 1);  // 2024年1月1日
let date3 = new Date("2024-01-01");

// 正则表达式
let regex1 = /pattern/;
let regex2 = new RegExp("pattern");
let regex3 = /pattern/gi;  // 全局、忽略大小写

// 错误对象
let error1 = new Error("错误信息");
let error2 = new TypeError("类型错误");
let error3 = new RangeError("范围错误");

// Map对象(ES2015+)
let map = new Map();
map.set("key", "value");
map.set(1, "number");
map.set(true, "boolean");

// Set对象(ES2015+)
let set = new Set();
set.add(1);
set.add(2);
set.add(2);  // 重复值不会添加
set.add("string");

// WeakMap对象(ES2015+)
let weakMap = new WeakMap();
let keyObj = {};
weakMap.set(keyObj, "value");

// WeakSet对象(ES2015+)
let weakSet = new WeakSet();
weakSet.add(keyObj);

// Promise对象(ES2015+)
let promise = new Promise((resolve, reject) => {
    resolve("成功");
});

// Proxy对象(ES2015+)
let target = {key: "value"};
let proxy = new Proxy(target, {
    get: function(obj, prop) {
        return prop in obj ? obj[prop] : "默认值";
    }
});

// Reflect对象(ES2015+)
let objReflect = {};
Reflect.set(objReflect, 'key', 'value');
console.log(Reflect.get(objReflect, 'key'));

// 类(ES2015+)
class Person {
    constructor(name, age) {
        this.name = name;
        this.age = age;
    }
    
    greet() {
        console.log(`Hello, ${this.name}`);
    }
}

// 静态方法和属性
class MyClass {
    static staticProp = "静态属性";
    
    static staticMethod() {
        return "静态方法";
    }
}

// 私有字段(ES2022+)
class PrivateClass {
    #privateField = "私有字段";
    
    getPrivate() {
        return this.#privateField;
    }
}

// 类型检查
let value = "hello";

console.log(typeof value);        // "string"
console.log(typeof 123);          // "number"
console.log(typeof true);         // "boolean"
console.log(typeof undefined);    // "undefined"
console.log(typeof null);         // "object" (历史遗留问题)
console.log(typeof {});           // "object"
console.log(typeof []);           // "object"
console.log(typeof function(){}); // "function"
console.log(typeof Symbol());     // "symbol"
console.log(typeof 123n);         // "bigint"

// instanceof 检查
console.log([] instanceof Array);        // true
console.log({} instanceof Object);       // true
console.log(function(){} instanceof Function); // true
console.log(new Date() instanceof Date); // true

// Array.isArray 检查
console.log(Array.isArray([]));  // true
console.log(Array.isArray({}));  // false

// Object.prototype.toString 检查
console.log(Object.prototype.toString.call([]));      // "[object Array]"
console.log(Object.prototype.toString.call({}));      // "[object Object]"
console.log(Object.prototype.toString.call(null));    // "[object Null]"
console.log(Object.prototype.toString.call(undefined)); // "[object Undefined]"

// 类型转换
// 字符串转换
let strNum = String(123);
let strBool = String(true);
let strObj = String({key: "value"});
let strNull = String(null);
let strUndef = String(undefined);

// 数字转换
let numStr = Number("123");
let numBool = Number(true);
let numObj = Number({valueOf: () => 42});
let numParseInt = parseInt("123");
let numParseFloat = parseFloat("3.14");

// 布尔转换
let boolStr = Boolean("hello");
let boolNum = Boolean(0);
let boolObj = Boolean({});
let boolArr = Boolean([]);

// 隐式类型转换
let result1 = "5" + 3;      // "53" (字符串拼接)
let result2 = "5" - 3;      // 2 (数字减法)
let result3 = "5" * 3;      // 15 (数字乘法)
let result4 = "5" / 2;      // 2.5 (数字除法)
let result5 = "5" % 2;      // 1 (数字取模)

// == 和 === 的区别
console.log(5 == "5");      // true (类型转换后比较)
console.log(5 === "5");     // false (类型和值都比较)
console.log(0 == false);    // true
console.log(0 === false);   // false
console.log(null == undefined);  // true
console.log(null === undefined); // false

// 类型转换规则
// ToPrimitive - 转换为原始值
let objToPrim = {
    valueOf: function() { return 42; },
    toString: function() { return "hello"; }
};
console.log(+objToPrim);  // 42 (优先调用valueOf)

// ToNumber - 转换为数字
console.log(Number("123"));    // 123
console.log(Number("3.14"));   // 3.14
console.log(Number(""));       // 0
console.log(Number("hello"));  // NaN
console.log(Number(true));     // 1
console.log(Number(false));    // 0
console.log(Number(null));     // 0
console.log(Number(undefined)); // NaN

// ToString - 转换为字符串
console.log(String(123));      // "123"
console.log(String(true));     // "true"
console.log(String(null));     // "null"
console.log(String(undefined)); // "undefined"
console.log(String({}));       // "[object Object]"

// ToBoolean - 转换为布尔值
// 以下值转换为false
console.log(Boolean(false));   // false
console.log(Boolean(0));       // false
console.log(Boolean(-0));      // false
console.log(Boolean(0n));      // false
console.log(Boolean(""));      // false
console.log(Boolean(null));    // false
console.log(Boolean(undefined)); // false
console.log(Boolean(NaN));     // false

// 其他所有值转换为true
console.log(Boolean("0"));     // true
console.log(Boolean("false")); // true
console.log(Boolean([]));      // true
console.log(Boolean({}));      // true
console.log(Boolean(function(){})); // true

// 类型安全函数
function safeAdd(a, b) {
    if (typeof a !== 'number' || typeof b !== 'number') {
        throw new TypeError('参数必须是数字');
    }
    return a + b;
}

// TypeScript类型注释(虽然不是原生支持)
/**
 * @param {number} a
 * @param {number} b
 * @returns {number}
 */
function add(a, b) {
    return a + b;
}

// JSDoc类型注释
/**
 * @typedef {Object} User
 * @property {string} name
 * @property {number} age
 * @property {string[]} hobbies
 */

/**
 * @param {User} user
 * @returns {string}
 */
function greetUser(user) {
    return `Hello, ${user.name}`;
}

// 类型守卫
function isString(value) {
    return typeof value === 'string';
}

function process(value) {
    if (isString(value)) {
        // TypeScript中这里value会被推断为string类型
        return value.toUpperCase();
    }
    return value;
}

以上是三种语言在变量和类型方面的详细对照,涵盖了基本语法、数据类型、类型转换、作用域等核心概念。每种语言都有其独特的特性和最佳实践。

2025年上海离境退税销售额同比增长80.5%

2026年4月1日 20:35
4月1日,上海税务部门开展第35个税收宣传月活动。活动现场,上海税务部门发布了2025年税收营商环境建设“成绩单”。数据显示:2025年,上海市落实支持科技创新和制造业发展的主要优惠政策减税降费退税1873.6亿元;通过“银税互动”帮助诚信纳税企业获得贷款1008亿元;离境退税销售额同比增长80.5%,离境退税规模连续十年稳居全国首位。(央视新闻)

中证协拟修订证券业廉洁从业实施细则,探索文化建设评估指标量化廉洁管理实效

2026年4月1日 20:32
记者4月1日从业内获悉,中国证券业协会今日向各证券公司发函,正式启动对《证券经营机构及其工作人员廉洁从业实施细则》的修订调研工作。根据中证协要求,各券商需组织公司的纪检、合规、内审稽核及相关业务部门,围绕现行《细则》的实施效果、行业廉洁风险高发环节、创新业务风险应对、提升内控机制有效性等核心议题,于4月10日前向中证协提交调研反馈。需要注意的是,中证协在此次调研中探讨了如何在证券公司文化建设实践评估中设定具体评估指标,以相对客观地反映机构的廉洁从业管理情况。(中证报)

小牛电动胡依林:一家成立12年的两轮电动车厂商决定转型AI,“all in or nothing”

2026年4月1日 20:21

文|邱晓芬

编辑|苏建勋

一家成立12年的两轮电动车厂商,在成功经历过中国的锂电化、智能化浪潮,并在纳斯达克上市后,决定加入AI时代全新的竞争。造AI时代的智能两轮电动车——被小牛电动称为:“下一个十年的护城河”。

在小牛电动不久前的发布会上,他们展示了初步的成果——在新发布的电动车NXT2和NX2上,搭载了自研的小牛灵犀AIOS、以及首个面向骑行的AI智能体。

当你开车上路时,小牛灵犀AIOS和智能体的搭配,将让你的电动车变成“会看路、懂说话”的骑行伙伴。

举个例子,当你在雨天转弯时,AI会识别到骑行状态,并自动开启防侧滑功能;你也可以让它“找附近人少的咖啡店”,电动车能听懂并导航……

△小牛AI电摩NX2

3月中旬,《智能涌现》与小牛电动的创始人胡依林有过一次交流。他也是我们访谈过的,最“独特”的一位创始人。

他过往的经历称得上传奇。折腾和偏执,被他列为人生信条。凭借出色的自学能力、设计天赋,在英语不通的情况下,年少时,他撬开了微软正式员工的大门。在创办小牛电动之前,他还曾经做过动画工作室,也曾因为买到不合脚的鞋,一气之下创办鞋品牌。

在带领公司向AI转型的过程中,同样的“偏执感”,也充斥其中。

“通过极端的测试,我们希望摸清AI的边界,找到最高效的模型和工作流,为公司验证可行性”,胡依林表示。这也保证了小牛电动这家成立已12年的成熟公司,依旧褒有创新的基因,产生新技术的种子,包括前述提到的AIOS、智能体等等。

△胡依林 《智能涌现》拍摄

不过,如今在“偏执”之外,胡依林身上还多了一些平衡感。或者用他自己的话说,他本就是一个“矛盾体”。

作为一家上市公司的创始人,他有着极其务实的一面。为了让自己保持在“22岁时的认知和状态”、贴近小牛的用户,他坚持骑两轮电动车出行,每年里程超过4000公里。

如今,尽管把AI列为一项重要战略,小牛也保持一丝谨慎。胡依林直言,他们不会创造全新的AI品类,也不会做超越出行边界的事情,“小牛是一家很克制的公司,AI不能制造新产品,一定是现有业务+AI”,胡依林表示。

AIOS、智能体,只是他们转向AI的一个起点。他们相比其他厂商,优势在于,过去十年积累了350亿公里的用户数据,也有着已经完备的场景和终端。

“all in or nothing”,这句广告词年轻时被他刺进手臂皮肤,一直没被抹去。

以下是《智能涌现》与胡依林访谈实录(略经摘编)

一家两轮电动车厂商,决定向AI转型

《智能涌现》:你觉得AI对于小牛这家公司,意味着什么?

胡依林:意味着思维方式的变化。从财务上看,是效率提升。从产品逻辑看,是能够快速试错、调整方向。我们不做底层大模型,而是紧跟开源,在开源生态上做贡献和收益。我们的护城河是用十年数据训练出的垂类模型,以及基于此构建的、与硬件深度结合的AIOS。

未来的竞争是拼科技。我认为“智能化”是个伪命题,因为所有智能化都建立在科技进步之上。科技进步是从0到1(比如锂电、5G),智能化是把科技打包成用户可感知的功能(比如BMS、OTA)。小牛要做的,就是在每个环节进行微创新,所有这些微创新积累起来,才能构成用户能感受到的综合体验的变革。

《智能涌现》:在公司核心业务的层面,您是如何推动AI与其业务的结合?

胡依林:十多年前我们刚创业的时候,有人问我为什么要做小牛,我说“只是想还原一辆电动车本来应该有的样子”。那时候我们拿着从汽车和手机行业“降维”下来的锂电池和智能中控,被传统厂商看成是异类。

今天,行业都在谈智能化,都在堆参数。但我认为,真正的拐点不是“联网”,而是“AI”。今天,我想聊聊小牛对AI研发的愿景,或者说,我们是怎么理解“AI好车”这件事的。

我们拒绝PPT智能,要让AI服务于每一次出行。小牛的后发优势,在于站在汽车AI技术成熟的肩膀上,通过定制化适配,破解两轮车空间。

我们在2026年小牛电动科技新品发布会上,与高通、海思、斑马智能、HCT智驾大陆、禾赛、高德等全球顶尖科技伙伴的深度协同,想体现出一个根本性的产业转向:竞争正从单一的硬件参数比拼,升级为以用户体验为核心、融合芯片、系统、算法与连接能力的全栈智慧生态竞争。小牛与“科技朋友圈”的携手,旨在共同推动两轮AI出行的技术标准化与产品落地,首次构建“硬件+软件+通信+生态”的完整行业解决方案,为业界提供以生态协同驱动价值升级的清晰范本。

《智能涌现》:如果请您从个人角度,回顾总结小牛电动十年来的智能化战略,您想怎么说?

过去十年,小牛并没有发明什么全新的东西。我们做的,是把物联网、汽车、手机这些行业里已经相对成熟的技术,通过极强的产品整合能力,落到两轮车上,给用户最好的体验。从最早的锂电化,到后来的智能化,我们一直在做“技术迁移”。今天,AI大模型、L2级智驾在汽车领域已经非常成熟了,这时候我们不把它拿过来用,才是傻子。

我们内部有个“753战略”:向前看7年,筹备测试5年,量产落地3年。今天你们在NXT2和NX2上看到的毫米波雷达,七八年前在汽车上要上千块,我们那时候就开始关注;筹备上车时降到七八百,等到三年前量产前,成本只有200多块。这不是临时抱佛脚,是七年前就埋下的种子。

小牛希望,将更安全、便捷、有趣的AI出行体验,从今天的“标杆产品”推向未来的“全民普及”,为全球普惠出行的智能化进程贡献中国智慧与力量。

《智能涌现》:您曾经提到你“烧了一个亿的TOKEN”,能展开说说吗?

胡依林:那个数据更偏向公司层面,不过我个人一晚上用掉两三千万TOKEN也是常事。我们是一家上市公司,很早就通过正规渠道采购了10张H100显卡用于模型训练和测试,这方面的投入是千万级别的。

《智能涌现》:小牛电动最近做了哪些关于AI的测试?

胡依林:举个例子,我们会做一些“极端”测试。比如,要求一个由前端、后端、安卓、iOS工程师组成的小组,在两周内“一行代码都不许手写”,也不许手动修改任何Bug,所有编码、调试、修改都必须通过向AI编程工具描述需求来完成。

同时,他们可以随意切换使用任何模型(GLM、通义、Claude、GPT等)。我们的目的是摸清AI的边界,找到最高效的模型和工作流。当一个小团队验证了某套流程有70%的可行性,我们才会把它推广到更大的团队中去。

《智能涌现》:具体是怎么推进AI转型?

胡依林:我拥有几乎所有国内外主流大模型的API Key。如果以前我有一个产品想法,流程是:我画框架图、做Mockup,然后交给团队去设计、开发和对接数据库。但从去年五六月开始,这个流程被完全颠覆了,更多时候是我自己用AI工具快速实现。

举一个制作发布会电子邀请函的例子,我自己花了六七个小时,用AI辅助编码(Vibe coding)开发了一套系统,把科技圈核心合作伙伴的名字输入进去,系统自动生成带名字和二维码的邀请函。在对方扫码填写信息后,数据会自动同步到我的飞书多维表格。

整个过程,我大概只花了10分钟写Prompt,告诉AI我要什么风格,给了两张素材图。AI Agent就会自动完成前端、后端代码编写,我再让另一个AI去审计这段代码。最后,我只需要测试一下,一个可用的工具就诞生了。

《智能涌现》:那你怎么决定什么事情需要用AI替代,什么还是要继续交给人类?

胡依林:对我而言,用AI的底层逻辑是,必须同时满足“重要”和“复杂”两个条件。发布会邀请名单非常重要,且涉及跨部门协作、程序开发、数据校验,流程繁琐,容易出错。用AI来做,是最快、最准确、最可控的方式。

这本质上是在解决一个“重要且复杂”的问题,把沟通成本、人力协调成本和出错率都降到最低。我认为,人力应该去处理更精细、更需要判断的事情。

《智能涌现》:你认为AI时代的两轮电动车,可能会变成什么样子?

胡依林:可能是在人不在的时候,也能自己去充电吧。但是我们要永远记住一点,AI解放的是人,车只占到人的每天的生活中 10% 都不到的时间,我们不要尝试在车上面去解决用户可以通过其他途径更好的解决的事情。

小牛是一家很克制的公司,况且现在AI发展的路径和速度,已经超乎了百分之八九十的人的想象力,包括我在内。

但我觉得中国的AI整体的趋势是比全球市场更好的,因为我们拥有大量的内容、开源产品,未来不管是系统层、代码层、逻辑层都会跑得都很快。所有核心团队要对这件事情关注得足够早,其次是要敢于试错。

AI,让过去十年的数据有用武之地

《智能涌现》:你提到小牛在过去十年积累了海量数据,但在没有大模型之前不太会用,为什么?主要是因为“用不起”?

胡依林:是的,主要是算力成本太高。以前做数据分析,每一条数据都需要单独运算,写复杂的算法公式来得出结果。现在有了Transformer架构,我们可以把粗分类的数据直接扔给模型,让它自己去发现数据之间的潜在关联和逻辑。

我举个电池的例子——早年我们用最好的电芯,比如索尼、三洋的18650,实验室测出的单体循环寿命是800-1000次,做成电池包后可能只有600次。按用户两天一充算,理论寿命只有一年半。这让我们非常焦虑,拼命想找循环寿命2000次的电芯。

但当我们拿到真实用户数据后,发现完全不是这样。实验室是“满充满放”,但用户到家可能只充到95%就拔了,或者还剩50%的电就充电,这种“浅充浅放”的使用习惯,使得电池的实际循环次数远超实验室数据,一块电池能用七八年。

这个结论不是靠用户访谈能得到的,而是通过真实数据发现的规律,这反过来指导了我们的产品定义,我们不需要一味追求超高循环的电芯,而可以根据用户真实的出行距离,推出更小、更轻的电池包。AI的意义是,让这种从数据中发现真相的效率,提升了十倍不止。

《智能涌现》:过去十年的数据,对今天做AI、AIOS的帮助,具体体现在哪里?

胡依林:最关键的是,我们知道用户最高频的需求是什么。在电动两轮车上,最高频的场景就是导航。但用户的导航需求和汽车不同。比如,两轮车或许可以骑上马路牙子抄近道,但汽车导航不会规划这种路径。当大量用户都在某个没有记录的小路通行,我们的系统就能发现这个“隐藏路径”,并可以反馈给地图伙伴,最终优化所有用户的路线。

所以,基于导航,我们结合AI,做了三件核心的事——

1.AI导航:结合���端与车端算力,在地图原有的基础上,融合车辆的实时电量、用户的骑行习惯,进行动态规划,目标是让用户“骑得更慢,到得更快”。比如你只剩15%的电,系统会计算并建议你保持一个能安全到达目的地的经济时速,同时避开红灯和拥堵。

2.AI FOC(电机控制):为每个用户建立个性化的动力曲线Profile。有些人喜欢“即给即有”的迅猛,有些人喜欢柔和线性。AI会通过学习用户的操控习惯,自动匹配和微调电机的输出曲线,让车越来越符合用户个人的“体感”。

3.AI BMS(电池管理):电池管理是最大的黑盒,因为电池厂也不知道每一颗电芯在真实复杂环境下的状态。但我们通过海量数据,可以为每一组电池建立“健康档案”。AI能模拟电化学过程,为健康度不同的电池,制定差异化的充电策略(比如健康电池快充,老化电池慢充),并极其精准地预测剩余电量,延长电池寿命。

《智能涌现》:那AI在用户侧带来的价值和变化是什么?为什么需要从操作系统层面去开始做起?

胡依林:AI本身不能创造新产品,一定是“现有业务+AI”,它解决的是效率问题。举个例子,我们在车上做语音交互,就是因为骑车时双手被占用,掏手机问导航,体验太差。

如果不从系统底层做原生融合,这个流程会非常长:麦克风拾音→软件降噪→通过网络发给云端大模型→结果返回→再转成语音,绕了一大圈。

我们做AIOS的核心意义,就是在系统底层实现AI Native的原生接入。当我想导航时,指令可以从芯片级直接处理,走最短路径,效率最高,延迟最低。如果前期底层架构的余量留得不够,以后升级就是灾难。我们吃过很多亏,现在非常重视底层Foundation的建设。

“我是一个矛盾体”

《智能涌现》:小牛上用到了很多汽车上的技术,但是你提到,你一直在“等”,是在等高通、禾赛这样的上游技术价格降到适合两轮车的区间吗?

胡依林:我更愿意称之为“拟合”。这是两条曲线交汇的过程:一条是技术价格下降的曲线,一条是用户需求上升的曲线。作为一个产品公司,你要做的就是预判那个“甜蜜点”——当技术价格下降到能支撑你的目标产品价位,同时用户需求也成长到能接受这个功能的时候。

我们不做“从0到1”的发明。比如激光雷达,11年前我就问禾赛的创始人,“有没有可能便宜到小牛能用”,当时激光雷达要30万,我们现在终于等到了。我们的小团队早就完成了前期技术验证,已经知道了原理、流程和策略,所以我们能做的就是等,等价格进入我们的区间。这背后也是全球科技产业进步的结果。

《智能涌现》:你有总结过自己的产品哲学吗?

胡依林:首先是,把自己当用户,你自己必须先是用户。我一年骑两轮车的里程超过4000公里,比很多电动车公司老板加起来都多。我每天用它通勤、带女儿玩。我永远会把自己设定在,二十二、三岁时的经济能力和认知水平,去判断一个产品值不值。

但这也会导致一种“既要又要”的困境:既要科技、体验、颜值,又要价格合适。这会逼得团队很痛苦,但往往逼一逼,总能出来点东西。

我是一个极其勤奋的懒人。懒就是能少一步,懒是很宝贵的资源,需要花无数的时间、科技能力去把懒这件事情搞定。

《智能涌现》:感觉你好像有一种狂热与理性的结合?

胡依林:我是一个极其矛盾的矛盾体。举个例子,我办公室的桌子就是一个mass,乱七八糟啥都有,但是我的包一定得整理得很整洁,所有的东西都要分门别类。

《智能涌现》:我看到你好像有一处纹身,是什么?

胡依林:执念。还有all in or nothing 是曾经 Adidas的广告语。

《智能涌现》:您对拓展新的AI硬件品类感兴趣吗?

胡依林:我非常克制,甚至可以说80%的新AI硬件在我看来是“废物”。我判断一个硬件是否值得留下的标准是:它是否真正解决了一个现有的、具体的痛点,并且带来了效率的显著提升。

如果一个硬件没有解决具体的问题,我就不会考虑。我不会为了AI而AI,也不会自己做。小牛的核心是车,我所有的兴趣和想法,都是会落在车上。一个人的精力是有限的。

end

end

 

宁波能源:全资子公司拟2961.35万收购4家项目公司

2026年4月1日 20:14
36氪获悉,宁波能源公告,公司全资子公司朗辰新能源拟以2961.35万元的对价收购宁波锦浪智慧能源有限公司及其全资子公司湛江锦冕新能源有限公司持有的4家项目公司(简称“目标公司”)100%股权和100%股东债权。目标公司内含8个光伏项目,光伏项目合计容量为10469.15KW,均采用“自发自用、余电上网”模式。宁波能源同日披露,拟1.95亿元增资全资子公司宁波宁能投资管理有限公司,增资完成后宁能投资注册资本金变更为2.05亿元。

1.基于依赖追踪和触发的响应式系统的本质

作者 Cobyte
2026年4月1日 19:57

前言

Vue1、Vue3、SolidJS、Mobx 它们的数据响应式基本原理都是一样的,具体区别只是设计理念和实现方式不一样。按以前读书时代考试做题一样,它们是同一类型的题目,都是基于依赖收集和触发的运行时的数据响应式,如果说你只会解答其中一道题,其他的题却不会解答,则说明你并没有真正彻底掌握这一类题。

为什么标题说“基于依赖追踪的响应式系统”,因为在前端响应式的框架有很多,但他们的实现原理却各有不同。 在前端一般谈到响应式框架,可能大家都会不约而同地联想到 Vue,除了 Vue 之外,也许还有人会想到 React 以及 Svelte、SolidJS。他们都有一个共同点,都是通过数据驱动视图。他们在实现方式上又互相有一些相似之处,其中 Vue 和 React 都采用了虚拟DOM技术,Vue 和 SolidJS 的数据响应式实现则都是采用了依赖追踪的方式,所以在数据响应式的实现方面 Vue 和 SolidJS 最相似,而 Svelte 的实现方式则跟 Vue 和 React 都不一样,Svelte 是基于编译响应式。当然 Vue 和 React 的具体实现技术也是不一样的,但它们在宏观层面则是一样,都是通过数据驱动视图,当数据发生变化时,视图会重新渲染,这种机制使得开发者只需要关注数据的变化,而不需要手动操作 DOM。Vue 通过数据劫持,使得操作数据需要额外的 API ,系统变能感知数据的变化,而 React 和 SolidJS 则需要手动调用 API 去触发数据变化。

手动操作 DOM 的上古时代

例如我们现在有一个这样的需求,有一个按钮 <button>0</button>,当我们点击按钮的时候,按钮中的文本就进行加 1。

<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-9" />
    <title>手动操作DOM</title>
  </head>
  <body>
    <button id="btn"></button>
    <script>
        let count = 0
        const btnEl = document.getElementById('btn')
        btnEl.textContent = count
        btnEl.addEventListener('click', function () {
            count++
            btnEl.textContent = count
        })
    </script>
  <body>
</html>

上述的 button 按钮是通过 HTML 进行渲染的,我们还可以通过 JavaScript API 进行创建。

<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-9" />
    <title>手动操作DOM</title>
  </head>
  <body>
-    <button id="btn"></button>
    <script>
        let count = 0
-        const btnEl = document.getElementById('button')
+        const btnEl = document.createElement('button')
+        const textNode = document.createTextNode(count)
+        btnEl.appendChild(textNode)
        btnEl.addEventListener('click', function () {
            count++
            btnEl.textContent = count
        })
+        document.body.appendChild(btnEl)
    </script>
  <body>
</html>

上述这种方式,在项目应用非常庞大的时候,开发效率是非常低下的,同时维护成本却又非常高的,所以就出现了像 React、Vue 这种通过数据进行驱动视图的前端框架。

通过数据驱动视图

虽然 Vue 和 React 在具体的实现技术方案上差异是非常大的,但在宏观层面它们则是一样的,都是通过操作数据来驱动视图,开发者只需要关注数据的变化,而不需要手动操作 DOM,同时它们都是通过虚拟DOM 和 Diff 算法进行实现响应式的,当组件的状态发生改变时,Vue 和 React 都会重新生成新的虚拟 DOM,并通过 Diff 算法比较新老虚拟 DOM 的差异,然后只更新需要更新的部分到真实 DOM 上,从而提高性能。

不管是 Vue 还是 React 的虚拟DOM 本质上都是一个对象,上面记录着一些真实 DOM 的信息,比如 type、props、children,我们这里简单模拟一下,并通过虚拟DOM 和 Diff 算法来改写上面的例子。

首先我们通过一个 createElement 的函数来创建一个节点的虚拟DOM对象,这里跟 React 的对齐,children 节点在 props 中,Vue 的 children 是跟 props 同级的,这些差别对我们进行宏观研究不重要。

function createElement(type, props) {
    return {
        type,
        props
    }
}

接着我们创建一个函数组件 App。

count = 0
function App (){
    return createElement('button', {
        onClick: () => {
            count ++
            setCount(count)
        },
        children: count
    })
}

接着我们通过以下方式把创建的虚拟DOM 挂载到根节点上。

render(App(), document.getElementById('app'))

接着我们实现 render 方法

let oldVnode
function render(vNode, container) {
    if (!oldVnode) {
        oldVnode = vNode
        const el = document.createElement(vNode.type)
        // 保存真实DOM 到虚拟DOM 的 el 属性上,将来更新的时候,不用重新创建,从而达到提高性能效果
        oldVnode.el = el
        const textNode =  document.createTextNode(vNode.props.children)
        el.appendChild(textNode)
        // 绑定虚拟DOM 上的事件
        el.addEventListener('click', vNode.props.onClick)
        container.appendChild(el)
    } else if(oldVnode.props.children !== vNode.props.children) {
        oldVnode.el.textContent = vNode.props.children
    }
}

我们在这里非常简单且宏观的实现了新老虚拟DOM 的对比,当不存在旧虚拟DOM 则是挂载阶段,创建真实DOM,并保存到虚拟DOM 的 el 属性上,将来更新的时候,不用重新创建,从而达到提高性能效果。在进行新老虚拟DOM 的时候,我们这里只比较 children 一个属性,如果新老 children 不一样就把新的虚拟DOM 上的 children 的值更新到对应的真实DOM 上。

我们在上面的 App 函数中的 props 中的点击事件函数中有一个 setCount 的方法还没实现,它的实现可以抽象成如下:

function setCount(val) {
    count = val
    render(App(), document.getElementById('app'))
}

从上述代码可以看到 setCount 的实现很简单,这个方法就是在点击之后进行更新数据 count 的,并且在更新数据 count 的同时重新渲染视图,把新的 count 值显示到页面上。

以下是测试效果:

00.gif

我们在上述例子中通过极简的代码,从宏观层面阐明了 React 的响应式原理,通过操作数据来驱动视图,开发者只需要关注数据的变化,而不需要手动操作 DOM,同时它们都是通过虚拟DOM 和 Diff 算法进行实现响应式的,当组件的状态发生改变时,Vue 和 React 都会重新生成新的虚拟 DOM,并通过 Diff 算法比较新老虚拟 DOM 的差异,然后只更新需要更新的部分到真实 DOM 上。

其实上述这段 React 的响应式原理放在 Vue 的响应式原理也是成立的,最大的不同则是 React 更新数据需要通过 setCount 函数,也就是所谓手动触发,而 Vue 则是自动触发的。那么下面我们来看看 Vue 的响应式是怎么实现的。

基于依赖追踪的响应式

我们知道 Vue1 的响应式数据是通过 Object.defineProperty 来实现的。基于 Object.defineProperty 来实现需要初始化对对象的每一个熟悉进行劫持监听。

例如我们有这么这个对象 const data = { count:0 },那么我们需要进行以下操作:

const data = { count: 0 }
Object.keys(data).forEach(key => {
   Object.defineProperty(data, key, {
    get() {
        return data[key]
    },
    set(val) {
        data[key] = val
    }
   }) 
})

上述这个写法会造成内存栈溢出,主要是因为在 Object.defineProperty 的 getter 中读取 data[key] 会触发 getter 循环读取,从而造成死循环。

00.png

我们可以把 getter 中 data[key] 的取值放在进行 Object.defineProperty 监听之前。

const data = { count: 0 }
Object.keys(data).forEach(key => {
+   const val = data[key]
   Object.defineProperty(data, key, {
    get() {
+        return val
    },
    set(val) {
        data[key] = val
    }
   }) 
})

但这样 val 会被循环取值进行了覆盖,没办法正确读取每个 key 的值,为了可以读取每个 key 的值,我们可以通过闭包的形式把每个 key 的值缓存下来。

const data = { count: 0 }
Object.keys(data).forEach(key => {
   defineReactive(data, key, data[key]) 
})

function defineReactive(data, key, val) {
    Object.defineProperty(data, key, {
        get() {
            return val
        },
        set(newVal) {
            val = newVal
        }
    })
}

接下来我们要做的就是在 getter 中进行依赖收集,然后在 setter 中进行依赖触发,这本质上就是一个订阅发布模式。

const data = { count: 0 }
+ // 声明一个依赖存储中心
+ const subscribers = new Set()
+ // 需要收集的依赖,在 Vue1 叫 wachter,Vue3 中叫 effect,本质上就是一个订阅者,关于发布订阅模式,我们后续再详细介绍
+ let activeEffect
Object.keys(data).forEach(key => {
   defineReactive(data, key, data[key]) 
})

function defineReactive(data, key, val) {
    Object.defineProperty(data, key, {
        get() {
+            // 存在依赖就把依赖收集到依赖存储中心
+            if(activeEffect) subscribers.add(activeEffect)
            return val
        },
        set(newVal) {
            val = newVal
+            // 值更新了,就需要去把依赖存储中心中的订阅者全部重新执行一遍
+            subscribers.forEach(sub => sub())
        }
    })
}

我们在上述代码中通过一个全局变量 subscribers 在响应式数据的 getter 把依赖收集到 subscribers 中, 在 setter 中则把 subscribers 中收集到的依赖进行循环遍历重新执行一遍,从而实现了依赖追踪和触发。

那么现在我们有了响应式数据 data 之后,我们就可以对我们前面的例子中的 App 函数中的 count 数据进行更改了,我们之前实现的是 React 的方式,我们现在要把它改成 Vue 的方式。

- count = 0
function App (){
    return createElement('button', {
        onClick: () => {
-            count ++
-            setCount(count)
+            data.count ++
        },
-        children: count
+        children: data.count
    })
}

此外渲染函数的执行方式也需要改成一个副作用函数,通过副作用函数进行调用执行。

    activeEffect = () => {
        render(App(), document.getElementById('app'))
    }

    activeEffect()

    activeEffect = null

我们把一个副作用函数赋值给了变量 activeEffect,然后再执行 activeEffect,那么在执行 activeEffect 函数的时候就会去执行 rander 函数,并通过 App 函数生成虚拟DOM,在 App 函数中对虚拟DOM 的 children 属性赋值的时候是通过读取响应式数据 data 中的 count 值,那么这时就会触发 count 属性的 getter,然后就会在 getter 中进行依赖收集,在 getter 中很明显这个时候 activeEffect 是有值的,所以会进行依赖收集。当点击的时候,就会触发 data.count ++ 的执行,这时就会触发 count 属性 setter,然后就会在 setter 中进行依赖触发。

以下是测试效果:

00.gif

至此我们把 Vue 的数据响应式也通过最少的代码量阐明了,以上的 Vue 的响应式原理估计很多同学都非常清楚,因为这是面试被问几率非常高的题目。我在这里重复讲解,是为了对比 React 和 Vue 响应式原理的差别,总的来说,React 和 Vue 的响应式原理在宏观层面是有非常大的相同之处的,都是通过操作数据来驱动视图,开发者只需要关注数据的变化,而不需要手动操作 DOM,同时它们都是通过虚拟DOM 和 Diff 算法进行实现响应式的,当组件的状态发生改变时,Vue 和 React 都会重新生成新的虚拟 DOM,并通过 Diff 算法比较新老虚拟 DOM 的差异,然后只更新需要更新的部分到真实 DOM 上。在宏观层面 React 和 Vue 响应式原理最大的不同则是数据触发方式的不同,React 是数据变更后需要开发者通过手动调用 React 提供的 API 进行触发视图的更新,而 Vue 则是自动触发的,因为 Vue 的状态数据是响应式的,而 React 的状态数据不是响应式的。

我们一般在很多的文章中都只讲了 Vue1 的数据响应式原理是通过 Object.defineProperty 来实现的,那么是否只能通过 Object.defineProperty 来实现呢?很明显不是,我们上述例子中的 data,我们现在如果想给它新增属性 data.name,那么 Object.defineProperty 是无法进行监听追踪的,所以我们通过一个工具来对 data 也进行监听。

function observe (data) {
    // 给对象 data 添加一个属于 data 对象的依赖存储中心
    data.__ob__ = new Set()
    Object.keys(data).forEach(key => {
        const value = data[key]
        defineReactive(data, key, value) 
    })
}

那么在 getter 中要对对象的依赖也进行收集

function defineReactive(data, key, val) {
    Object.defineProperty(data, key, {
        get() {
            // 存在依赖就把依赖收集到依赖存储中心
            if(activeEffect) {
                subscribers.add(activeEffect)
                // 如果读取的值是对象,那么还要给这个对象进行依赖收集,并且新的对象也要通过 observe 进行监听
                if(Object.prototype.toString.call(val) === '[object Object]') {
                    observe(val)
                    val.__ob__.add(activeEffect)
                }
            } 
            return val
        },
        set(newVal) {
            val = newVal
            // 值更新了,就需要去把依赖存储中心中的依赖全部重新执行一遍
            subscribers.forEach(sub => sub())
        }
    })
}

然后我们要专门通过一个单独的 API 来给响应式对象添加属性,并且在添加属性之后进行依赖触发。

function set(target, key, val) {
    target[key] = val
    // 新添加的属性也需要通过 Object.defineProperty 进行监听
    defineReactive(target, key, val)
    const ob = target.__ob__
    // 进行对象的依赖触发
    ob.forEach(sub => sub())
}

接着我们把 App 函数进行以下修改

// 修改数据结构
const data = { data: { count: 0 } }
// 通过 observe 处理
observe(data)
function App (){
    return createElement('button', {
        onClick: () => {
            set(data.data, 'count1', 2)
        },
        children: JSON.stringify(data.data)
    })
}

然后我们重新运行代码结果如下:

01.gif

我们看到可以正常运行,但对象中的私有属性 __ob__ 也显示出来了,我们希望它不要被枚举出来,我们可以通过 Object.defineProperty 对它进行以下设置。

function observe (data) {
    // 给对象 data 添加一个属于 data 对象的订阅者中心
-    data.__ob__ = new Set()
+    Object.defineProperty(data, '__ob__', {
+        value: new Set(), // 属性的值,默认为 undefined
+        enumerable: false, // 属性是否可枚举,默认为 false
+        writable: true, // 值是否可写,默认为 false
+        configurable: true // 属性是否可配置,默认为 false
+    })
    Object.keys(data).forEach(key => {
        const value = data[key]
        defineReactive(data, key, value) 
    })
}

修改后再进行测试,我们可以看到 __ob__ 属性不再出现了。

02.gif

可以通过 Object.defineProperty 对数组进行监听,但监听不了 push、pop、shift 等对数组进行操作的方法,所以我们需要对数组的操作方法进行重写,重写的方法就是覆盖数组数据上的原型对象 __proto__

function observe (data) {
// 省略 ...
+   if (Array.isArray(value)) {
+      // 如果是数组则重新数组上的原型
+      value.__proto__ = {
+          join(val) {
+             // 通过原生数组上方法进行调用
+             return Array.prototype.join.call(value, val)
+          },
+          push(val) {
+             // 通过原生数组上的方法进行调用
+             Array.prototype.push.call(value, val)
+             subscribers.forEach(sub => sub())
+          }
+      }
+   } else {
        Object.keys(data).forEach(key => {
            const value = data[key]
            defineReactive(data, key, value) 
        })
+    }
}

我们这里只测试 join 和 push 方法,而 join 方法没有更改到数据,所以是不用进行依赖触发的。

然后我们对 App 应用也进行修改一下,以便测试数组响应式数据

// 数据
const data = ['cobyte']
// 通过 bserver 处理
observe(data)
function App (){
    return createElement('button', {
        onclick: () => {
            data.push('=')
        },
        children: data.join('-')
    })
}

测试结果如下:

03.gif

小结

至此,我们通过手写已经基本实现了 Vue1 的数据响应式原理,我们可以通过对 Vue2 数据响应式原理的分析进行一个宏观总结。我们需要在实践中总结规律,然后又通过规律更好地指导实践

首先我们都知道 Vue1 的数据响应式原理是通过 Object.defineProperty 实现的,通过 Object.defineProperty 可以监听一个对象的属性的读取(getter)和修改(setter),这样就可以在 getter 的时候进行依赖收集,在 setter 的时候进行依赖触发。但 Vue2 不单单只是通过 Object.defineProperty 实现数据响应式的,因为只有被 Object.defineProperty 初始化了的属性才可以进行监听,而当一个对象新增一个属性时,则监听不了。这时我们需要通过额外的手段来实现对象新增属性时的监听,具体方案就是通过给对象新增一个私有的属性 __ob__,去记录属于该对象的依赖,当该对象新增属性时则触发该对象的依赖重新执行。同时 Object.defineProperty 也监听不了数组的原生方法,例如:push、pop、shift、unshift、splice、sort、reverse,我们观察一下这些数组方法发现都有一个共同特点,就是他们都会修改数组,使数组数据发生变化,那么根据数据响应式的原理,数据发生了改变就需要进行依赖触发,那么我们需要对响应式数据类型为数组的数据进行重写它们的原型,这样我们就可以在响应式数组通过 push、pop、shift、unshift、splice、sort、reverse 方法修改数组的时候进行依赖触发了。

我们可以总结出,不管是通过 Object.defineProperty 进行监听对象属性还是通过给对象添加私有属性 __ob__,去记录该对象的依赖,还是重写数组的原型方法,目的都只有一个:进行数据的依赖追踪和触发。

我们还可以进一步进行总结规律:这种基于依赖追踪的响应式系统,并不是某一种技术,而是一种模式。核心只有一个,就是在数据读取的时候进行依赖收集,在数据更改的时候进行依赖触发。

基于这种指导思想,我们就可以很好去实践 Vue2 的数据响应式原理了。

Vue3 只是通过 Proxy 实现数据响应式吗

Vue3 是通过 Proxy 对数据实现 getter/setter 代理,从而实现响应式数据,然后在副作用函数中读取响应式数据的时候,就会触发 Proxy 的 getter,在 getter 里面把对当前的副作用函数保存起来,将来对应响应式数据发生更改的话,则把之前保存起来的副作用函数取出来执行。这个过程跟 Vue2 是一样的,只是实现细节不一样。

实现起来也非常简单:

function reactive(data) {
    return new Proxy(data, {
        get(target, key) {
            // 存在依赖就把依赖收集到依赖存储中心
            activeEffect && subscribers.add(activeEffect)
            return Reflect.get(target, key) 
        },
        set(target, key, val) {
            const result = Reflect.set(target, key, val)
            // 值更新了,就需要去把依赖存储中心中的依赖全部重新执行一遍
            subscribers.forEach(sub => sub())
            return result
        }
    })
}

我们再把 App 函数进行修改:

const data = reactive({count: 0})
function App (){
    return createElement('button', {
        onClick: () => {
            data.count ++
        },
        children: data.count
    })
}

测试结果如下:

00.gif

我们这里不是为了深入探讨 Vue2 的数据响应式原理的,而是为了验证上面实现 Vue2 的数据响应式原理总结的规律。也就是:这种基于依赖追踪的响应式系统,并不是某一种技术,而是一种模式。核心只有一个,就是在数据读取的时候进行依赖收集,在数据更改的时候进行依赖触发。后续我们基于数据响应式原理的规律便可以很好去理解其他数据响应式系统了,例如 React 的状态管理库——Mobx、SolidJS,我们在后续也将探讨这些库的数据响应式原理的实现。

Vue1 是通过 Object.defineProperty 实现对数据的读写监听,但由于 Object.defineProperty 的局限性,Vue2 并不只是通过 Object.defineProperty 实现数据响应式的,但都为了实现在数据读取时进行依赖收集,在数据更改时进行依赖触发。Vue3 则通过新的 API:Proxy 可以实现对数据的读写监听,但核心也是为了实现在数据读取时进行依赖收集,在数据更改时进行依赖触发。

那么问题来了,Vue1 并不只是通过 Object.defineProperty 实现数据响应式的,那么 Vue3 只是通过 Proxy 实现了数据响应式吗?

其实这个问题可以转化得更具体一些,Vue2 的 reactive 和 ref 的底层实现原理是一样的吗?有人认为 ref 和 reactive 的底层实现原理都是一样的,也就是 ref 也是通过 reactive 实现的,也就是 ref 也是通过 Proxy 实现的。如果说 ref 和 reactive 的底层实现原理不一样的话,也就是说 Vue3 可以不通过 Proxy 实现数据的响应式。

很明显 Vue3 可以不通过 Proxy 实现数据的响应式的,也就是 ref 和 reactive 的底层实现原理是不一样的。那么根据我们上面总结的实践规律,我们只需要可以实现在数据读取的时候进行依赖收集,然后在数据更改的时候进行依赖触发就可以了。那么明显可以使用 Vue2 中的 Object.defineProperty 中的 getter/setter,这种方式也叫属性访问器。根据上面 Vue2 的数据响应式原理我们可以知道如果通过 Object.defineProperty 实现对数据的监听,还要通过闭包的方式,就显得不够简洁。那么属性访问器除了使用 Object.defineProperty 进行显式声明之外,还可以通过字面量的方式,本质还是属性访问器

例如:

function ref(value) {
    return {
        _value: value,
        get value() {
            // 存在依赖就把依赖收集到依赖存储中心
            activeEffect && subscribers.add(activeEffect)
            return this._value
        },
        set value(val) {
            this._value = val
            // 值更新了,就需要去把依赖存储中心中的依赖全部重新执行一遍
            subscribers.forEach(sub => sub())
        }
    }
}

然后我们通过 ref 函数来创建一个响应式数据,再修改 App 函数。

const count = ref(0)

function App (){
    return createElement('button', {
        onClick: () => {
            count.value ++
        },
        children: count.value
    })
}

测试运行结果:

00.gif

这也就是 Vue2 的 ref API 的实现原理,当然在 Vue3 源码中如果 ref 传进来的值是一个引用对象的话,还是通过 reactive 进行实现。此外在 Vue3 的源码中 ref API 是通过一个 class 类来实现的,但原理是一样的。

我们下面也可以简单实现一下:

class RefImpl {
    _value
    constructor(value) {
        this._value = value
    }
    get value() {
       // 存在依赖就把依赖收集到依赖存储中心
       activeEffect && subscribers.add(activeEffect)
       return this._value 
    }
    set value(val) {
        this._value = val
        // 值更新了,就需要去把依赖存储中心中的依赖全部重新执行一遍
        subscribers.forEach(sub => sub())
    }
}

function ref(value) {
    return new RefImpl(value)
}

修改好的测试结果还是一样的。

00.gif

通过沙箱模式实现依赖追踪的数据响应式

通过上面对 Vue1 和 Vue3 的数据响应式原理的实现与分析,我们知道都借助了 JavaScript 的原生 API(Object.defineProperty 和 Proxy) 来实现依赖追踪的响应式系统,那么不借助 JavaScript 原生 API 还可以实现依赖追踪的响应式系统吗? 我们上面总结出的结论是,基于依赖追踪的响应式系统的本质是在读取数据的时候收集依赖,在更新数据的时候触发依赖。那么基于此原理,我们只需要把读写进行分离那么可以实现了。

我们把上面第一版 ref 的实现通过闭包的形式改造一下:

function ref(value) {
    const s = {
        value
    }

    function getState() {
        return s.value
    }

    function setState(val) {
        s.value = val
    }
    return [getState, setState]
}

const [getState, setState] = ref(0)

console.log('初始值:', getState())
// 修改
setState(1)
console.log('修改后:', getState())

我们可以看到通过闭包的我们实现了读写分离,这种模式有一个专业的术语叫:沙箱模式,这样我们就可以在读取数据的时候收集依赖,在修改数据的时候触发依赖了。

function ref(value) {
    const s = {
        value
    }

    function getState() {
        // 存在依赖就把依赖收集到依赖存储中心
        activeEffect && subscribers.add(activeEffect)
        return s.value
    }

    function setState(val) {
        s.value = val
        // 值更新了,就需要去把依赖存储中心中的依赖全部重新执行一遍
        subscribers.forEach(sub => sub())
    }
    return [getState, setState]
}

接着我们把 App 函数也进行修改一下:

const [count, setCount] = ref(0)

function App (){
    return createElement('button', {
        onClick: () => {
            setCount(count() + 0)
        },
        children: count()
    })
}

其实上述这种实现依赖追踪的响应式系统的方式就是 SolidJS 的响应式原理,长得像 React,实际上是 Vue。所以我们只要把核心原理搞清楚,就可以举一反三了,像读书时候一样,以后同类型的题目,你都回作答了。当然 SolidJS 的响应式原理远不止这些,我们将在后续章节继续进行深入探讨,搞明白了 SolidJS, Vue Vapor 的原理也非常容易理解了。

总结

上述所有例子中的依赖收集和触发的过程,本质就是一个发布订阅模式,而关于发布订阅模式,我们将在下一篇文章中进行详细介绍。当我们掌握了发布订阅模式后,我们再去理解这些通过依赖收集和触发实现的数据响应式系统,就会如鱼得水。

上述文章写于:2023 年,由于个人原因今年 2026 年发布。

我是程序员Cobyte,现在已转向研究 AI Agent,欢迎添加 v: icobyte,学习交流 AI Agent 应用开发。

460家沪市公司披露年报分红方案,分红总额逾8000亿元

2026年4月1日 19:54
截至2026年4月1日,沪市共有577家公司披露年报,其中460家披露了年报分红方案,占比近八成,上述公司期末分红预案对应的分红总额超过8000亿元,对应的年度累计分红总额达到1.38亿元,同比上升近4%。在已披露现金分红方案的460家公司中,43%的公司在2025年内一年多次分红,而上述公司整体在2024年多次分红比例为33%,多次分红家数大幅提升。(界面)

超捷股份:特定股东拟合计减持不超1.33%公司股份

2026年4月1日 19:52
36氪获悉,超捷股份公告,持股1.7%的员工持股平台泰州誉威投资有限公司、持股0.5%的员工持股平台泰州文超投资有限公司分别计划以集中竞价方式减持公司股份不超过120.5万股(占公司总股本比例0.9%)、不超过57.12万股(占公司总股本比例0.43%)。
❌
❌