AOP 面向切片编程
AOP(面向切面编程) 的主要作用是把一些跟核心业务逻辑模块无关的功能抽离出来,这些跟业务逻辑无关的功能通常包括日志统计、安全控制、异常处理 等。把这些功能抽离出来之后,再通过 动态植入 的方式掺入业务逻辑模块中。这样做的好处首先是可以保持业务逻辑模块的 纯净 和 高内聚性 ,其次是可以很方便地复用日志统计等功能模块。
在 JavaScript中实现 AOP,都是指把一个函数动态植入到另外一个函数之中,通常是基于高阶函数实现的。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30
| Function.prototype.before = function (beforefn) { var __self = this; return function () { beforefn.apply(this, arguments); return __self.apply(this, arguments); } };
Function.prototype.after = function (afterfn) { var __self = this; return function () { var ret = __self.apply(this, arguments); afterfn.apply(this, arguments); return ret; } }; var func = function () { console.log(2); }; func = func.before(function () { console.log(1); }).after(function () { console.log(3); }); func();
|
数据统计上报
分离业务代码和数据统计 代码,无论在什么语言中,都是 AOP 的经典应用之一。在项目开发的结尾阶段难免要加上很多统计数据的代码,这些过程可能让我们被迫改动早已封装好的函数。
比如页面中有一个登录 button,点击这个 button会弹出登录浮层,与此同时要进行数据上报,来统计有多少用户点击了这个登录 button:
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| <html> <button tag="login" id="button">点击打开登录浮层</button> <script> var showLogin = function(){ console.log('打开登录浮层'); log( this.getAttribute( 'tag' ) ); } var log = function( tag ){ console.log('上报标签为: ' + tag); } document.getElementById( 'button' ).onclick = showLogin; </script> </html>
|
showLogin 函数里,既要负责打开登录浮层,又要负责数据上报,这是两个层面的功能,在此处却被耦合在一个函数里。使用 AOP 分离之后,代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
| <html> <button tag="login" id="button">点击打开登录浮层</button> <script> Function.prototype.after = function (afterfn) { var __self = this; return function () { var ret = __self.apply(this, arguments); afterfn.apply(this, arguments); return ret; } }; var showLogin = function () { console.log('打开登录浮层'); } var log = function () { console.log('上报标签为: ' + this.getAttribute('tag')); } showLogin = showLogin.after(log); document.getElementById('button').onclick = showLogin; </script>
</html>
|
用AOP动态改变函数的参数
1 2 3 4 5 6 7 8 9 10 11 12
| var ajax = function (type, url, param) { console.log(param); }; var getToken = function () { return 'Token'; } ajax = ajax.before(function (type, url, param) { param.Token = getToken(); }); ajax('get', 'http:// xxx.com/userinfo', { name: 'sven' });
|
beforefn 和原函数 __self 共用一组参数列表arguments ,当我们在 beforefn 的函数体内改变 arguments 的时候,原函数 __self 接收的参数列表自然也会变化。
明显可以看到,用 AOP 的方式给 ajax 函数动态装饰上 Token 参数,保证了 ajax 函数是一个相对纯净的函数,提高了 ajax 函数的可复用性,它在被迁往其他项目的时候,不需要做任何修改。
插件式的表单验证
在表单数据提交给后台之前,常常要做一些校验,比如登录的时候需要验证用户名和密码是否为空,代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48
| <html>
<body> 用户名:<input id="username" type="text" /> 密码: <input id="password" type="password" /> <input id="submitBtn" type="button" value="提交"> </body> <script> var username = document.getElementById('username'), password = document.getElementById('password'), submitBtn = document.getElementById('submitBtn'); Function.prototype.before = function (beforefn) { var __self = this; return function () { if (beforefn.apply(this, arguments) === false) { return; } return __self.apply(this, arguments); } }
var validata = function () { if (username.value === '') { alert('用户名不能为空'); return false; } if (password.value === '') { alert('密码不能为空'); return false; } }
var formSubmit = function () { var param = { username: username.value, password: password.value } ajax('http:// xxx.com/login', param); } formSubmit = formSubmit.before(validata); submitBtn.onclick = function () { formSubmit(); } </script>
</html>
|
校验输入和提交表单的代码完全分离开来,它们不再有任何耦合关系。
值得注意的是,因为函数通过 Function.prototype.before 或者 Function.prototype.after 被装饰之后,返回的实际上是一个新的函数,如果在原函数上保存了一些属性,那么这些属性会丢失。代码如下:
1 2 3 4 5 6 7 8
| var func = function () { alert(1); } func.a = 'a'; func = func.after(function () { alert(2); }); alert(func.a);
|
另外,这种装饰方式也叠加了函数的作用域,如果装饰的链条过长,性能上也会受到一些影响。
参考
javascript 设计模式与开发实践