当前位置: 首页 > news >正文

JavaScript中的Proxy详解

1. 什么是Proxy?

Proxy是ES6引入的一个强大特性,它允许你创建一个对象的代理,从而可以拦截和自定义该对象的基本操作。Proxy提供了一种机制,可以在对象的基本操作,如属性查找、赋值、枚举、函数调用等之前或之后执行自定义行为。

基本语法如下:

const proxy = new Proxy(target, handler);

target:要代理的目标对象,可以是任何类型的对象,包括数组、函数或另一个代理;

handler:一个对象,其属性是定义代理行为的函数,称为"陷阱"或"trap";

2. 所有的handler方法

2.1. get(target, prop, receiver) - 拦截属性读取

const person = {
    name: "Alice",
    age: 30,
};

const proxy = new Proxy(person, {
    get(target, prop) {
        console.log(`Reading property: ${prop}`);
        return prop in target ? target[prop] : "Default";
    },
});

console.log(proxy.name); // "Reading property: name" → "Alice"
console.log(proxy.gender); // "Reading property: gender" → "Default"

2.2. set(target, prop, value, receiver) - 拦截属性设置

const validator = {
    set(target, prop, value) {
        if (prop === "age") {
            if (!Number.isInteger(value) || value < 0) {
                throw new TypeError("Age must be a positive integer");
            }
        }
        console.log(`Setting ${prop} to ${value}`);
        target[prop] = value;
        return true; // 表示设置成功
    },
};

const person = new Proxy({}, validator);
person.age = 25; // "Setting age to 25"
person.age = -5; // 抛出错误: Age must be a positive integer

2.3. has(target, prop) - 拦截 in 操作符

const hiddenProps = ["secret", "password"];
const target = {
    name: "Bob",
    secret: "123",
};

const proxy = new Proxy(target, {
    has(target, prop) {
        if (hiddenProps.includes(prop)) {
            return false; // 隐藏属性
        }
        return prop in target;
    },
});

console.log("name" in proxy); // true
console.log("secret" in proxy); // false

2.4. deleteProperty(target, prop) - 拦截 delete 操作

const protectedData = {
    name: "Charlie",
    id: "12345",
};

const proxy = new Proxy(protectedData, {
    deleteProperty(target, prop) {
        if (prop === "id") {
            throw new Error("Cannot delete ID property");
        }
        delete target[prop];
        return true;
    },
});

delete proxy.name; // 成功
delete proxy.id; // 抛出错误: Cannot delete ID property

2.5. apply(target, thisArg, arguments) - 拦截函数调用

function sum(a, b) {
    return a + b;
}

const loggingProxy = new Proxy(sum, {
    apply(target, thisArg, args) {
        console.log(`Function called with args: ${args}`);
        console.log(`thisArg: ${thisArg}`);
        const result = target.apply(thisArg, args);
        console.log(`Function returned: ${result}`);
        return result;
    },
});

loggingProxy(2, 3);
// "Function called with args: 2,3"
// "thisArg: undefined"
// "Function returned: 5"
// → 5

2.6. construct(target, argumentsList, newTarget) - 拦截 new 操作符

class Person {
    constructor(name) {
        this.name = name;
    }
}

const PersonProxy = new Proxy(Person, {
    construct(target, args, newTarget) {
        console.log(`Creating instance with args: ${args}`);
        // 可以修改参数或添加额外逻辑
        if (args.length === 0) {
            args = ["Anonymous"];
        }
        return new target(...args);
    },
});

const p1 = new PersonProxy("Alice");
// "Creating instance with args: Alice"
const p2 = new PersonProxy();
// "Creating instance with args:" → name: "Anonymous"

2.7. ownKeys(target) - 拦截 Object.keys() 等操作

const user = {
    name: "Dave",
    age: 40,
    _password: "****",
};

const proxy = new Proxy(user, {
    ownKeys(target) {
        return Object.keys(target).filter((key) => !key.startsWith("_"));
    },
});

