Search code examples
javascriptnode.jsiifefunction-constructor

Why is this JS code working most places but not on my machine?


I wrote some code which works fine in the Node env provided by repl.it (https://repl.it/repls/LimpingCharmingGravity), in the code snippet here (see below), and on codepen.io (https://codepen.io/tjfwalker/pen/OERXry?editors=0012#0). It's not working, however, with Node on my machine. Trying to run in produces the following error message:

ReferenceError: Entity is not defined
    at Entity.eval [as addSub] (eval at <anonymous> (/Users/…pathtofile…/app.js:18:69), <anonymous>:3:11)
    at Object.<anonymous> (/Users/…pathtofile…/app.js:60:20)
    at Module._compile (internal/modules/cjs/loader.js:702:30)
    at Object.Module._extensions..js (internal/modules/cjs/loader.js:713:10)
    at Module.load (internal/modules/cjs/loader.js:612:32)
    at tryModuleLoad (internal/modules/cjs/loader.js:551:12)
    at Function.Module._load (internal/modules/cjs/loader.js:543:3)
    at Function.Module.runMain (internal/modules/cjs/loader.js:744:10)
    at startup (internal/bootstrap/node.js:238:19)
    at bootstrapNodeJSCore (internal/bootstrap/node.js:572:3)

My local node is 10.4.0 via nvm on macOS... though, I imagine it's not about that.

What gives?

let activeUserMock = 'someUserName'

const Entity = (function() {
    let lastID     = 0
      , entityList = []
    
    function Entity(userFields, userMethods) {
        this.meta = {id: ++lastID, dob: new Date, creator: activeUserMock}
        
        userFields.forEach(
            function(field) {
                this[field.key] = field.value
            }
        ,this)
        
        userMethods.forEach(
            function(method) {
                this[method.name] = Function(...method.args, method.body)
            }
        ,this)
        
        entityList.push(this)
    }
    
    Entity.findByID = function(id) {
        return entityList.find(function(entity) {
            return entity.meta.id === id
        })
    }
    
    return Entity
})();

// ======= SIMULATE AN END USER —FROM SOME CLIENT UI— MODELING THEIR OWN DOMAIN ==========

new Entity([ //stuff from the user ↴
    {key: 'type', value: 'feature'}
   ,{key: 'name', value: 'LMS'}
   ,{key: 'subs', value: []}
   ,{key: 'sups', value: []}
   ,{key: 'desc', value: 'a module to facilitate learning.'}
],[
    {name: 'addSub', args: ['subID'], body: 'let sub = Entity.findByID(subID); this.subs.push(sub); sub.sups.push(this); return this'}
   ,{name: 'addSup', args: ['supID'], body: 'let sup = Entity.findByID(supID); this.sups.push(sup); sup.subs.push(this); return this'}
])

new Entity([ //stuff from the user ↴
    {key: 'type', value: 'feature'}
   ,{key: 'name', value: 'SRS'}
   ,{key: 'subs', value: []}
   ,{key: 'sups', value: []}
   ,{key: 'desc', value: 'a module that implements the spaced repetition learning technique.'}
],[
    {name: 'addSub', args: ['subID'], body: 'let sub = Entity.findByID(subID); this.subs.push(sub); sub.sups.push(this); return this'}
   ,{name: 'addSup', args: ['supID'], body: 'let sup = Entity.findByID(supID); this.sups.push(sup); sup.subs.push(this); return this'}
])

Entity.findByID(1).addSub(2)

// ==========================================================

console.log(Entity.findByID(1))


Solution

  • Unlike eval(), Function() constructor does not have access to the calling scope, it only have access to the global scope.

    That script will fail in node, because every module has it's own scope, and Entity is not on the global namespace.

    To check this, just wrap your code inside an IIFEE, and you will see that it will fail on the browser too. (See snippet below)

    To "fix" your code in node, you will need to do:

    global.Entity = (function() {
     /* ... */
    })():
    

    But you should probably rethink your approach, and use bind instead, and access Entity using this.

    The following will fail in the browser too:

    (function() {
      let activeUserMock = 'someUserName'
    
      const Entity = (function() {
          let lastID     = 0
            , entityList = []
    
          function Entity(userFields, userMethods) {
              this.meta = {id: ++lastID, dob: new Date, creator: activeUserMock}
    
              userFields.forEach(
                  function(field) {
                      this[field.key] = field.value
                  }
              ,this)
    
              userMethods.forEach(
                  function(method) {
                      this[method.name] = Function(...method.args, method.body)
                  }
              ,this)
    
              entityList.push(this)
          }
    
          Entity.findByID = function(id) {
              return entityList.find(function(entity) {
                  return entity.meta.id === id
              })
          }
    
          return Entity
      })();
    
      // ======= SIMULATE AN END USER —FROM SOME CLIENT UI— MODELING THEIR OWN DOMAIN ==========
    
      new Entity([ //stuff from the user ↴
          {key: 'type', value: 'feature'}
         ,{key: 'name', value: 'LMS'}
         ,{key: 'subs', value: []}
         ,{key: 'sups', value: []}
         ,{key: 'desc', value: 'a module to facilitate learning.'}
      ],[
          {name: 'addSub', args: ['subID'], body: 'let sub = Entity.findByID(subID); this.subs.push(sub); sub.sups.push(this); return this'}
         ,{name: 'addSup', args: ['supID'], body: 'let sup = Entity.findByID(supID); this.sups.push(sup); sup.subs.push(this); return this'}
      ])
    
      new Entity([ //stuff from the user ↴
          {key: 'type', value: 'feature'}
         ,{key: 'name', value: 'SRS'}
         ,{key: 'subs', value: []}
         ,{key: 'sups', value: []}
         ,{key: 'desc', value: 'a module that implements the spaced repetition learning technique.'}
      ],[
          {name: 'addSub', args: ['subID'], body: 'let sub = Entity.findByID(subID); this.subs.push(sub); sub.sups.push(this); return this'}
         ,{name: 'addSup', args: ['supID'], body: 'let sup = Entity.findByID(supID); this.sups.push(sup); sup.subs.push(this); return this'}
      ])
    
      Entity.findByID(1).addSub(2)
    
      // ==========================================================
    
      console.log(Entity.findByID(1))
    })();

    UPDATE

    what concretely does code look like that follows your recommendation to "use bind instead, and access Entity using this

    this[method.name] = Function(...method.args, method.body).bind(this)
    

    And then use:

    body: 'let sub = this.constructor.findByID(subID) ...'
    

    Instead of:

    body: 'let sub = Entity.findByID(subID) ...'