4

I'm curious if JavaScript has a way to bind a function when a property is changed without an enormous overhead like watching all the auto-properties with a timer, but without setting them via a function call. For example, I know you could do something like:

var c = new (function () {
    this.Prop = 'test';
    this.Prop_get = function (value) {
        return('Prop = ' + this.Prop);
    };
    this.Prop_set = function (value) {
        if (value != 'no') {
            this.Prop = value;
        }
    };
})();

document.write(c.Prop_get());
document.write('<BR />');
c.Prop_set('no');
document.write(c.Prop_get());
document.write('<BR />');
c.Prop_set('yes');
document.write(c.Prop_get());
document.write('<BR />');

But I'm looking for some way to allow the following to produce the same result:

document.write(c.Prop);
document.write('<BR />');
c.Prop = 'no';
document.write(c.Prop);
document.write('<BR />');
c.Prop = 'yes';
document.write(c.Prop);
document.write('<BR />');

With any changes to the pseudoclass other than adding a timer to watch the Prop property for changes or similarly high-overhead solutions.

CoryG
  • 2,429
  • 3
  • 25
  • 60
  • Look at the get and set operator, for example http://stackoverflow.com/questions/6012897/javascript-get-and-set-availability-in-browsers – Wolfgang Kuehn Feb 17 '13 at 22:18

2 Answers2

3

Any solution to this issue comes down to what it is that you need to support.

If you require IE6-IE8, it's probably more sane to resort to timers or horrible abuse of the DOM to make changes to hidden DOM objects, which will fire listenable events, etc...

There are a few blogs which have talked about their efforts to squeeze these browsers into conformity with some kind of mutation-aware library.
Results and caveats vary.

If you're talking about ES5-compliant browsers, most support "get" and "set" keywords directly inside of objects.
This might lead to even cleaner constructors/interfaces than C#, because constructors can be as simple as var a = {};, but you also get the magic-methods, rather than the Java list of getX, getY, z, and the headache of trying to remember what's a method and what's a property, when you get to the interface.

Seriously, this is kinda pretty:

var person = {
    person_name : "Bob",
    get name () { return this.person_name; },
    set name (value) {
        console.log("But my parents named me " + this.person_name + "!");
    }
};


person.name;
person.name = "Mark";

But there's an issue here: person.person_name isn't private at all.
Anybody could swoop in and change that.

Not to fret -- get and set don't actually have to operate on properties of the object.

var Person = function (name, age) {
    // we don't need to save these; closures mean they'll be remembered as arguments
    // I'm saving them as `private_*` to illustrate
    var private_name = name,
        private_age  = age;

    var public_interface = {
        get name () { return private_name; },
        set name (value) { console.log("Nope!"); },
        get age () { return private_age; },
        set age (value) { console.log("Nope!"); },
        set court_appointed_name (value) {
            console.log("If I must...");
            private_name = value;
        }
    };

    return public_interface;
};

var mark = Person("Mark", 32);
mark.name; // "Mark";
mark.name = "Bubba"; // log: "Nope!";
mark.name; // "Mark";
mark.court_appointed_name = "Jim-Bob"; // log: "If I must..."
mark.name; // "Jim-Bob"

You could also force assignments to pass in objects, with auth-tokens, et cetera.

mark.name = {
    value : "Jimmy Hoffa",
    requested_by : system.user.id,
    auth : system.user.auth.token
};

This is all fantastic, isn't it?
Why aren't we doing it?

Browser support.

Problem is this requires brand new syntax: all objects are defined as key-value pairs.
Messing with the syntax means any non-supporting browser will crash and burn unless you wrap your entire program in a try/catch (which is performance-suicide).

You could do one try-catch test, and lazy-load the awesome interface, versus fugly workarounds, at page-load, which is the right way to go about it, but now you're developing two versions of the application.

Or three versions, as case may be (new browsers, intermediate-browsers like FF3, and hacks for Ghetto_IE).