console.log(Object.keys(proxy)); // ["name", "age"]
console.log(Object.getOwnPropertyNames(proxy)); // ["name", "age"]

2.8. getOwnPropertyDescriptor(target, prop) - 拦截 Object.getOwnPropertyDescriptor()

const target = {
    name: "Eve",
};

const proxy = new Proxy(target, {
    getOwnPropertyDescriptor(target, prop) {
        console.log(`Getting descriptor for: ${prop}`);
        const descriptor = Object.getOwnPropertyDescriptor(target, prop);
        // 可以修改描述符
        if (prop === "name") {
            descriptor.enumerable = false;
        }
        return descriptor || undefined;
    },
});

console.log(Object.getOwnPropertyDescriptor(proxy, "name"));
// "Getting descriptor for: name" → {value: "Eve", writable: true, enumerable: false, configurable: true}

2.9. defineProperty(target, prop, descriptor) - 拦截 Object.defineProperty()

const target = {};

const proxy = new Proxy(target, {
    defineProperty(target, prop, descriptor) {
        console.log(`Defining property ${prop}`);
        if (prop.startsWith("_")) {
            throw new Error(`Cannot define private property: ${prop}`);
        }
        return Reflect.defineProperty(target, prop, descriptor);
    },
});

Object.defineProperty(proxy, "name", { value: "Frank" }); // 成功
Object.defineProperty(proxy, "_secret", { value: "123" }); // 抛出错误

2.10. preventExtensions(target) - 拦截 Object.preventExtensions()

const target = { name: "Grace" };
const proxy = new Proxy(target, {
    preventExtensions(target) {
        console.log("Attempt to prevent extensions");
        // 可以决定是否允许阻止扩展
        return false; // 返回false表示不允许阻止扩展
        // 或者调用 Reflect.preventExtensions(target)
    },
});

Object.preventExtensions(proxy);
// "Attempt to prevent extensions"
// 抛出 TypeError (因为返回了false)

2.11. isExtensible(target) - 拦截 Object.isExtensible()

const target = {};

const proxy = new Proxy(target, {
    isExtensible(target) {
        console.log("Checking extensibility");
        // 可以返回自定义值
        return false; // 总是返回不可扩展
        // 或者调用 Reflect.isExtensible(target)
    },
});

console.log(Object.isExtensible(proxy));
// "Checking extensibility" → false

2.12. getPrototypeOf(target) - 拦截 Object.getPrototypeOf()

const originalProto = { version: 1 };
const obj = Object.create(originalProto);

const proxy = new Proxy(obj, {
    getPrototypeOf(target) {
        console.log("Getting prototype");
        // 可以返回不同的原型
        return { version: 2 }; // 返回假原型
        // 或者调用 Reflect.getPrototypeOf(target)
    },
});

console.log(Object.getPrototypeOf(proxy));
// "Getting prototype" → {version: 2}

2.13. setPrototypeOf(target, prototype) - 拦截 Object.setPrototypeOf()

const target = {};
const proxy = new Proxy(target, {
    setPrototypeOf(target, proto) {
        console.log(`Setting prototype to: ${proto}`);
        // 可以阻止设置原型
        throw new Error("Prototype modification not allowed");
        // 或者调用 Reflect.setPrototypeOf(target, proto)
    },
});

Object.setPrototypeOf(proxy, {});
// "Setting prototype to: [object Object]"
// 抛出错误: Prototype modification not allowed

2.14. 综合示例:实现一个全面的数据验证代理

