『面试的底气』—— 设计模式之装饰者模式(二)|8月更文挑战

这是我参与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的原型上增加beforeafter来给函数进行装饰,使得函数具有动态增加功能的能力。

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();
复制代码
© 版权声明
THE END
喜欢就支持一下吧
点赞0 分享