I have spotted buggy behavior in javascript es6 inheritance using Singleton pattern.
Code is:
let instanceOne = null;
class One {
constructor() {
if (instanceOne) return instanceOne;
this.name = 'one';
instanceOne = this;
return instanceOne;
}
method() {
console.log('Method in one');
}
}
let instanceTwo = null;
class Two extends One {
constructor() {
super();
if (instanceTwo) return instanceTwo;
this.name = 'two';
instanceTwo = this;
return instanceTwo;
}
method() {
console.log('Method in two');
}
}
const objOne = new One();
const objTwo = new Two();
console.log(objOne.name);
console.log(objTwo.name);
objOne.method();
objTwo.method();
Display is:
two
two
Method in one
Method in one
The inheritance get fucked up somehow. Here the attributes get overridden but not the object methods.
My question is why is it working (like now throw) and can you explain this behavior?
It appears that new objects need brand new object as parent (see solution below).
If you encounter the same problem, here is my solution:
let instanceOne = null;
class One {
constructor(brandNewInstance = false) {
if (instanceOne && !brandNewInstance) return instanceOne;
this.name = 'one';
if (brandNewInstance) return this;
instanceOne = this;
return instanceOne;
}
method() {
console.log('Method in one');
}
}
let instanceTwo = null;
class Two extends One {
constructor() {
super(true);
if (instanceTwo) return instanceTwo;
this.name = 'two';
instanceTwo = this;
return instanceTwo;
}
method() {
console.log('Method in two');
}
}
I use node.js v6.9.1
This happens because of this line:
if (instanceOne) return instanceOne;
One constructor runs twice in the code above. Second One call is super(), in this case this is created from Two.prototype, and object method is Two.prototype.method.
Return statement from super() substitutes this with One singleton, and then Two constructor just modifies One singleton instance.
Static property can be used instead to hold instances:
constructor() {
if (this.constructor.hasOwnProperty('instance'))
return this.constructor.instance;
this.constructor.instance = this;
this.name = 'one';
}
Or if sharing an instance with descendant classes is the expected behaviour,
constructor() {
if ('instance' in this.constructor)
return this.constructor.instance;
this.name = 'one';
this.constructor.instance = this;
}
In this case all singleton mechanics is done by One constructor, Two just needs to call super:
constructor() {
super();
this.name = 'two';
}
Also, ending return statement is redundant. this doesn't have to be returned explicitly.
You are doing something a bit strange. Constructors and subclasses in ecmascript 6 do not work in the way you think they do. You may wish to read this blog post (particularly section 4) to learn more.
Taking from that article, your code looks like this under the hood:
let instanceOne = null;
function One() {
// var this = Object.create(new.target.prototype); // under the hood
if (instanceOne) return instanceOne;
this.name = 'one';
instanceOne = this;
return instanceOne;
}
One.prototype.method = function() { console.log('Method in one'); }
let instanceTwo = null;
function Two() {
var that = undefined;
that = Reflect.construct(One, [], new.target);
if (instanceTwo) return instanceTwo;
that.name = 'two';
instanceTwo = that;
return instanceTwo;
}
Two.prototype.method = function() { console.log('Method in two'); }
Object.setPrototypeOf(Two, One);
Object.setPrototypeOf(Two.prototype, One.prototype);
const objOne = Reflect.construct(One, [], One);
const objTwo = Reflect.construct(Two, [], Two);
console.log(objOne.name);
console.log(objTwo.name);
objOne.method();
objTwo.method();
(new.target is the value passed as the third argument of Reflect.construct)
You can see that for the Two class, no new object is being created and Two.prototype is not used. Instead, the One singleton instance is used and mutated.
Related
Let say we have an object:
#:checkDirty
class Test {
var a:Int;
var b(default, default):String;
var c(get, set):Array<Int>;
public function new() {
...
}
public function get_c() {
...
}
public function set_c(n) {
...
}
}
Could we write a macro checkDirty so that any change to field/properties would set property dirty to true. Macro would generate dirty field as Bool and clearDirty function to set it to false.
var test = new Test();
trace(test.dirty); // false
test.a = 12;
trace(test.dirty); // true
test.clearDirty();
trace(test.dirty); //false
test.b = "test"
trace(test.dirty); //true
test.clearDirty();
test.c = [1,2,3];
trace(test.dirty); //true
Just to note - whenever you consider proxying access to an object, in my experience, there are always hidden costs / added complexity. :)
That said, you have a few approaches:
First, if you want it to be pure Haxe, then either a macro or an abstract can get the job done. Either way, you're effectively transforming every property access into a function call that sets the value and also sets dirty.
For example, an abstract using the #:resolve getter and setter can be found in the NME source code, replicated here for convenience:
#:forward(decode,toString)
abstract URLVariables(URLVariablesBase)
{
public function new(?inEncoded:String)
{
this = new URLVariablesBase(inEncoded);
}
#:resolve
public function set(name:String, value:String) : String
{
return this.set(name,value);
}
#:resolve
public function get(name:String):String
{
return this.get(name);
}
}
This may be an older syntax, I'm not sure... also look at the operator overloading examples on the Haxe manual:
#:op(a.b) public function fieldRead(name:String)
return this.indexOf(name);
#:op(a.b) public function fieldWrite(name:String, value:String)
return this.split(name).join(value);
Second, I'd just point out that if the underlying language / runtime supports some kind of Proxy object (e.g. JavaScript Proxy), and macro / abstract isn't working as expected, then you could build your functionality on top of that.
I wrote a post (archive) about doing this kind of thing (except for emitting events) before - you can use a #:build macro to modify class members, be it appending an extra assignment into setter or replacing the field with a property.
So a modified version might look like so:
class Macro {
public static macro function build():Array<Field> {
var fields = Context.getBuildFields();
for (field in fields.copy()) { // (copy fields so that we don't go over freshly added ones)
switch (field.kind) {
case FVar(fieldType, fieldExpr), FProp("default", "default", fieldType, fieldExpr):
var fieldName = field.name;
if (fieldName == "dirty") continue;
var setterName = "set_" + fieldName;
var tmp_class = macro class {
public var $fieldName(default, set):$fieldType = $fieldExpr;
public function $setterName(v:$fieldType):$fieldType {
$i{fieldName} = v;
this.dirty = true;
return v;
}
};
for (mcf in tmp_class.fields) fields.push(mcf);
fields.remove(field);
case FProp(_, "set", t, e):
var setter = Lambda.find(fields, (f) -> f.name == "set_" + field.name);
if (setter == null) continue;
switch (setter.kind) {
case FFun(f):
f.expr = macro { dirty = true; ${f.expr}; };
default:
}
default:
}
}
if (Lambda.find(fields, (f) -> f.name == "dirty") == null) fields.push((macro class {
public var dirty:Bool = false;
}).fields[0]);
return fields;
}
}
which, if used as
#:build(Macro.build())
#:keep class Some {
public function new() {}
public var one:Int;
public var two(default, set):String;
function set_two(v:String):String {
two = v;
return v;
}
}
Would emit the following JS:
var Some = function() {
this.dirty = false;
};
Some.prototype = {
set_two: function(v) {
this.dirty = true;
this.two = v;
return v;
}
,set_one: function(v) {
this.one = v;
this.dirty = true;
return v;
}
};
I have two classes one and two
class One {
constructor(field1, field2) {
this.field1 = field1;
this.field2 = field2;
}
}
module.exports = one;
class Two {
constructor(field11, field22, list) {
this.field11 = field11;
this.field22 = field22;
this.list = list;
}
add(one) {
this.list.push(one);
}
}
module.exports = Two;
Third class imports both classes
const one= require('./one.js');
const two= require('./two.js');
Now, I have a function which creates an object of class two and add some values like,
two = new two();
two.add(new one(1,1000));
two.add(new one(2,2000));
console.log(two.list);
////list is a collection of class one object
Till this point is working fine, I am getting collection
My query is how to iterate through collection
like, I want to access
two.list[0].field1
// not getting the property field1
Try this:
class One {
constructor(field1, field2) {
this.field1 = field1; this.field2 = field2;
}
}
class Two {
constructor(field11, field22, list = []) {
this.field11 = field11; this.field22 = field22;
this.list = list
}
add(one) {
this.list.push(one);
}
}
two = new Two();
two.add(new One(1, 1000));
two.add(new One(2, 2000));
console.log(two.list);
There are some issues in code:
Naming and bracket is not closing correct
Default list parameter is also written in wrong format
class One {
constructor(field1, field2) {
this.field1 = field1;
this.field2 = field2;
}
}
class Two {
constructor(field11, field22, list = []) {
this.field11 = field11;
this.field22 = field22;
this.list = list;
}
add(one) {
this.list.push(one);
}
}
two = new Two();
two.add(new One(1,1000));
two.add(new One(2,2000));
console.log(two.list[0].field1);
Updated your code. Try running it
I've got a question: If I have a constructor in a class:
module.exports = class ClassA{
constructor(stuffA, stuffB) {
this.stuffA = stuffA;
this.stuffB = stuffB;
}
NonStaticMethod() {
console.log(this);
}
static StaticMethod(stuffA, stuffB) {
const element = new ClassA(stuffA, stuffB);
console.log(element)
element.NonStaticMethod();
});
}
};
So, the NonStaticMethod prints other information for the object than the StaticMethod. So two questions:
Can I call the constructor from a static method from the same class?
What should be the correct way of calling the non-static method from the static method?
The following code prints "true", so in NonStaticMethod this.stuffA rely correctly on value defined in constructor:
class ClassA{
constructor(stuffA, stuffB) {
this.stuffA = stuffA;
this.stuffB = stuffB;
}
NonStaticMethod() {
console.log(this.stuffA === "a");
}
static StaticMethod(stuffA, stuffB) {
const element = new ClassA(stuffA, stuffB);
element.NonStaticMethod();
};
}
ClassA.StaticMethod("a","b")
It seems that only if I fill out the child object directly in the Base function that is the only way that the getSettings function can see the this.name property correctly, but I was trying to have my objects in different files to avoid having one large file.
***child.js***
module.exports : {
getSettings: ()=>{
return this.name === 'foobar'
}
}
***base.js***
var Child = require('./child.js')
function Base(){
this.name = 'foobar'
this.child = Child
this.child2 = {}
for (var prop in Child){
this.child2[prop] = Child[prop]
}
this.child3 = {
getSettings: ()=>{
return this.name === 'foobar'
}
}
}
***index.js****
var Base = require('./base.js')
var b = new Base()
b.child.getSettings() //FAILS BECAUSE this.name is undefined
b.child2.getSettings() //FAILS BECAUSE this.name is undefined
b.child3.getSettings() //PASSES. this.name is defined
It's conventional in JS OOP to refer class instance as this, so it's semantically incorrect for child objects to refer to parent object as this. This also makes it difficult to work with an object like child that doesn't get desirable context as lexical this (like child3 does).
child object likely should be a class that are injected with parent instance as a dependency.
module.exports = class Child(parent) {
constructor(parent) {
this.parent = parent;
}
getSettings() {
return this.parent.name === 'foobar'
}
}
var Child = require('./child.js')
function Base(){
this.name = 'foobar'
this.child = new Child(this);
...
}
I am making a simple note taking app to learn node and ES6. I have 3 modules - App, NotesManager and Note. I am importing the Note class into the NotesManager and am trying to instantiate it in its addNote function. The problem is that even though the import is correct, it turns out to be undefined inside the class definition. A simpler solution would be to just instantiate the NotesManager class and add the Note class to its constructor however, I want to have NotesManager as a static utility class.
Here is my code.
Note.js
class Note {
constructor(title, body) {
this.title = title;
this.body = body;
}
}
module.exports = Note;
NotesManager.js
const note = require("./Note");
console.log("Note: ", note); //shows correctly
class NotesManager {
constructor() {}
static addNote(title, body) {
const note = new note(title, body); //Fails here as note is undefined
NotesManager.notes.push(note);
}
static getNote(title) {
if (title) {
console.log(`Getting Note: ${title}`);
} else {
console.log("Please provide a legit title");
}
}
static removeNote(title) {
if (title) {
console.log(`Removing Note: ${title}`);
} else {
console.log("Please provide a legit title");
}
}
static getAll() {
//console.log("Getting all notes ", NotesManager.notes, note);
}
}
NotesManager.notes = []; //Want notes to be a static variable
module.exports.NotesManager = NotesManager;
App.js
console.log("Starting App");
const fs = require("fs"),
_ = require("lodash"),
yargs = require("yargs"),
{ NotesManager } = require("./NotesManager");
console.log(NotesManager.getAll()); //works
const command = process.argv[2],
argv = yargs.argv;
console.log(argv);
switch (command) {
case "add":
const title = argv.title || "No title given";
const body = argv.body || "";
NotesManager.addNote(title, body); //Fails here
break;
case "list":
NotesManager.getAll();
break;
case "remove":
NotesManager.removeNote(argv.title);
break;
case "read":
NotesManager.getNote(argv.title);
break;
default:
notes.getAll();
break;
}
Is it possible for me to create a strict utility class which I can use without instantiating like in Java? Pretty new here and have tried searching for it without any luck. Thank you for your help.
When you do this:
const note = new note(title, body);
you redefine note shadowing the original note from the outer scope. You need to pick a different variable name.
Something like this should work better:
static addNote(title, body) {
const some_note = new note(title, body); //Fails here as note is undefined
NotesManager.notes.push(some_note);
}