const createValidatedObject = (schema, initialData = {}) => {
    return new Proxy(initialData, {
        set(target, prop, value) {
            // 检查属性是否在schema中定义
            if (!(prop in schema)) {
                throw new Error(`Property "${prop}" is not allowed`);
            }

            // 获取验证函数
            const validator = schema[prop];

            // 验证值
            if (!validator(value)) {
                throw new Error(`Invalid value for property "${prop}"`);
            }

            // 设置值
            target[prop] = value;
            return true;
        },

        get(target, prop) {
            if (prop in target) {
                return target[prop];
            }
            throw new Error(`Property "${prop}" does not exist`);
        },

        deleteProperty(target, prop) {
            if (prop in schema && !schema[prop].configurable) {
                throw new Error(`Cannot delete property "${prop}"`);
            }
            delete target[prop];
            return true;
        },
    });
};

// 定义schema
const userSchema = {
    name: (value) => typeof value === "string" && value.length > 0,
    age: (value) => Number.isInteger(value) && value >= 0,
    email: (value) => /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(value),
};

// 创建代理对象
const user = createValidatedObject(userSchema);

// 使用示例
user.name = "Alice"; // 成功
user.age = 30; // 成功
user.email = "alice@example.com"; // 成功

try {
    user.age = -5; // 抛出错误: Invalid value for property "age"
} catch (e) {
    console.error(e.message);
}

try {
    user.gender = "female"; // 抛出错误: Property "gender" is not allowed
} catch (e) {
    console.error(e.message);
}

这些示例涵盖了 Proxy 的主要 handler 方法,展示了它们在实际开发中的应用场景。通过这些示例,你可以看到 Proxy 如何为 JavaScript 对象提供强大的拦截和自定义能力。

3. 常用场景

下面是Proxy在实际开发中的10种常见应用场景的完整代码示例:

3.1. 数据验证

const createValidator = (rules) => {
    return new Proxy(
        {},
        {
            set(target, prop, value) {
                if (!rules[prop]) {
                    throw new Error(`Property "${prop}" is not allowed`);
                }
                if (!rules[prop](value)) {
                    throw new Error(`Invalid value for "${prop}"`);
                }
                target[prop] = value;
                return true;
            },
        }
    );
};

// 使用示例
const userValidator = createValidator({
    name: (val) => typeof val === "string" && val.length >= 2,
    age: (val) => Number.isInteger(val) && val >= 18,
    email: (val) => /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(val),
});

userValidator.name = "Alice"; // 成功
userValidator.age = 25; // 成功
// userValidator.age = '25'; // 抛出错误
// userValidator.gender = 'female'; // 抛出错误

3.2. 观察者模式(数据变化监听)

function createObservable(target, callback) {
    return new Proxy(target, {
        set(target, prop, value) {
            const oldValue = target[prop];
            target[prop] = value;
            callback(prop, oldValue, value);
            return true;
        },
    });
}

// 使用示例
const user = { name: "Bob", age: 30 };
const observableUser = createObservable(user, (prop, oldVal, newVal) => {
    console.log(`Property ${prop} changed from ${oldVal} to ${newVal}`);
});

observableUser.name = "Alice"; // 控制台输出: Property name changed from Bob to Alice
observableUser.age = 31; // 控制台输出: Property age changed from 30 to 31

3.3. 属性访问控制

const createPrivateProps = (obj, privateProps = []) => {
    return new Proxy(obj, {
        get(target, prop) {
            if (privateProps.includes(prop)) {
                throw new Error(`Property "${prop}" is private`);
            }
            return target[prop];
        },
        set(target, prop, value) {
            if (privateProps.includes(prop)) {
                throw new Error(`Cannot set private property "${prop}"`);
            }
            target[prop] = value;
            return true;
        },
    });
};

// 使用示例
const account = createPrivateProps(
    {
        username: "alice123",
        _password: "secret123",
    },
    ["_password"]
);

console.log(account.username); // 'alice123'
// console.log(account._password); // 抛出错误
// account._password = 'newpass'; // 抛出错误

3.4. 虚拟属性(动态计算属性)

const createVirtualProps = (obj, virtualProps = {}) => {
    return new Proxy(obj, {
        get(target, prop) {
            if (prop in virtualProps) {
                return virtualProps[prop](target);
            }
            return target[prop];
        },
    });
};

