reusable javascript objects, prototypes and scope

2019-03-08 22:10发布

问题:

MyGlobalObject;

function TheFunctionICanUseRightAwaySingleForAllInstansesAndWithoutInstanse() {
    function() {
        alert('NO CONSTRUCTOR WAS CALLED');
    }
};

The Long-named function must be callable from MyGlobalObject, which in turn must be available as a global (to window) variable in all times after script was loaded. It should support extensibility in accordance with latest standards.

I'm at architectural dilemma of how to built JS base for an application (almost 100% JS).

We need an object i.e. window.MyObject (like a module, like jQuery) so

It can be created with

VAR1

 var MyGlobalObjConstructor = function(){
     this.GlobalFunctionInObject = function(){
        alert('called with MyGlobalObj.GlobalFunctionInObject()');
        }        
};
window.MyGlobalObj = new MyGlobalObjConstructor();    

Is MyGlobalObj extensible? Can I create child objects, which will inherit current state of MyGlobalObj (extended functions/properties MyGlobalObj.NewFunc e.g.)? What is the main difference between using prototype (VAR3)?

By GlobaldFunction I mean single instance for all initialized/instantiated (possibly instantializable) instances..

Or with

VAR2

var MyGlobalObj = {
    GlobalFunctionInObject: function...
    GlobalFunctionInObject2: function...
};
MyGlobalObj.GlobalFunctionInObject();
// here I lose all hierarchy elements, no prototype, 
// can I use GlobalFunctionInObject2 in GlobalFunctionInObject?

Or with

VAR3

var MyGlobalConstuctor = function(){} // already 'well-formed' object
MyGlobalConstuctor.prototype.GlobalFunctionInObject = function...
};
var MyGlobalObj = new MyGlobalConstuctor();

// so I'm sceptical to NEW, because I have ALREADY wrote my functions 
// which I expect to be in memory, single instance of each of them, 
// so creating MyObject2,3,4 with NEW MyGC() makes no sense to me.
// DO I REALLY HAVE TO USE "MyGlobalConstuctor.prototype." FOR EACH FUNCTION?!!!!

What's the difference defining MyGlobalObj as a function and as an object (result of func or VAR2)?

OR VAR4?

I see in Chrome Debugger both prototype and __proto__ special fields. I've read that that's OK, but why are they not saved in a single prototype?

So, what is the correct/optimal way to implement window.MyObject, so one could MyObject.MyFunction(); What are the differences (pro/contra) of variants 1 2 and 3?

回答1:

Variation 1 - Mixin

function SomeType() {
    var priv = "I'm private";
    this.publ = "I'm public";
    this.action = function() {
        return priv + this.publ;
    };
}

var obj = new SomeType();

With this method you are creating a new object every time you call new SomeType(), creating all its methods and adding all this method to the new object. Every time you create an object.

Pros

  • It looks like classical inheritance so it's easy to understand to Java-C#-C++-etc people.
  • It can have private variables per instance since you have one function closure per each object you create
  • It allows multiple inheritance, also known as Twitter-mixins or functional mixins
  • obj instanceof SomeType will return true

Cons

  • It consumes more memory as more objects you create because with each object you are creating a new closure and creating each of it's methods again.
  • Private properties are private, not protected, subtypes can't access them
  • No easy way to know if a object has some Type as superclass.

Inheritance

function SubType() {
    SomeType.call(this);
    this.newMethod = function() {
        // can't access priv
        return this.publ;
    };
}

var child = new SubType();

child instanceof SomeType will return false there is no other way to know if child has SomeType methods than look if it has them one by one.

Variation 2 - Object literal with prototyping

var obj = {
    publ: "I'm public",
    _convention: "I'm public too, but please don't touch me!",
    someMethod: function() {
        return this.publ + this._convention;
    }
};

In this case you are creating a single object. If you are going to need only one instance of this type it can be the best solution.

Pros

  • It's quick and easy to understand.
  • Performant

Cons

  • No privacy, every property is public.

Inheritance

You can inherit a object prototyping it.

var child = Object.create(obj);
child.otherMethod = function() {
    return this._convention + this.publ;
};

If you are on a old browser you will need to garantee Object.create works:

if (!Object.create) {
    Object.create = function(obj) {
        function tmp() { }
        tmp.prototype = obj;
        return new tmp;
    };
}

To know if a object is a prototype of another you can use

obj.isPrototypeOf(child); // true

Variation 3 - Constructor pattern

UPDATE: This is the pattern ES6 classes are sugar syntax of. If you use ES6 classes you are following this pattern under the hood.

class SomeType {
    constructor() {
        // REALLY important to declare every non-function property here
        this.publ = "I'm public";
        this._convention = "I'm public too, but please don't touch me!";
    }
    someMethod() {
        return this.publ + this._convention;
    }
}

class SubType extends SomeType {
    constructor() {
        super(/* parent constructor parameters here */);
        this.otherValue = 'Hi';
    }
    otherMethod() {
        return this._convention + this.publ + this.otherValue;
    }
}

function SomeType() {
    // REALLY important to declare every non-function property here
    this.publ = "I'm public";
    this._convention = "I'm public too, but please don't touch me!";
}

