JavaScript 代理介绍

源节点: 1085371

您是否曾经遇到过希望能够对对象或数组中的值进行一些控制的情况?也许您想阻止某些类型的数据,甚至在将数据存储到对象中之前验证数据。假设您想以某种方式对传入数据甚至传出数据做出反应?例如,您可能希望通过显示结果来更新 DOM,或者在数据更改时交换样式更改类。是否曾经想要研究一个简单的想法或页面部分,需要框架的一些功能,如 Vue 或 React,但又不想启动一个新的应用程序?

然后 JavaScript 代理 可能就是您正在寻找的!

简要介绍

我先说一下:当涉及到前端技术时,我更多的是一个 UI 开发人员;很像所描述的 -专注于 JavaScript 的一面 大分水岭。我很高兴能够创建在浏览器中保持一致的漂亮项目以及随之而来的所有怪癖。因此,当涉及到更纯粹的 JavaScript 功能时,我倾向于不会深入探讨。

然而我仍然喜欢做研究,并且我总是在寻找一些东西来添加到要学习的新事物列表中。事实证明,JavaScript 代理是一个有趣的主题,因为只要回顾一下基础知识,就会开启许多关于如何利用此功能的可能想法。尽管如此,乍一看,代码可能很快就会变得繁重。当然,这一切都取决于您的需要。

代理对象的概念已经伴随我们很长一段时间了。我可以在几年前的研究中找到它的参考资料。但它在我的列表中并不靠前,因为它从未在 Internet Explorer 中得到支持。相比之下,多年来它在所有其他浏览器中都得到了出色的支持。这就是 Vue 3 与 Internet Explorer 11 不兼容的原因之一,因为使用了 最新 Vue 项目中的代理.

那么,代理对象到底是什么?

Proxy 对象

MDN 描述了 Proxy 对象 作为这样的东西:

[...] 使您能够为另一个对象创建代理,该代理可以拦截并重新定义该对象的基本操作。

总的想法是,您可以创建一个对象,该对象具有的功能可以让您控制使用对象时发生的典型操作。最常见的两个是获取和设置存储在对象中的值。

const myObj = { mykey: 'value'
} console.log(myObj.mykey); // "gets" value of the key, outputs 'value'
myObj.mykey = 'updated'; // "sets" value of the key, makes it '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); // "gets" value of the key, outputs 'value'
proxy.mykey = 'updated'; // "sets" value of the key, makes it 'updated'

首先我们从标准对象开始。然后我们创建一个处理程序对象来保存 处理函数,通常称为陷阱。这些代表可以在传统对象上完成的操作,在本例中是 getset 只是传递事物而不做任何改变。之后,我们使用构造函数以及目标对象和处理程序对象创建代理。此时,我们可以在获取和设置值时引用代理对象,该对象将成为原始目标对象的代理, myObj.

备注 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); // "gets" value of the key, outputs 'value'
myObj.mykey = 'updated'; // "sets" value of the key, makes it '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); // "gets" value of the key, outputs 'value'
proxy.mykey = 'updated'; // "sets" value of the key, makes it '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); // "gets" value of the key, outputs 'value'
proxy.mykey = 'updated'; // "sets" value of the key, makes it 'updated'

事实上,这是我在下面的示例中使用的最常见的模式。值得庆幸的是,创建代理对象的方式很灵活。只需使用适合您的任何模式即可。

以下是一些示例,涵盖从基本数据验证到通过获取更新表单数据的 JavaScript 代理的使用。请记住,这些示例确实涵盖了 JavaScript 代理的基础知识;如果你愿意的话,它可以很快地深入。在某些情况下,它们只是创建常规 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;
});

首先,我们创建新的代理,目标对象是一个键 value 它保存输入中输入的任何内容。这 get trap 不存在,因为我们只需要一个简单的传递,因为我们没有任何与之相关的实际功能。在这种情况下无需执行任何操作。我们稍后会讨论这个问题。

如报名参加 set trap 我们确实有一小部分功能需要执行。仍然有一个简单的传递,其中值设置为 value 像平常一样键入目标对象。然后有一个 querySelectorAll 查找所有带有 a 的元素 data-reverse 页面上的数据属性。这使我们能够定位页面上的多个元素并一次性更新它们。这为我们提供了每个人都喜欢看到的类似框架的绑定操作。这也可以更新为目标输入,以允许适当的双向绑定类型的情况。

