简体   繁体   中英

Complex circular Node module dependency throwing “TypeError: The super constructor to 'inherits' must have a prototype”

I've got a complex Node SDK project that uses some class inheritance to try and static-ify Javascript. I'm using Node's module caching behavior to create a singleton-like behavior for the SDK (the Project class, a shared instance of ProjectClient ). To init, it looks like this:

var Project = require('./project'),
Project.init(params)
// Project is now an instance of ProjectClient

I've got some object type classes for data as well: Entity (a standard parsed JSON payload object) and User (a special type of Entity that contains user properties).

The ProjectClient class has several methods that allow RESTful API calls to occur, eg Project.GET() , Project.PUT() . These work just fine when instantiating the Project "singleton".

I'm now trying to create convenience methods attached to Entity that will leverage ProjectClient 's RESTful operations, eg Entity.save() , Entity.refresh() .

When I try to import Project into the Entity :

var Project = require('../project')

I get:

TypeError: The super constructor to `inherits` must have a prototype.
    at Object.exports.inherits (util.js:756:11)

Troubleshooting leads me to this being related to util.inherits(ProjectUser, ProjectEntity) in User , because if I comment it out, I get this instead:

Uncaught TypeError: ProjectEntity is not a function

What's going on with inherits ? why does it think that Entity doesn't have a prototype? My best guess is that it's related to the fact that I'm recursively nesting modules within other modules (bad, I know), but I've even tried doing something like this in the various classes but to no avail:

module.exports = _.assign(module.exports, **ClassNameHere**)

Here's some reduced code for each class:

Entity

var Project = require('../Project'),
    _ = require('lodash')

var ProjectEntity = function(obj) {
    var self = this

    _.assign(self, obj)

    Object.defineProperty(self, 'isUser', {
        get: function() {
            return (self.type.toLowerCase() === 'user')
        }
    })

    return self
}

module.exports = ProjectEntity

User (a subclass of Entity)

var ProjectEntity = require('./entity'),
    util = require('util'),
    _ = require('lodash')

var ProjectUser = function(obj) {

    if (!ok(obj).has('email') && !ok(obj).has('username')) {
        // This is not a user entity
        throw new Error('"email" or "username" property is required when initializing a ProjectUser object')
    }

    var self = this

    _.assign(self, ProjectEntity.call(self, obj))

    return self
}

util.inherits(ProjectUser, ProjectEntity)

module.exports = ProjectUser

Project ("singleton" but not really)

'use strict'

var ProjectClient = require('./lib/client')
var Project = {
    init: function(options) {
        var self = this
        if (self.isInitialized) {
            return self
        }
        Object.setPrototypeOf(Project, new ProjectClient(options))
        ProjectClient.call(self)
        self.isInitialized = true
    }
}

module.exports = Project

Client

var ProjectUser = require('./user'),
    _ = require('lodash')

var ProjectClient = function(options) {
    var self = this

    // some stuff happens here to check options and init with default values

    return self
}

ProjectClient.prototype = {
    GET: function() {
        return function() {
            // async GET request with callback
        }
    },
    PUT: function() {
        return function() {
            // async PUT request with callback
        }
    }
}

module.exports = ProjectClient

So, as you have correctly deducted there's a problem with a circular dependency. Your Entity module requires the Project Module which requires the Client module which requires the User Module which requires the Entity Module.

There's something you can do about it but it will depend on your starting point. If you require the Project module first then it should work with the code provided because the Entity module doesn't do anything with the Project module. Nothing has been exported on that module, so it's just an empty object. Then again, any source of errors on that module would be related on whatever exported objects are depended inside that module. So if you'd need the object with init inside Entity then there would be a problem.

You can export some methods/functions before initializing the dependency chain, which will make them available by then. Take the example of NodeJS docs:

a.js

console.log('a starting');
exports.done = false;
var b = require('./b.js');
console.log('in a, b.done = %j', b.done);
exports.done = true;
console.log('a done');

b.js

console.log('b starting');
exports.done = false;
var a = require('./a.js');
console.log('in b, a.done = %j', a.done);
exports.done = true;
console.log('b done');

main.js

console.log('main starting');
var a = require('./a.js');
var b = require('./b.js');
console.log('in main, a.done=%j, b.done=%j', a.done, b.done);

So, main.js is the starting point. It requires a.js which immediately exports a done property and then requires b.js . b.js also exports a done property. The next line requires a.js which doesn't load a.js again but returns the exported properties so far (which includes the done property). At this point, a is incomplete but it has managed to give b enough to continue working. It's next line (on b.js ) will print the exported property (a.done, which is false), then reset the exported property done to true. We are back on a.js in the require('b.js') line. b.js is now loaded completely and the rest is pretty easy to explain.

Output is:

main starting
a starting
b starting
in b, a.done = false
b done
in a, b.done = true
a done
in main, a.done=true, b.done=true

Here's the example, just in case you want to read the official docs.

Right, so point being... what can you do?

It's possible to export some things before initializing the dependency cycle, just so long as you don't actually need those dependencies. For example you can:

a.js

exports.func = function(){ console.log('Hello world'); }
var b = require('./b.js');
console.log('a done');

b.js

var a = require('./a.js');
a.func(); //i'm still incomplete but i got func!
console.log('b done');

You can't:

a.js

b.func(); //b isn't even an object yet.
var b = require('./b.js');
console.log('a done');

b.js

exports.func = function(){ console.log('hello world'); }
var a = require('./a.js');
a.func();
console.log('b done');

However, if your a.js module only exports functions then there's no real problem as long as these functions aren't being called somewhere else:

a.js

exports.func = function(){ b.func(); }
var b = require('./b.js');
console.log('a done');

b.js

exports.func = function(){ console.log('hello world'); }
var a = require('./a.js');
console.log('b done');

You are exporting a function that uses b , the function doesn't need to know about b right then, only when it's called. So both modules are being loaded correctly. If you are just exporting functions there's no problem with having the dependencies declared afterwards.

So you can require a.js from your main point and func will work correctly as the b reference points now to the complete b.js module. You can follow this pattern so long as you don't use the functions you export when loading the dependencies.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM