Adding custom properties to a function

2019-01-05 09:36发布

Searching for appropriate answer proved difficult because of the existence of many other problems related to my keywords, so I'll ask this here.

As we know, functions in javascript are objects and they have their own properties and methods (more properly, function istances, inherited from Function.prototype).

I was considering adding custom properties for one function (method), let's skip the "why?" part and go straight to the code:

var something = {
    myMethod: function () {
        if (something.myMethod.someProperty === undefined) {
            something.myMethod.someProperty = "test";
        }
        console.log(something.myMethod);
    }
}

When inspected with Firebug's DOM explorer, the property is defined as expected. However, as I don't consider myself a javascript expert, I have the following questions:

  1. Can this method be considered "proper" and standards compliant? It works in Firefox but there are many things working as expected in web browsers and aren't by any means standards.
  2. Is this kind of altering objects by adding new properties to them a good practice?

9条回答
神经病院院长
2楼-- · 2019-01-05 10:15

I agree that this is a difficult question that could have multiple answers, so I prefer to make an different example:

Let's suppose to have an JavaScript Array, populated by a generator:

var arr = [...new Array(10).keys()];

that is

[0, 1, 2, 3, 4, 5, 6, 7, 8, 9]

Now we want to map this to a new array - same length, applying some function, so we could use the native map function property:

arr = arr.map((value,index) => ++value)

We have just done a value=value+1 and return, so now the array will look like

[1, 2, 3, 4, 5, 6, 7, 8, 9, 10]

Ok, now supposed to have a JavaScript Object like

var obj=new Object()

that was defined like the previous array (for some crazy reason):

arr.forEach((value,index) => obj[value]=value)

i.e.

{0: 0, 1: 1, 2: 2, 3: 3, 4: 4, 5: 5, 6: 6, 7: 7, 8: 8, 9: 9}

At this point we cannot apply the same map method since it's not defined for an Object so we have to define it as a new prototype of an Object:

Object.defineProperty(Object.prototype, 'mapObject', {
      value: function(f, ctx) {
          ctx = ctx || this;
          var self = this, result = {};
          Object.keys(self).forEach(function(k) {
              result[k] = f.call(ctx, self[k], k, self);
          });
          return result;
      }
    });

At this point we could do as for the array before:

obj=obj.mapObject((value,key) => ++value )

so that we have:

{0: 1, 1: 2, 2: 3, 3: 4, 4: 5, 5: 6, 6: 7, 7: 8, 8: 9, 9: 10}

You can see that we have updated the values only:

[...Object.keys(obj)]
["0", "1", "2", "3", "4", "5", "6", "7", "8", "9"]

and we can turn back then into the output array:

[...Object.keys(obj).map(k=>obj[k])]
[1, 2, 3, 4, 5, 6, 7, 8, 9, 10]

Here it is at work:

// Array.map
var arr = [...new Array(10).keys()];
console.log("array", arr)
arr = arr.map((value, index) => ++value)
console.log("mapped array", arr)
// new property
Object.defineProperty(Object.prototype, 'mapObject', {
  value: function(f, ctx) {
    ctx = ctx || this;
    var self = this,
      result = {};
    Object.keys(self).forEach(function(k) {
      result[k] = f.call(ctx, self[k], k, self);
    });
    return result;
  }
});

// Object.mapObject
var obj = new Object()
arr = [...new Array(10).keys()];
arr.forEach((value, index) => obj[value] = value)
console.log("object", obj)
obj = obj.mapObject((value, key) => ++value)
console.log("mapped object", obj)
console.log("object keys", [...Object.keys(obj)])
console.log("object values", [...Object.keys(obj).map(k => obj[k])])

查看更多
做个烂人
3楼-- · 2019-01-05 10:19

If you just want to add custom properties to a function then you only need to add those properties to Function.prototype. For example:

Function.prototype.SomeNewProperty = function () {//Do something awesome here...}
查看更多
Anthone
4楼-- · 2019-01-05 10:19

It's perfectly acceptable to add properties or methods to a function object. It's done quite often. The jQuery/$ object is an example of this. It's a function with quite a few methods attached.

When properties are added to a constructor they are called 'static' properties and can be invoked without an an instance of the class. e.g. Object.create.

I don't have enough rep to write a comment so I will say here: It generally considered bad practice to extend the prototypes of built in objects, especially if your code has to play with other people's code. It can have unpredictable consequences that are hard to to track.

查看更多
混吃等死
5楼-- · 2019-01-05 10:23

Possible addition to John Slegers great answer

Isnt it possible that after John Slegers:

Way 2 : adding properties after defining the function

Adding a Way 2.5

function doSomething() {
    doSomething.prop = "Bundy";
    doSomething.doSomethingElse = function() {
        alert("Why Hello There! ;)");

    };

    let num = 3;
    while(num > 0) {
        alert(num);
        num--;  
    }
}

sayHi();
sayHi.doSomethingElse();
alert(doSomething.prop);

var ref = doSomething;

ref();
ref.doSomethingElse();
alert(ref.prop);

Putting in both a "variable" property and a function property for completeness sake, straight in the function declaration. Thus avoiding it to be "disconnected". Left the inner default workings of the function (a simple loop) to show that it still works. No?

查看更多
爷、活的狠高调
6楼-- · 2019-01-05 10:27

It's a little bit difficult to give a very meaningful answer to your question, because you've sort of said "Here is my solution, is it OK?" without explaining what problem you are trying to solve (you even said explicitly that you are not going to explain the "why"). Your code looks to be valid JavaScript that will run, but it also looks like a less than optimal way of doing things.

If you explain what you actually want to achieve you may get some good suggestions on better ways to structure your code. Still, I'll give you some kind of answer:

Can this method be considered "proper" and standards compliant? It works in Firefox but there are many things working as expected in web browsers and aren't by any means standards.

Functions are objects (as you've said), and thus it is possible to add properties to them. This isn't really a standards issue as such in that it is a core part of JavaScript that all browsers support.

Is this kind of altering objects by adding new properties to them a good practice?

It's your object, you can add whatever properties you like. The whole point of objects is that they have properties that you can manipulate. I can't really envisage a way of using objects that doesn't involve altering them, including adding, deleting and updating properties and methods.

Having said that, to me it doesn't really make sense to add properties to the myMethod function, it would be more usual to add other properties to your something object (your myMethod function would, if called correctly, have access to the other properties of something via the this keyword).

If you are using a function as a constructor it typically makes sense to add methods to the associated prototype and add (non-method) properties to each instance, but you can do either or both the other way when appropriate. (Noting that a "method" is essentially just a property that happens to reference a function.)

The specific code you have shown doesn't add properties, it tests whether the someProperty property already exists and if so assigns it a new value.

You might benefit from reading some articles such as these at MDN:

查看更多
兄弟一词,经得起流年.
7楼-- · 2019-01-05 10:28

First of all, it's important to realise that standard function properties (arguments, name, caller & length) cannot be overwritten. So, forget about adding a property with that name.

Adding your own custom properties to a function can be done in different ways that should work in every browser.


Adding your own custom properties to a function

Way 1 : adding properties while running the function :

var doSomething = function() {
    doSomething.name = 'Tom';
    doSomething.name2 = 'John';
    return 'Beep';
};

console.log('doSomething.name : ' + doSomething.name);
console.log('doSomething.name2 : ' + doSomething.name2);
console.log('doSomething() : ' + doSomething());
console.log('doSomething.name : ' + doSomething.name);
console.log('doSomething.name2 : ' + doSomething.name2);

Output :

doSomething.name : 
doSomething.name2 : undefined
doSomething() : Beep
doSomething.name : 
doSomething.name2 : John 

Way 1 (alternate syntax) :

function doSomething() {
    doSomething.name = 'Tom';
    doSomething.name2 = 'John';
    return 'Beep';
};

console.log('doSomething.name : ' + doSomething.name);
console.log('doSomething.name2 : ' + doSomething.name2);
console.log('doSomething() : ' + doSomething());
console.log('doSomething.name : ' + doSomething.name);
console.log('doSomething.name2 : ' + doSomething.name2);

Output :

doSomething.name : doSomething
doSomething.name2 : undefined
doSomething() : Beep
doSomething.name : doSomething
doSomething.name2 : John 

Way 1 (second alternate syntax) :

var doSomething = function f() {
    f.name = 'Tom';
    f.name2 = 'John';
    return 'Beep';
};

console.log('doSomething.name : ' + doSomething.name);
console.log('doSomething.name2 : ' + doSomething.name2);
console.log('doSomething() : ' + doSomething());
console.log('doSomething.name : ' + doSomething.name);
console.log('doSomething.name2 : ' + doSomething.name2);

Output :

doSomething.name : f
doSomething.name2 : undefined
doSomething() : Beep
doSomething.name : f
doSomething.name2 : John 

A problem with this strategy is that you need to run your function at least once to assign the properties. For many functions, that's obviously not what you want. So let's consider the other options.


Way 2 : adding properties after defining the function :

function doSomething() {
    return 'Beep';
};

doSomething.name = 'Tom';
doSomething.name2 = 'John';

console.log('doSomething.name : ' + doSomething.name);
console.log('doSomething.name2 : ' + doSomething.name2);
console.log('doSomething() : ' + doSomething());
console.log('doSomething.name : ' + doSomething.name);
console.log('doSomething.name2 : ' + doSomething.name2);

Output :

doSomething.name : doSomething
doSomething.name2 : John
doSomething() : Beep
doSomething.name : doSomething
doSomething.name2 : John 

Now, you don't need to run your function first before you're able to access your properties. However, a disadvantage is that your properties feel disconnected from your function.


Way 3 : wrap your function in anonymous function :

var doSomething = (function(args) {
    var f = function() {
        return 'Beep';
    };
    for (i in args) {
        f[i] = args[i];
    }
    return f;
}({
    'name': 'Tom',
    'name2': 'John'
}));

console.log('doSomething.name : ' + doSomething.name);
console.log('doSomething.name2 : ' + doSomething.name2);
console.log('doSomething() : ' + doSomething());
console.log('doSomething.name : ' + doSomething.name);
console.log('doSomething.name2 : ' + doSomething.name2);

Output :

doSomething.name : 
doSomething.name2 : John
doSomething() : Beep
doSomething.name : 
doSomething.name2 : John 

Wrapping your function in an anonymous function, you can collect your attributes into an object and use a loop to add those attributes one-by-one within the anonymous function. That way, your attributes feel more connected to your function. This technique is also very useful for when your attributes need to be copied from an existing object. A disadvantage, however, is that you can only add multiple attributes at the same time when you define your function. Also, it doesn't exactly result in DRY code if adding properties to a function is something you want to do often.


Way 4 : add an 'extend' function to your function, that adds the properties of an object to itself one by one :

var doSomething = function() {
    return 'Beep';
};

doSomething.extend = function(args) {
    for (i in args) {
        this[i] = args[i];
    }
    return this;
}

doSomething.extend({
    'name': 'Tom',
    'name2': 'John'
});

console.log('doSomething.name : ' + doSomething.name);
console.log('doSomething.name2 : ' + doSomething.name2);
console.log('doSomething() : ' + doSomething());
console.log('doSomething.name : ' + doSomething.name);
console.log('doSomething.name2 : ' + doSomething.name2);

Output :

doSomething.name : 
doSomething.name2 : John
doSomething() : Beep
doSomething.name : 
doSomething.name2 : John 

This way, you can extend multiple properties and/or copy properties from another project at any time. Again, however, your code isn't DRY if this is something you do more often.


Way 5 : Make a generic 'extend' function :

var extend = function(obj, args) {
    if (isArray(args) || (args !== null && typeof args === 'object')) {
        for (i in args) {
            this[i] = args[i];
        }
    }
    return obj;
}

var Job = extend(
    function() {
        return 'Beep';
    }, {
        'name': 'Tom',
        'name2': 'John'
    }
);

console.log('doSomething.name : ' + doSomething.name);
console.log('doSomething.name2 : ' + doSomething.name2);
console.log('doSomething() : ' + doSomething());
console.log('doSomething.name : ' + doSomething.name);
console.log('doSomething.name2 : ' + doSomething.name2);

Output :

doSomething.name : 
doSomething.name2 : John
doSomething() : Beep
doSomething.name : 
doSomething.name2 : John 

A genetic extend function allows for a more DRY approach, allowing you to add the object or any project to any other object.


Way 6 : Create an extendableFunction object and use it to attach an extend function to a function :

var extendableFunction = (function() {
    var extend = function(args) {
        if (isArray(args) || (args !== null && typeof args === 'object')) {
            for (i in args) {
                this[i] = args[i];
            }
        }
        return this;
    };
    var ef = function(v, obj) {
        v.extend = extend;
        return v.extend(obj);
    };

    ef.create = function(v, args) {
        return new this(v, args);
    };
    return ef;
})();

var doSomething = extendableFunction.create(
    function() {
        return 'Beep';
    }, {
        'name': 'Tom',
        'name2': 'John'
    }
);

console.log('doSomething.name : ' + doSomething.name);
console.log('doSomething.name2 : ' + doSomething.name2);
console.log('doSomething() : ' + doSomething());
console.log('doSomething.name : ' + doSomething.name);
console.log('doSomething.name2 : ' + doSomething.name2);

Output :

doSomething.name : 
doSomething.name2 : John
doSomething() : Beep
doSomething.name : 
doSomething.name2 : John 

Rather than using a generic 'extend' function, this technique allows you to generate functions that have an 'extend' method attached to it.


Way 7 : Add an 'extend' function to the Function prototype :

Function.prototype.extend = function(args) {
    if (isArray(args) || (args !== null && typeof args === 'object')) {
        for (i in args) {
            this[i] = args[i];
        }
    }
    return this;
};

var doSomething = function() {
    return 'Beep';
}.extend({
    name : 'Tom',
    name2 : 'John'
});

console.log('doSomething.name : ' + doSomething.name);
console.log('doSomething.name2 : ' + doSomething.name2);
console.log('doSomething() : ' + doSomething());
console.log('doSomething.name : ' + doSomething.name);
console.log('doSomething.name2 : ' + doSomething.name2);

Output :

doSomething.name : 
doSomething.name2 : John
doSomething() : Beep
doSomething.name : 
doSomething.name2 : John 

A great advantage to this technique is that it makes adding new properties to a function very easy and DRY as well as completely OO. Also, it's pretty memory friendly. A downside, however, is that it's not very future proof. In case future browsers ever add a native 'extend' function to the Function prototype, this that could break your code.


Way 8 : Run a function recursively once and then return it :

var doSomething = (function f(arg1) {
    if(f.name2 === undefined) {
        f.name = 'Tom';
        f.name2 = 'John';
        f.extend = function(obj, args) {
            if (isArray(args) || (args !== null && typeof args === 'object')) {
                for (i in args) {
                    this[i] = args[i];
                }
            }
            return obj;
        };
        return f;
    } else {
        return 'Beep';
    }
})();

console.log('doSomething.name : ' + doSomething.name);
console.log('doSomething.name2 : ' + doSomething.name2);
console.log('doSomething() : ' + doSomething());
console.log('doSomething.name : ' + doSomething.name);
console.log('doSomething.name2 : ' + doSomething.name2);

Output :

doSomething.name : f
doSomething.name2 : John
doSomething() : Beep
doSomething.name : f
doSomething.name2 : John 

Run a function once and have it test whether one of its properties is set. If not set, set the properties and return itself. If set, execute the function. If you include an 'extend' function as one of the properties, you can later execute that to add new properties.


Adding your own custom properties to an object

In spite of all these options, I would nevertheless recommend against adding properties to a function. It's much better to add properties to objects!

Personally, I prefer the singleton classes with the following syntax.

var keyValueStore = (function() {
    return {
        'data' : {},
        'get' : function(key) { return keyValueStore.data[key]; },
        'set' : function(key, value) { keyValueStore.data[key] = value; },
        'delete' : function(key) { delete keyValueStore.data[key]; },
        'getLength' : function() {
            var l = 0;
            for (p in keyValueStore.data) l++;
            return l;
        }
    }
})();

An advantage to this syntax is that it allows for both public and private variables. For example, this is how you make the 'data' variable private :

var keyValueStore = (function() {
    var data = {};

    return {
        'get' : function(key) { return data[key]; },
        'set' : function(key, value) { data[key] = value; },
        'delete' : function(key) { delete data[key]; },
        'getLength' : function() {
            var l = 0;
            for (p in data) l++;
            return l;
        }
    }
})();

But you want multiple datastore instances, you say? No problem!

var keyValueStore = (function() {
    var count = -1;

    return (function kvs() {
        count++; 
        return {
            'data' : {},
            'create' : function() { return new kvs(); },
            'count' : function() { return count; },
            'get' : function(key) { return this.data[key]; },
            'set' : function(key, value) { this.data[key] = value; },
            'delete' : function(key) { delete this.data[key]; },
            'getLength' : function() {
                var l = 0;
                for (p in this.data) l++;
                return l;
            }
        }
    })();
})();

Finally, you can seperate the instance and singleton properties and use a prototype for the instance's public methods. That results in the following syntax :

var keyValueStore = (function() {
    var count = 0; // Singleton private properties

    var kvs = function() {
        count++; // Instance private properties
        this.data = {};  // Instance public properties
    };

    kvs.prototype = { // Instance public properties
        'get' : function(key) { return this.data[key]; },
        'set' : function(key, value) { this.data[key] = value; },
        'delete' : function(key) { delete this.data[key]; },
        'getLength' : function() {
            var l = 0;
            for (p in this.data) l++;
            return l;
        }
    };

    return  { // Singleton public properties
        'create' : function() { return new kvs(); },
        'count' : function() { return count; }
    };
})();

With this syntax, you can have :

  • multiple instances of an object
  • private variables
  • class variables

You use it like this :

kvs = keyValueStore.create();
kvs.set('Tom', "Baker");
kvs.set('Daisy', "Hostess");
var profession_of_daisy = kvs.get('Daisy');
kvs.delete('Daisy');
console.log(keyValueStore.count());
查看更多
登录 后发表回答