Intermediate browsers used {}.__defineGetter__ and {}.__defineSetter__.
Object.prototype.defineProperty (/.defineProperties) are the methods which instill hope of IE compatibility, until you realize that older versions of IE only supported the mutations on DOM objects (attached to the actual DOM tree), hence the headaches. Hooray.

Norguard
  • 26,167
  • 5
  • 41
  • 49
  • I like the get/set nomenclature, but I always write my pseudoclasses in the format: function C() { this.Prop = null; }; C.prototype.constructor = C; var c = new C(); - which doesn't seem to be compatible. – CoryG Feb 18 '13 at 01:42
  • 1
    @CoryG I'm not sure where you're going with that, given that in terms of JS, you're sort of programming in a circle, there. Given: `function Foo () {} var f = new Foo();`, `f.constructor` is already `Foo`, which is what you're setting when you set `Foo.prototype.constructor`. Unless you're talking about subclassing. But if you are, then unless you're getting really crazy with your boilerplate for superclasses, all of your properties are public (or public-static) anyway, at which point, why does `get/set` magic matter? – Norguard Feb 18 '13 at 01:52
  • It just gets really messy putting everything into a function definition, for instance: function Datagrid(settings) { ... Control.prototype.Initialize.call(this); ... } Datagrid.prototype = new Control(); Datagrid.prototype.constructor = Datagrid; Datagrid.prototype.CheckAutoColumns = Datagrid_CheckAutoColumns; ... Datagrid.prototype.Scroll = Datagrid_Scroll; It's not _that_ bad to make massive classes with inheritance as long as you aren't trying to pack everything into a single function definition - and forget about polymorphism in such an event. – CoryG Feb 18 '13 at 01:56
  • 1
    Ahh. Yes. But if you're going to attempt to subclass, rather than embracing patterns which JS is more accepting of (components/mix-ins/etc), then you end up dealing with a lot of funny wiring like that, regardless of the methodology you use. – Norguard Feb 18 '13 at 01:59
  • The JS patterns don't work to represent things in the best manner in many cases - it is an amazing language when it comes to scripting, but I think we're going to see more large JS applications coming out (especially with the optimizations in JS engines and things like node.js with support for clustering on the back end, local async background workers, sockets, etc). In my particular case I'm working to move my company onto entirely open source platforms and decided on Ubuntu running a modified version of Chrome (to go past the 5MB local storage limit) and a node.js-based private cloud. – CoryG Feb 18 '13 at 02:02
  • 1
    That's sort of it. I might suggest looking at Dependency Injection, rather than subclassing -- and by DI/IOC, I really don't mean the super-huge factories which make everything... I just mean that you might consider building components/modules of reusable functionality and passing them into the constructors and/or registering them between construction and init. It's a bit of rethinking, and you might end up needing to get into message-passing/custom-events, rather than trying to recreate the abstracts/virtuals of other languages, but it might make your code more modular and less stringy. – Norguard Feb 18 '13 at 02:06
  • Also, I might suggest that if you're working in Node, if this code is 100% intended to live in Node-land, then the syntax should be quite valid, and you'd only need to wrack your brain on legacy stuff if you intend to port it directly to the client (rather than writing a separate client-side interface). – Norguard Feb 18 '13 at 02:10
  • My case is a cross between node and chrome, and not at all standard for a web environment (it's designed for intranet use on machines that will hit the data a lot) - everything is stored in postgresql, the client syncs to a local storage-based javascript-powered db via socket.io and node.js (node just handles security and syncronization between clients and the server copy, revisioning, etc). Most of the code for the apps is actually stored in the db and I'm trying to make it look as much like c# as possible (functions loaded dynamically on the client end, so there is a little room – CoryG Feb 18 '13 at 02:16
  • for string processing to format them into true js functions, apps can take a few seconds to load up because once they are loaded there is no transition from page to page, they are true applications running in a js-based managed code environment - word processing, spreadsheets, data-heavy components, the code editor, CAD, hardware control interfaces, h264 streams from dedicated nix servers, everything is in the browser - it's a private cloud system and this question relates to the managed code functionality - appending methods to pseudoclasses via prototype is a _lot_ easier than compiling them – CoryG Feb 18 '13 at 02:18
  • into the constructor definition, and to support polymorphism I really need the ability to modify them after the initial creation, at least past the core dataset - each table is an object, each row can be an object, methods can be declared per table, for all table rows, for specific rows, etc - essentially everything is a pseudoclass with a minimal definition and operating in a functional manner in spite of being a virtual sort of OO structure. – CoryG Feb 18 '13 at 02:22
  • 1
    @CoryG Regarding polymorphism, or duck-typing, how would a `.register_*()` method not apply. `instance.register_gun(pistol);` `instance.fire();` `instance.register_gun(rifle);` `instance.fire();` – Norguard Feb 18 '13 at 03:11
  • Don'y you lose the ability to use the instanceof operator with duck typing and need to create your own inheritance chain with the overhead that entails by not using the native operator when checking types? At the very least you would be looking at a check against a variable identifying the type (in my case probably a lookup against the table or row of origin) - something like shape instanceof circle vs shape instanceof ellipse wouldn't work, with true duck typing you would have to check any number of members and depending on how robust your set of objects are, still might not get it right. – CoryG Feb 18 '13 at 05:27
  • 1
    It's certainly a probability that you might run into those issues, even if you were 100% composition, I suppose, with objects being little more than bags of components with similarly-named interfaces (`CircleComp { draw(), set_radius(r) }`, `EllipseComp{ draw(), set_radius(r1, r2) }`), you can still run into times where you'd want to do type-checking. You could probably play around with the `.__proto__` properties, there. But the goal of the technique, I've always felt, was to conform to more-rigid interfaces, rather than more-rigid hierarchies, curtailing the need for so many checks. – Norguard Feb 18 '13 at 05:49
  • I do like the concept of duck typing and want to support it in addition to stricter types - one of the major failings of c# is the complexity of creating a duck typing system with it's strict interfaces - one thing I've been having an issue with you might have some insight into is http://stackoverflow.com/questions/14914698/how-to-set-a-dynamically-generated-pseudoclass-name-in-javascript-to-work-with-t – CoryG Feb 18 '13 at 06:07
1

I found the solution to this after coming across this link relating to getters and setters. Here is a generic method of applying properties to objects I put together as a result if anyone is interested in it:

Object.prototype.Property = function (name, fn) {
    if (fn.hasOwnProperty('get')) { this.__defineGetter__(name, fn.get); }
    else { this.__defineGetter__(name, function () { throw ('Cannot read property ' + name + '.'); }); }
    if (fn.hasOwnProperty('set')) { this.__defineSetter__(name, fn.set); }
    else { this.__defineSetter__(name, function () { throw ('Cannot write property ' + name + '.'); }); }
};

function C() {
    var _Field = 'test';
    this.Property('Field', {
        get: function () {
            return ('Field = ' + _Field);
        },
        set: function (value) {
            if (value != 'no') {
                _Field = value;
            }
        }
    });
};
C.prototype.constructor = C;

var c = new C();
document.write(c.Field);
document.write('<BR />');
c.Field = 'no';
document.write(c.Field);
document.write('<BR />');
c.Field = 'yes';
document.write(c.Field);
document.write('<BR />');

Edit: A JQuery-friendly Object.prototype.Property function like the above:

Object.defineProperty(Object.prototype, 'Property', {
    enumerable: false,
    value: function (name, fn) {
        if (fn.hasOwnProperty('get')) { this.__defineGetter__(name, fn.get); }
        else { this.__defineGetter__(name, function () { throw ('Cannot read property ' + name + '.'); }); }
        if (fn.hasOwnProperty('set')) { this.__defineSetter__(name, fn.set); }
        else { this.__defineSetter__(name, function () { throw ('Cannot write property ' + name + '.'); }); }
    }
});

And a working JSFiddle.

CoryG
  • 2,429
  • 3
  • 25
  • 60