← JavaScript Proxy的preventExtensions:对象锁定机制的精确保留 JavaScript Number.isFinite()方法:准确判断有限数值 →

JavaScript Proxy的set:属性赋值的精准拦截

原创 2026-03-25 JavaScript 已有人查阅

在 JavaScript 的 Proxy 体系中,set 是最常用、最核心的拦截方法之一。它专门用于拦截对象属性的赋值操作。无论是直接赋值 obj.prop = value,还是通过 Object.assign()、解构赋值等方式,只要涉及属性写入,set 都能介入控制。理解这个,对于实现数据验证、响应式系统、权限控制等功能非常重要。

什么是 set ?

简单来说,set 让我们能够在属性被赋值时执行自定义逻辑。当代码尝试给代理对象的某个属性赋值时,我们定义的 set 方执行,而不是直接修改目标对象。

的语法结构:

const handler = {
    set: function(target, property, value, receiver) {
        // target: 被代理的原始对象
        // property: 要赋值的属性名(字符串或 Symbol)
        // value: 要赋予的新值
        // receiver: 通常是代理对象本身,或者是实际接收赋值的对象
        // 返回值: 必须返回一个布尔值,表示赋值是否成功
    }
};

参数解析:

  • target:这是被代理的原始对象。我们可以将修改应用到它上面。

  • property:需要赋值的属性名,可以是字符串或 Symbol 类型。

  • value:要赋予的新值。

  • receiver:这个参数比较特殊,它通常是代理对象本身。但在涉及原型链继承的场景中,它可能是实际接收赋值的对象。

返回值:
这个必须返回一个布尔值。返回 true 表示赋值操作成功,返回 false 表示操作失败(在严格模式下会抛出 TypeError)。

从基础示例开始

我们先从一个简单的例子入手,看看 set 的基本工作方式。

示例 1:基础拦截与值转换

在这个例子中,我们在赋值时对值进行了转换处理。

// 代码号:learning-proxy-set-01
const targetData = { name: "初始值", count: 0 };
const handler = {
    set: function(target, property, value) {
        console.log(`[set] 正在设置属性 "${String(property)}" = ${value}`);
        
        // 对特定属性进行转换处理
        if (property === 'name') {
            // 将名字转换为大写
            target[property] = value.toUpperCase();
        } else if (property === 'count') {
            // 确保 count 是数字
            target[property] = Number(value);
        } else {
            target[property] = value;
        }
        
        // 必须返回 true 表示操作成功
        return true;
    }
};
const proxyInstance = new Proxy(targetData, handler);

proxyInstance.name = "hello world";
proxyInstance.count = "42";
proxyInstance.other = "其他值";

console.log(proxyInstance.name);  // 输出: HELLO WORLD
console.log(proxyInstance.count); // 输出: 42(数字类型)
console.log(proxyInstance.other); // 输出: 其他值
console.log(targetData.name);     // 输出: HELLO WORLD(原始对象也被修改)

个人经验: 在开发中,set 是我用得最多的 Proxy ,没有之一。从数据验证到响应式更新,从日志记录到值转换,几乎每个复杂项目都会用到它。一个常见的场景是在调试阶段给所有属性赋值添加日志,快速定位问题。

数据验证与类型检查

set 最实用的场景之一,就是实现数据验证和类型检查,确保只有符合要求的值才能被写入。

示例 2:严格的数据验证

// 代码号:learning-proxy-set-02
const userSchema = {
    name: { type: 'string', required: true, minLength: 2 },
    age: { type: 'number', required: true, min: 0, max: 150 },
    email: { type: 'string', required: true, pattern: /^[^\s@]+@[^\s@]+\.[^\s@]+$/ }
};