// 使用示例
const person = createVirtualProps(
    {
        firstName: "John",
        lastName: "Doe",
    },
    {
        fullName: (target) => `${target.firstName} ${target.lastName}`,
        initials: (target) => `${target.firstName[0]}${target.lastName[0]}`,
    }
);

console.log(person.fullName); // 'John Doe'
console.log(person.initials); // 'JD'

3.5. API封装(简化复杂API)

const api = {
    _baseUrl: "https://api.example.com",
    _endpoints: {
        users: "/users",
        posts: "/posts",
    },
    _makeRequest(url, options) {
        console.log(`Making request to ${url}`);
        // 实际这里会是fetch或axios调用
        return Promise.resolve({ data: `${url} response` });
    },
};

const apiProxy = new Proxy(api, {
    get(target, prop) {
        if (prop in target._endpoints) {
            return (options = {}) => {
                const url = `${target._baseUrl}${target._endpoints[prop]}`;
                return target._makeRequest(url, options);
            };
        }
        return target[prop];
    },
});

// 使用示例
apiProxy.users().then((res) => console.log(res.data));
// "Making request to https://api.example.com/users"
// → "https://api.example.com/users response"

apiProxy.posts({ page: 2 }).then((res) => console.log(res.data));
// "Making request to https://api.example.com/posts"
// → "https://api.example.com/posts response"

3.6. 性能优化(延迟加载/缓存)

const createCachedProxy = (fn) => {
    const cache = new Map();
    return new Proxy(fn, {
        apply(target, thisArg, args) {
            const key = JSON.stringify(args);
            if (cache.has(key)) {
                console.log("Returning cached result");
                return cache.get(key);
            }
            console.log("Calculating new result");
            const result = target.apply(thisArg, args);
            cache.set(key, result);
            return result;
        },
    });
};

// 使用示例
const expensiveCalculation = (a, b) => {
    console.log("Running expensive calculation...");
    return a * b;
};

const cachedCalc = createCachedProxy(expensiveCalculation);

console.log(cachedCalc(2, 3)); // "Running expensive calculation..." → 6
console.log(cachedCalc(2, 3)); // "Returning cached result" → 6
console.log(cachedCalc(4, 5)); // "Running expensive calculation..." → 20

3.7. 负索引数组(类似Python)

const createNegativeIndexArray = (arr) => {
    return new Proxy(arr, {
        get(target, prop) {
            const index = parseInt(prop);
            if (!isNaN(index)) {
                // 处理负索引
                prop = index < 0 ? target.length + index : index;
            }
            return Reflect.get(target, prop);
        },
    });
};

// 使用示例
const array = createNegativeIndexArray(["a", "b", "c", "d"]);

console.log(array[0]); // 'a'
console.log(array[-1]); // 'd'
console.log(array[-2]); // 'c'

3.8. 函数调用日志(调试)

const withLogging = (fn) => {
    return new Proxy(fn, {
        apply(target, thisArg, args) {
            console.log(`Calling ${target.name} with args:`, args);
            const start = performance.now();
            const result = target.apply(thisArg, args);
            const end = performance.now();
            console.log(
                `Called ${target.name}, took ${(end - start).toFixed(2)}ms`
            );
            return result;
        },
    });
};

// 使用示例
function calculate(a, b, c) {
    // 模拟耗时操作
    for (let i = 0; i < 100000000; i++) {}
    return a + b * c;
}

const loggedCalc = withLogging(calculate);
console.log(loggedCalc(2, 3, 4));
// 控制台输出:
// Calling calculate with args: [2, 3, 4]
// Called calculate, took 123.45ms
// → 14

3.9. 自动填充默认值

const withDefaults = (obj, defaults) => {
    return new Proxy(obj, {
        get(target, prop) {
            if (prop in target) {
                return target[prop];
            }
            if (prop in defaults) {
                const defaultValue = defaults[prop];
                // 如果默认值是函数则调用它
                return typeof defaultValue === "function"
                    ? defaultValue()
                    : defaultValue;
            }
            return undefined;
        },
    });
};

