红茶的个人站点

  • 首页
  • 专栏
  • 开发工具
  • 其它
  • 隐私政策
Awalon
Talk is cheap,show me the code.
  1. 首页
  2. 前端学习笔记
  3. 正文

JavaScript 学习笔记 10:代理

2026年4月20日 3点热度 0人点赞 0条评论

JS 中实现代理的语法:

let target = {};
let proxy = new Proxy(target, {});
proxy.a = 1;
console.log(proxy.a);
console.log(target.a);
for(let i in proxy){
    console.log(i);
}
// 1
// 1
// a

new Proxy接收两个参数,要代理的目标对象以及代理行为,这里代理行为使用的是空对象,因此所有对代理对象属性的读写都是由被代理的原始对象承担的。

get

可以设置代理行为的 get 方法以改变读取代理对象属性时的行为,比如:

let numbers = [1, 2, 3, 4, 5];
numbers = new Proxy(numbers, {
    get(target, index) {
        if(index in target){
            return target[index];
        }
        return 0;
    }
});
console.log(numbers[1]);
console.log(numbers[100]);
// 2
// 0

在正常情况下,JS 中读取一个数组的不存在的下标,会返回undefined,这里使用代理改变了获取数组元素的行为,如果下标不存在,返回的将是 0。

再看一个例子:

let dictionary = { 
    'hello': '你好', 
    'world': '世界' 
}
dictionary = new Proxy(dictionary, {
    get(target, phrase) {
        if (phrase in target) {
            return target[phrase];
        }
        return phrase;
    }
});
console.log(dictionary['hello']);
console.log(dictionary['world']);
console.log(dictionary['universe']);
// 你好
// 世界
// universe

读取字典中不存在的索引,同样会返回undefined,使用代理改变字典对象的行为,如果查询不存在的索引,返回的是索引本身。

set

使用 set 可以改变代理对象写入属性的行为:

let numbers2 = [];
numbers2 = new Proxy(numbers2, {
    set(target, index, value) {
        if (typeof value === 'number') {
            target[index] = value;
            return true;
        }
        return false;
    }
});
numbers2.push(1);
numbers2.push(2);
console.log(numbers2);
// [ 1, 2 ]
numbers2.push('a');
// TypeError: 'set' on proxy: trap returned falsish for property '2'

这里代理的目标对象是一个数组,并在代理对象中使用set方法检查添加属性值是否是数字类型,如果是就添加,不是就报错(set方法返回false)。

代理的set方法中只需要直接给原始对象的属性赋值即可,JS 预定义的push等方法都可以和set方法配合的很好。

ownKeys

前面说过,JS 默认约定以下划线开始的属性是私有属性,不应该被对象外部直接访问,但 JS 语言本身并不提供语法层面的保护,外部代码依然可以访问和遍历。

我们可以通过代理实现保护,不让外部代码遍历到这些属性:

let user = {
    name: '张三',
    age: 20,
    _password: '123'
};
user = new Proxy(user, {
    ownKeys(target) {
        return Object.keys(target).filter(key => !key.startsWith('_'));
    }
});
for (let key in user) {
    console.log(key);
}
// name
// age

如果仅用于隐藏某些属性,这样做已经足够,但如果要给代理对象添加某些原始对象不存在的属性:

let user2 = {
    name: '张三',
    age: 20,
    _password: '123'
};
​
user2 = new Proxy(user2, {
    ownKeys(target) {
        let keys = Object.keys(target);
        return [...keys, 'a', 'b', 'c'];
    }
});
​
for (let key in user2) {
    console.log(key);
}
// name
// age
// _password

可以看到,遍历时并没有通过ownKeys添加的新属性(a,b,c)。这是因为 JS 在获取可遍历的属性时(Object.keys(...)),会通过内部方法[[GetOwnProperty]]获取属性的描述符,查看其是否是可遍历的属性,上面的示例中通过ownKeys方法添加的新属性没有属性描述符,因此这里遍历不到。

可以通过getOwnPropertyDescriptor方法改变内部方法[[GetOwnProperty]]的行为:

// ...
user2 = new Proxy(user2, {
    ownKeys(target) {
        let keys = Object.keys(target);
        return [...keys, 'a', 'b', 'c'];
    },
    getOwnPropertyDescriptor(target, key) {
        if (key in target) {
            return Object.getOwnPropertyDescriptor(target, key);
        }
        if (['a','b','c'].includes(key)) {
            return {
                value: 0,
                writable: true,
                enumerable: true,
                configurable: true
            };
        }
    }
});
​
for (let key in user2) {
    console.log(key);
}
// name
// age
// _password
// a
// b
// c

deleteProperty

可以使用上述方法结合deleteProperty方法实现一个对私有属性进行保护的代理:

function propertyProtectProxy(obj){
    return new Proxy(obj, {
        get(target, key) {
            if (key.startsWith('_')) {
                throw new Error(`属性 ${key} 是受保护的`);
            }
            let value = target[key];
            return typeof value === 'function' ? value.bind(target) : value;
        },
        set(target, key, value) {
            if (key.startsWith('_')) {
                throw new Error(`属性 ${key} 是受保护的`);
            }
            target[key] = value;
            return true;
        },
        deleteProperty(target, key) {
            if (key.startsWith('_')) {
                throw new Error(`属性 ${key} 是受保护的`);
            }
            delete target[key];
            return true;
        },
        ownKeys(target) {
            return Object.keys(target).filter(key => !key.startsWith('_'));
        }
    });
}
​
let user3 = {
    name: '张三',
    age: 20,
    _password: '123'
};
user3 = propertyProtectProxy(user3);
console.log(user3.name);
console.log(user3.age);
console.log(user3._password);
// 张三
// 20
// Error: 属性 _password 是受保护的

需要注意的是,get方法需要考虑获取的属性是不是函数,如果是函数,需要使用func.bind()将函数对象绑定到原始对象(target),这是因为这里获取到的函数对象的上下文中this对应的是代理对象,如果不将其重新绑定到原始对象,该方法中就无法正常使用私有属性(_开头的属性):

let user4 = {
    name: '张三',
    age: 20,
    _password: '123',
    checkPassword(password) {
        return this._password === password;
    }
};
​
user4 = propertyProtectProxy(user4);
console.log(user4.checkPassword('123'));
// true

has

in运算符的行为可以通过代理行为的has方法改变:

let range = {
    from: 1,
    to: 5
}
​
range = new Proxy(range, {
    has(target, key) {
        return key >= target.from && key <= target.to;
    }
});
for (let i = 0; i <= 10; i++) {
    if (i in range) {
        console.log(i);
    }
}
// 1
// 2
// 3
// 4
// 5

包装函数

之前展示过如何包装函数:

function displayPerson(name, age){
    console.log(`Name is ${name} and age is ${age}`);
}
​
function delayFunc(func, wait){
    return function(...args){
        setTimeout(() => {
            func.apply(this, args);
        }, wait);
    }
}
​
displayPerson = delayFunc(displayPerson, 1000);
displayPerson('Tom', 20);
// Name is Tom and age is 20

这样做有一个缺陷,包装后的函数会丢失一些原始函数的信息,比如:

console.log(displayPerson.name);
console.log(displayPerson.length);
// displayPerson
// 2
displayPerson = delayFunc(displayPerson, 1000);
console.log(displayPerson.name);
console.log(displayPerson.length);
//
// 0   

可以使用代理对象包装函数:

function displayPerson(name, age){
    console.log(`Name is ${name} and age is ${age}`);
}
​
function delayFunc(func, wait){
    return new Proxy(func, {
        apply(target, thisArg, args){
            setTimeout(() => {
                target.apply(thisArg, args);
            }, wait);
        }
    });
}
​
console.log(displayPerson.name);
console.log(displayPerson.length);
// displayPerson
// 2
displayPerson = delayFunc(displayPerson, 1000);
console.log(displayPerson.name);
console.log(displayPerson.length);
// displayPerson
// 2
displayPerson('Tom', 20);
// Name is Tom and age is 20

Reflect