const validateAndSet = (target, prop, value) => {
    const rule = userSchema[prop];
    if (!rule) {
        // 允许未定义的属性,也可以选择拒绝
        target[prop] = value;
        return true;
    }
    
    // 类型检查
    if (rule.type === 'string' && typeof value !== 'string') {
        throw new TypeError(`属性 ${prop} 必须是字符串类型`);
    }
    if (rule.type === 'number' && typeof value !== 'number') {
        throw new TypeError(`属性 ${prop} 必须是数字类型`);
    }
    
    // 字符串长度检查
    if (rule.minLength && value.length < rule.minLength) {
        throw new Error(`属性 ${prop} 长度不能小于 ${rule.minLength}`);
    }
    
    // 数字范围检查
    if (rule.min !== undefined && value < rule.min) {
        throw new Error(`属性 ${prop} 不能小于 ${rule.min}`);
    }
    if (rule.max !== undefined && value > rule.max) {
        throw new Error(`属性 ${prop} 不能大于 ${rule.max}`);
    }
    
    // 正则表达式检查
    if (rule.pattern && !rule.pattern.test(value)) {
        throw new Error(`属性 ${prop} 格式不正确`);
    }
    
    console.log(`[验证通过] ${prop} = ${value}`);
    target[prop] = value;
    return true;
};

const userData = {};
const userProxy = new Proxy(userData, { set: validateAndSet });

// 正常赋值
userProxy.name = "张三";
userProxy.age = 25;
userProxy.email = "zhangsan@example.com";

console.log(userProxy.name); // 输出: 张三

// 验证失败的情况
try {
    userProxy.name = "李"; // 长度不足
} catch (e) {
    console.log(e.message); // 输出: 属性 name 长度不能小于 2
}

try {
    userProxy.age = 200; // 超出范围
} catch (e) {
    console.log(e.message); // 输出: 属性 age 不能大于 150
}

try {
    userProxy.email = "invalid-email"; // 格式错误
} catch (e) {
    console.log(e.message); // 输出: 属性 email 格式不正确
}

为什么不用 Object.defineProperty 而要使用 Proxy?
Object.defineProperty 需要逐个属性定义 getter/setter,对于动态添加的属性无能为力。而 Proxy 的 set 可以拦截所有属性的赋值,无论是预先定义的还是动态新增的,这种方式更加灵活和强大。

实现响应式系统

set 是实现响应式数据系统(如 Vue、MobX 的核心机制)的基础。

示例 3:简单的响应式数据

// 代码号:learning-proxy-set-03
class ReactiveData {
    constructor(data) {
        this._subscribers = new Map(); // 存储每个属性的订阅函数
        this._data = data;
        
        return new Proxy(this._data, {
            set: (target, prop, value) => {
                const oldValue = target[prop];
                // 值未变则跳过
                if (oldValue === value) {
                    return true;
                }
                
                target[prop] = value;
                console.log(`[响应式] 属性 ${String(prop)} 从 ${oldValue} 变为 ${value}`);
                
                // 通知订阅者
                const subs = this._subscribers.get(prop);
                if (subs) {
                    subs.forEach(callback => callback(value, oldValue));
                }
                
                return true;
            }
        });
    }
    
    // 订阅属性变化
    watch(prop, callback) {
        if (!this._subscribers.has(prop)) {
            this._subscribers.set(prop, []);
        }
        this._subscribers.get(prop).push(callback);
        
        // 返回取消订阅的函数
        return () => {
            const subs = this._subscribers.get(prop);
            const index = subs.indexOf(callback);
            if (index !== -1) subs.splice(index, 1);
        };
    }
    
    getData() {
        return this._data;
    }
}

const reactive = new ReactiveData({ count: 0, name: "初始" });
const proxy = reactive.getData();

// 订阅 count 变化
reactive.watch('count', (newVal, oldVal) => {
    console.log(`[订阅通知] count 从 ${oldVal} 变为 ${newVal}`);
});

// 触发变化
proxy.count = 1;  // 输出: [响应式] 属性 count 从 0 变为 1
                  //       [订阅通知] count 从 0 变为 1

proxy.count = 2;  // 输出: [响应式] 属性 count 从 1 变为 2
                  //       [订阅通知] count 从 1 变为 2

proxy.name = "新名字"; // 输出: [响应式] 属性 name 从 初始 变为 新名字

与 receiver 参数的关系

receiver 参数在涉及原型链继承时非常重要。它代表实际接收赋值的对象。

示例 4:receiver 参数的作用

// 代码号:learning-proxy-set-04
const parent = { inherited: "父级值" };
const child = { own: "子级值" };

