23

Is there an elegant way to tell Harmony's slim arrow functions apart from regular functions and built-in functions?

The Harmony wiki states that:

Arrow functions are like built-in functions in that both lack .prototype and any [[Construct]] internal method. So new (() => {}) throws a TypeError but otherwise arrows are like functions

Which means, you can test for arrow functions like:

!(()=>{}).hasOwnProperty("prototype") // true
!(function(){}).hasOwnProperty("prototype") // false

But the test will also return true for any built-in function, e.g. setTimeout or Math.min.

It sort of works in Firefox if you get the source code and check if it's "native code", but it doesn't seem much reliable nor portable (other browser implementations, NodeJS / iojs):

setTimeout.toSource().indexOf("[native code]") > -1

The small GitHub project node-is-arrow-function relies on RegExp-checks against the function source code, which isn't that neat.

edit: I gave the JavaScript parser acorn a try and it seems to work quite okay - even though it's pretty overkill.

acorn = require("./acorn");

function fn_sample(a,b){
    c = (d,e) => d-e;
    f = c(--a, b) * (b, a);
    return f;
}

function test(fn){
    fn = fn || fn_sample;
    try {
        acorn.parse("(" + fn.toString() + ")", {
            ecmaVersion: 6,
            onToken: function(token){
                if(typeof token.type == "object" && token.type.type == "=>"){
                    console.log("ArrowFunction found", token);
                }
            }
        });
    } catch(e) {
        console.log("Error, possibly caused by [native code]");
        console.log(e.message);
    }
}

exports.test = test;
Thank you
  • 107,507
  • 28
  • 191
  • 224
CodeManX
  • 9,290
  • 3
  • 41
  • 58
  • 5
    Out of curiosity, why would you want to do this in the first place? – Alexis King Jan 29 '15 at 18:47
  • I'm not sure, maybe ask the author of that node module... Maybe to check whether you need to bind `this` to the function? Arrow functions are automatically bound to it, so there's no need for the `self = this` hack or this-binding from outside. It might also be "better" to test for an arrow function instead of try/catch `new func` (equally applies to arrow and built-in functions). Either way, it feels like an oversight in the ECMAScript specs to not be able to reflect about these 3 different function types. – CodeManX Jan 29 '15 at 18:54
  • I believe arrow functions were made similar to built-ins for a reason actually. – raina77ow Jan 29 '15 at 19:00
  • It might turn out that, as for generators, there's no valid reason apart style checking or code optimization to check if a function is an arrow function. It was purposely decided to not offer a way to check if a function is a generator, it's very possible the same reasoning prevailed here. – Denys Séguret Jan 29 '15 at 19:47
  • 1
    Firefox does implement `Function.prototype.isGenerator`. – CodeManX Jan 29 '15 at 20:08
  • Yes but it wasn't in ES6, I think, because there's no good reason to use such a property – Denys Séguret Jan 29 '15 at 20:36
  • @CoDEmanX: I don't see any reason why you would want to distinguish a built-in function from a "normal" function? If it doesn't have a prototype, it means it should not be used as a constructor - which is equally valid for `setTimeout` as for an arrow function. – Bergi Jan 29 '15 at 20:58
  • 3
    The reason I'm interested in this is to provide feedback to users of a library. If I invoke passed `callback` with `this` bound to something, I want to throw an error, if `callback` is unboundable. – Aleksei Zabrodskii Aug 08 '16 at 16:45
  • @AlexisKing My usecase is a unit test in which I need to check if all prototype functions of a class are "normal" and not arrow functions, because they obviously use `this` and won't work the same way if they were arrow functions. – Nano Miratus Feb 17 '18 at 01:44
  • 1
    Doesn't work with method shorthands defined on objects. `var g = { f() { return 'x'; } }; g.f.hasOwnProperty('prototype') /* false */` – Mikal Madsen Mar 02 '18 at 12:28

10 Answers10

12

Believe it or not...

Testing for presence of "=>" in string representation of a function is likely the most reliable way (but not 100%).

Obviously we can't test against either of 2 conditions you mentioned — lack of prototype property and lack of [[Construct]] as that might give false positives with either host objects or built-in ones that lack [[Construct]] (Math.floor, JSON.parse, etc.)

