I think the best way to ask my question is by giving an example.
In JavaScript, imagine the following scenario:
function Tab(options) {
this.options = options;
}
Tab.prototype.doSomething = function () {
if(...) {
// Change tab1's options
//tab1.disabled = true
} else {
// Change tab2's options
//tab2.disabled = true
}
// Call a method on of mySlider instance (NOT myOtherSlider instance)
//mySlider.helloWorld();
}
// Slider class
function Slider(options) {
....
}
Slider.prototype.helloWorld = function () {
...
// Access tab1's properties
// tab1.disabled should be "TRUE" since it was changed previously
// Access tab2's properties
...
}
function Accordion() {
this.name = 'accordion';
var tab1 = new Tab({disabled: true}),
tab2 = new Tab({disabled: false),
mySlider = new Slider({color: red}),
myOtherSlider = new Slider({color: blue});
}
Pretty much I would like all the classes to be aware of the objects that has been instantiated in their own class as well as other classes.
The important part is for the instances to be synchronized. For example, a change to tab1's properties should be applied/visible to any other objects accessing tab1.
I managed to answer my own question by using an object manager class:
function ObjectManager () {
}
ObjectManager.objects = {};
ObjectManager.register = function (name, object) {
var t = this;
t.objects[name] = object;
}
ObjectManager.getObject = function (name) {
var t = this;
return t.objects[name];
}
function Tab () {
this.name = 'tab object';
}
Tab.prototype.init = function (name) {
var t = this;
t.name = name;
}
Tab.prototype.changeProperty = function () {
var tab1 = ObjectManager.getObject('tab1');
tab1.name = 'changed tab1 name';
}
function Accordion() {
var tab1 = new Tab();
tab1.init('tab number 1');
var tab2 = new Tab();
tab2.init('tab number 2');
ObjectManager.register('tab1', tab1);
ObjectManager.register('tab2', tab2);
console.log(ObjectManager.objects);
tab2.changeProperty();
console.log(ObjectManager.objects);
console.log(tab1.name);
}
var accordion = new Accordion();
Though I am not sure how efficient this solution is, but it gets the job done.
There are many different approaches to this problem. Let me explain two common patterns:
The observer pattern. Each object which needs to be informed about changes in other objects ("observer"), is passed to the "register" method of the objects it needs to be informed about ("observed"). The observed object keeps an internal array of all observers which registered for it. The observed also registers handlers for all relevant input events on the DOM element it represents (onclick, onchange etc). When the observed is changed, it informs all observers by calling a common method in them.
The controller pattern. All objects are managed by a central controller object. The controller keeps arrays of all objects which need to be managed. All input events are handled by the controller. When an input event occurs, it determines which objects need to be changed because of this event and changes them.
Related
I have been trying to learn the ins and outs of Javascript's inheritance structure and I came across this problem. I'm trying to create several subclass objects and assign values to them immediately by passing a variable upon their creation.
For instance, below the parent class GamePiece receives a random property as a variable upon its creation and sets it as myProperty which works fine of course when creating a new GamePiece object. However, if I wanted to set that variable on the creation of a Pawn object, it does not pass into its parent and remains unset. The obvious fix is to just define the variable again on the subclass but that, correct me if I'm wrong, seems to defeat the purpose of defining a parent class. You can also successfully set the variable by passing the parameter as Pawn.prototype = new GameObject("foo"); but that doesn't help when creating multiple Pawn() objects. Is there a common way of doing this that I am missing?
var GamePiece = function (randomProperty) {
this.myProperty = randomProperty || "never set";
this.print = function () {
console.log(this.myProperty);
}
}
var Pawn = function (randomProperty) {
this.print = function () {
console.log(this.myProperty);
}
}
//Setting a value on creation
piece = new GamePiece("foo");
piece.print(); // Produces "foo" naturally
//Setting the prototype
Pawn.prototype = new GamePiece();
//Try to pass value through the creation of subclass
pawn = new Pawn("foo");
pawn.print(); // Produces "never set"
You must call you parent class in context of current this, using call or apply method:
var GamePiece = function (randomProperty) {
this.myProperty = randomProperty || "never set";
this.print = function () {
console.log(this.myProperty);
}
}
var Pawn = function (randomProperty) {
Game.call(this, randomProperty);
// or Game.apply(this, [randomProperty]);
}
But much it's better to keep methods in prototype. So next code will be better:
var GamePiece = function (randomProperty) {
this.myProperty = randomProperty || "never set";
//... some another properties initialization
};
GamePiece.prototype.print = function () {
console.log(this.myProperty);
};
var Pawn = function (randomProperty) {
Game.call(this, randomProperty);
// or Game.apply(this, [randomProperty]);
//... some Pawn properties initialization
};
Pawn.prototype = Object.create(Game.prototype, { constructor: { value: Pawn }});
Pawn.prototype.someMethod = function() {
// Some Pawn method logic
};
But ES6 is coming (will become recommendation in June 2015), so you can start preparation for using them. See here, here and here
I'm new to javascript prototypes.
In examples, prototypes are assigned in-line with the main program definition, but doing so has start-up sequence ramifications.
The following shows how I currently apply a prototype to a group of singletons. It would be nice to instead assign the prototype within the descendant class, or somewhere more visibly 'bound' to it, for clarity. (Note the panels are instantiated within the controller to enforce separation.)
Is there another location/method to accomplish this I'm overlooking? Also, am I violating any well-known styles with the current approach?
var controller = new Controller();
function Controller() {
var panels = {
search: SearchPanel,
results: ResultsPanel,
details: DetailsPanel,
action: ActionPanel,
};
$.each(panels, function (i, v) {
// THE QUESTION REFERS TO THIS FOLLOWING STATEMENT:
v.prototype = new PanelCommon();
panels[i] = new v();
});
this.publish = function (site, message) {
$.each(panels, function (i, v) {
if (v[site]) v[site](message);
});
}
/*...*/
}
function PanelCommon() { /*...*/ }
function SearchPanel() { /*...*/ }
function ResultsPanel() { /*...*/ }
function DetailsPanel() { /*...*/ }
function ActionPanel() { /*...*/ }
Another fit for the dynamic nature of JavaScript is the concept of Mixins or Augmentation, which are sometimes more natural than prototypical inheritance.
What do I mean by a mixin?
A "mixin" that takes an object, and injects more functionality. Basically, the idea is that we are going to take an object, and start adding behavior to it.
Consider the following mixinPanelTo() function. It'll be a function that takes a constructor and adds a common render() function to it's prototype.
var mixinPanelTo = (function() {
var render = function() {
// a render function that all panels share
console.log("rendering!")
}
// Augment
return function(cls) {
cls.prototype.render = render;
}
})();
Now that we have this, we can mix that functionality into any constructor we want:
var SearchPanel = function() {}
SearchPanel.prototype.search = function(query) {
/* search stuff */
this.render();
}
mixinPanelTo(SearchPanel)
Then, we should be able to
var panel = new SearchPanel()
panel.search("foo"); // "rendering!" on the console
Multiple mixins
One advantage of mixins over inheritance is a more granular control over applied functionality, and also the ability to borrow functionality from multiple parents
var mixinRender = function(cls) { /* inject render */ }
var mixinSearch = function(cls) { /* inject search */ }
var mixinInfiniteScroll = function(cls) { /* inject infinite scroll */ }
var customPanel = function() {}
mixinRender(customPanel);
mixinSearch(customPanel);
mixinInfiniteScroll(customPanel)
This would be difficult to accomplish with prototypical inheritance. Other than trying to make a bizarre class hierarchy.
Borrowing functionality
You can also have your mixin's require functionality/configuration from your target class. For instance, lets take mixinInfinitScroll
var mixinInfiniteScroll = function(cls, fetch) {
var page = 0;
cls.prototype.more = function() {
var data
// get more results
if(typeof fetch == "function")
data = fetch.call(this, ++page)
else
// assume a key in this
data = this[fetch](++page)
/* do work with data */
}
}
And then when mixing in this functionality, we can inject specific functionality:
// by key
var Panel1 = function() { }
Panel1.prototype.fetch = function() { /* go get more results */ }
mixinInifiniteScroll(Panel1, "fetch")
// or even with a direct reference
var Panel1 = function() { }
Panel1.prototype.fetch = function() { /* go get more results */ }
mixinInifiniteScroll(Panel1, Panel1.prototype.fetch)
// or even an anonymous function
var Panel1 = function() { }
mixinInifiniteScroll(Panel1, function() { /* go get more results */ })
Overriding methods
You can also override prototype methods in mixins, which makes them quite powerful
var augmentRender = function(cls, renderFn) {
var oldRender = cls.prototype[renderFn];
cls.prototype[renderFn] = function() {
/* prep */
oldRender.apply(this, arguments);
/* make some more changes */
}
}
And then we can say:
var Panel = function() { }
Panel.prototype.render = function() { /* my render */ }
augmentRender(Panel, "render")
Anyway, not that there is anything wrong with prototypical inheritance, but this might give you some more ideas of different ways to solve your problem by approaching it in a different way.
Usually prototypes are assigned immediately following the declaration of the constructor function. Also, don't forget to modify the constructor property of the newly instantiated prototype.
Sean also makes an interesting point about using Object.create, but whether or not you want to do that really depends on the contents of the PanelCommon constructor function. You also may have to shim Object.create in older browsers.
function PanelCommon() {}
function SearchPanel() {}
SearchPanel.prototype = new PanelCommon();
SearchPanel.prototype.constructor = SearchPanel;
function ResultsPanel() {}
ResultsPanel.prototype = new PanelCommon();
ResultsPanel.prototype.constructor = ResultsPanel;
function DetailsPanel() {}
DetailsPanel.prototype = new PanelCommon();
DetailsPanel.prototype.constructor = DetailsPanel;
function ActionPanel() {}
ActionPanel.prototype = new PanelCommon();
ActionPanel.prototype.constructor = ActionPanel;
You can use Object.create - that will avoid the new SuperClass weirdness of the ES3 solution:
> SearchPanel.prototype = Object.create(PanelCommon.prototype)
> SearchPanel.prototype.constructor = SearchPanel
> new SearchPanel instanceof PanelCommon
true
This can be extracted into a very simple extends function:
function extends(cls, superClass) {
cls.prototype = Object.create(superClass.prototype);
cls.prototype.constructor = cls;
return cls;
}
Which can then be used like this:
var SpecialPanel = extends(function SpecialPanel() {}, PanelCommon);
I have a class in JS with field
Widget = function ()
{
this.Attributes = []; // key=value
}
and another class iherited from Widget
BusinessStatisticWidget = function ()
{
// some code
};
BusinessStatisticWidget.prototype = new Widget();
At initialization stage I have assigned this Attributes field with values (only once) and at some point Atttibutes field becomes empty:
BusinessStatisticWidget.prototype.SetEventsOnControls = function ()
{
var dropDown = document.getElementById(this.DropDownName + this.type + "Id");
var _this = this; // **Not empty here**
dropDown.addEventListener("change", function (event)
{
// **Not empty even here**
_this.CalculateAndSetTimeRangeForTimeSpan(event.target.value);
}, false);
}
BusinessStatisticWidget.prototype.CalculateAndSetTimeRangeForTimeSpan = function (val)
{
// **Empty here**
if (this.Attributes["fromDate"].value != '' && this.Attributes["toDate"].value != '')
{}
}
The code above works fine in Chrome and IE10 (I mean that array is not empty) but dont work in Firefox(20.0.1)
As array is empty I get TypeError: this.Attributes.fromDate is undefined.
And I dont know why it is empty and how to fix this.
There are multiple problems with your code:
Don't use arrays for arbitrary key, value pairs. Use only numerical keys for arrays.
Each instance will share the same Attributes array. This is usually not the desired behaviour.
Solutions:
Use an object instead.
Setup inheritance properly and call the parent constructor in the child constructor.
Code:
Widget = function () {
this.Attributes = {}; // use an pbject
};
var BusinessStatisticWidget = function () {
// call parent constructor
Widget.call(this);
// some code
};
// set up inheritance
BusinessStatisticWidget.prototype = Object.create(Widget.prototype);
More information (and polyfill) about Object.create.
Now, I don't know if that fixes your problem, but it makes your code at least more correct so that finding the issue becomes easier. I recommend to learn how to debug JavaScript.
I'm trying to make classical Collection/Instance model via javascript. So Collection object has some method for working with full collection and ((new Collection()) instanceof Instance) has methods to work with the instance. My code is rather simple.
var Collection = function Collection() {
this.message = "collection";
var I = Instance.bind(null, this);
I.__proto__ = this;
return I;
};
Collection.prototype = {
collectionMethod: function () {
console.log(this.message);
}
};
var Instance = function Instance(collection) {
this.collection = collection;
this.message = "instance";
};
Instance.prototype = {
instanceMethod: function () {
console.log(this.message);
}
};
// Test exec (values are like expected);
var C = new Collection();
var i = new C();
C.collectionMethod(); // collection
i.instanceMethod(); // instance
i.collection.collectionMethod(); // collection
C.newMethod(); // TypeError
i.newMethod(); // TypeError
Collection.prototype.newMethod = Instance.prototype.newMethod = function () {
console.log("newMethod: " + this.message);
}
C.newMethod(); // newMethod: collection
i.newMethod(); // newMethod: instance
But i don't want to use proto because it's not a part of standart and doesn't work in IE at all. Is there any way around in this case?
Some explanations about what's all about. For example you have a collection of users. And you want to be able find the user and create new one.
So you create new collection like
var User = new Collection();
Then you create new instance like.
var me = new User({name: "alex"});
And now you find this instance like
User.find_by_name("alex"); // === me
Also (in fact this is the main reason i'm doing this way instead of just creating something like User.new function to use it like var me = User.new({name: "alex"});) you can know who I am doing something like (if you for example have also var Dog = new Collection())
me instanceof Dog // false
me instanceof User // true
This code:
var I = Instance.bind(null, this);
I.__proto__ = this;
return I;
really doesn't make much sense. Function.bind creates a new function, so anyone calling your Collection function, in any way, will get back a function, not an object whose prototype is set to the function's prototype.
In general, if you want to create an object whose prototype is set to a specific object, you don't set __proto__ since that's not standard, as you stated. The best way is to just use Object.create (which is shimable if you want to support IE8).
var I = Object.create(this);
Also, the reason you're getting errors on newMethod is because you're trying to call them before you add them to the prototype:
Collection.prototype.newMethod = Instance.prototype.newMethod = function () {
console.log("newMethod: " + this.message);
}
C.newMethod(); // should work now
i.newMethod(); // should work now
So seems like it's impossible for now. More information can be found here.
How do I inherit javascript functions ?
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