SomeType.prototype.someMethod = function() {
    return this.publ + this._convention;
};

var obj = new SomeType();

You can re-assign the prototype insteadd of adding each method if you are not inheriting and remember to re-assign the constructor property:

SomeType.prototype = {
    constructor: SomeType,
    someMethod = function() {
        return this.publ + this._convention;
    }
};

Or use _.extend or $.extend if you have underscore or jquery in your page

_.extend(SomeType.prototype, {
    someMethod = function() {
        return this.publ + this._convention;
    }
};

The new keyword under the hood simply does this:

function doNew(Constructor) {
    var instance = Object.create(Constructor.prototype);
    instance.constructor();
    return instance;
}

var obj = doNew(SomeType);

What you have is a function than has no methods; it just has a prototype property with a list of functions, the new operator means to create a new object and use this function's prototype (Object.create) and constructor property as initializer.

Pros

  • Performant
  • Prototype chain will allow you to know if a object inherits from some type

Cons

  • Two-step inheritance

Inheritance

function SubType() {
    // Step 1, exactly as Variation 1
    // This inherits the non-function properties
    SomeType.call(this);
    this.otherValue = 'Hi';
}

// Step 2, this inherits the methods
SubType.prototype = Object.create(SomeType.prototype);
SubType.prototype.otherMethod = function() {
    return this._convention + this.publ + this.otherValue;
};

var child = new SubType();

You may think it looks like a super-set of Variation 2... and you'll be right. It's like variation 2 but with a initializer function (the constructor);

child instanceof SubType and child instanceof SomeType will return both true

Curiosity: Under the hood instanceof operator does is

function isInstanceOf(obj, Type) {
    return Type.prototype.isPrototypeOf(obj);
}

Variation 4 - Overwrite __proto__

When you do Object.create(obj) under the hood it does

function fakeCreate(obj) {
    var child = {};
    child.__proto__ = obj;
    return child;
}

var child = fakeCreate(obj);

The __proto__ property modifies directly the object's hidden [Prototype] property. As this can break JavaScript behaviour, it's not standard. And the standard way is preferred (Object.create).

Pros

  • Quick and performant

Cons

  • Non-standard
  • Dangerous; you can't have a hashmap since the __proto__ key can change the object's prototype

Inheritance

var child = { __proto__: obj };
obj.isPrototypeOf(child); // true

Comment questions

1. var1: what happens in SomeType.call(this)? Is 'call' special function?

Oh, yes, functions are objects so they have methods, I will mention three: .call(), .apply() and .bind()

When you use .call() on a function, you can pass one extra argument, the context, the value of this inside the function, for example:

var obj = {
    test: function(arg1, arg2) {
        console.log(this);
        console.log(arg1);
        console.log(arg2);
    }
};

// These two ways to invoke the function are equivalent

obj.test('hi', 'lol');

// If we call fn('hi', 'lol') it will receive "window" as "this" so we have to use call.
var fn = obj.test;
fn.call(obj, 'hi', 'lol');

So when we do SomeType.call(this) we are passing the object this to function SomeCall, as you remember this function will add methods to object this.

2. var3: With your "REALLY define properties" do you mean if I use them in functions? Is it a convention? Because getting this.newProperty without it being defined at the same level with other member functions is not a problem.

I mean any property your object will have that is not a function must be defined on the constructor, not on the prototype, otherwise you will face one of the more confusing JS problems. You can see it here, but it's outside of the focus of this question.

3. Var3: what happens if I don't re-assign constructor?

Actually you might not see the difference and this is what makes it a dangerous bug. Every function's prototype object has a constructor property so you can access the constructor from an instance.

function A() { }

// When you create a function automatically, JS does this:
// A.prototype = { constructor: A };

A.prototype.someMethod = function() {
    console.log(this.constructor === A); // true
    this.constructor.staticMethod();
    return new this.constructor();  
};

A.staticMethod = function() { };

It's not a best practice because not everybody knows about it, but sometimes it helps. But if you reassign the prototype...

A.prototype = {
    someMethod = function() {
        console.log(this.constructor === A); // false
        console.log(this.constructor === Object); // true
        this.constructor.staticMethod();
        return new this.constructor();  
    }
};

A.prototype is a new object, a instance of Object than prototypes Object.prototype and Object.prototype.constructor is Object. Confusing, right? :P

So if you overwrite the prototype and don't reset the "constructor" property, it will refer to Object instead of A, and if you try to use the "constructor" property to access some static method you may get crazy.



回答2:

I usually settle with returning an object with functions as properties:

var newCat = function (name) {
return {name: name, purr: function () {alert(name + ' purrs')}};
};

var myCat = newCat('Felix');
myCat.name; // 'Felix'
myCat.purr(); // alert fires

You can have inheritance by calling the newCat function and extend the object you get:

var newLion = function (name) {
    var lion = newCat(name);
    lion.roar = function () {
        alert(name + ' roar loudly');
    }
    return lion;
}

If you want a global cats object:

var cats = (function () {

var newCat = function (name) {
    return {
        name: name,
        purr: function () {
            alert(name + ' is purring')
        }
    };
};

return {
    newCat: newCat
};
}());

Now you can call:

var mySecondCat = cats.newCat('Alice');