We could, however, use good old Function.prototype.toString to check if function representation contains "=>".

Now, I've always recommended against using Function.prototype.toString (so-called function decompilation) due to its implementation-dependent and historically unreliable nature (more details in State of function decompilation in Javascript).

But ES6 actually tries to enforce rules on the way (at least) built-in and "user-created" (for the lack of better term) functions are represented.

  1. If Type(func) is Object and is either a Built-in function object or has an [[ECMAScriptCode]] internal slot, then

    a. Return an implementation-dependent String source code representation of func. The representation must conform to the rules below.

...

toString Representation Requirements:

  • The string representation must have the syntax of a FunctionDeclaration FunctionExpression, GeneratorDeclaration, GeneratorExpession, ClassDeclaration, ClassExpression, ArrowFunction, MethodDefinition, or GeneratorMethod depending upon the actual characteristics of the object.

  • The use and placement of white space, line terminators, and semicolons within the representation String is implementation-dependent.

  • If the object was defined using ECMAScript code and the returned string representation is not in the form of a MethodDefinition or GeneratorMethod then the representation must be such that if the string is evaluated, using eval in a lexical context that is equivalent to the lexical context used to create the original object, it will result in a new functionally equivalent object. In that case the returned source code must not mention freely any variables that were not mentioned freely by the original function’s source code, even if these “extra” names were originally in scope.

  • If the implementation cannot produce a source code string that meets these criteria then it must return a string for which eval will throw a SyntaxError exception.

I highlighted relevant chunks.

Arrow functions have internal [[ECMAScriptCode]] (which you can track from 14.2.17 — evaluation of arrow function - to FunctionCreate to FunctionInitialize).

This means they must conform to ArrowFunction syntax:

ArrowFunction[In, Yield] :
  ArrowParameters[?Yield] [no LineTerminator here] => ConciseBody[?In]

..which means they must have => in Function.prototype.toString's output.

You'll obviously need to ensure "=>" follows ArrowParameters and is not just something present in FunctionBody:

function f() { return "=>" }

As for reliability — remember that this behavior is/might not be supported by any/all engines at the moment and that host objects' representation might lie (despite specs efforts) for whatever reasons.