Reflect是一个内置对象,利用它可以简化代理对象的创建过程:

let user = {
    name: '张三',
    age: 20,
}
user = new Proxy(user, {
    get(target, key) {
        return Reflect.get(target, key);
    },
    set(target, key, value) {
        return Reflect.set(target, key, value);
    }
})
console.log(user.name);
console.log(user.age);

这和使用索引(target[key])效果是相同的,但有额外的好处,比如如果代理对象存在原型继承:

let user = {
    _name: '张三',
    get name(){
        return this._name;
    }
}
user = new Proxy(user, {
    get(target, key) {
        return target[key];
    },
})
let admin = {
    _name: '李四',
    __proto__: user
}
console.log(admin.name);
// 张三

这个例子中存在原型继承,admin 对象通过原型继承代理对象,访问代理对象属性(admin.name)时,该属性不存在,所以通过原型链上的代理对象检索属性,而代理对像通过target[key]返回原始对象的name属性,该属性是一个访问器属性,返回的是this._name,所以最后获取到的是张三,而不是期望的李四。

如果使用Reflect,结果就会不同:

let user = {
    _name: '张三',
    get name(){
        return this._name;
    }
}
user = new Proxy(user, {
    get(target, key, receiver) {
        return Reflect.get(target, key, receiver);
    },
})
let admin = {
    _name: '李四',
    __proto__: user
}
console.log(admin.name);
// 李四

这个例子中get多接收了一个receiver参数,该参数保存了正确的this,因此最后获取到了期望的结果。

局限性

内部槽

一些预定义类型数据存储在只能内部访问的特殊属性中,比如Map访问的是内部槽[[MapData]]中的数据:

let map = new Map();
let proxy = new Proxy(map, {});
proxy.set('name', '张三');
// TypeError: Method Map.prototype.set called on incompatible receiver #<Map>

这里报错是因为代理对象并没有[[MapData]],因此报错。

这个问题可以通过修改代理行为,获取代理对象方法时重新绑定this到原始对象:

let map = new Map();
let proxy = new Proxy(map, {
    get(target, key, receiver) {
        let value = Reflect.get(...arguments);
        return typeof value === 'function' ? value.bind(target) : value;
    },
});
proxy.set('name', '张三');
console.log(proxy.get('name'));

私有字段

代理对象不能访问原始对象的私有属性:

class Person {
    #name = '张三';
    getName() {
        return this.#name;
    }
}
let person = new Person();
person = new Proxy(person, {});
console.log(person.getName());
// TypeError: Cannot read private member #name from an object whose class did not declare it

同样可以通过绑定this的方式解决:

class Person {
    #name = '张三';
    getName() {
        return this.#name;
    }
}
let person = new Person();
person = new Proxy(person, {
    get(target, key, receiver) {
        let value = Reflect.get(...arguments);
        return typeof value === 'function' ? value.bind(target) : value;
    }
});
console.log(person.getName());

可撤销代理

可以创建一个随时可以撤销的代理:

let obj = {
    data: 'hello'
}
let { proxy, revoke } = Proxy.revocable(obj, {});
console.log(proxy.data);
revoke();
console.log(proxy.data);
// TypeError: Cannot perform 'get' on a proxy that has been revoked

在调用revoke方法后,代理对象中对原始对象的引用会被删除,因此所有的相关访问都会失败。

The End.

本文的完整示例可以从这里获取。

参考资料

  • 现代 JavaScript 指南

本作品采用 知识共享署名 4.0 国际许可协议 进行许可
标签: 暂无
最后更新:2026年4月20日

魔芋红茶

加一点PHP,加一点Go,加一点Python......

点赞
< 上一篇
下一篇 >

文章评论

razz evil exclaim smile redface biggrin eek confused idea lol mad twisted rolleyes wink cool arrow neutral cry mrgreen drooling persevering
取消回复

COPYRIGHT © 2021 icexmoon.cn. ALL RIGHTS RESERVED.
本网站由提供CDN加速/云存储服务

Theme Kratos Made By Seaton Jiang

宁ICP备2021001508号

宁公网安备64040202000141号