关于Javascript的Proxy

我只是想开橘子罐头,你却要卖我一把瑞士军刀Plus。为了用这把瑞士军刀Plus开罐头,我需要先折叠锤子,收起剪子,掰开螺丝刀……

一个只是想吃橘子罐头的人

某天,我要写一个显示某服务实时状态的页面。
动手之前感觉这个需求还是挺简单的。不就是新建一组HTML元素,然后用javascript对其中的元素赋值。So easy。

一边哼着小曲,一边就把HTML元素整整齐齐的码好了。接来下,让我看看有几个信息来源会更新HTML的值。1,2,3,4,5,6……什么?三十个模块?OK,OK,让我一个一个写,只是体力活而已。

首先,我的第一步是要实现更新元素的值。
更新元素的值。唔。说到这,可能已经有一部分同学脑海中已经浮现了经典的$(“#ABC”).val(xx);
JQuery在远古时代,确实将开发者从繁重的DOM操作中解放出来,可以将精力放在业务实现上。可是,随着web标准的日益完善,原生JS也愈发完善与强大。
可能很多人对原生Javascript取得元素的印象还停留在
document.getElementById();
document.getElementsByName();
document.getElementsByTagName();
……
等名字又臭又长又难用的getElement家族。
殊不知,原生Javascript已经进化到了不输JQuery的
document.querySelector();
document.qurrySelectorAll();
时代。与JQuery一样,querySelector也拥有了使用元素选择器的能力。
比如,一个来自MDN官网的例子,
var el = document.querySelector(“div.user-panel.main input[name=’login’]”);代表选择一个class属性为”user-panel main”的div元素<div>(<div class="user-panel main">)内包含一个name属性为”login”的input元素<input> (<input name="login"/>)  。

著名的全球大型同性交友网站Github在2018年9月份已经宣布在全段代码中完全移除了所有的JQuery。
我们从来不否认JQuery的强大,JQuery除了充当元素选择器以外,也提供了诸多强大的功能与拓展能力。但是已经2019年了,如果只是为了选中HTML元素赋值就引入JQuery,未免太得不偿失。就如同我只是为了开个罐头,却买了一把瑞士军刀。

这里肯定有同学提出了反对意见。就算是用更现代的querySelector,仍然没有JQuery方便,看$(“#ABC”)比document.querySelector(“#ABC”)少敲了多少个字母。时间就是金钱我的朋友。
这个好办,我们只是想有一个和JQuery一样简短干练的元素选择器而已,那么自己用querySelector封装一个就好了。比如我用在Chrome插件中的DomTool.js  (插件还在开发中,所以本代码未完成,不对任何使用后果负责,只是用来讲故事。比如,目前无法处理选择多个元素的情况。)
就实现了一个类JQuery画风的工具,比如
选择元素并且增加class => $(“#ABC”).addClass(“ClassA”);
创建元素并赋值 => $().create(“span”).setContent(“My Content”);
这样,花费了最小的代码量,就拥有了一个专门开罐头的工具。

好了,现在简便的获取元素与赋值的自有工具已经准备完毕,我准备好了两组代码,分别用来给每个HTML元素进行取值与赋值。我一共有五个HTML元素,这样对全部元素赋值,一共需要5行代码,接下来我需要把这5行代码拷贝到三十个需要调用他们的模块,一共行代150码开枝散叶到了系统的各个部分。

$("#field1").setContent("Content1");
$("#field2").setContent("Content2");
......
$("#field5").setContent("Content5");

是不是闻到了不好的味道。日积月累的编程经验与对优美代码的追求告诉你,你应该将取值/赋值的代码封装成方法集群,然后在三十个模块里调用封装之后的方法。否则,当需要改动的时候,就不用去三十个模块里面逐个去改。只要修改这个方法就好了。

 function(arg1, arg2, arg3, arg4, arg5) {     $("#field1").setContent(arg1);     $("#field2").setContent(arg2);     ......     $("#field5").setContent(arg5); }

更进一步的,你也许会想,如果给 HTML元素赋值能像给数据赋值一样简单该多好。
这样一来,只要定义好一个数据实例,无论是在什么地方,哪个模块,在需要赋值的时候直接改变数据实例的字段的值就好了,赋值者不必去关心操作DOM的细节问题。那么我就先定义好一个js object,叫做myApp,然后增加五个字段

var myApp = {};
myApp.field1 = "";
myApp.field2= "";
.......
myApp.field5 = "";

之后定义一个根据数据实例更新HTML元素的方法。
function updateDOM() {
    $("#field1").setContent(myApp.field1);    
    $("#field2").setContent(myApp.field2);
    .......
    $("#field5").setContent(myApp.field5);
}

这样一来,需要更新状态的模块,只需将myApp的对应字段赋值,之后调用一下updateDOM()就好了。
myApp.field1 = "Content1";
updateDOM();

看起来好多了。但是,如果没有最后的updateDOM()该多完美。
这时候,也许你想起了AngularJS等一众现代Js框架。用他们可以轻松的实现HTML<=>数据双向绑定。
片头语再次响起,我只是想开个罐头,何必上瑞士军刀。
我们仍然可以自己实现。

仔细想想,对于目前的需求,我们需要的是一个trigger。这个trigger可以在给myApp.field1赋值的时候,将所赋其值写进对应HTML元素。
很巧的是,原生Javascript(ES6特性)已经提供了这样一个功能,Proxy
使用Proxy,我们捕获get/set方法,加上自己的业务代码。比如,仍然是为chrome插件准备的AppConnector。(
插件还在开发中,所以本代码未完成,不对任何使用后果负责,只是用来讲故事。 )

调用者只需使用proxy实例进行正常赋值即可。在提供的DEMO中,向字段写入值,会自动用字段名作为元素ID去寻找元素,并将值写入该元素。如果试图赋值的字段名并不是已经存在的元素ID,则会报错该元素不存在。

 let myApp = appConnector("app1"); myApp.text1 = "Hello"; myApp.inputText = "12345";

亲自试一试
具体Proxy的使用讲解请参照阮一峰老师的《ECMAScript6入门》相关章节


这样,通过原生Javascript的提供的两个方法,实现了可能需要两大框架才能实现的功能,避免了类库与框架的过度引用,减轻系统的负荷。量身定做的衣服才合身,按自己口味烹调的料理才可口。框架与类库都是为了解决普遍问题(瑞士军刀),也许在具体业务中能使用到的功能不足其百分之一。这种情况不妨考虑使用原生Javascript量身打造一个趁手的工具。也希望前端从业者们不要一味的惟框架适从,开口闭口就是“用没用过XX框架”。没有框架,js照样好用。

发表回复

您的电子邮箱地址不会被公开。 必填项已用 * 标注

此站点使用Akismet来减少垃圾评论。了解我们如何处理您的评论数据