kangax
  • 37,379
  • 12
  • 94
  • 132
  • I'm not sure why a builtin function would necessarily be a false positive - may it *is* an arrow function in the engine source? – Bergi Jan 31 '15 at 15:26
  • No, not necessarily, but possible. I was talking about those without `[[Construct]]`. As for built-in functions represented as arrow ones in the engine... that's an interesting thought. I don't really see much benefit in that; arrow functions seem more of a syntax sugar (which engines usually don't care about) rather than runtime optimization. – kangax Jan 31 '15 at 15:51
  • As for runtime semantics, both arrow and regular function declarations use _FunctionCreate_ (just with different "kind" parameter) only arrow ones don't create `[[Construct]]` (in _FunctionAllocate_) and set `[[ThisMode]]` to "lexical" (in _FunctionIntialize_). So theoretically they're "lighter" but it still feels unlikely that they'd be used internally. Especially because most of built-ins probably need "heavy" logic. – kangax Jan 31 '15 at 15:52
  • `Math.min` might easily want to use a "lighter" function I thought (of course I didn't mean to say that were necessarily constructed with arrow *syntax*). I just wondered why OP would want to distinguish between "arrow functions" and "builtin/host function" when there might not be any difference in their behaviour. – Bergi Jan 31 '15 at 16:02
  • I had some success with this JS parser written in JS to determine whether a function contains an arrow expression node: https://github.com/marijnh/acorn – CodeManX Feb 24 '15 at 18:27
  • You wrote: "Testing for presence of "=>" in string representation of a function is likely the most reliable way (but not 100%)." But, in my code-base at least arrow-functions typically exist inside other NON-arrow functions. Then if I test the outer non-arrow function for the presence of '=>' in its source it is often there, so there would be a lot of false positives in my code-base at least . See my answer below for another proposal: Test whether the function source-code starts with "function ". – Panu Logic Sep 25 '17 at 15:45
8

Kangax has made a great point. If you wanted perfection, you'd use an AST parser library to determine the function type, but that is likely to be overkill for many of you.

To that end, here's a method to implement using Regular Expressions:

/** Check if function is Arrow Function */
const isArrowFn = (fn) => (typeof fn === 'function') && /^[^{]+?=>/.test(fn.toString());

/* Demo */
const fn = () => {};
const fn2 = function () { return () => 4 }

isArrowFn(fn)  // True
isArrowFn(fn2) // False

Logic

This assumes that all non-arrow function blocks must be surrounded by { }. I don't think that there is an environment which would render otherwise, but please let me know if I'm wrong.

Note: The regex is also written with the assumption that the function's => will always be on the first line. This can be easily changed, but again, I can't imagine anything rendering a newline before it.

How does it work?

  • ^ - start at the beginning of the line
  • [ ^{ ]+? - Look through everything that follows until the next pattern (=>), but if you find a { first, it's not a match
  • => - Found => before {, so it's a match

Problem?

Leave a comment if you find a case where it doesn't work, and I'll see if we can accommodate.

Ron S.
  • 308
  • 2
  • 10
4

I wrote this for Node, should work in Chrome as well.

"Boundness" is detected (apparently, only on ES6) and reported as native && bound. This might or might not be an issue, depending on what you are using that information for.

const flags = {
  function: f instanceof Function,
  name: undefined,
  native: false,
  bound: false,
  plain: false,
  arrow: false
};

if (flags.function) {
  flags.name = f.name || '(anonymous)';
  flags.native = f.toString().trim().endsWith('() { [native code] }');
  flags.bound = flags.native && flags.name.startsWith('bound ');
  flags.plain = !flags.native && f.hasOwnProperty('prototype');
  flags.arrow = !(flags.native || flags.plain);
}

return flags;
Community
  • 1
  • 1
Aleksei Zabrodskii
  • 2,140
  • 3
  • 17
  • 41
  • that's a nice one. I hate the fact that we have to decompile the function into a string - which could be very long, but I accept the verdict... Ah - and you could also write it without assigning twice to each flag except `flags.function`. what I'd do is declare 6 consts, assign once, and return an object literal of all 6. Also halfs the number of code lines :) – Radagast the Brown Apr 29 '20 at 18:02
1

ECMAScript waives a lot of its guarantees for host objects, and thus by extension, host functions. That makes the properties accessible via reflection mostly implementation-dependent with little guarantees for consistency, at least as far as the ecmascript spec is concerned, W3C specs may be more specific for browser host objects.

E.g. see

8.6.2 Object Internal Properties and Methods

The Table 9 summarises the internal properties used by this specification that are only applicable to some ECMAScript objects. [...] Host objects may support these internal properties with any implementation-dependent behaviour as long as it is consistent with the specific host object restrictions stated in this document.

So built-in functions might be callable but have no prototype (i.e. not inherit from function). Or they could have one.

The spec says they may behave differently. But they also may implement all the standard behavior, making them indistinguishable from normal functions.

Note that I'm quoting the ES5 spec. ES6 is still undergoing revisions, native and host objects are now called exotic objects. But the spec pretty much says the same. It provides some invariants that even they must fulfill, but otherwise only says that they may or may not fulfill all optional behaviors.

the8472
  • 35,110
  • 4
  • 54
  • 107
1

As far as I can tell this should work:

All non-arrow functions when converted to String START with 'function '. Arrow functions don't.

Trying to test for the presence of '=>' is not a reliable way to test whether the function is an arrow or not because any non-arrow function can contain arrow-functions inside them and thus the '=>' can be present in their source-code.

Panu Logic
  • 1,495
  • 1
  • 13
  • 20
  • 1
    I checked the standard (ES6 and ES2017) and it sounds like you can rely on `!func.toString().startsWith('function ')` to test for arrow functions, because the syntax doesn't allow a leading `function` keyword for them and `Function.prototype.toString()` is supposed to reproduce that syntax. I didn't check if it's mandatory however, that the character sequence "function " starts at position 0 of the stringified function (maybe comments or whitespace are allowed?) – CodeManX Sep 26 '17 at 15:19
  • As far as I can think when a non-arrow function is converted to String there is no white-space before the word 'function' because why would there be? If there was a comment before the function there would be no reason to consider it to be part of the function that follows, would there? Now it's of course possible that the toString() of one or more functions has been replaced for some reason – Panu Logic Sep 30 '17 at 00:25
  • 1
    I concern was that some engines might add information in the form of comments to the stringified function code e.g. for debugging purposes, which appears to be allowed as per standard because it wouldn't affect the code and is legal in terms of syntax (engine that do this are yet to be found however). I remember a slightly related issue with inaccurate tracebacks in some framework, because the developers didn't consider that the called functions were wrapped with an IIFE, which added two lines to the starting position. – CodeManX Oct 04 '17 at 07:33
  • 3
    Doesn't work if the function is defined with a method shorthand. `var g = { f() { return 'x'; } }` `g.f.toString() /* "f() { return 'x'; }" */` – Mikal Madsen Mar 02 '18 at 12:24
1

Ron S's solution works great but can detect false positives:

/** Check if function is Arrow Function */
const isArrowFn = (fn) => (typeof fn === 'function') && /^[^{]+?=>/.test(fn.toString());

/* False positive */
const fn = function (callback = () => null) { return 'foo' }

console.log(
  isArrowFn(fn)  // true
)
adiga
  • 28,937
  • 7
  • 45
  • 66
Nicolas Keller
  • 413
  • 4
  • 7
1

I couldn't find false positives. Small change to Ron S's approach:

const isArrowFn = f => typeof f === 'function' && (/^([^{=]+|\(.*\)\s*)?=>/).test(f.toString().replace(/\s/, ''))

const obj = {
    f1: () => {},
    f2 () {}
}

isArrowFn(obj.f1) // true
isArrowFn(() => {}) // true
isArrowFn((x = () => {}) => {}) // true

isArrowFn(obj.f2) // false
isArrowFn(function () {}) // false
isArrowFn(function (x = () => {}) {}) // false
isArrowFn(function () { return () => {} }) // false
isArrowFn(Math.random) // false
Raul Vitor
  • 36
  • 3
0

Based documentation on mozilla.org and taking into account side effects of Use of the new operator and that page we could try to do something like:

function isArrow (fn) {
  if (typeof fn !== 'function') return false
  try {
    new fn()
  } catch(err) {
   if(err.name === 'TypeError' && err.message === 'fn is not a constructor') {
    return true
   }
  }
  return false
}

console.log(isArrow(()=>{})) // true
console.log(isArrow(function () {})) // false
console.log(isArrow({})) // false
console.log(isArrow(1)) // false


let hacky = function () { throw new TypeError('fn is not a constructor') }
console.log(isArrow(hacky)) // unfortunately true
  • `isArrow(Math.min)` is unfortunately also true, also see https://stackoverflow.com/questions/28222228/javascript-es6-test-for-arrow-function-built-in-function-regular-function/62073784#comment107716973_60881334 – CodeManX May 29 '20 at 11:35
0

Modern solution to validate arrow function:

const isArrowFunction = obj => typeof obj === 'function' && obj.prototype === undefined;
isArrowFunction(() => 10); // true
isArrowFunction(function() {}); // false
htndev
  • 1
  • 1
  • 1
  • Produces false-positives e.g. for `Math.min`, also see my previous [comment](https://stackoverflow.com/questions/28222228/javascript-es6-test-for-arrow-function-built-in-function-regular-function/65570273#comment107716973_60881334) – CodeManX Jan 11 '21 at 13:10
  • @CodeManX you are right, this case is escaped. – htndev Jan 12 '21 at 11:27
-2
  • Convert Function to string toString
  • removing all spaces of this string.
  • if ")=>" exists with index greater or equal to 1 => 99.99% is an arrow function .

        F.toString().replace(/\s+/g, '').indexOf(')=>')>=1
    

DEMO :

var fn1=function(e){
   e.target.value=new Date();
};

var fn2=(a,b)=> a+b;

function isArrow(name,F){
         if(F.toString().replace(/\s+/g, '').indexOf(')=>')>=1){
                 console.log(`${name} is arrow-function`);
         }else{
           console.log(`${name} is classic-function`);
          }
}

isArrow('fn1',fn1);
isArrow('fn2',fn2);
Abdennour TOUMI
  • 64,884
  • 28
  • 201
  • 207