字节笔记本

2026年3月22日

ES6 Proxy 代理对象完全指南

这篇文章将全面介绍 ES6 Proxy 代理对象,从基本概念到实际用例,涵盖默认值处理、负索引数组、属性隐藏、缓存代理、只读视图、枚举对象、运算符重载等高级技巧。

Proxy 基本介绍

Proxy 用于修改某些操作的默认行为,可以理解为在目标对象之前架设一层拦截,外部所有的访问都必须先通过这层拦截,因此提供了一种机制,可以对外部的访问进行过滤和修改。

ES6 原生提供了 Proxy 构造函数:

javascript
var proxy = new Proxy(target, handler);
  • new Proxy 用来生成 Proxy 实例
  • target 是表示所要拦截的对象
  • handler 是用来定制拦截行为的对象

最简单的例子——一个总是返回 55 的 get 陷阱:

javascript
let target = { x: 10, y: 20 };

let handler = {
  get: (obj, prop) => 55
};

target = new Proxy(target, handler);

target.x; // 55
target.y; // 55

所有代理陷阱

Proxy 提供了十几个不同的拦截陷阱:

陷阱触发时机
handler.get读取属性
handler.set设置属性
handler.hasin 操作符
handler.apply函数调用
handler.constructnew 操作符
handler.ownKeysObject.getOwnPropertyNames
handler.deletePropertydelete 操作符
handler.definePropertyObject.defineProperty
handler.isExtensibleObject.isExtensible
handler.preventExtensionsObject.preventExtensions
handler.getPrototypeOfObject.getPrototypeOf
handler.setPrototypeOfObject.setPrototypeOf
handler.getOwnPropertyDescriptorObject.getOwnPropertyDescriptor

默认值 / "零值"

模仿 Go 语言的零值概念,为未设置的属性提供默认值:

javascript
const withZeroValue = (target, zeroValue) =>
    new Proxy(target, {
        get: (obj, prop) => (prop in obj ? obj[prop] : zeroValue)
    });

let pos = { x: 4, y: 19 };

console.log(pos.x, pos.y, pos.z); // 4, 19, undefined

pos = withZeroValue(pos, 0);

console.log(pos.x, pos.y, pos.z); // 4, 19, 0

负索引数组

在 JavaScript 中使用负索引访问数组元素,类似 Python 的 arr[-1]

javascript
const negativeArray = els =>
    new Proxy(els, {
        get: (target, propKey, receiver) =>
            Reflect.get(
                target,
                +propKey < 0 ? String(target.length + +propKey) : propKey,
                receiver
            )
    });

const unicorn = negativeArray(["a", "b", "c"]);
unicorn[-1]; // 'c'
unicorn[-2]; // 'b'

隐藏属性

模拟私有属性,屏蔽以下划线开头的属性:

javascript
const hide = (target, prefix = "_") =>
    new Proxy(target, {
        has: (obj, prop) => !prop.startsWith(prefix) && prop in obj,
        ownKeys: obj =>
            Reflect.ownKeys(obj).filter(
                prop => typeof prop !== "string" || !prop.startsWith(prefix)
            ),
        get: (obj, prop, rec) => (prop in rec ? obj[prop] : undefined)
    });

let userData = hide({
    firstName: "Tom",
    mediumHandle: "@tbarrasso",
    _favoriteRapper: "Drake"
});

userData._favoriteRapper;    // undefined
"_favoriteRapper" in userData; // false

缓存代理

根据时间策略使属性过期,实现缓存效果:

javascript
const ephemeral = (target, ttl = 60) => {
    const CREATED_AT = Date.now();
    const isExpired = () => Date.now() - CREATED_AT > ttl * 1000;

    return new Proxy(target, {
        get: (obj, prop) => (isExpired() ? undefined : Reflect.get(obj, prop))
    });
};

let bankAccount = ephemeral({ balance: 14.93 }, 10);

console.log(bankAccount.balance); // 14.93
// 10 秒后
console.log(bankAccount.balance); // undefined

只读视图

将对象包装为完全不可修改的只读视图:

javascript
const NOPE = () => { throw new Error("Can't modify read-only view"); };

const NOPE_HANDLER = {
    set: NOPE,
    defineProperty: NOPE,
    deleteProperty: NOPE,
    preventExtensions: NOPE,
    setPrototypeOf: NOPE
};

const readOnlyView = target => new Proxy(target, NOPE_HANDLER);

let config = readOnlyView({ debug: false, version: "1.0" });
config.debug = true; // Uncaught Error: Can't modify read-only view

枚举视图

访问不存在的属性时抛出异常,而非返回 undefined:

javascript
const createEnum = target =>
    readOnlyView(
        new Proxy(target, {
            get: (obj, prop) => {
                if (prop in obj) {
                    return Reflect.get(obj, prop);
                }
                throw new ReferenceError(`Unknown prop "${prop}"`);
            }
        })
    );

let SHIRT_SIZES = createEnum({ S: 10, M: 15, L: 20 });
SHIRT_SIZES.S;   // 10
SHIRT_SIZES.S = 15; // Uncaught Error: Can't modify read-only view
SHIRT_SIZES.XL;     // Uncaught ReferenceError: Unknown prop "XL"

运算符重载

使用 handler.has 重载 in 操作符,实现范围检查:

javascript
const range = (min, max) =>
    new Proxy(Object.create(null), {
        has: (_, prop) => +prop >= min && +prop <= max
    });

const X = 10.5;
const nums = [1, 5, X, 50, 100];

if (X in range(1, 100)) {
    // true
}

nums.filter(n => n in range(1, 10)); // [1, 5]

Cookie 对象代理

用 Proxy 简化 document.cookie 的操作:

javascript
const getCookieObject = () => {
    const cookies = document.cookie
        .split(";")
        .reduce(
            (cks, ck) => ({
                [ck.substr(0, ck.indexOf("=")).trim()]: ck.substr(
                    ck.indexOf("=") + 1
                ),
                ...cks
            }),
            {}
        );
    const setCookie = (name, val) => (document.cookie = `${name}=${val}`);
    const deleteCookie = name =>
        (document.cookie = `${name}=; expires=Thu, 01 Jan 1970 00:00:01 GMT;`);

    return new Proxy(cookies, {
        set: (obj, prop, val) => (
            setCookie(prop, val), Reflect.set(obj, prop, val)
        ),
        deleteProperty: (obj, prop) => (
            deleteCookie(prop), Reflect.deleteProperty(obj, prop)
        )
    });
};

let docCookies = getCookieObject();
docCookies.has_recent_activity; // "1"
docCookies.has_recent_activity = "2"; // "2"
delete docCookies["has_recent_activity"]; // true

组合多个代理

Proxy 的优势在于可以轻松组合:

javascript
let docCookies = withZeroValue(
    hide(readOnlyView(getCookieObject())),
    "Cookie not found"
);

docCookies.has_recent_activity  // "1"
docCookies.nonExistentCookie    // "Cookie not found"
docCookies._ga                  // "Cookie not found"(隐藏的属性)
docCookies.newCookie = "1"      // Error: Can't modify read-only view

注意事项

性能

Proxy 的主要缺点是性能开销。对于性能敏感的代码,需要衡量代理的优势是否超过性能影响。

限制

javascript
// 目标必须是 Object,不能是原始值
new Proxy("hello", {});
// TypeError: Cannot create proxy with a non-object as target

// 无法检测对象是否是代理
// 根据 JS 规范,无法确定一个对象是否是 Proxy

Polyfill

Proxy 没有完整的 polyfill,但 Google 提供了一个部分 polyfill,支持 getsetapplyconstruct 陷阱。

Proxy vs Object.defineProperty

特性ProxyObject.defineProperty
拦截范围13 种操作get/set
动态添加属性自动拦截需要重新定义
数组变化检测支持不支持
性能较低较高
Vue 3基于 ProxyVue 2 基于 defineProperty

Proxy 提供了 JavaScript 中强大的元编程能力,是 Vue 3 响应式系统的底层实现基础。

分享: