您是否曾经遇到过希望能够控制对象或数组中值的情况?也许您想阻止某些类型的数据,甚至在将数据存储到对象中之前验证数据。假设您想以某种方式对传入数据甚至传出数据做出反应?例如,也许您想通过显示结果来更新 DOM 或交换样式更改的类,因为数据会发生变化。是否曾经想过在一个只需要 Vue 或 React 等框架的一些功能的简单页面创意或部分上工作,但又不想启动一个新应用程序?
那么 JavaScript Proxy 可能正是您所需要的!
我先声明一下:在前端技术方面,我更像是一个 UI 开发人员;就像描述的 非 JavaScript 集中型方面一样,属于“巨大分歧”的一部分。我很乐意只创建在浏览器中一致且具有所有相关特性的美观的项目。因此,在更纯粹的 JavaScript 功能方面,我倾向于不深入研究。
然而,我仍然喜欢做研究,我总是在寻找一些东西来添加到我的新学习清单中。事实证明,JavaScript 代理是一个有趣的话题,因为仅仅回顾基础知识就会打开许多关于如何利用此功能的可能性。尽管如此,乍一看,代码可能会很快变得很重。当然,这完全取决于您的需求。
代理对象的概念已经存在了相当一段时间。在我的研究中,我可以找到几年前的参考资料。然而,它在我的清单上并不靠前,因为它从未在 Internet Explorer 中得到支持。相比之下,多年来,它在所有其他浏览器中都得到了极好的支持。这就是 Vue 3 与 Internet Explorer 11 不兼容的原因之一,因为在最新的 Vue 项目中使用了代理。
那么,代理对象究竟是什么呢?
MDN 将 Proxy 对象描述为:
[…] 使您能够为另一个对象创建一个代理,该代理可以拦截并重新定义该对象的根本操作。
总的想法是,您可以创建一个具有功能的对象,让您控制使用对象时发生的典型操作。最常见的两个是获取和设置存储在对象中的值。
const myObj = { mykey: 'value' } console.log(myObj.mykey); // "获取" 密钥的值,输出 'value' myObj.mykey = 'updated'; // "设置" 密钥的值,使其变为 'updated'
因此,在我们的代理对象中,我们将创建“陷阱”来拦截这些操作并执行我们可能希望完成的任何功能。最多可以有十三种这样的陷阱可用。我不一定会涵盖所有这些陷阱,因为并非所有这些陷阱对于我下面提供的简单示例都是必要的。再次声明,这取决于您为创建内容的特定上下文所需的内容。相信我,仅仅掌握基础知识就可以走很远。
为了扩展上面的示例以创建代理,我们将执行以下操作:
const myObj = { mykey: 'value' } const handler = { get: function (target, prop) { return target[prop]; }, set: function (target, prop, value) { target[prop] = value; return true; } } const proxy = new Proxy(myObj, handler); console.log(proxy.mykey); // "获取" 密钥的值,输出 'value' proxy.mykey = 'updated'; // "设置" 密钥的值,使其变为 'updated'
首先,我们从标准对象开始。然后,我们创建一个处理程序对象,该对象保存 处理程序函数,通常称为陷阱。这些表示可以对传统对象执行的操作,在本例中,这些操作只是在没有任何更改的情况下传递内容。之后,我们使用带有目标对象和处理程序对象的构造函数创建我们的代理。那时,我们可以引用代理对象来获取和设置值,这些值将是原始目标对象 myObj 的代理。
请注意 set 陷阱末尾的 return true。这旨在通知代理设置值应被视为成功。在某些情况下,如果您希望阻止设置值(考虑验证错误),则应返回 false。这还会导致控制台错误,并输出 TypeError。
现在,记住这种模式的一件事是原始目标对象仍然可用。这意味着您可以绕过代理并更改对象的值,而无需使用代理。在我阅读有关使用 Proxy 对象的内容时,我发现了一些有用的模式可以帮助解决这个问题。
let myObj = { mykey: 'value' } const handler = { get: function (target, prop) { return target[prop]; }, set: function (target, prop, value) { target[prop] = value; return true; } } myObj = new Proxy(myObj, handler); console.log(myObj.mykey); // "获取" 密钥的值,输出 'value' myObj.mykey = 'updated'; // "设置" 密钥的值,使其变为 'updated'
在这种模式下,我们使用目标对象作为代理对象,同时在代理构造函数中引用目标对象。是的,就是这样。这有效,但我发现很容易混淆正在发生的事情。因此,让我们在代理构造函数中创建目标对象:
const handler = { get: function (target, prop) { return target[prop]; }, set: function (target, prop, value) { target[prop] = value; return true; } } const proxy = new Proxy({ mykey: 'value' }, handler); console.log(proxy.mykey); // "获取" 密钥的值,输出 'value' proxy.mykey = 'updated'; // "设置" 密钥的值,使其变为 'updated'
事实上,如果我们愿意,我们可以在构造函数中创建目标对象和处理程序对象:
const proxy = new Proxy({ mykey: 'value' }, { get: function (target, prop) { return target[prop]; }, set: function (target, prop, value) { target[prop] = value; return true; } }); console.log(proxy.mykey); // "获取" 密钥的值,输出 'value' proxy.mykey = 'updated'; // "设置" 密钥的值,使其变为 'updated'
事实上,这是我在下面的示例中最常用的模式。值得庆幸的是,创建代理对象的方式很灵活。只需使用适合您的任何模式即可。
以下是一些示例,涵盖了从基本数据验证到使用 fetch 更新表单数据的 JavaScript Proxy 的用法。请记住,这些示例确实涵盖了 JavaScript Proxy 的基础知识;如果您愿意,它可以很快深入研究。在某些情况下,它们只是在代理对象中创建常规 JavaScript 代码来执行常规 JavaScript 操作。将它们视为通过更多地控制数据来扩展一些常见 JavaScript 任务的方法。
我的第一个示例涵盖了我一直觉得是一个相当简单而奇怪的编码面试问题:反转字符串。我一直不喜欢这个问题,在进行面试时也从不问这个问题。作为一个喜欢在这种事情上逆流而上的人,我尝试了非传统的解决方案。你知道,只是为了好玩有时会把它抛出来,其中一个解决方案是很棒的前端乐趣。它也提供了一个简单的示例,展示了代理的用法。
如果您在输入中键入内容,您将看到键入的内容在下方以反向方式打印出来。显然,可以使用许多反转字符串的方法。然而,让我们来看看我这种奇怪的反转方法。
const reverse = new Proxy( { value: '' }, { set: function (target, prop, value) { target[prop] = value; document.querySelectorAll('[data-reverse]').forEach(item => { let el = document.createElement('div'); el.innerHTML = '\u{202E}' value; item.innerText = el.innerHTML; }); return true; } } ) document.querySelector('input').addEventListener('input', e => { reverse.value = e.target.value; });
首先,我们创建新的代理,目标对象是一个单键值,它保存输入中键入的任何内容。get 陷阱不存在,因为我们只需要简单的直通,因为我们没有任何实际的功能与之绑定。在这种情况下,无需执行任何操作。我们稍后会讨论这个问题。
对于 set 陷阱,我们确实有一些功能需要执行。仍然有一个简单的直通,其中值像往常一样设置为目标对象中的 value 密钥。然后有一个 querySelectorAll 查找页面上所有具有 data-reverse 数据属性的元素。这允许我们一次性定位页面上的多个元素并更新它们。这为我们提供了每个人都喜欢看到的框架式绑定操作。这也可以更新为定位输入,以允许适当的双向绑定类型的情况。
这就是我反转字符串的古怪方法发挥作用的地方。一个 div 在内存中创建,然后使用字符串更新元素的 innerHTML。字符串的第一部分使用特殊的 Unicode 十进制代码,实际上会反转后面的所有内容,使其从右到左。然后,页面上实际元素的 innerText 将获得内存中 div 的 innerHTML。每次在输入中输入内容时都会运行此操作;因此,所有具有 data-reverse 属性的元素都会更新。
最后,我们在输入上设置一个事件侦听器,该侦听器通过输入的值(即事件的目标)设置目标对象中的 value 密钥。
最后,一个非常简单的示例,通过将值设置为对象来对页面的 DOM 执行副作用。
常见的 UI 模式是将输入的值格式化为比仅仅是字母和数字字符串更精确的序列。这方面的一个例子是电话输入。有时,如果键入的电话号码看起来像电话号码,它看起来和感觉更好。不过,诀窍是,当我们格式化输入的值时,我们可能仍然需要数据的非格式化版本。
对于 JavaScript Proxy 来说,这是一项简单的任务。
当您在输入中键入数字时,它们将被格式化为标准的美国电话号码(例如 (123) 456-7890)。请注意,电话号码也以纯文本形式显示在输入下方,就像上面的反向字符串示例一样。该按钮将数据的格式化版本和非格式化版本都输出到控制台。
因此,以下是代理的代码:
const phone = new Proxy( { _clean: '', number: '', get clean() { return this._clean; } }, { get: function (target, prop) { if (!prop.startsWith('_')) { return target[prop]; } else { return 'entry not found!' } }, set: function (target, prop, value) { if (!prop.startsWith('_')) { target._clean = value.replace(/\D/g, '').substring(0, 10); const sections = { area: target._clean.substring(0, 3), prefix: target._clean.substring(3, 6), line: target._clean.substring(6, 10) } target.number = target._clean.length > 6 ? `(${sections.area}) ${sections.prefix}-${sections.line}` : target._clean.length > 3 ? `(${sections.area}) ${sections.prefix}` : target._clean.length > 0 ? `(${sections.area}` : ''; document.querySelectorAll('[data-phone_number]').forEach(item => { if (item.tagName === 'INPUT') { item.value = target.number; } else { item.innerText = target.number; } }); return true; } else { return false; } } } );
此示例中的代码更多,因此让我们将其分解。第一部分是我们正在代理内部初始化的目标对象。它有三个方面。
{ _clean: '', number: '', get clean() { return this._clean; } },
第一个键 _clean 是我们的变量,它保存数据的非格式化版本。它以下划线开头,采用传统的变量命名模式,将其视为“私有”。我们希望在正常情况下使其不可用。随着我们的深入,我们将对此进行更多介绍。
第二个键 number 简单地保存格式化的电话号码值。
第三个“键”是使用名称 clean 的 get 函数。这将返回我们私有 _clean 变量的值。在这种情况下,我们只是返回该值,但如果我们愿意,这提供了对它执行其他操作的机会。这就像代理的 get 函数的代理 getter。这似乎很奇怪,但它为控制数据提供了一种简单的方法。根据您的具体需求,这可能是一种处理这种情况的相当简单的方法。它适用于我们这里的简单示例,但可能需要采取其他步骤。
现在是代理的 get 陷阱。
get: function (target, prop) { if (!prop.startsWith('_')) { return target[prop]; } else { return 'entry not found!' } },
首先,我们检查传入的 prop 或对象键,以确定它是否 不 以下划线开头。如果它不以下划线开头,我们只需返回它。如果它以下划线开头,那么我们返回一个字符串,说明未找到条目。根据需要,可以以不同的方式处理这种类型的负面返回。返回字符串、返回错误或运行具有不同副作用的代码。这完全取决于具体情况。
在我的示例中需要注意的一点是,我没有处理可能与在代理中被认为是私有变量的内容一起使用的其他代理陷阱。为了更全面地保护这些数据,您必须考虑其他陷阱,例如 [defineProperty](https://www.php.cn/link/cd69510f4a69bc0ef6ba504331b9d546 或 ownKeys——通常是关于操作或引用对象键的任何内容。您是否走这么远可能取决于谁将使用代理。如果它用于您自己,那么您就知道如何使用代理。但如果它是其他人,您可能需要考虑尽可能多地锁定内容。
现在是此示例中大部分魔法发生的地方——set 陷阱:
set: function (target, prop, value) { if (!prop.startsWith('_')) { target._clean = value.replace(/\D/g, '').substring(0, 10); const sections = { area: target._clean.substring(0, 3), prefix: target._clean.substring(3, 6), line: target._clean.substring(6, 10) } target.number = target._clean.length > 6 ? `(${sections.area}) ${sections.prefix}-${sections.line}` : target._clean.length > 3 ? `(${sections.area}) ${sections.prefix}` : target._clean.length > 0 ? `(${sections.area}` : ''; document.querySelectorAll('[data-phone_number]').forEach(item => { if (item.tagName === 'INPUT') { item.value = target.number; } else { item.innerText = target.number; } }); return true; } else { return false; } }
首先,对代理中私有变量进行相同的检查。我并没有真正测试其他类型的 prop,但您可能需要在这里考虑这样做。我假设只有代理目标对象中的 number 键将被调整。
传入的值(输入的值)将剥离除数字字符以外的所有内容,并保存到 _clean 键。然后在整个过程中使用此值来重建为格式化的值。基本上,每次键入时,整个字符串都会实时重建为预期的格式。substring 方法将数字锁定为十位数。
然后创建一个 sections 对象来保存我们电话号码的不同部分,这些部分基于美国电话号码的细分。随着 _clean 变量长度的增加,我们将 number 更新为我们希望在那时看到的格式模式。
一个 querySelectorAll 正在查找任何具有 data-phone_number 数据属性的元素,并通过 forEach 循环运行它们。如果该元素是更新值的输入,则更新其他任何元素的 innerText。这就是文本如何在输入下方显示的方式。如果我们要放置另一个具有该数据属性的输入元素,我们将看到它的值实时更新。这是一种创建单向或双向绑定的方法,具体取决于需求。
最后,返回 true 以让代理知道一切顺利。如果传入的 prop 或键以下划线开头,则返回 false。
最后,使这能够工作的事件侦听器:
document.querySelectorAll('input[data-phone_number]').forEach(item => { item.addEventListener('input', (e) => { phone.number = e.target.value; }); }); document.querySelector('#get_data').addEventListener('click', (e) => { console.log(phone.number); // (123) 456-7890 console.log(phone.clean); // 1234567890 });
第一组查找所有具有我们特定数据属性的输入,并向它们添加事件侦听器。对于每个输入事件,代理的 number 键值都将使用当前输入的值进行更新。由于我们每次发送输入的值时都会对其进行格式化,因此我们删除了任何不是数字的字符。
第二组查找输出两组数据的按钮(按要求),输出到控制台。这显示了我们如何编写代码来按需请求所需的数据。希望很清楚的是,phone.clean 正在引用目标对象中的 get 代理函数,该函数返回对象中的 _clean 变量。请注意,它没有像 phone.clean() 一样被调用为函数,因为它在我们的代理中充当 get 代理。
您可以使用数组作为代理中的目标“对象”而不是对象。由于它将是一个数组,因此需要考虑一些事项。数组的功能(例如 push())将在代理的 setter 陷阱中以某种方式处理。此外,在这种情况下,在目标对象概念中创建自定义函数实际上不起作用。然而,将数组作为目标可以做一些有用的事情。
当然,在数组中存储数字并不是什么新鲜事。显然。然而,我将向这个数字存储数组附加一些规则,例如不允许重复值并且只允许数字。我还将提供一些输出选项,例如排序、求和、平均值和清除值。然后更新一个控制所有这些的小型用户界面。
以下是代理对象:
const numbers = new Proxy([], { get: function (target, prop) { message.classList.remove('error'); if (prop === 'sort') return [...target].sort((a, b) => a - b); if (prop === 'sum') return [...target].reduce((a, b) => a b); if (prop === 'average') return [...target].reduce((a, b) => a b) / target.length; if (prop === 'clear') { message.innerText = `${target.length} number${target.length === 1 ? '' : 's'} cleared!`; target.splice(0, target.length); collection.innerText = target; } return target[prop]; }, set: function (target, prop, value) { if (prop === 'length') return true; dataInput.value = ''; message.classList.remove('error'); if (!Number.isInteger(value)) { console.error('Data provided is not a number!'); message.innerText = 'Data provided is not a number!'; message.classList.add('error'); return false; } if (target.includes(value)) { console.error(`Number ${value} has already been submitted!`); message.innerText = `Number ${value} has already been submitted!`; message.classList.add('error'); return false; } target[prop] = value; collection.innerText = target; message.innerText = `Number ${value} added!`; return true; } });
对于此示例,我将从 setter 陷阱开始。
首先要做的是检查是否将 length 属性设置为数组。它只是返回 true,以便它以通常的方式发生。如果需要对设置的长度做出反应,它始终可以在适当的位置添加代码。
接下来的两行代码引用页面上使用 querySelector 存储的两个 HTML 元素。dataInput 是输入元素,我们希望在每次输入时都清除它。message 是保存对数组更改的响应的元素。由于它具有错误状态的概念,因此我们确保它在每次输入时都不处于该状态。
第一个 if 检查输入是否实际上是一个数字。如果不是,那么它会执行几件事。它发出一个控制台错误,说明问题所在。message 元素获得相同的语句。然后,message 通过 CSS 类进入错误状态。最后,它返回 false,这也会导致代理向控制台发出它自己的错误。
第二个 if 检查输入是否已存在于数组中;记住我们不希望重复。如果存在重复,则会发生与第一个 if 中相同的消息传递。消息传递略有不同,因为它是一个模板文字,因此我们可以看到重复的值。
最后一部分假设一切顺利,并且可以继续进行。值像往常一样设置,然后我们更新 collection 列表。collection 引用页面上的另一个元素,该元素向我们显示数组中当前的数字集合。同样,消息会使用已添加的条目进行更新。最后,我们返回 true 以让代理知道一切顺利。
现在,get 陷阱与之前的示例略有不同。
get: function (target, prop) { message.classList.remove('error'); if (prop === 'sort') return [...target].sort((a, b) => a - b); if (prop === 'sum') return [...target].reduce((a, b) => a b); if (prop === 'average') return [...target].reduce((a, b) => a b) / target.length; if (prop === 'clear') { message.innerText = `${target.length} number${target.length === 1 ? '' : 's'} cleared!`; target.splice(0, target.length); collection.innerText = target; } return target[prop]; },
这里发生的事情是利用了一个不是普通数组方法的“prop”;它作为 prop 传递给 get 陷阱。例如,第一个“prop”由这个事件侦听器触发:
dataSort.addEventListener('click', () => { message.innerText = numbers.sort; });
因此,当单击 sort 按钮时,message 元素的 innerText 将使用 numbers.sort 返回的内容进行更新。它充当代理拦截并返回非典型数组相关结果的 getter。
删除 message 元素的潜在错误状态后,我们然后确定是否预期会发生非标准数组获取操作。每个操作都会返回对原始数组数据的操作,而不会更改原始数组。这是通过使用目标上的扩展运算符来创建一个新数组,然后使用标准数组方法来完成的。每个名称都应该暗示它所做的操作:排序、求和、平均值和清除。好吧,清除并不是一个标准的数组方法,但这听起来不错。由于条目可以按任何顺序排列,因此我们可以让它为我们提供排序列表或对条目执行数学函数。清除简单地清除数组,正如您可能预期的那样。
以下是用于按钮的其他事件侦听器:
dataForm.addEventListener('submit', (e) => { e.preventDefault(); numbers.push(Number.parseInt(dataInput.value)); }); dataSubmit.addEventListener('click', () => { numbers.push(Number.parseInt(dataInput.value)); }); dataSort.addEventListener('click', () => { message.innerText = numbers.sort; }); dataSum.addEventListener('click', () => { message.innerText = numbers.sum; }); dataAverage.addEventListener('click', () => { message.innerText = numbers.average; }); dataClear.addEventListener('click', () => { numbers.clear; });
我们可以通过多种方式扩展和向数组添加功能。我见过允许使用负索引选择条目的数组示例,该负索引从末尾计数。根据对象内的属性值查找对象数组中的条目。尝试在数组中获取不存在的值时返回消息而不是 undefined。有很多想法可以利用和探索数组上的代理。
地址表单在网页上是相当标准的东西。让我们为它添加一些交互性以获得乐趣(和非标准)确认。它还可以充当表单值在单个对象中的数据收集,可以按需请求。
以下是代理对象:
const model = new Proxy( { name: '', address1: '', address2: '', city: '', state: '', zip: '', getData() { return { name: this.name || 'no entry!', address1: this.address1 || 'no entry!', address2: this.address2 || 'no entry!', city: this.city || 'no entry!', state: this.state || 'no entry!', zip: this.zip || 'no entry!' }; } }, { get: function (target, prop) { return target[prop]; }, set: function (target, prop, value) { target[prop] = value; if (prop === 'zip' && value.length === 5) { fetch(`https://api.zippopotam.us/us/${value}`) .then(response => response.json()) .then(data => { model.city = data.places[0]['place name']; document.querySelector('[data-model="city"]').value = target.city; model.state = data.places[0]['state abbreviation']; document.querySelector('[data-model="state"]').value = target.state; }); } document.querySelectorAll(`[data-model="${prop}"]`).forEach(item => { if (item.tagName === 'INPUT' || item.tagName === 'SELECT') { item.value = value; } else { item.innerText = value; } }) return true; } } );
目标对象非常简单;表单中每个输入的条目。getData 函数将返回对象,但如果属性的值为空字符串,它将更改为“no entry!”这是可选的,但该函数提供比我们仅通过获取代理对象的状态所能获得的更清晰的对象。
getter 函数只是像往常一样传递内容。您可能不需要这样做,但我喜欢将其包含在内以使其完整。
setter 函数将值设置为 prop。但是,if 检查是否将要设置的 prop 恰好是邮政编码。如果是,那么我们检查值长度是否为 5。当评估为 true 时,我们将执行一个 fetch,该 fetch 使用邮政编码命中地址查找器 API。返回的任何值都将插入到对象属性、城市输入中,并在 select 元素中选择州。这是一个方便的快捷方式示例,可以让用户不必键入这些值。如果需要,可以手动更改这些值。
对于下一部分,让我们来看一个输入元素的示例:
<code></code>
代理有一个 querySelectorAll,它查找任何具有匹配数据属性的元素。这与我们之前看到的反向字符串示例相同。如果它找到匹配项,它将更新输入的值或元素的 innerText。这就是旋转卡实时更新以显示完成的地址外观的方式。
需要注意的一点是输入上的 data-model 属性。该数据属性的值实际上会在其操作过程中告知代理要锁定的键。代理根据涉及的键查找涉及的元素。事件侦听器通过让代理知道哪个键正在使用来执行相同的操作。这就是它的样子:
document.querySelector('main').addEventListener('input', (e) => { model[e.target.dataset.model] = e.target.value; });
因此,将定位 main 元素内的所有输入,并且当触发输入事件时,代理将更新。data-model 属性的值用于确定要定位代理中的哪个键。实际上,我们正在使用模型系统。考虑一下这种事情如何进一步利用。
至于“获取数据”按钮?它是 getData 函数的简单控制台日志……
getDataBtn.addEventListener('click', () => { console.log(model.getData()); });
这是一个有趣的示例,用于构建和使用以探索该概念。这就是让我思考我可以使用 JavaScript Proxy 构建什么的示例。有时,您只需要一个小型窗口小部件,它具有一些数据收集/保护功能,并且只需与数据交互即可操作 DOM。是的,您可以使用 Vue 或 React,但有时即使它们对于如此简单的事情来说也可能过于复杂。
“就此为止”的意思是,这取决于你们每个人以及您是否会更深入地研究 JavaScript Proxy。正如我在本文开头所说,我只介绍了此功能的基础知识。它可以提供更多功能,并且可以比我提供的示例更大。在某些情况下,它可以为利基解决方案提供小型帮助程序的基础。很明显,可以使用执行相同功能的基本函数轻松创建这些示例。甚至我的大部分示例代码都是常规 JavaScript 与代理对象混合在一起。
不过,重点是提供使用代理的示例,以展示如何对数据交互做出反应——甚至控制如何对这些交互做出反应以保护数据、验证数据、操作 DOM 和获取新数据——所有这些都基于某人尝试保存或获取数据。从长远来看,这可能非常强大,并且允许创建可能不需要更大库或框架的简单应用程序。
因此,如果您是一位更关注 UI 方面的前端开发人员,就像我一样,您可以探索一些基础知识,看看是否有可能从 JavaScript Proxy 中获益的小型项目。如果您更是一位 JavaScript 开发人员,那么您可以开始更深入地研究代理以用于更大的项目。也许是一个新的框架或库?
只是一个想法……
以上是JavaScript代理的介绍的详细内容。更多信息请关注PHP中文网其他相关文章!