const handler = {
    set: function(target, prop, value, receiver) {
        console.log(`[set] target: ${target === parent ? 'parent' : 'child'}`);
        console.log(`[set] receiver: ${receiver === child ? 'child' : 'parent'}`);
        console.log(`[set] 属性: ${String(prop)} = ${value}`);
        
        target[prop] = value;
        return true;
    }
};

const parentProxy = new Proxy(parent, handler);
Object.setPrototypeOf(child, parentProxy);

// 赋值给 child 上不存在的属性,会沿着原型链触发 parentProxy 的 set
child.newProp = "新值";
// 输出: [set] target: parent
//       [set] receiver: child
//       [set] 属性: newProp = 新值

// 注意:值实际被设置在了 parent 上,因为 set 修改的是 target
console.log(parent.newProp); // 输出: 新值
console.log(child.newProp);  // 输出: undefined(child 上没有该属性)

个人建议: 在实现 set 时,如果希望赋值行为符合直觉(即赋值给实际接收者),需要使用 Reflect.set(target, prop, value, receiver)。这可以正确处理原型链场景。

// 更规范的实现
const betterHandler = {
    set: function(target, prop, value, receiver) {
        console.log(`设置属性: ${String(prop)}`);
        // 使用 Reflect.set 正确处理原型链
        return Reflect.set(target, prop, value, receiver);
    }
};

返回值的严格要求

set 的返回值非常重要。在严格模式下,返回 false 会导致抛出 TypeError

示例 5:返回值的影响

// 代码号:learning-proxy-set-05
"use strict";

const target = {};

// 错误示例:返回 false 导致异常
const illegalProxy = new Proxy(target, {
    set: function(target, prop, value) {
        console.log(`尝试设置 ${prop},但被拒绝`);
        return false; // 返回 false 表示失败
    }
});

try {
    illegalProxy.name = "测试";
} catch (e) {
    console.log(e.message); 
    // 输出: 'set' on proxy: trap returned falsish for property 'name'
}

// 正确示例:返回 true 表示成功
const legalProxy = new Proxy(target, {
    set: function(target, prop, value) {
        target[prop] = value;
        return true; // 必须返回 true
    }
});

legalProxy.name = "测试"; // 正常工作
console.log(legalProxy.name); // 输出: 测试

本节课程知识要点:

  1. 必须返回布尔值set 必须返回 true 或 false,返回 false 在严格模式下会抛出 TypeError

  2. 值验证时机:在修改目标对象之前进行验证,避免部分修改。

  3. receiver 的重要性:在原型链场景中,使用 Reflect.set 可以正确处理 receiver 参数。

  4. 与 get 配合:通常 set 和 get 会成对出现,实现完整的属性访问控制。

  5. 不可变属性:对于不可配置、不可写的属性,set 的返回值会受到约束。

实际应用:数据脱敏与权限控制

下面是一个更贴近实际应用的例子,展示如何根据用户权限控制属性的写入。

示例 6:基于角色的权限控制

// 代码号:learning-proxy-set-06
class SecureStore {
    constructor(initialData, userRole) {
        this._data = { ...initialData };
        this._userRole = userRole;
        
        // 权限定义
        this._permissions = {
            admin: { write: true, read: true, fields: '*' },
            editor: { write: true, read: true, fields: ['name', 'description', 'status'] },
            viewer: { write: false, read: true, fields: '*' }
        };
        
        return new Proxy(this._data, {
            set: (target, prop, value) => {
                const perms = this._permissions[this._userRole];
                
                // 检查是否有写入权限
                if (!perms.write) {
                    console.warn(`[权限拒绝] ${this._userRole} 角色没有写入权限`);
                    return false;
                }
                
                // 检查字段权限
                if (perms.fields !== '*' && !perms.fields.includes(prop)) {
                    console.warn(`[权限拒绝] ${this._userRole} 角色不能修改字段 ${String(prop)}`);
                    return false;
                }
                
                // 特殊字段的额外验证
                if (prop === 'salary' && this._userRole !== 'admin') {
                    console.warn(`[权限拒绝] 只有管理员可以修改薪资`);
                    return false;
                }
                
                console.log(`[权限通过] ${this._userRole} 修改 ${String(prop)} = ${value}`);
                target[prop] = value;
                return true;
            },
            
            get: (target, prop) => {
                const perms = this._permissions[this._userRole];
                
                // 检查读取权限
                if (!perms.read) {
                    console.warn(`[权限拒绝] ${this._userRole} 角色没有读取权限`);
                    return undefined;
                }
                
                // 检查字段可见性
                if (perms.fields !== '*' && !perms.fields.includes(prop) && prop !== 'salary') {
                    console.warn(`[权限拒绝] ${this._userRole} 角色不能读取字段 ${String(prop)}`);
                    return undefined;
                }
                
                // 薪资字段特殊处理
                if (prop === 'salary' && this._userRole !== 'admin') {
                    console.warn(`[权限拒绝] 只有管理员可以查看薪资`);
                    return undefined;
                }
                
                return target[prop];
            }
        });
    }
}