// 使用示例
const config = withDefaults(
    {
        theme: "dark",
    },
    {
        theme: "light",
        fontSize: 14,
        timestamp: () => new Date().toISOString(),
    }
);

console.log(config.theme); // 'dark' (使用已有值)
console.log(config.fontSize); // 14 (使用默认值)
console.log(config.timestamp); // 当前时间ISO字符串 (调用函数默认值)
console.log(config.nonExisting); // undefined

3.10. 数据绑定(双向绑定)

function createBinding(sourceObj, targetObj, propertyMap) {
    const handler = {
        set(target, prop, value) {
            target[prop] = value;
            // 更新绑定的属性
            if (propertyMap[prop]) {
                const targetProp = propertyMap[prop];
                targetObj[targetProp] = value;
                console.log(`Updated ${targetProp} to ${value}`);
            }

            return true;
        },
    };

    return new Proxy(sourceObj, handler);
}

// 使用示例
const form = {};
const model = { firstName: "", lastName: "" };

const boundForm = createBinding(form, model, {
    "first-name": "firstName",
    "last-name": "lastName",
});

boundForm["first-name"] = "Alice";
// 控制台输出: Updated firstName to Alice
boundForm["last-name"] = "Smith";
// 控制台输出: Updated lastName to Smith

console.log(model);
// { firstName: 'Alice', lastName: 'Smith' }

这些示例展示了Proxy在实际开发中的强大能力。每个示例都可以根据具体需求进行扩展和组合使用,Proxy为JavaScript提供了元编程的强大工具,可以优雅地解决许多复杂问题。

4. 注意事项

1. Proxy的handler方法中应该尽量使用Reflect对象的方法来保持默认行为;

2. 不是所有操作都可以被拦截,例如严格相等===;

3. 过度使用Proxy可能会影响性能;

4. 某些库可能无法正确处理Proxy对象;

5. Proxy的this绑定可能与原对象不同;

Proxy为JavaScript提供了强大的元编程能力,合理使用可以大大简化代码并实现复杂的功能,但也要注意不要过度使用,以免影响代码的可读性和性能。

相关文章:

  • CUDA GPU 学习资源
  • 第三方软件测试服务公司分享:功能测试和性能测试的区别与联系
  • 小型园区组网图
  • AlDente Pro for Mac电脑 充电限制保护工具
  • 解码 __all__ - 模块接口的守护者
  • Django SaaS案例:构建一个多租户博客应用
  • SQL LIKE 语句详解
  • I²C、SPI、UART、CAN 通信协议详解
  • docker配置redis容器时配置文件docker-compose.yml示例
  • deepseek对openGauss 6.0启动日志的分析与处理
  • TCP/IP五层协议
  • 销售心理学工具包:100个可复用的话术模板与案例库-第一部分:销售心理学核心理论与工具-1.2情感共鸣构建:镜像神经元理论与情绪同步话术设计
  • 【教程】MacBook 使用 iTerm2 连接跳板机和开发机
  • 增益调度控制 —— 理论、案例与交互式 GUI 实现
  • LeetCode Hot100 刷题笔记(3)—— 链表
  • Python作业2 蒙特卡罗方法手搓图形
  • 使用 VIM 编辑器对文件进行编辑
  • 路由器学习
  • 【C++奇遇记】C++中的进阶知识(多态(一))
  • 使用MySQL时出现 Ignoring query to other database 错误
  • 网站建设最新技术/八戒
  • 做门户网站的系统/今日热点新闻事件简介
  • 用rp怎么做网站导航菜单/简述获得友情链接的途径
  • 网站建设上海网站建设公司网站/网络营销属于哪个专业
  • 做网站简单吗/百度账号客服
  • 做网站需要什么证件吗/网络推广app是违法的吗