“这是我参与8月更文挑战的第23天,活动详情查看:8月更文挑战”
前言
在面试高级前端时,往往会遇到一些关于设计模式的问题,每次都回答不太理想。恰逢8月更文挑战的活动,准备用一个月时间好好理一下关于设计模式方面的知识点,给自己增加点面试的底气。
在 上一篇简单实现了JavaScript中类和普通对象的装饰者模式,本文来实现JavaScript中一等对象-函数的装饰者模式。
为啥函数需要装饰者模式
在开发过程中,想要为函数添加一些功能,最简单粗暴的方式就是直接修改该函数,但是这是最不好的方法,直接违反了开放-封闭原则。
很多时候我们也不想去修改原函数,只因原函数是其他同事开发的,里面的逻辑复杂无比,怕修改出问题来,根据开放-封闭原则一般这么解决这类问题。
window.onload = function () {
// 复杂无比的逻辑
}
var _onload = window.onload || function () { };
window.onload = function () {
_onload(arguments);
// 新增功能
}
以上根据开放-封闭原则提供的解决方式,似乎也实现了在不修改原函数window.onload
的前提下给原函数新增一些功能。
但是以上的解决方式还有两个严重的弊端。
- 必须维护
_onload
这个中间变量; this
被劫持的问题,这个问题在给window.onload
的函数添加新功能中是没有问题的,但是要给document.getElementById
添加一些功能,就会出现this
被劫持的问题。
const _getElementById = document.getElementById;
document.getElementById = function(id){
// 新增的功能
return _getElementById(id);
}
const button = document.getElementById('button');
执行后会在控制台报 Uncaught TypeError: Illegal invocation 的错误。错误发生在_getElementById( id )
这句代码上,此时_getElementById
是一个全局函数,当调用一个全局函数时,this
是指向window的,而document.getElementById
方法的内部实现需要使用this
引用,this
在这个方法内预期是指向document,而不是window。 按以下修改就正常了。
const _getElementById = document.getElementById;
document.getElementById = function(id){
// 新增的功能
return _getElementById.apply(this,arguments);;
}
const button = document.getElementById('button');
这样显然是非常不方便,要利用装饰者模式来实现一个简便的动态增加功能的方法。
利用 AOP 来装饰函数
AOP(面向切面编程)的主要作用是把一些跟核心业务逻辑模块无关的功能抽离出来,把这些功能抽离出来之后,再通过动态织入的方式掺入业务逻辑模块中。这样可以保持业务逻辑模块的纯净和高内聚性,其次是可以很方便地复用这些抽离出来的功能模块。
在 JavaScript 中实现 AOP,具体的实现技术有很多,这里通过扩展 Function.prototype 来实现。
Function.prototype.before = function (fn) {
// 保存原函数的引用
let __self = this;
// 返回包含了原函数和函数fn的"代理"函数
return function () {
// 用apply来绑定this执行函数fn,保证this不被劫持,函数fn接受的参数也会被原封不动地传入原函数,
// 函数fn在原函数之前执行
fn.apply(this, arguments);
// 用apply来绑定this执行原函数并返回原函数的执行结果,保证this不被劫持
return __self.apply(this, arguments);
};
};
Function.prototype.after = function (fn) {
// 保存原函数的引用
let __self = this;
// 返回包含了原函数和函数fn的"代理"函数
return function () {
// 用apply来绑定this执行原函数并返回原函数的执行结果,保证this不被劫持
let ret = __self.apply(this, arguments);
// 用apply来绑定this执行函数fn,保证 this 不被劫持,函数fn接受的参数也会被原封不动地传入原函数,
// 函数fn在原函数之后执行
fn.apply(this, arguments);
return ret;
};
};
以上给Function
的原型上增加before
和after
来给函数进行装饰,使得函数具有动态增加功能的能力。
Function.prototype.before
的参数是一个函数fn
,函数fn
里面是给原函数新添加的功能代码。把当前的this
保存起来到__self
,这个this
就是原函数。然后返回一个函数,执行这个函数,会分别执行作函数fn
和原函数,且可以控制它们的执行顺序,让函数fn
在原函数之前执行(前置装饰),这样就实现了动态装饰的效果。
且通过Function.prototype.apply
执行作为函数fn
和原函数,在第一参数传入正确的this
,此时的this
和原函数执行时的this
是一致的,保证了函数在被装饰之后,this
不会被劫持。
Function.prototype.after
的原理跟Function.prototype.before
一模一样,唯一不同的地方在
于让函数fn
在原函数执行之后再执行(后置装饰)。
使用 Function.prototype.after
可以很轻松地document.getElementById
扩展。
document.getElementById = document.getElementById.after(() =>{
console.log(2)
})
document.getElementById('button');
不污染原型的实现装饰函数
如果你不喜欢污染函数的原型链来实现函数装饰,可以采用以下方式来装饰函数。
const before = function(fn, beforefn) {
return function() {
beforefn.apply(this, arguments);
return fn.apply(this, arguments);
}
}
const a = () =>{
console.log(1)
}
const b = before(a, function() {
console.log(2)
});
b();
const after = function(fn, afterfn) {
return function() {
const ret = fn.apply(this, arguments);
afterfn.apply(this, arguments);
return ret
}
}
const a = () =>{
console.log(1)
}
const b = after(a, function() {
console.log(2)
});
b();
常见问题FAQ
- 免费下载或者VIP会员专享资源能否直接商用?
- 本站所有资源版权均属于原作者所有,这里所提供资源均只能用于参考学习用,请勿直接商用。若由于商用引起版权纠纷,一切责任均由使用者承担。更多说明请参考 VIP介绍。
- 提示下载完但解压或打开不了?
- 找不到素材资源介绍文章里的示例图片?
- 模板不会安装或需要功能定制以及二次开发?
发表评论
还没有评论,快来抢沙发吧!