目录
Proxy的概念和作用
Proxy是ES6中新增的一个功能,它可以在某个对象前架设一个“拦截器”,从而可以对该对象的访问进行拦截和控制。可以理解为是对对象访问的一个代理,通过代理可以改变对象的默认行为。
Proxy的作用主要有以下几个方面:
1. 对象的拦截和控制:可以对对象的属性访问、赋值、函数调用等操作进行拦截和控制,从而实现对对象行为的定制。
2. 数据劫持:可以通过Proxy实现数据双向绑定、深度监听、表单校验等数据劫持操作。
3. 权限控制:可以使用Proxy实现对象属性的访问权限控制,限制一些敏感属性的访问。
4. 性能优化:可以使用Proxy进行缓存、懒加载和单例模式等性能优化操作。
Proxy和Object.defineProperty都可以用于拦截和控制对象的属性访问,但是它们之间有以下几个区别:
1. Proxy支持拦截更多的操作:Proxy可以拦截更多的对象操作,包括对象属性的读取和设置、函数调用、in操作符、for...in循环等等,而Object.defineProperty只能拦截属性的访问和设置。
2. Proxy是基于对象的拦截:Proxy是基于对象的拦截,即一个Proxy实例对应一个被拦截的对象,通过代理可以改变整个对象的行为。而Object.defineProperty是基于属性的拦截,可以对单个属性进行拦截。
3. Proxy具有别名效应:在不需要拦截的情况下,可以直接使用对象的别名和引用来访问对象。而Object.defineProperty修改对象行为之后,不可以直接使用对象的别名和引用来访问属性。
4. Proxy可以使用Reflect对象:Proxy通过Reflect对象来执行默认操作,而Object.defineProperty则不能。
Proxy作为ES6中新增的一个功能,具有以下优势和劣势:
优势:
1. 更加灵活和强大:Proxy比Object.defineProperty更加灵活和强大,可以拦截并控制更多的对象操作,包括读取和设置属性、函数调用等等。
2. 可以动态修改对象行为:通过修改Proxy的拦截函数,可以动态地改变对象的行为,而Object.defineProperty不能实现这样的功能。
3. 容易扩展:当需要添加新的拦截函数时,可以通过添加新的Proxy拦截器来实现,而不需要对代码进行大规模的修改,开发起来更加容易。
劣势:
1. 兼容性不足:虽然现在很多主流浏览器都支持Proxy,但是一些旧版的浏览器还不支持,因此在实际使用中还需要考虑兼容性的问题。
2. 性能问题:相比Object.defineProperty等原生API,使用Proxy会带来一些额外的性能开销,尤其是在递归拦截、大量拦截操作等复杂场景下,会对程序的性能造成一定影响。但是在大多数场景下,Proxy的性能问题可以被忽略不计。
基本语法与API
Proxy的基本语法和API包括以下几个方面:
1. 基本语法:
let target = {}; // 被拦截的对象
let handler = {}; // 对象的拦截器
let proxy = new Proxy(target, handler); // 创建Proxy实例
上述代码中,通过创建Proxy实例来拦截target对象的访问。handler是一个拦截器对象,代表对target对象进行拦截和处理。Proxy拦截器需要实现get、set、apply等方法。
2. 常用API:
(1) get:拦截对象属性的读取操作。
let target = {a: 1};
let handler = {
get(target, property, receiver) {
console.log(`getting ${property}`);
return target[property];
}
};
let proxy = new Proxy(target, handler);
console.log(proxy.a); // 输出getting a 和 1
(2) set:拦截对象属性的赋值操作。
let target = {a: 1};
let handler = {
set(target, property, value, receiver) {
console.log(`setting ${property} = ${value}`);
target[property] = value;
return true;
}
};
let proxy = new Proxy(target, handler);
proxy.a = 2; // 输出setting a = 2
(3) apply:拦截函数的调用操作。
let target = function (a, b) {
return a + b;
}
let handler = {
apply(target, thisArg, args) {
console.log(`calling ${target.name}`);
return target(...args);
}
};
let proxy = new Proxy(target, handler);
console.log(proxy(1, 2)); // 输出calling proxy 和 3
(4) has:拦截in操作符的操作。
let target = {a: 1};
let handler = {
has(target, property) {
console.log(`checking ${property} in target`);
return property in target;
}
};
let proxy = new Proxy(target, handler);
console.log('a' in proxy); // 输出checking a in target 和 true
(5) ownKeys:拦截Object.getOwnPropertyNames和Object.getOwnPropertySymbols操作。
let target = {a: 1};
let handler = {
ownKeys(target) {
console.log(`getting ownKeys`);
return ['b', ...Object.getOwnPropertyNames(target)];
}
};
let proxy = new Proxy(target, handler);
console.log(Object.keys(proxy)); // 输出getting ownKeys 和 ['b', 'a']
(6) Reflect:Reflect对象提供了Proxy内置拦截函数的基础实现,可以通过Reflect对象执行默认操作。
let target = {a: 1};
let handler = {
ownKeys(target) {
console.log(`getting ownKeys`);
return ['b', ...Object.getOwnPropertyNames(target)];
}
};
let proxy = new Proxy(target, handler);
console.log(Object.keys(proxy)); // 输出getting ownKeys 和 ['b', 'a']
使用 Proxy 进行数据劫持
Proxy可以用于实现数据劫持,从而实现双向绑定。下面是一种简单的实现方式,可以通过Proxy监听对象的变化并将变化反映到页面上。
HTML代码:
<body>
<input id="name" type="text">
<p>你好,<span id="greeting"></span>!</p>
<script src="app.js"></script>
</body>
JavaScript代码:
let data = {
name: '',
greeting: ''
};
let handler = {
set(target, key, value) {
target[key] = value;
document.querySelector(`#${key}`).innerText = value;
return true;
}
};
let proxy = new Proxy(data, handler);
// 在input元素中输入文字,会自动更新到greeting元素中
document.querySelector('#name').addEventListener('input', (event)=> {
proxy.name = event.target.value;
proxy.greeting = `欢迎,${event.target.value}!`;
});
上面的代码中,我们定义了一个名为data的对象,并通过Proxy来监听它的属性变化。在handler的set拦截方法中,我们将新的属性值写入target对象,并将更新后的属性值实时同步到greeting元素中。在input元素中输入文字时,我们通过Proxy修改name属性的值,并根据修改后的name属性值动态生成greeting属性的值。
Proxy可以深度监听对象的变化,实现对嵌套对象的深度监听。下面是一个简单的实现方式,可以通过Proxy监听对象的变化并输出变化日志。
let data = {
name: 'Jack',
age: 29,
address: {
city: 'Shanghai',
district: 'Pudong New Area'
}
};
let logHandler = {
get(target, key, receiver) {
console.log(`getting ${key}: ${JSON.stringify(target[key])}`);
return Reflect.get(target, key, receiver);
},
set(target, key, value, receiver) {
console.log(`setting ${key}: ${JSON.stringify(value)}`);
return Reflect.set(target, key, value, receiver);
}
};
let deepProxy = function (obj) {
if (typeof obj === 'object' && obj !== null) {
for (let key in obj) {
obj[key] = deepProxy(obj[key]);
}
return new Proxy(obj, logHandler);
} else {
return obj;
}
}
let proxyData = deepProxy(data);
// 改变对象的属性值,触发日志输出
proxyData.name = 'John';
proxyData.age = 30;
proxyData.address.city = 'Beijing';
proxyData.address.district = 'Haidian District';
在上面的例子中,我们定义了一个名为data的对象,并通过deepProxy函数来深度处理这个对象,返回一个深层次的Proxy代理对象proxyData。在logHandler的get和set方法中,我们输出当前正在访问(或者修改)的属性的名称和值。在最后的代码中,我们修改了proxyData对象的属性值,会触发日志输出。
Proxy可以用于实现表单校验功能,通过代理对象对表单数据进行监听和校验,从而实现表单的数据合法性校验。下面是一个简单的实现方式,可以通过Proxy进行表单数据校验。
HTML代码:
<body>
<form id="form">
<label for="username">用户名:</label>
<input type="text" id="username" name="username" required minlength="5" maxlength="10">
<span class="error-message"></span>
<br>
<label for="password">密码:</label>
<input type="password" id="password" name="password" required minlength="6">
<span class="error-message"></span>
<br>
<button type="submit">提交</button>
</form>
<script src="app.js"></script>
</body>
JavaScript代码:
let form = document.querySelector('#form');
let rules = {
username: {
minlength: 5,
maxlength: 10,
},
password: {
minlength: 6,
}
};
let handler = {
set(target, key, value) {
target[key] = value;
let field = form.querySelector(`[name="${key}"]`);
let error = field.nextElementSibling;
let valid = true;
for (let rule in rules[key]) {
if (key !== 'confirm_password' && !validator[rule](value, rules[key][rule], form)) {
error.innerHTML = validator.errorMessages[rule](key, rules[key][rule]);
valid = false;
break;
}
}
if (valid) {
error.innerHTML = '';
}
return true;
}
};
let validator = {
minlength(value, length) {
return value.length >= length;
},
maxlength(value, length) {
return value.length <= length;
},
required(value) {
return value !== '';
},
password(value, length) {
return /[a-z]+/.test(value) && /[A-Z]+/.test(value) && /\d+/.test(value) && value.length >= length;
},
confirm_password(value) {
let password = form.querySelector('[name="password"]').value;
return value === password;
},
errorMessages: {
minlength(fieldName, value) {
return `${fieldName}长度不能小于${value}。`;
},
maxlength(fieldName, value) {
return `${fieldName}长度不能大于${value}。`;
},
required(fieldName) {
return `${fieldName}不能为空。`;
},
password() {
return `密码必须包含至少一个大写字母、一个小写字母和一个数字。`;
},
confirm_password() {
return `两次输入的密码不一致。`;
}
}
};
let proxy = new Proxy({}, handler);
for (let field of form.elements) {
if (field.nodeName === 'INPUT') {
proxy[field.name] = field.value;
field.addEventListener('input', (event) => {
proxy[field.name] = event.target.value;
event.target.nextElementSibling.innerHTML = '';
event.preventDefault();
});
}
}
form.addEventListener('submit', (event) => {
if (!event.target.checkValidity()) {
let fields = form.elements;
for (let field of fields) {
if (!field.validity.valid) {
field.nextElementSibling.innerText = field.validationMessage;
}
}
event.preventDefault();
}
});
在上面的代码中,我们定义了一个名为handler的对象,用于处理表单数据的变化。当表单数据变化时,handler的set方法会处理表单数据的校验,如果校验没有通过,会将错误信息输出到DOM元素中。我们还使用了一个validator对象,定义了表单校验规则和错误信息,用于实现表单数据的校验。在最后的代码中,我们在表单中添加了事件监听器,用于在提交表单时检查表单数据是否合法。
用 Proxy 进行权限控制
通过Proxy,我们可以控制对象属性的访问权限,进而实现对象属性的读取和写入的控制。下面是一个简单的实现方式,可以通过Proxy控制对象属性的访问权限。
let secretData = {
name: 'Jack',
age: 29,
salary: 10000,
};
let secureHandler = {
get(target, key, receiver) {
if (key === 'salary') {
console.log('You do not have access to salary');
return undefined;
} else {
return Reflect.get(target, key, receiver);
}
},
set(target, key, value, receiver) {
if (key === 'salary') {
console.log('You do not have access to set salary');
return false;
} else {
return Reflect.set(target, key, value, receiver);
}
}
};
let secureData = new Proxy(secretData, secureHandler);
console.log(secureData.name); // Jack
console.log(secureData.age); // 29
console.log(secureData.salary); // You do not have access to salary, undefined
secureData.name = 'John';
secureData.age = 30;
secureData.salary = 20000; // You do not have access to set salary, false
console.log(secureData.name); // John
console.log(secureData.age); // 30
console.log(secureData.salary); // You do not have access to salary, undefined
在上面的代码中,我们定义了一个名为secretData的对象,并通过secureHandler来控制该对象属性的访问权限。在get拦截方法中,我们判断了访问的属性是否是salary,如果是则输出错误信息并返回undefined,否则返回属性值。在set拦截方法中,我们判断了要设置的属性是否是salary,如果是则输出错误信息并返回false,否则将属性值设置成功。
Proxy可以用来实现函数的节流(throttle)操作,即控制函数的调用频率,为了减少函数执行的次数和提升性能。下面是一个简单的实现方式,可以通过Proxy实现函数的节流操作。
function throttle(func, delay) {
let timer = null;
return new Proxy(function() {
if (!timer) {
timer = setTimeout(() => {
Reflect.apply(func, this, arguments);
timer = null;
}, delay);
}
}, {
apply(target, thisArg, args) {
clearTimeout(timer);
timer = setTimeout(() => {
Reflect.apply(target, thisArg, args);
timer = null;
}, delay);
}
});
}
function handleResize() {
console.log('Resized');
}
let throttledResize = throttle(handleResize, 1000); // 设置节流时间为1秒
window.addEventListener('resize', throttledResize);
在上述例子中,我们首先定义了一个throttle函数,它接受一个函数和一个时间间隔作为参数,返回一个代理函数。这个代理函数内部使用了setTimeout函数,用于控制函数的执行间隔。
在使用throttle函数时,我们将要进行节流控制的函数handleResize和时间间隔作为参数,创建了一个代理函数throttledResize。在将throttledResize绑定到resize事件上时,每次窗口尺寸变化都会触发函数的调用,但实际上只有等到上一个延时结束后,才会调用新的函数。