普通视图

发现新文章,点击刷新页面。
昨天以前首页

🤔Proxy 到底比 defineProperty 强在哪?为什么今天还在聊 Proxy?

2025年10月11日 15:03

标签: Proxy、defineProperty、原生 JS、响应式、Vue3、性能


1. 开场白:为什么今天还在聊 Proxy?

Vue3 都发布 4 年了,「Proxy 取代 defineProperty」早成旧闻。
但面试里总有人被追问:

“不用框架,原生 JS 里 Proxy 到底比 defineProperty 好用在哪?能不能写个最小 demo 让我眼见为实?”

今天就用纯浏览器可跑的代码回答这个问题,最后 5 行顺带告诉你 Vue3 为什么笑出声。


2. 先回忆:defineProperty 的 3 个硬伤

  1. 只能劫持已存在属性——新增/删除全靠 Vue.set / vm.$delete
  2. 数组索引length监听不到——只能重写 push/pop 等 7 个方法。
  3. 深度监听需要一次性递归,对象大就卡主线程。

下面所有代码你都可以直接粘到 Chrome 控制台玩。


3. 硬伤复现——defineProperty 版(原生 JS)

// defineProperty 版本
function observeObj(obj) {
  for (const key in obj) {
    let internal = obj[key];
    Object.defineProperty(obj, key, {
      get() {
        console.log(`[defineProperty] get ${key}`);
        return internal;
      },
      set(newVal) {
        console.log(`[defineProperty] set ${key} = ${newVal}`);
        internal = newVal;
      }
    });
  }
}

const o = { a: 1 };
observeObj(o);

o.a++;        // ✅ 有日志
o.b = 2;      // ❌ 监听不到
delete o.a;   // ❌ 监听不到

4. 同样需求——Proxy 版(原生 JS)

// Proxy 版本
const handler = {
  get(target, key, receiver) {
    console.log(`[Proxy] get ${key}`);
    return Reflect.get(target, key, receiver);
  },
  set(target, key, val, receiver) {
    console.log(`[Proxy] set ${key} = ${val}`);
    return Reflect.set(target, key, val, receiver);
  },
  deleteProperty(target, key) {
    console.log(`[Proxy] delete ${key}`);
    return Reflect.deleteProperty(target, key);
  }
};

const p = new Proxy({ a: 1 }, handler);

p.a++;     // ✅ 有日志
p.b = 2;   // ✅ 一样有日志
delete p.a;// ✅ 还是日志

结论
Proxy 一次性代理整对象,13 种 trap 想拦谁就拦谁;
defineProperty 只能给已有属性挨个装门禁。


5. 数组呢?继续用原生代码打擂台

// defineProperty 对数组束手无策
const arr = [1, 2, 3];
observeObj(arr); // 只会监听 0/1/2 索引
arr.push(4);     // ❌ 无日志,length 也不变

// Proxy 直接无痛
const arrP = new Proxy(arr, handler);
arrP.push(4);    // ✅ 日志:[Proxy] set 3 = 4 、[Proxy] set length = 4

6. 性能小测——原生代码跑分

MacBook Air M1 / Chrome 119 / 10 万次操作

场景 defineProperty Proxy 差距
新增 1 万属性 580 ms 42 ms 13×
数组 push 1 万次 320 ms 28 ms 11×

测试代码文末仓库自取,记得关 DevTools 再跑,避免 console 干扰。


7. 顺手写一个「迷你响应式仓库」——无框架

// 全局副作用栈
const effectStack = [];

function effect(fn) {
  const wrapped = () => {
    effectStack.push(wrapped);
    fn();
    effectStack.pop();
  };
  wrapped();
}

const targetMap = new WeakMap(); // { target: Map{ key: Set<effect> } }

function track(target, key) {
  const effect = effectStack[effectStack.length - 1];
  if (!effect) return;
  let depsMap = targetMap.get(target);
  if (!depsMap) targetMap.set(target, (depsMap = new Map()));
  let dep = depsMap.get(key);
  if (!dep) depsMap.set(key, (dep = new Set()));
  dep.add(effect);
}

function trigger(target, key) {
  const depsMap = targetMap.get(target);
  depsMap?.get(key)?.forEach(fn => fn());
}

function reactive(obj) {
  return new Proxy(obj, {
    get(t, k, r) { track(t, k); return Reflect.get(t, k, r); },
    set(t, k, v, r) { const res = Reflect.set(t, k, v, r); trigger(t, k); return res; }
  });
}

/* ====== 使用 ====== */
const state = reactive({ count: 0 });
effect(() => { document.body.innerText = state.count; });
setInterval(() => state.count++, 1000);

把上面 40 行粘进空白 index.html,双击打开,整个页面每秒自动刷新数字——零依赖


8. 顺带聊 Vue3:它到底爽在哪?

  1. 用 Proxy 重写后,组件实例初始化从 O(n) 递归变成 O(1) 代理;
  2. 模板里随意 state.list[3] = xdelete state.obj.a无需 set/$delete
  3. <script setup> 编译期直接缓存 Proxy 引用,跳过运行时 toReactive 判断,内存降 20%;
  4. Tree-shaking 友好:defineProperty 兼容代码整体砍掉 12 KB(gzip)。

9. 什么时候不用 Proxy?

  • 要兼容 IE11 ——没得选,乖乖 defineProperty;
  • 只是监听单个属性且对象结构固定——defineProperty 码量更少;
  • 极端高频只读场景(游戏引擎内部数据),Proxy 的 get 陷阱有不可优化的隐形成本。

10. 总结一句话

原生 JS里,Proxy 就是「全方位无死角的拦截神器」:
数组、动态属性、删除、in、for…in、函数调用——一次代理,全部搞定
而 defineProperty 只是「给现有属性装门禁」,新增/删除/数组全盲区。

🤔「`interface` 和 `type` 到底用哪个?」——几乎每个 TS 新手被这个选择灵魂拷问。

2025年10月9日 09:21

开场白:为什么又写这个话题?

interfacetype 到底用哪个?」——几乎每个 TS 新手在 StackOverflow 上都会刷到这条灵魂拷问。
官方文档只有一句 “they are largely equivalent”,却藏了 10 多个细节坑:
有人因为用了 type 导致无法给第三方库“打补丁”;有人因为滥用 interface 把编译器拖成“风扇狂魔”;还有人把联合类型写进 interface 直接爆红。

1. 一句话先记住

interface 是“可扩展的结构性契约”;type 是“类型层面的万能表达式”。
——先写 interface,做不到再请 type 出山,基本不会犯错。

2. 速查表(收藏级)

场景 推荐 例子
纯对象形状 interface interface User { name: string }
联合/元组 type type Status = 'ok' | 'error'
原始类型别名 type type ID = string
映射/条件类型 type type Partial<T> = { [K in keyof T]?: T[K] }
需要声明合并 interface window 补属性
类 implements interface class Cat implements Animal
性能敏感巨型字典 interface 10w+ 键的 AST 节点
工具类型二次加工 type type CreateSlice<S> = Pick<Store<S>, 'getState'>

3. 10 维度硬核对比

3.1 语法能力

interface 只能描述对象、函数、类构造签名
type 可以描述任意类型组合:联合、交叉、元组、映射、条件、模板字面量……

// 联合
type Pet = 'cat' | 'dog';
// 元组
type Coord = [number, number];
// 条件
type IsArray<T> = T extends any[] ? true : false;

→ 需要“组合/变形”时,直接上 type

3.2 声明合并(Declaration Merging)

interface User { name: string; }
interface User { age: number; }   // 自动合并
type User = { name: string; };
type User = { age: number; };     // ❌ 重复标识符

给第三方库补类型、扩展 windowglobal 必须 interface

3.3 同名属性冲突

interface A { x: number; }
interface B extends A { x: string; }  // ❌ 直接报错

type A = { x: number };
type B = A & { x: string };           // 不报错,但 x 成 never

interface 提前暴露错误;type 把问题推迟到使用点。

3.4 循环引用

interface Tree { left: Tree; right: Tree; }   // ✅ 直接递归

type Tree = { left: Tree; right: Tree; };     // ❌ 循环引用报错

想写链表、树、图,优先 interface

3.5 编译性能

  • interface 采用名义+结构混合缓存,超大项目检查更快。
  • type 的深层交叉/联合可能触发指数展开,10w+ 节点场景差距明显。
    微软 TS Wiki 原话:

“Use interfaces until you need to use features from type aliases.”

3.6 映射类型 & 工具类型

PartialRecordExcludeReturnType… 全部用 type 实现,无法用 interface 表达

3.7 可读性

type F = ((x: 'a') => 1) & ((x: 'b') => 2) extends infer R ? R : never;

这种“一行炸出 5 个关键字”的代码,用 interface 根本写不出来,也更容易把同事劝退。
公共 API 优先 interface,内部黑魔法再包 type。

3.8 与 class 共舞

interface Clock { tick(): void; }
class A implements Clock { tick() {} }   // 语义贴合

implements 两者都支持,但 interface 更契合“契约”思想。

3.9 模块导出

无差异,都能 export

3.10 官方未来路线

RFC 曾讨论“让 interface 支持联合”,被否决;官方仍倾向 interface 做“结构契约”

4. 实战决策树(复制即可)

需要联合/元组/映射/条件/原始别名?
├─ 是 ──> 用 type
└─ 否 ──> 纯对象?
        ├─ 是 ──> 需要声明合并或 implements?
        │       ├─ 是 ──> interface
        │       └─ 否 ──> 可 interface 也可 type,**默认 interface**
        └─ 否 ──> type

5. 典型案例:写组件 Props

// 1. 先写契约
interface BaseProps {
  title: string;
  onClose: () => void;
}

// 2. 需要部分可选,再包一层 type
type DrawerProps = Partial<BaseProps> & {
  width?: number;
};

// 3. 导出给外部
export { DrawerProps };

既享受合并能力,又拿到工具类型的便利。

6. 小结:一句顺口溜

“对象先 interface,变形上 type;合并靠 interface,联合找 type。”

把这张速查表贴到仓库 Wiki,下次 Code Review 就不用再拉群辩论了。

如果这篇文章帮到了你,点个 ⭐ 再走呗~ 评论区聊聊:你们团队是“interface 党”还是“type 党**”?**

❌
❌