I'm trying to create a (class) instance that copies the behavior of another class, but using itself as the state context (this). Normal functions work fine (like the setValue function in the example below), but getters do not work. Here is a simple example:
const should = require('chai').should();
class One {
setValue(val) {
this._val = val;
}
get val() {
return this._val;
}
}
class Two {
constructor(one) {
this.one = one;
}
setValue(val) {
this.one.setValue.call(this, val);
}
get val() {
this.one.val.call(this);
}
}
let one = new One();
let two = new Two(one);
one.setValue(1);
two.setValue(2);
one.val.should.equal(1);
two.val.should.equal(2);
The above code explodes on the last line with the error:
TypeError: this.one.val.call is not a function
at Two.get val [as val] (C:\all\code\node-tests\invoke_getter.js:23:22)
at Object.<anonymous> (C:\all\code\node-tests\invoke_getter.js:32:5)
How can I make something like this work?
Recall that property getters for class ends in its prototype
So to access the method, you may want to get it from its prototype:
const descriptor = Object.getOwnPropertyDescriptor(Object.getPrototypeOf(this.one), 'val')
Then you can call the getter on your two instance:
class One {
setValue(val) {
this._val = val
}
get val() {
return this._val
}
}
class Two {
constructor(one) {
this.one = one
}
setValue(val) {
this.one.setValue.call(this, val)
}
get val () {
const desc = Object.getOwnPropertyDescriptor(Object.getPrototypeOf(this.one), 'val')
return desc.get.call(this)
}
}
const one = new One()
const two = new Two(one)
one.setValue(1)
console.log(two.val) // undefined (since _val does not exist on two yet)
two.setValue(2)
console.log(two.val) // 2
two._val = 3
console.log(two.val) // 3
Related
For pure educational and curiosity purposes, I am trying to create an element-wrapper object that allows me to tack-on my own properties and methods to an element. The behavior I'm trying to simulate is basically this:
// get a button element to wrap
const button = document.querySelector('button');
// some function that wraps new properties/methods around a given element
function wrap(element) {
this.customName = 'John';
this.customAge = 100;
this.printName = function() {
console.log(this.customName);
}
// ...
// ...somehow inherit element fields...
// ...
}
// wrap the button element
const customElement = new wrap(button);
// custom behavior:
console.log(customElement.customAge) // output => 100
customElement.printName() // output => 'John'
// legacy behavior
console.log(customElement.clientHeight) // output => client height
customElement.remove() // => should still call 'remove' on the element
So, here I should be able to add my own methods/properties but still access the original fields normally. Is this even possible?
I'm using a constructor function here as an example just to demonstrate the intended behavior, but I don't actually know if this would be relevant for the solution. I'm new to Javascript and I have done a ton of research on prototypes and classes, but I'm still confused on what approach I would take here.
Edit: As Brad pointed out in the comments, I also tried this implementation using classes:
class MyButton extends HTMLButtonElement {
constructor() {
super();
this.customName = 'John';
this.customAge = 100;
}
printName() {
console.log(this.customName);
}
}
const myBtn = new MyButton();
But this resulted in the error:
Uncaught TypeError: Illegal constructor
I haven't test this, but maybe something like this:
// get a button element to wrap
const button = document.querySelector('button');
// some function that wraps new properties/methods around a given element
function wrap(element) {
Object.defineProperties(element, {
customName: {value:"John"},
customAge: {value:100},
printName:{value: () => console.log(element.customName)}
})
return element
}
// wrap the button element
const customElement = wrap(button);
// custom behavior:
console.log(customElement.customAge) // output => 100
customElement.printName() // output => 'John'
// legacy behavior
console.log(customElement.clientHeight) // output => client height
customElement.remove() // => should still call 'remove' on the element
<button>Hello world!</button>
Another method could be used is wrap element into proxy()
This will allow return custom data if property doesn't exist and send notifications when properties changed:
const customElement = function (element, properties = {})
{
this.element = element;
this.customName = 'John';
this.customAge = 100;
this.printName = function() {
console.log(this.customName);
}
//override default properties
for(let i in properties)
{
if (i in element)
element[i] = properties[i];
else
this[i] = properties[i];
}
return new Proxy(this, {
get(target, prop)
{
if (prop in target.element) //is property exists in element?
{
if (target.element[prop] instanceof Function)
return target.element[prop].bind(target.element);
return target.element[prop];
}
else if (prop in target) //is property exists in our object?
return target[prop];
else
return "unknown property"; //unknown property
},
set(target, prop, value, thisProxy)
{
const oldValue = thisProxy[prop];
if (prop in target.element)
target.element[prop] = value;
else
target[prop] = value;
// send notification
target.element.dispatchEvent(new CustomEvent("propertyChanged", {
detail: {
prop,
oldValue,
value
}
}));
}
});
}
const button = new customElement(document.createElement("button"), {customName: "Not John"});
button.addEventListener("propertyChanged", e =>
{
console.log("property changed", e.detail);
});
button.printName();
console.log("age:", button.customAge);
console.log("height:", button.clientHeight);
console.log("blah:", button.blah);
button.blah = "ok";
console.log("blah:", button.blah);
I'm using an entity-component system. I've defined some components as ES6 classes, and I can create instances of those components by calling their constructors with new. I'm trying to use these class instances as mixins. I've found that using Object.assign doesn't copy the methods of those instances onto my target object because the methods are bound to the object's prototype.
I've found a hacky solution as follows:
function getAllPropertyNames(obj) {
return Object
.getOwnPropertyNames(obj)
.concat(Object.getOwnPropertyNames(obj.__proto__))
.filter(name => (name !== "constructor"))
}
function assembleFromComponents(obj, ...cmp) {
for(let i of cmp) {
for(let j of getAllPropertyNames(i)) {
obj[j] = i[j]
}
}
}
This method is not ideal because it doesn't access the component's full prototype chain, although I don't think I'll need it anyway. However, upon inspection, getters and setters don't appear to work.
Is there a better way to use a class instance as a mixin?
I probably wouldn't define mixins using class syntax. I'd define them as objects:
const myMixin = {
doThis() {
// ...
},
doThat() {
// ...
},
// ...
};
One issue with class syntax is that super may not work as you expect, because even after being copied, the methods will still refer to their original home object. (Or that may be what you expect in which case you're fine.) More on this below.
But if you want to use class syntax, you can define an Object.assign-like function that applies all of the methods and other properties from the entire chain via Object.defineProperties and Object.getOwnPropertyDescriptors, which will copy getters and setters. Something like (off the cuff, not tested):
function assignAll(target, source, inherited = false) {
// Start from the prototype and work upward, so that overrides work
let chain;
if (inherited) {
// Find the first prototype after `Object.prototype`
chain = [];
let p = source;
do {
chain.unshift(p);
p = Object.getPrototypeOf(p);
} while (p && p !== Object.prototype);
} else {
chain = [source];
}
for (const obj of chain) {
// Get the descriptors from this object
const descriptors = Object.getOwnPropertyDescriptors(obj);
// We don't want to copy the constructor or __proto__ properties
delete descriptors.constructor;
delete descriptors.__proto__;
// Apply them to the target
Object.defineProperties(target, descriptors);
}
return target;
}
Using it:
assignAll(Example.prototype, Mixin.prototype);
Live Example:
function assignAll(target, source, inherited = false) {
// Start from the prototype and work upward, so that overrides work
let chain;
if (inherited) {
// Find the first prototype after `Object.prototype`
chain = [];
let p = source;
do {
chain.unshift(p);
p = Object.getPrototypeOf(p);
} while (p && p !== Object.prototype);
} else {
chain = [source];
}
for (const obj of chain) {
// Get the descriptors from this object
const descriptors = Object.getOwnPropertyDescriptors(obj);
// We don't want to copy the constructor or __proto__ properties
delete descriptors.constructor;
delete descriptors.__proto__;
// Apply them to the target
Object.defineProperties(target, descriptors);
}
return target;
}
class Example {
method() {
console.log("this is method");
}
}
const mixinFoos = new WeakMap();
class Mixin {
mixinMethod() {
console.log("mixin method");
}
get foo() {
let value = mixinFoos.get(this);
if (value !== undefined) {
value = String(value).toUpperCase();
}
return value;
}
set foo(value) {
return mixinFoos.set(this, value);
}
}
assignAll(Example.prototype, Mixin.prototype, true);
const e = new Example();
e.foo = "hi";
console.log(e.foo);
// HI
Here's an example where the mixin is a subclass and uses super, just to demonstrate what super means in that context:
function assignAll(target, source, inherited = false) {
// Start from the prototype and work upward, so that overrides work
let chain;
if (inherited) {
// Find the first prototype after `Object.prototype`
chain = [];
let p = source;
do {
chain.unshift(p);
p = Object.getPrototypeOf(p);
} while (p && p !== Object.prototype);
} else {
chain = [source];
}
for (const obj of chain) {
// Get the descriptors from this object
const descriptors = Object.getOwnPropertyDescriptors(obj);
// We don't want to copy the constructor or __proto__ properties
delete descriptors.constructor;
delete descriptors.__proto__;
// Apply them to the target
Object.defineProperties(target, descriptors);
}
return target;
}
class Example {
method() {
console.log("this is Example.method");
}
}
class MixinBase {
method() {
console.log("this is MixinBase.method");
}
}
class Mixin extends MixinBase {
method() {
super.method();
console.log("this is Mixin.method");
}
}
assignAll(Example.prototype, Mixin.prototype, true);
const e = new Example();
e.method();
// "this is MixinBase.method"
// "this is Mixin.method"
You've said you want to use class instances as mixins. The above works just fine doing that. Here's an example:
function assignAll(target, source, inherited = false) {
// Start from the prototype and work upward, so that overrides work
let chain;
if (inherited) {
// Find the first prototype after `Object.prototype`
chain = [];
let p = source;
do {
chain.unshift(p);
p = Object.getPrototypeOf(p);
} while (p && p !== Object.prototype);
} else {
chain = [source];
}
for (const obj of chain) {
// Get the descriptors from this object
const descriptors = Object.getOwnPropertyDescriptors(obj);
// We don't want to copy the constructor or __proto__ properties
delete descriptors.constructor;
delete descriptors.__proto__;
// Apply them to the target
Object.defineProperties(target, descriptors);
}
return target;
}
class Example {
method() {
console.log("this is Example.method");
}
}
class MixinBase {
method() {
console.log("this is MixinBase.method");
}
}
const mixinFoos = new WeakMap();
class Mixin extends MixinBase {
constructor(value) {
super();
this.value = value;
}
mixinMethod() {
console.log(`mixin method, value = ${this.value}`);
}
get foo() {
let value = mixinFoos.get(this);
if (value !== undefined) {
value = String(value).toUpperCase();
}
return value;
}
set foo(value) {
return mixinFoos.set(this, value);
}
method() {
super.method();
console.log("this is Mixin.method");
}
}
// Here I'm using it on `Example.prototype`, but it could be on an
// `Example` instance as well
assignAll(Example.prototype, new Mixin(42), true);
const e = new Example();
e.mixinMethod();
// "mixin method, value = 42"
e.method();
// "this is MixinBase.method"
// "this is Mixin.method"
e.foo = "hi";
console.log(e.foo);
// "HI"
But really, you can design it however you want to; assignAll is just an example, as are the runnable ones above. The key things here are:
Use Object.getOwnPropertyDescriptors to get property descriptors and Object.defineProperties (or their singular counterparts, getOwnPropertyDescriptor and defineProperty), so that accessor methods get transferred as accessors.
Work from the base prototype up to the instance, so that overriding at each level works correctly.
super will continue to work in its original inheritance chain, not in the new location the mixin has been copied to.
How can I update a variable of an object of a particular class by passing an callback to another function inside a separate object. Here in the code in the class Example when I make an object of it and then call its callOtherMethod the console.log made by the updateArr() method shows this.arr as undefined. This is an example of the problem that I faced. Thank you for help
class Example{
constructor(){
this.arr = [];
this.index = 2;
}
updateArr(str){
this.arr[this.index] = str;
console.log(this.arr)
}
callOtherMethod(){
this.index = 1;
anotherObject.method(this.updateArr)
}
}
anotherObject= {
method : (callback) =>{
callback('Hello')
}
}
const ex = new Example();
ex.callOtherMethod()
//logs this.arr undefined
When you pass this.updateArr and execute it in your method function:
method : (callback) =>{
callback('Hello')
}
... the this that gets bound to updateArr gets lost, as the calling context to callback is not specified.
To fix this, you can pass a special version of your this.updateArr to your anotherObject.method() method, which has its this explicitly bound to it using the .bind() function. The .bind() method will return a new function with the this being bound to be the first argument that you pass.
See example below:
class Example {
constructor() {
this.arr = [];
this.index = 2;
}
updateArr(str) {
this.arr[this.index] = str;
console.log(this.arr)
}
callOtherMethod() {
this.index = 1;
// pass a function with the `this` being bound to the current object
anotherObject.method((...args) => this.updateArr(...args))
}
}
anotherObject = {
method: (callback) => {
callback('Hello')
}
}
const ex = new Example();
ex.callOtherMethod();
Alternatively, you can use:
anotherObject.method((...args) => this.updateArr(...args))
This will pass a function into arnotherObject.method() which when executed will run:
this.updateArr(...args)
since the this here refers to your object instance (as arrow function's don't have their own this binding) you will be able to call your instance's updateArr method.
You could either use the traditional approach of binding the object to the function as suggested in another answer.
Or also pass the function name as a string along with the original object. And then access the function using the property of the object:
class Example{
constructor(){
this.arr = [];
this.index = 2;
}
updateArr(str){
this.arr[this.index] = str;
console.log(this.arr)
}
callOtherMethod(){
this.index = 1;
anotherObject.method(this, "updateArr")
}
}
anotherObject= {
method : (obj, callback) =>{
obj[callback]('Hello');
}
}
const ex = new Example();
ex.callOtherMethod();
In the code below, I want to use One's reference to Two to call its saySomething() function. When I try it this way, I get this error:
Uncaught ReferenceError: other is not defined.
How can I change my code to make it work?
class One
{
constructor (other)
{
this.other = other;
}
doSomething ()
{
this.other.saySomething();
}
}
class Two
{
saySometing ()
{
console.log("hi");
}
}
const t = new Two();
const o = new One(t);
o.doSomething();
You have a typo in your saySometing() declaration. It should be saySomething().
Not sure what would be the best pattern with JavaScript classes but prototypes (which is what classes use under the hood anyway) will allow you to do so:
var One = function() {
Two.prototype.saySomething();
}
One.prototype = {
}
var Two = function() {
}
Two.prototype = {
saySometing: function() {
console.log("hi");
}
}
const t = new One();
I really never used a javascript function type or class before, I understand Java and Python, but not javascript. So, I build a class like this:
function FormStore (type) {
this.setup = () =>{
this.store = {};
this.ERR_LINE_PREFIX = '#err_';
this.NO_DISPLAY_CLASS = 'no-display';
this.settings = {
'myID':{'hide':false},
}
}
this.checkVal= () => {
var geoArr = ['id_xx','myID', (...)];
var id;
$.each( geoArr, function(val) {
id = geoArr[val];
console.log(this.store) //-> returns undefined, below line is error
if (!(this.store[id])) {
return false;
}
});
};
var FS = new FormStore();
FS.setup();
The store is filled by components on document.ready. There is a function that looks up if the aligned components (glyph, label, input) have some classes or values and for the specific component fills a dict: {label:false,glyph:false, input:false}. However, for some reason it doesn't matter. Even if I enter some values in to the store right away (in setup) or create them on the fly, in checkVal the store doesn't exist, it's undefined.
Please, anybody, what am I not understanding about javascript type and classes here? I am googling this a lot and trying to find good resources but, "javascipt variable class" (or type) just yields a lot of DOM manipulation.
edit
There is a context problem in checkVal, you are using a non-arrow (and not explicitly bound) callback function and trying to access this inside of it. Change that to an arrow function as well, and the parent context (this) will be preserved:
$.each( geoArr, (val) => {
id = geoArr[val];
console.log(this.store)
if (!(this.store[id])) {
return false;
}
});
And while you are at changing that section, it's not going to work. You will not get access to $.each's return value. You should rely on native array APIs for this task and use Array.every to determine if all geoArr items are in the store (assuming that's your goal):
// returns false if not all geoArr items are in the store
geoArr.every(id => this.store[id])
original
I don't see you calling checkVal() anywhere, but based on the error you are getting it is called prior to setup() (since setup initializes the store). You could solve that problem straight away by moving this.store = {} out of setup (right at the top), e.g.:
function FormStore(type) {
this.store = {};
...
Having said that, I would suggest either defining your methods on the prototype, or utilizing ES6 classes. Here is a simplified version of both:
ES5 class
function FormStore(type) {
// make sure user didn't forget new keyword
if (this === window) {
throw new Error('FormStore must be called with "new" keyword')
}
// initialize state, this is the constructor
this.type = type;
this.store = {};
// any other state the class manages
}
FormStore.prototype = {
setup: function() {
// do setup stuff
// "this" points to instance
console.log('setup', this.type)
},
checkVal: function() {
}
}
var formStore = new FormStore('foo')
console.log(formStore.store) // <-- not undefined
formStore.setup()
ES6 Class
class FormStore {
constructor(type) {
this.type = type;
this.store = {};
}
setup() {
console.log('setup', this.type)
}
checkVal() {
}
}
const formStore = new FormStore('bar')
console.log(formStore.store) // <-- not undefined
formStore.setup()
It has to do with scoping. Your $.each in checkVal has a normal function. Inside the function the scope if this is different. If you want to keep the original scope you could use a fat arrow function like you do when defining the methods.
this.checkVal= () => {
var geoArr = ['id_xx','myID', (...)];
var id;
$.each( geoArr, val => {
id = geoArr[val];
console.log(this.store) //-> returns undefined, below line is error
if (!(this.store[id])) {
return false;
}
});
}
When you run your original code and place a breakpoint on the line with console.log you can see in the inspector that this is set to the Window object and no longer points to your FormStore.
function FormStore () {
this.setup = function(){
this.store = {};
this.ERR_LINE_PREFIX = '#err_';
this.NO_DISPLAY_CLASS = 'no-display';
this.settings = {
'myID':{'hide':false},
}
}
this.checkVal= function(){
var geoArr = ['id_xx','myID'];
var id;
$.each( geoArr, function(val) {
id = geoArr[val];
console.log(this.store) //-> returns undefined, below line is error
if (!(this.store[id])) {
return false;
}
});
}
};
var FS = new FormStore();
FS.setup();
Works absolutely fine, the code you provided had a missing bracket and you were using some broken es6 syntax