1

I'm asking this because I think I found a solution to a JavaScript problem, and I'd like to know if I'm way off base. I hope that asking this here is appropriate.

As I'm sure you know, JavaScript does not have private properties. This issue is usually solved using one of two patterns.

The first is to create privileged methods inside the constructor, using closures, and bind them to the object using the this keyword. This is the technique used by Douglas Crockford. Here's an example:

function Person(name) {
    function getName() {
        return name;
    }
    this.who = getName;
}

The problem with this pattern is that there's a big danger of polluting the global namespace:

var me = Person("Karl"); // Oops! no "new"
me.who(); // TypeError: Cannot read property 'who' of undefined
who(); // "Karl" - "who" is in global namespace!

The second solution is to use the module pattern. Instead of binding the privileged method to this, you bind it to a new object:

function Person(name) {
    function getName() {
        return name;
    }
    return {
        who: getName
    }
}
var me = Person("Karl"); // No "new," but that's OK
me.who(); // "Karl"
who(); // TypeError: undefined is not a function

The problem with that pattern is that the object does not have Person's prototype chain. So, you can't properly check its type, and you can't extend the object using the constructor's prototype:

console.log(me instanceof Person); // "false"
Person.prototype.what = function() {
    return this.constructor.name + ": " + this.who();
}
me.what(); // TypeError: undefined is not a function

I've found a solution to this. Use a temporary constructor that has Person's prototype chain, and return an object constructed with the temporary constructor:

function Person(name) {
    function getName() {
        return name;
    }
    // Temporary constructor
    function F() {
        this.who = getName;
    }
    F.prototype = Person.prototype;
    return new F();    
}
// Can't pollute the global namespace
var me = Person("Karl");
me.who(); // "Karl"
who(); // TypeError: undefined is not a function

// Proper prototype chain: correct type checking, extensible
console.log(me instanceof Person); // "true"
Person.prototype.what = function() {
    return this.constructor.name + ": " + this.who();
}
me.what(); // "Person: Karl"

I have a couple of related questions about this solution:

  1. Are there any drawbacks to this technique?
  2. Though the module pattern and the temporary constructor pattern have been around for a while now, I've never seen anyone put them together. Is this a pattern that I just didn't know about before?
  3. If the answer to number 2 is "no," can I get mad props for this? :)
