2

I am trying to create a simple menu toggle functionality with JavaScript. And I am using constructors with functions to achieve it like below:

Constructor with anonymous function:

(function(){
  // Constructor
  function Menu(selector){
      let menu = document.querySelector(selector);
      return {
          activateToggle: function(){
              menu.addEventListener("click", function(){
                  menu.parentNode.querySelector("ul").classList.toggle("hidden");
              });
          },
          default: function(){
              menu.parentNode.querySelector("ul").classList.add("hidden");
          }
      }
  }

  let topMenu = new Menu(".burger");
  topMenu.default();
  topMenu.activateToggle();
  let bottomMenu = new Menu(".bottom-burger");
  bottomMenu.activateToggle();
})();
.hidden {
    display: none;
}
<!--Top Menu-->
<div class="topmenu">
    <button class="burger">Top Menu</button>
    <ul><li>Item 1</li><li>Item 2</li><li>Item 3</li><li>Item 4</li><li>Item 5</li></ul>
</div>
<!--Bottom Menu-->
<div class="bottommenu">
    <button class="bottom-burger">Bottom Menu</button>
    <ul><li>Item 1</li><li>Item 2</li><li>Item 3</li><li>Item 4</li><li>Item 5</li></ul>
</div>

Same Menu with arrow function:

(function(){
    // Constructor
    let Menu = (selector) => {
        let menu = document.querySelector(selector);
        return {
            activateToggle: () => menu.addEventListener("click", () => menu.parentNode.querySelector("ul").classList.toggle("hidden")),
            default: () => menu.parentNode.querySelector("ul").classList.add("hidden")
        }
    }

    let topMenu = new Menu(".burger");
    topMenu.default();
    topMenu.activateToggle();
    let bottomMenu = new Menu(".bottom-burger");
    bottomMenu.activateToggle();
})();
.hidden {
    display: none;
}
<!--Top Menu-->
<div class="topmenu">
    <button class="burger">Top Menu</button>
    <ul><li>Item 1</li><li>Item 2</li><li>Item 3</li><li>Item 4</li><li>Item 5</li></ul>
</div>
<!--Bottom Menu-->
<div class="bottommenu">
    <button class="bottom-burger">Bottom Menu</button>
    <ul><li>Item 1</li><li>Item 2</li><li>Item 3</li><li>Item 4</li><li>Item 5</li></ul>
</div>

The above code throws an error "Menu is not a constructor". I have tried to find the answer on how to create constructor with arrow functions but most of the article says that we can not use new keyword with arrow function.

Then what is the correct approach to create constructor with arrow functions?

Kiran Dash
  • 4,816
  • 12
  • 53
  • 84
  • 2
    Yes, arrow functions cannot be used as constructors. If you want to avoid `function`s, use `class`es. – Sebastian Simon Sep 06 '19 at 16:31
  • 3
    Just drop the `new` keywords and you're good to go. Your functions never really were constructors anyway, they're factory functions. – Bergi Sep 06 '19 at 16:34
  • 1
    Arrow function are not just syntax, the actual function is different in important ways, and cannot be used with `new`. 1. They *close over* `this`, so even if they could be used with `new`, they wouldn't have access to the new instance in order to set properties on it. 2. They don't have the `[[Construct]]` internal feature that constructors must have. 3. They don't have the `prototype` property that constructor functions must have. But as Bergi points out: Your functions aren't constructors anyway! – T.J. Crowder Sep 06 '19 at 16:34
  • @SebastianSimon Thanks a lot for the clarification. Will have a look at classes. – Kiran Dash Sep 06 '19 at 16:34
  • Thanks @Bergi. Makes sense now. It is just a straight forward function call then. – Kiran Dash Sep 06 '19 at 16:37

2 Answers2

2

Your functions (even your traditional function functions) aren't constructor functions, they're builder functions; you shouldn't use new with them in the first place. A constructor function sets properties on this and such and doesn't issue a return statement. Your functions build their own objects (no need for new) and return them.

And since you shouldn't use new with them, it's just fine to make them arrow functions. Arrow functions can't be constructor functions, but they can be builders.

When a function is a builder and not a constructor, overwhelmingly it shouldn't start with a capital letter. So for instance, createMenu rather than Menu. Here's your code with that change and without new:

(function(){
    // Constructor
    let createMenu = (selector) => {
        let menu = document.querySelector(selector);
        return {
            activateToggle: () => menu.addEventListener("click", () => menu.parentNode.querySelector("ul").classList.toggle("hidden")),
            default: () => menu.parentNode.querySelector("ul").classList.add("hidden")
        }
    }

    let topMenu = createMenu(".burger");
    topMenu.default();
    topMenu.activateToggle();
    let bottomMenu = createMenu(".bottom-burger");
    bottomMenu.activateToggle();
})();
.hidden {
    display: none;
}
<!--Top Menu-->
<div class="topmenu">
    <button class="burger">Top Menu</button>
    <ul><li>Item 1</li><li>Item 2</li><li>Item 3</li><li>Item 4</li><li>Item 5</li></ul>
</div>
<!--Bottom Menu-->
<div class="bottommenu">
    <button class="bottom-burger">Bottom Menu</button>
    <ul><li>Item 1</li><li>Item 2</li><li>Item 3</li><li>Item 4</li><li>Item 5</li></ul>
</div>
T.J. Crowder
  • 1,031,962
  • 187
  • 1,923
  • 1,875
  • Thanks for the clarification. I have been using new with all my anonymous functions with properties set via this keyword, for almost all my projects. Seems unnecessary then. – Kiran Dash Sep 06 '19 at 16:46
  • 1
    @KiranDash - Both styles are in common use in the JavaScript world. There's a bit more syntactic support for constructor functions than builders thanks to `class` syntax (which creates constructor functions and prototypical chains), but you can do much the same with builder functions and `Object.create` (to create prototype chains). – T.J. Crowder Sep 06 '19 at 16:50
0

Here is how I would structure with a class based approach:

class Menu {
    constructor (selector) {
    this.topMenu = document.querySelector(selector);
    this.parentUl = this.topMenu.parentNode.querySelector("ul");
    this.init();
  }
  init() {
    this.menu.addEventListener('click', this.toggleHiddenState.bind(this));
  }
  toggleHiddenState(e) {
    this.parentUl.classList.toggle("hidden");
  }
  hide() {
    this.parentUl.classList.add("hidden");
  }

}

// Usage
let topmenu = new Menu('.burger');
topmenu.hide();

let bottomMenu = new Menu('.bottom-burger');
mwilson
  • 12,295
  • 7
  • 55
  • 95