开发者问题收集

JavaScript 装饰器模式。错误:超出最大调用堆栈大小

2018-01-26
444

以下是 Decorator 模式的一个工作示例:

class Dummy {
    run() {
        console.log('run');
    }
}

function get() {
    let instance = new Dummy();
    instance.run = ((func) => {
        return function() {
            func();
            console.log('decorator run');    
        }
    })(instance.run);

    return instance;
}

let obj = get();
obj.run();

但是,如果我们将 get 函数更改为:

function get() {
    let instance = new Dummy();
    instance.run = function() {
        instance.run();
        console.log('decorator run');  
    }       

    return instance;
}

我们将面临错误: VM68418:6 Uncaught RangeError: Maximum call stack size reached at Dummy.instance.run (:6:32)

为什么会发生这种情况? instance.run 仍然是原始方法的包装器,没有“无用”的额外自执行函数。

我很高兴听到答案

3个回答

instance.run() 在其自身定义内被调用,因此导致永无止境的递归,进而导致 超出最大调用堆栈大小 错误。

Waleed Iqbal
2018-01-26

我认为纠结于“装饰器”、“装饰器模式”甚至“模式”等虚构的东西是很危险的。问题的核心是,你有一个函数,你想改变它的行为,或者 装饰 ...

const original = x =>
  x * x
  
const decorate = f =>
  x => f (x) + 1
  
const decorated =
  decorate (original)

console.log (original (4))  // 16   4 * 4
console.log (decorated (4)) // 17   (4 * 4) + 1

因此,使用 装饰 ,我们捕获了这种递增的 + 1 效果,但请注意,我们被迫决定何时递增;在调用原始函数 之前 之后 。也许在不同的变体中,我们想使用这个+1效果来“装饰”,但在相反的时间。

下面, firstAdd1 是一个“装饰器”,它在调用原始函数 之前 递增。 thenAdd1 是一个装饰器,它在调用原始函数 之后 递增。

const original = x =>
  x * x
  
const thenAdd1 = f =>
  x => f (x) + 1

const firstAdd1 = f =>
  x => f (x + 1)
  
const decoratedA =
  thenAdd1 (original)

const decoratedB =
  firstAdd1 (original)
  
console.log (original (4))   // 16   4 * 4
console.log (decoratedA (4)) // 17   (4 * 4) + 1
console.log (decoratedB (4)) // 25   (4 + 1) * (4 + 1)

但现在我们已经复制了 +1 效果。事实证明,“装饰”只是 函数组合 。意识到这一点,我们就可以消除程序中的痛苦和折磨。

下面,我们在纯函数 add1 中捕获 +1 效果,然后在给定的 f 之前或之后简单地 组合

const add1 = x =>
  x + 1

const compose = (f, g) =>
  x => f (g (x))

const thenAdd1 = f =>
  compose (add1, f)

const firstAdd1 = f =>
  compose (f, add1)

在制作此程序时没有损害任何对象

const original = x =>
  x * x

const add1 = x =>
  x + 1
  
const compose = (f, g) =>
  x => f (g (x))

const thenAdd1 = f =>
  compose (add1, f)

const firstAdd1 = f =>
  compose (f, add1)
  
const decoratedA =
  thenAdd1 (original)

const decoratedB =
  firstAdd1 (original)
  
console.log (original (4))   // 16   4 * 4
console.log (decoratedA (4)) // 17   (4 * 4) + 1
console.log (decoratedB (4)) // 25   (4 + 1) * (4 + 1)

当然,函数组合非常强大。我们可以修改 compose 以接受任意数量的函数。现在我们可以按任意顺序对任意数量的效果进行排序。在这里,我们还跳过了“装饰器”的中间创建,而是直接根据 compose

const original = x =>
  x * x

const add1 = x =>
  x + 1
  
const compose = (f, ...fs) => x =>
  f === undefined
    ? x
    : f (compose (...fs) (x))

const decoratedA =
  compose (add1, original, add1)
  
const decoratedB =
  compose (add1, add1, add1, original, original)

const decoratedC =
  compose (decoratedB, decoratedA)
  
console.log (original (4))   // 16       4 * 4
console.log (decoratedA (4)) // 26       ((4 + 1) * (4 + 1)) + 1
console.log (decoratedB (4)) // 259      ((4 * 4) * (4 * 4)) + 1 + 1 + 1
console.log (decoratedC (4)) // 456979   (((((4 + 1) * (4 + 1)) + 1) * (((4 + 1) * (4 + 1)) + 1)) * ((((4 + 1) * (4 + 1)) + 1) * (((4 + 1) * (4 + 1)) + 1))) + 1 + 1 + 1

是的,因为 compose 返回一个 函数,我们甚至可以将其他组合组合起来。甚至可以使用 effect 组合诸如 console.log 之类的副作用函数,以确保输出与输入匹配

下面的 logger 允许我们通过在返回最终值之前将结果记录到 console 来可视化任何特定函数的影响 - 为此,您可以说 logger (f) 通过添加日志记录行为来装饰 f - 但这只是经典的函数组合

const square = x =>
  x * x

const add1 = x =>
  x + 1
  
const compose = (f, ...fs) => x =>
  f === undefined
    ? x
    : f (compose (...fs) (x))
      
const effect = f => x =>
  (f (x), x)
  
const logger = f =>
  compose (effect (console.log), f)

const main =
  compose (logger (add1), logger (square))

console.log (main (4))
// 16     (console.log side effect)
// 17     (console.log side effect)
// => 17  (return value)

如果您使用类和方法编写 OO 样式,这并不重要; compose 仍然是您的首选

const compose = (f, ...fs) => x =>
  f === undefined
    ? x
    : f (compose (...fs) (x))
      
const effect = f => x =>
  (f (x), x)

const addExcitement = x =>
  x + '!'
  
const capitalize = x =>
  x.toUpperCase ()

class Person {
  constructor (name) {
    this.name = name
  }
  greet () {
    return `I am ${this.name}`
  }
}

// "decorator"
const Shouter =
  effect (m => 
    m.greet = compose (addExcitement, capitalize, m.greet.bind(m)))

const p = new Person ('me')
console.log (p.greet ())    // I am me

Shouter (p)
console.log (p.greet ())    // I AM ME!
Mulan
2018-01-26

在第一个示例中, instance.run 的当前值保留在 func 封闭变量中,然后为 instance.run 分配一个新值::

instance.run = <old function>
func = instance.run
instance.run = <new function>
// func === <old function> here

因此,当 instance.run 调用 func 时,它本质上调用了 <old function>

您可以在没有 IIFE 的情况下执行相同的操作,只需在 get() 中关闭 func 即可:

class Dummy {
    run() {
        console.log('run');
    }
}

function get() {
    let instance = new Dummy();
    
    let func = instance.run;
    instance.run = function() {
       func();
       console.log('decorator run');    
    }
    
    return instance;
}

let obj = get();
obj.run();

在第二个代码片段中, instance.run 的旧值丢失,并且它有效地调用自身,导致堆栈溢出。

georg
2018-01-26