Karl Giesing
  • 1,654
  • 3
  • 16
  • 24
  • 4
    *"The problem with this pattern is that there's a big danger of polluting the global namespace:"* That's not a problem with that pattern, it's a "problem" with all "normal" constructor functions. If that's what you are worried about, then a simple solution is to make an `instanceof` check: `if (!(this instanceof Person)) { return new Person(name); }`. Personally I think that trying to implement "private properties" is not worth the effort and drawbacks. We'll get `Symbol`s soon with which this should be easier. – Felix Kling Aug 18 '14 at 22:49
  • "`this.who = getname`" would add `who() to the global namespace? Really? – GolezTrol Aug 18 '14 at 22:52
  • 1
    @GolezTrol: Yes, indeed it would. If the function is called without the "new" keyword, it's not a constructor, and `this` binds to the global object. – Karl Giesing Aug 18 '14 at 22:55
  • @FelixKling: Your solution seems very simple, which makes me question why I haven't run across it in the JavaScript books I've read. Are you sure the `instanceof` operator works in the constructor (i.e. that the prototype chain has already been bound)? What about if your function is called as a "super constructor?" Any edge cases? I'll readily admit my ignorance here. – Karl Giesing Aug 18 '14 at 23:08
  • 1
    Yes `instanceof` works inside constructors. When you call a function with `new`, basically the following happens: `var newObj = Object.create(Constr.prototype); Constr.apply(newObj, arguments); return newObj;`. Super calls should be no problem since you should make this test inside the child constructor, not in the parent. It doesn't work properly if this test is only made inside parents. The other disadvantage is if you have a variadic constructor. You can't use `.apply` together with the `new` operator (but see http://stackoverflow.com/q/1606797/218196). – Felix Kling Aug 18 '14 at 23:13
  • @FelixKling: Using the `instanceof` operator is the way to go, since it's available in all browsers. Also, I just tested it, and it DOES work inside "parent constructors" - at least if the prototype chain is actually set up properly. Using `new` with `apply()` is such an edge case that I'm not even worrying about it. If you make it an answer, I'll accept it. – Karl Giesing Aug 19 '14 at 00:41
  • you can make private properties using Object.defineProperty() – dandavis Aug 19 '14 at 02:37
  • @dandavis: How would you do that? – Felix Kling Aug 19 '14 at 02:47
  • The problem with forgetting var is that you can pollute the global namespace. See how silly that statement is? Re setting prototype on every instance creation shows a clear misunderstanding of prototype and its role http://stackoverflow.com/questions/16063394/prototypical-inheritance-writing-up/16063711#16063711 – HMR Aug 19 '14 at 03:16
  • @FelixKling: well "private properties" make no sense in js because if class methods can read it, anyone can add a new method and read the "private" variable. it only makes sense as limiting access to in-constructor methods, using closure to local variables, as is common. There are many ways to validate who is asking for a value using a getter via Object.defineProperty, and arguments.callee. i think we can imagine a local WeakMap providing another validation method. i don't think those "look what i can do" anti-patterns are prudent... – dandavis Aug 19 '14 at 03:35
  • @HMR: I have no idea what your point is. The "forgetting var" part is not silly at all to anyone who comes to JavaScript from any other OOP language, where it would result in a compile-time error. Also, I'm fairly certain I understand the role of prototypes: to define shared members (like methods - what would be called "global properties" or "virtual methods" in other languages), to define inheritance, and to make instances extendable. My solution may not be the best, but it allows all of those things, so what's the problem? – Karl Giesing Aug 20 '14 at 03:06
  • If you don't see why it makes no sense to re set the prototype on every instance creation then you may not understand that prototype defines shared members (usually behavior) that doesn't change after defining it. You set it every time you create an instance well after defining it. Although it's possible it makes no sense. Privileged functions can be defined as instance specific and access private variables. – HMR Aug 20 '14 at 03:57
  • The problem with forgetting new is not in the new (or var) but in the programmer not understanding the language. This can be solved by learning the language (its been around for many years now so ample of opportunity there). You're trying to shift the problem with the language and want JavaScript to be java so it fits what you know. Maybe typescript or dart is a good alternative for you as these do address some of the shortcomings of using JavaScript in big projects like type checking and better tooling (ide and debuggers) – HMR Aug 20 '14 at 04:03
  • @HMR - about setting the prototype on every instance creation: JavaScript already does this. What do you think the hidden `__proto__` property is? I'm just making it explicit, because it's necessary if you return a "revealing object." About the programmer not understanding the language by leaving off `new`: Even seasoned programmers are not immune to typos. Also, there are plenty of functions that are both constructors and non-constructors - look at the wrapper functions. You're being the bad doctor in that old joke: "Doc, it hurts when I move my arm." "Well, don't move your arm." – Karl Giesing Aug 20 '14 at 16:27
  • Try and assign the prototype to 3 different objects on each creation and see each instance having a different prototype. You're de referencing the prototype of all pref instances and re referencing prototype for current instance on every instance creation. I would think it may be better to define your privileged methods as this.privileged=... Missing new and var are things that come with the language that can be solved by understanding the language as well as better tooling (ide) – HMR Aug 20 '14 at 16:48
  • @HMR: "Try and assign the prototype to 3 different objects on each creation and see each instance having a different prototype." - Nope, they all have the same prototype: `Person.prototype`. Just tested it. It seems like you're criticizing the "temporary constructor" pattern - which has been around for many years, and does pretty much what `Object.create` does. So I still don't see your point. – Karl Giesing Aug 20 '14 at 17:33
  • [facepalm] Think I misunderstood the question, you're setting the prototype within the constructor so people are allowed to use the constructor function without the the `new` keyword. It's not so Person.prototype methods can access the private member called `name`. Changing the prototype chain of existing instances has a negative effect on performance but I can't be sure if the JS engine sees assigning it with the same value every time as a change so it could be a valid solution for "forgetting new" – HMR Aug 21 '14 at 03:03
  • @HMR: It was actually the answer to a common criticism about the revealing module pattern: that it isn't extensible at all (so you can't, for example, supply patches for your library's module). The revealing module pattern is itself a solution to the problem of poisoning the global namespace. (That's not the ONLY problem it solves, of course.) – Karl Giesing Aug 23 '14 at 03:12

1 Answers1

1

I don't have an answer at your questions, but a suggestion that should solve all your problems in a simpler way:

function Person(name) {
    "use strict";
    if (!this) return new Person(name);
    function getName() {
        return name;
    }
    this.who = getName;
}
Person.prototype.what = function() {
    return this.constructor.name + ": " + this.who();
}

"use strict"; compatibility

MDN documentation about function context


Alternative that works in all browsers

function Person(name) {
    if (!(this instanceof Person)) return new Person(name);
    function getName() {
        return name;
    }
    this.who = getName;
}
Person.prototype.what = function() {
    return this.constructor.name + ": " + this.who();
}
Volune
  • 4,324
  • 22
  • 23
  • That would be awesome, but "use strict" isn't supported in older browsers. @FelixKling suggested using `instanceof` inside the constructor (instead of your `if (!this)` test) to achieve the same result. – Karl Giesing Aug 19 '14 at 00:26
  • if (this.Array===Array) return new Person(name); works in old browsers – dandavis Aug 19 '14 at 02:35
  • 1
    @dandavis: that will work in old browsers, but not in strict mode (where `this` would be `undefined`). Bad idea if you're mixing strict and non-strict code, or if you want your code to be forward-compatible to the time when all JavaScript engines use strict mode. Edit: Also bad if your object is purposefully designed to extend the global object. – Karl Giesing Aug 20 '14 at 03:26
  • @KarlGiesing: i don't push strict, but point made. make that: if (!this || this.Math) return new Person(name); to work in 100% of cases. unless you defined "Math" on the prototype, in which case use another quack. – dandavis Aug 20 '14 at 03:33
  • @dandavis: That takes care of strict mode, but not the edge case where your object is designed to extend the global object. In that case, `this.Array === Array` will always return true, and you'll end up in an infinite loop. But that is an edge case, and if you're knowledgeable enough to do it, you probably are knowledgeable enough to know why that check is bad. – Karl Giesing Aug 20 '14 at 03:38