这就是我有趣的奇怪的反转字符串的方式开始发挥作用的地方。在内存中创建一个 div,然后 innerHTML 元素的值用字符串更新。字符串的第一部分使用特殊的 Unicode 十进制代码,该代码实际上反转后面的所有内容,使其从右到左。这 innerText 然后给出页面上实际元素的 innerHTML 内存中的div。每次在输入中输入内容时都会运行;因此,所有带有 data-reverse 属性已更新。

最后,我们在输入上设置一个事件监听器,用于设置 value 通过作为事件目标的输入值键入我们的目标对象。

最后,这是一个非常简单的示例,通过为对象设置值来对页面的 DOM 执行副作用。

实时格式化输入值

常见的 UI 模式是将输入的值格式化为更精确的序列,而不仅仅是字母和数字的字符串。电话输入就是一个例子。有时,如果输入的电话号码实际上看起来像一个电话号码,那么它看起来和感觉起来都会更好。但诀窍是,当我们格式化输入值时,我们可能仍然需要数据的未格式化版本。

对于 JavaScript 代理来说,这是一项简单的任务。

当您在输入框中输入数字时,它们会被格式化为标准的美国电话号码(例如 (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,仅保存格式化的电话号码值。

第三 "key" 是一个 get 使用名称的函数 clean。这返回了我们私有的值 _clean 多变的。在本例中,我们只是返回该值,但是如果我们愿意的话,这提供了用它做其他事情的机会。这就像一个代理 getter get 代理的功能。这看起来很奇怪,但它提供了一种控制数据的简单方法。根据您的具体需求,这可能是处理这种情况的一种相当简单的方法。它适用于我们这里的简单示例,但可能还需要采取其他步骤。

现在为了 get 代理的陷阱。

get: function (target, prop) { if (!prop.startsWith('_')) { return target[prop]; } else { return 'entry not found!' }
},

首先,我们检查传入的 prop 或对象键,以确定它是否存在 不能 以下划线开头。如果它不以下划线开头,我们只需返回它。如果是,那么我们返回一个字符串,表示未找到该条目。这种类型的负回报可以根据需要以不同的方式处理。返回字符串、返回错误或运行具有不同副作用的代码。这一切都取决于具体情况。

在我的示例中需要注意的一件事是,我没有处理其他代理陷阱,这些陷阱可能会与代理中被视为私有变量的内容一起发挥作用。为了更完整地保护这些数据,您必须考虑其他陷阱,例如 [defineProperty](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Proxy/Proxy/defineProperty), deletePropertyownKeys - 通常是有关操作或引用对象键的任何内容。您是否走得这么远可能取决于谁将使用代理。如果适合您,那么您就知道如何使用代理。但如果是其他人,您可能需要考虑尽可能地锁定事物。

现在来说说这个例子中最神奇的地方—— 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; }
}

首先,对代理中的私有变量进行相同的检查。我并没有真正测试其他类型的道具,但您可能会考虑在这里这样做。我仅假设代理目标对象中的数字键将被调整。

传入的值,即输入的值,将被除去除数字字符之外的所有内容,并保存到 _clean 钥匙。然后在整个过程中使用该值来重建格式化值。基本上,每次您输入时,整个字符串都会实时重建为预期的格式。子字符串方法将数字锁定为十位数字。

然后a sections 创建对象是为了根据美国电话号码的细分来保存我们电话号码的不同部分。作为 _clean 变量长度增加,我们更新 number 到我们希望在那个时间点看到的格式模式。

A 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
});

第一组查找具有特定数据属性的所有输入,并向它们添加事件侦听器。对于每个输入事件,代理的数字键值都会更新为当前输入的值。由于我们正在格式化每次发送的输入值,因此我们会删除所有非数字字符。

第二组找到根据请求将两组数据输出到控制台的按钮。这展示了我们如何编写代码来随时请求所需的数据。希望很清楚 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 这样它就会以正常的方式发生。如果我们需要的话,它总是可以有适当的代码,以防对设置的长度做出反应。

接下来的两行代码引用页面上存储的两个 HTML 元素 querySelector。 该 dataInput 是输入元素,我们希望在每个条目上清除它。这 message 是保存对数组更改的响应的元素。由于它具有错误状态的概念,因此我们确保它不在每个条目上都处于该状态。

最快的 if 检查该条目是否确实是一个数字。如果不是,那么它会做几件事。它会发出一个控制台错误来说明问题。消息元素得到相同的声明。然后通过 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”;它被传递给 get 陷阱作为道具。举个例子,第一个“prop”是由这个事件监听器触发的:

dataSort.addEventListener('click', () => { message.innerText = numbers.sort;
});

因此,当单击排序按钮时,消息元素的 innerText 已更新为任何内容 numbers.sort 返回。它充当代理拦截并返回典型数组相关结果以外的内容的 getter。

删除消息元素的潜在错误状态后,我们将确定是否会发生除标准数组获取操作之外的其他操作。每个都返回对原始数组数据的操作,而不改变原始数组。这是通过在目标上使用扩展运算符来创建新数组,然后使用标准数组方法来完成的。每个名称都应该表明它的作用:排序、求和、平均和清除。好吧,clear 并不完全是标准的数组方法,但听起来不错。由于条目可以按任何顺序排列,因此我们可以让它提供排序列表或对条目执行数学函数。如您所料,清除只是清除数组。

以下是用于按钮的其他事件侦听器:

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 函数将返回对象,但如果属性的值是空字符串,它将更改为“无条目!”这是可选的,但该函数提供的对象比我们仅通过获取代理对象的状态所获得的对象更干净。

getter 函数只是像往常一样简单地传递东西。您可能不需要它,但为了完整起见,我喜欢将其包括在内。

setter 函数将值设置为 prop。这 if但是,会检查正在设置的属性是否恰好是邮政编码。如果是,那么我们检查该值的长度是否为 5。当评价为 true,我们使用邮政编码执行访问地址查找器 API 的获取。返回的任何值都会插入到对象属性、城市输入中,并在 select 元素中选择州。这是一个方便的快捷方式的示例,可以让人们不必键入这些值。如果需要,可以手动更改这些值。

在下一节中,我们来看一个输入元素的示例:

<input class="in__input" id="name" data-model="name" placeholder="name" />

代理有一个 querySelectorAll 查找具有匹配数据属性的任何元素。这与我们之前看到的反向字符串示例相同。如果找到匹配项,它会更新输入的值或元素的值 innerText。这就是旋转卡片实时更新以显示完整地址的方式。

需要注意的一件事是 data-model 输入上的属性。该数据属性的值实际上通知代理在其操作期间要锁定哪个密钥。代理根据该涉及的密钥查找涉及的元素。事件侦听器通过让代理知道哪个键正在播放来执行大致相同的操作。看起来是这样的:

document.querySelector('main').addEventListener('input', (e) => { model[e.target.dataset.model] = e.target.value;
});

因此,主元素中的所有输入都是目标,并且当触发输入事件时,代理将被更新。的价值 data-model 属性用于确定代理中的目标键。实际上,我们有一个类似模型的系统在发挥作用。想想如何进一步利用这样的东西。

至于“获取数据”按钮?这是一个简单的控制台日志 getData 功能…

getDataBtn.addEventListener('click', () => { console.log(model.getData());
});

这是一个有趣的例子,可以构建和使用来探索这个概念。这个例子让我思考可以使用 JavaScript 代理构建什么。有时,您只需要一个具有一定数据收集/保护功能并且能够通过与数据交互来操作 DOM 的小部件。是的,你可以使用 Vue 或 React,但有时对于如此简单的事情来说,它们也太过分了。

目前为止就这样了

“现在”的意思可能取决于你们每个人以及你们是否会更深入地研究 JavaScript 代理。正如我在本文开头所说,我仅介绍此功能的基础知识。它可以提供更多的东西,并且比我提供的示例更广泛。在某些情况下,它可以提供基础 小众解决方案的小帮手。显然,可以使用执行几乎相同功能的基本函数轻松创建示例。甚至我的大部分示例代码都是与代理对象混合的常规 JavaScript。

但重点是提供使用代理的示例来展示如何对数据交互做出反应——甚至控制如何对这些交互做出反应以保护数据、验证数据、操作 DOM 和获取新数据——所有这些都基于某人尝试保存或获取数据。从长远来看,这可能非常强大,并且允许使用可能不需要更大的库或框架的简单应用程序。

因此,如果您是一名像我一样更关注 UI 方面的前端开发人员,您可以探索一些基础知识,看看是否有较小的项目可以从 JavaScript 代理中受益。如果您是一名 JavaScript 开发人员,那么您可以开始深入研究大型项目的代理。也许是一个新的框架或库?

只是一个想法…

来源:https://css-tricks.com/an-intro-to-javascript-proxy/

时间戳记:

更多来自 CSS技巧