Skip to content
Advertisement

function.name returns empty string if the function is added to an object by property

Consider this code example:

let test = {
    test: function(){}
  }

test.print = function(){}

console.log(test.print.name) // Outputs ""
console.log(test.test.name) // Outputs "test"
console.log(test) // Outputs { test: function(){}, print: function(){} }

Why is there a difference between the name properties of these two functions?

Advertisement

Answer

The technical explanation: NamedEvaluation

The specification tells you:

({ test: function(){} })

13.2.5.5 Runtime Semantics: PropertyDefinitionEvaluation

PropertyDefinition : PropertyName : AssignmentExpression

  1. Let propKey be the result of evaluating PropertyName.
  2. […]
  3. If IsAnonymousFunctionDefinition(AssignmentExpression) is true, then
    1. Let propValue be ? NamedEvaluation of AssignmentExpression with argument propKey.
  4. Else,
    1. Let exprValueRef be the result of evaluating AssignmentExpression.
    2. Let propValue be ? GetValue(exprValueRef).
  5. […]

In step 3, IsAnonymousFunctionDefinition of the expression function(){} returns true, because it is a function definition, and it lacks a BindingIdentifier.

Therefore, a NamedEvaluation is performed: the function is created with "test" as the value of the name property.


test.print = function(){};

13.15.2 Runtime Semantics: Evaluation

AssignmentExpression : LeftHandSideExpression = AssignmentExpression

  1. Let lref be the result of evaluating LeftHandSideExpression.
  2. […]
  3. If IsAnonymousFunctionDefinition(AssignmentExpression) and IsIdentifierRef of LeftHandSideExpression are both true, then
    1. Let rval be NamedEvaluation of AssignmentExpression with argument lref.[[ReferencedName]].
  4. Else,
    1. Let rref be the result of evaluating AssignmentExpression.
    2. Let rval be ? GetValue(rref).
  5. Perform ? PutValue(lref, rval).
  6. […]

In step 3,

  • IsAnonymousFunctionDefinition of the expression function(){} returns true, just like it does in the other snippet,
  • but IsIdentifierRef of the expression test.print returns false: it’s a MemberExpression, not an Identifier.

Therefore, step 4, the “Else” case, is taken and no NamedEvaluation is performed.


The rationale: security

The only difference is the additional IsIdentifierRef step, which is also the key to finding the rationale (on esdiscuss.org):

2015-07-25 14:22:59 UTC a.d.bergi at web.de writes:

[If] a function (or class) definition is assigned to a property of an object, [the function automatically being given the name of the target identifier] doesn’t happen:

var o = {};

o.someProperty = function() { … };
o.otherProperty = class { … };

I don’t see any reason for not doing this. [It] just seems to be advantageous and make the language more consistent. I’m quite sure it wouldn’t break any compatibility.

This is one of the replies:

2015-07-26 19:48:15 UTC Allen Wirfs-Brock writes (emphasis mine):

[T]he possibility that the property key is a symbol is a primary reason that this expression form does not set the name property.

There may also be security concerns. The name property potentially leaks via the function object the name of the variable it is initially assigned to. But there isn’t much someone could do with a local variable name, outside of the originating function. But a leaked property name potentially carries a greater capability.

Allen goes on, 2015-07-26 20:33:07 UTC:

TC39 reached consensus on automatically assigning the name property for expression forms like:

Identifier = FunctionExpression

and so it is part of ES2015. We did not have consensus on doing the same for:

MemberExpression . IdentifierName = FunctionExpression

or

MemberExpression [ IdentifierName ] = FunctionExpression

so it is not part of ES2015. There were various objections that would have to be overcome before we could adopt that.

Another comment, 2016-12-13 09:03:40 UTC by T.J. Crowder states that it’s unclear what those “various objections” were. They link to the original proposal (archived from wiki.ecmascript.org, 2016-09-15, last available version) which does list your expected behavior in an example:

obj.property = function(){}; // "property"

But keep in mind that this was still an unfinished proposal back then. When the proposal got accepted into the specification, it seems that it underwent some changes, but the proposal article didn’t get changed to reflect these changes.

This thread mentions certain TC39 meeting notes where this is discussed, but no link is provided. Allen is claiming that this kind of information leak is the reason why TC39 couldn’t reach a consensus on allowing this behavior. They mention 2017-01-28 15:46:54 UTC:

[F]or cache[getUserSecret(user)] = function(){}; it would leak the secret user info as the value of name.

Even though, as T.J. Crowder mentions 2017-01-28 16:11:26 UTC, the same thing is possible with:

cache = {
  [getUserSecret(user)]: function() {}
};

There isn’t an easy workaround with anonymous functions

“Is there a way to get the name of a function declared [like test.print = function(){};]?”

Not really. You could iterate through the object’s entries and find candidates for key names where the value matches the function. But if the function is referenced by another key name, you may get multiple results. Also, this gets more complicated if the key is a symbol.

const test = {
    test: function(){}
  }

test.print = function(){};
test.someOtherAliasForPrint = test.print;
test[Symbol("someSymbolForPrint")] = test.print;

console.log(
  "Possible function names:",
  Object.entries(Object.getOwnPropertyDescriptors(test))
    .filter(([key, {
      value
    }]) => value === test.print)
    .map(([key]) => key)
);
console.log(
  "Possible function symbols:",
  Object.getOwnPropertySymbols(test)
    .filter((symbol) => test[symbol] === test.print)
    .map((symbol) => String(symbol)) // Or `.map(({description}) => description)`
);

Your best bet is to define the method like this:

test.print = function print(){};

However, consider using method syntax instead:

({
  test(){},
  print(){}
})
Advertisement