const employeeData = {
    name: "李四",
    email: "lisi@company.com",
    salary: 100000,
    status: "active",
    department: "技术部"
};

// 不同角色的访问
const adminStore = new SecureStore(employeeData, 'admin');
const editorStore = new SecureStore(employeeData, 'editor');
const viewerStore = new SecureStore(employeeData, 'viewer');

// 管理员:完整权限
adminStore.name = "李四(管理员修改)";
console.log(adminStore.salary); // 输出: 100000

// 编辑者:只能修改特定字段
editorStore.name = "李四(编辑修改)"; // 成功
editorStore.salary = 120000; // 输出: [权限拒绝] 只有管理员可以修改薪资
console.log(editorStore.salary); // 输出: [权限拒绝] 只有管理员可以查看薪资 \n undefined

// 查看者:不能写入
viewerStore.name = "尝试修改"; // 输出: [权限拒绝] viewer 角色没有写入权限
console.log(viewerStore.name); // 输出: 李四(编辑修改)

结合 Reflect API 的实践

在实现 set 时,使用 Reflect.set 可以正确处理所有边缘情况。

// 代码号:learning-proxy-set-07
const baseObject = { a: 1 };
const handler = {
    set: function(target, prop, value, receiver) {
        console.log(`[拦截] 设置 ${String(prop)} = ${value}`);
        
        // 使用 Reflect.set 正确处理所有情况
        // 包括原型链、不可写属性等边界情况
        const success = Reflect.set(target, prop, value, receiver);
        
        if (success) {
            console.log(`[成功] 属性 ${String(prop)} 已设置`);
        } else {
            console.log(`[失败] 属性 ${String(prop)} 设置被拒绝`);
        }
        
        return success;
    }
};

const proxy = new Proxy(baseObject, handler);
proxy.a = 2;      // 输出: [拦截] 设置 a = 2 \n [成功] 属性 a 已设置
proxy.b = 3;      // 输出: [拦截] 设置 b = 3 \n [成功] 属性 b 已设置

// 尝试设置不可扩展对象的属性
Object.preventExtensions(baseObject);
proxy.c = 4;      // 输出: [拦截] 设置 c = 4 \n [失败] 属性 c 设置被拒绝

个人建议: 在实现 set 时,优先使用 Reflect.set(target, prop, value, receiver) 来执行实际赋值。这样做不仅能正确处理所有边界情况,还能让代码更简洁、更符合规范。只有在需要自定义赋值行为(如值转换、验证失败时阻止赋值)时,才手动操作 target[prop]

set 是 JavaScript Proxy 中最常用、最核心的拦截方法之一。它让我们能够精确控制属性的赋值行为,实现数据验证、类型转换、响应式更新、权限控制、审计日志等丰富功能。理解它的参数含义(尤其是 receiver)、返回值要求以及与 Reflect.set 的配合使用,是正确运用这个的关键。在开发中,set 是构建健壮、灵活、安全的 JavaScript 应用不可或缺的工具。

← JavaScript Proxy的preventExtensions:对象锁定机制的精确保留 JavaScript Number.isFinite()方法:准确判断有限数值 →
分享笔记 (共有 篇笔记)
验证码:
微信公众号