0

I have this problem that I'm trying to wrap my head around and was wondering if any JS wizards could help me out.

I want to write a factory function that takes a class as an input and returns a new class with all of the same methods as the input class, but with some logging added to each method.

I'm thinking I could loop over each method in the prototype, save the old method, and then reassigned the method to a new function that calls the old method along with the logging as well. However, I'm not sure how I would even get started on this.

Could anyone help me out?

jamk
  • 1
  • 1
  • Can you give an example of how your class is defined? – XCS Aug 30 '22 at 18:13
  • 2
    You probably want to `extend` the class – Andy Ray Aug 30 '22 at 18:13
  • 1
    It's called a class decorator, but I'm not sure how it's realized in javascript – Konrad Aug 30 '22 at 18:17
  • I agree with Andy Ray. Extend the class. JavaScript is made for this: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Classes/extends – Josh Aug 30 '22 at 18:24
  • 1
    Great [article](https://blog.logrocket.com/understanding-javascript-decorators/) about decorators – Konrad Aug 30 '22 at 18:29
  • Does this answer your question? "[How to listen to elements method calls in Javascript](/q/134374/90527)", "[How to inject javascript code to the beginning of every prototype method?](/q/17502932/90527)", "[Javascript AOP support](/q/3756419/90527)", … – outis Aug 30 '22 at 18:31
  • …"[Adding console.log to every function automatically](/q/5033836/90527)" (in particular, using [`Proxy`](/a/52018834/90527)) – outis Aug 30 '22 at 18:38

3 Answers3

1

You can grab the method names with getOwnPropertyNames, then modify the value of these descriptors, and redefine them on the new class.

class MyClass {
  foo() { return "foo"; }
  
  bar() { return "bar"; }
}

function logging(cls) {
  const c = class extends cls {}; // extend old class
  
  Object
    .getOwnPropertyNames(cls.prototype)
    .filter((v) => (
      v !== "constructor" && // ignore 'constructor'
      typeof cls.prototype[v] === "function" // only functions (methods)
    ))
    .forEach((key) => {
    const desc = Object.getOwnPropertyDescriptor(cls.prototype, key);
    
    const method = desc.value; // store old method
    
    desc.value = function (...args) {
      console.log(key, "was called");
    
      return method.apply(this, args); // call old method with correct 'this'
    };
    
    Object.defineProperty(c.prototype, key, desc); // redefine on new class
  });
  
  return c; // returning the new class
}

new (logging(MyClass))().foo(); // foo was called
kelsny
  • 23,009
  • 3
  • 19
  • 48
0

You could try something like this. Here, Square extends a parent class, calling the base implementation and then its logs on top.

class Rectangle {
  constructor(height, width) {
    this.name = 'Rectangle';
    this.height = height;
    this.width = width;
  }
  sayName() {
    console.log(`Hi, I am a ${this.name}.`);
  }
  get area() {
    return this.height * this.width;
  }
  set area(value) {
    this._area = value;
  }
}

class Square extends Rectangle {
  constructor(length) {

    // Here, it calls the parent class's constructor with lengths
    // provided for the Rectangle's width and height
    super(length, length);

  }
  
  sayName() {
      super.sayName();
      console.log("logs go here")
  }
}
Shane Sepac
  • 806
  • 10
  • 20
0

You can also crated a method to set params through which you set new/update methods & data, and by using a static class or a builder function you can copy methods without cross-referencing them. e. g.

const addMethod = (key, options, target) => {
  let col = {
    foo(arg, options) {
      //..
    }, 
    bar(arg, options) {
      //..
    }
  } 

  target[key] = col[key].bind(target)
}


const extendClass = (target, source) => {
  let arr = [] 

  for (let key in source) 
    if (typeof source[key] === 'function' && !target[key]) 
      arr.push({ key, options: {}) 

  // new target(arr) // new instance of target class
  target.setParams(arr) // extend instance

  return target
}

class A {
  //.. predefined stuff
  constructor(params) {
    setParams(params) 
  } 

  setParams(params) {
    for (let method of params)
      addMethod(method.key, method.options, this) 
    //.. and so on, make sure to condition different actions properly, this can also live mostly in the extendClass function
  } 
}

class B {
  // same same but diff
}

const extendedInstance = extendClass(A, B) 
marc_s
  • 732,580
  • 175
  • 1,330
  • 1,459
zergski
  • 793
  • 3
  • 10