JavaScript ES6: Test for arrow function, built-in function, regular function?
Asked Answered
A

11

36

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;
Archeology answered 29/1, 2015 at 18:45 Comment(10)
Out of curiosity, why would you want to do this in the first place?Grandpa
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.Archeology
I believe arrow functions were made similar to built-ins for a reason actually.Kurus
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.Ritualist
Firefox does implement Function.prototype.isGenerator.Archeology
Yes but it wasn't in ES6, I think, because there's no good reason to use such a propertyRitualist
@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.Nicolis
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.History
@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.Bunker
Doesn't work with method shorthands defined on objects. var g = { f() { return 'x'; } }; g.f.hasOwnProperty('prototype') /* false */Hasheem
W
14

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.

Wilmot answered 30/1, 2015 at 10:55 Comment(6)
I'm not sure why a builtin function would necessarily be a false positive - may it is an arrow function in the engine source?Nicolis
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.Wilmot
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.Wilmot
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.Nicolis
I had some success with this JS parser written in JS to determine whether a function contains an arrow expression node: github.com/marijnh/acornArcheology
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 ".Statistical
C
11

Updated

Originally, I implemented Kangax's solution using regex, however, as several pointed out, there were some false positives and gotcha situations, indicating that we need a slightly more thorough approach.

With that in mind, I took a moment to look through the latest ES spec to work out a complete method. In the following exclusionary solution, we detect syntax for all non-arrow functions which have the function JS type. We also ignore comments and line breaks, which accounts for the bulk of the regex.

Provided the JS engine conforms to ES spec, the following should work in all scenarios:

/** Check if function is Arrow Function */
const isArrowFn = (fn) => 
  (typeof fn === 'function') &&
  !/^(?:(?:\/\*[^(?:\*\/)]*\*\/\s*)|(?:\/\/[^\r\n]*))*\s*(?:(?:(?:async\s(?:(?:\/\*[^(?:\*\/)]*\*\/\s*)|(?:\/\/[^\r\n]*))*\s*)?function|class)(?:\s|(?:(?:\/\*[^(?:\*\/)]*\*\/\s*)|(?:\/\/[^\r\n]*))*)|(?:[_$\w][\w0-9_$]*\s*(?:\/\*[^(?:\*\/)]*\*\/\s*)*\s*\()|(?:\[\s*(?:\/\*[^(?:\*\/)]*\*\/\s*)*\s*(?:(?:['][^']+['])|(?:["][^"]+["]))\s*(?:\/\*[^(?:\*\/)]*\*\/\s*)*\s*\]\())/.test(fn.toString());

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

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

Problem?

If you have any issues, leave me a comment, and I'll work out a revised solution. Be sure to leave a comment under this answer, however. I don't monitor this page, so I won't see it if you say something doesn't work as a separate answer.

Charlenecharleroi answered 27/8, 2019 at 20:49 Comment(4)
False positive for <code>function(arg="=>"){ }</code> and <code>function(arg/*=>*/){ }</code> (tested on Chrome)Tinned
Thanks @Jens. UpdatedCharlenecharleroi
@RonS. False positive for isArrowFn({["=>"](){}}["=>"])Lacombe
Thanks @Harry! Looks like I missed computed method names. I've updated the answer to cover that caseCharlenecharleroi
H
5

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.

function check(f) {
    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;
}

let o = {f(){}}

const res = check(o.f)

console.log(res)
History answered 8/8, 2016 at 13:48 Comment(3)
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 :)Ander
Just need to add test against class, async, generator functionsVeriee
The only solution that works as expected, I tested everyone and all have false positives.Hammerlock
B
2

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
Betti answered 28/5, 2020 at 20:17 Comment(4)
isArrow(Math.min) is unfortunately also true, also see #28222728Archeology
To detect if the function is Arrow you are calling it first. So, this is not a very functional hack for many situations.Hammerlock
The problem with this is that isArrow(function(){alert("Hello World!")}) returns false as expected, but also has the side-effect of showing an alert box.Rhizotomy
Also isArrow(async function(){}) returns true.Rhizotomy
J
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.

Jemappes answered 30/1, 2015 at 4:32 Comment(0)
S
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.

Statistical answered 25/9, 2017 at 15:44 Comment(5)
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?)Archeology
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 reasonStatistical
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.Archeology
Doesn't work if the function is defined with a method shorthand. var g = { f() { return 'x'; } } g.f.toString() /* "f() { return 'x'; }" */Hasheem
@Mikal Madsen. Good point. It seems it also does not work for methods of a class. For example: (class temp { static test () {} }) .test + "" . But, the original question did not mention methods. The method-shorthands are like methods, they are called "method shorthands".Statistical
G
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
Gaygaya answered 23/6, 2020 at 20:0 Comment(2)
Thank you for answer @Raul , what is this regex intended to do?Foulmouthed
This produces a false positive for function f(/*()=>*/){}Kike
S
0

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
)
Squamation answered 7/9, 2019 at 11:58 Comment(1)
Thanks for pointing this out. I've updated my answer.Charlenecharleroi
M
0

For me, this one works perfect with given tests:

function isLambda(func){
   if(typeof func !== "function") throw new Error("invalid function was provided")
   let firstLine =  func.toString()
       .split("\n")['0']
       .replace(/\n|\r|\s/ig, "")

   return (!firstLine.includes("function") && (firstLine.includes(")=>{") || firstLine.includes("){")))
}

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


console.log(isLambda(obj.f1), true) // true
console.log(isLambda(() => {}), true) // true
console.log(isLambda((x = () => {}) => {}), true) // true
console.log(isLambda(obj.f2), true) // true
console.log(isLambda(function () {}), false) // false
console.log(isLambda(function (x = () => {}) {}), false) // false
console.log(isLambda(function () { return () => {} }), false) // false
console.log(isLambda(Math.random), false) // false
Mcrae answered 16/10, 2022 at 14:5 Comment(0)
M
0

This shouldn't give false positives.

function isArrowFn(f) {
    if(!(f instanceof Function)) return false;
    f = f.toString();
    f = f.replace(/\s|\/\/.+/g, '');
    f = f.replace(/\/\*.+\*\//g, '');
    f = f.replace(/\(.+|=>.+/g, '');
    return !(f.indexOf('function') !== -1 || f.indexOf('method') !== -1);
}

I know running multiple string searches is not the best in terms of performance but clearing the string before checking seems the only way.
Otherwhise you can directly parse the string like this:

function isArrowFn(f) {
    if(!(f instanceof Function)) return false;
    let cmmt = {block: false, line: false};
    let insideParams = false;
    f = f.toString(); let len = f.length;
    for(let i=0, ch; i<len; i++){
        ch = f[i];
        if(cmmt.block || cmmt.line){ 
            if(ch === '*' && f[i+1] === '/'){ cmmt.block = false; i++; }
            else if(ch === '\n') cmmt.line = false;
        }
        else if(ch === '/' && f[i+1] === '*'){ cmmt.block = true;  i++; }
        else if(ch === '/' && f[i+1] === '/'){ cmmt.line = true; i++; }
        else if(insideParams){ if(ch === ')') insideParams = false; }
        else if(ch === '(') insideParams = true;
        else if(ch === '=' && f[i+1] === '>') return true;
        else if(ch === '{') return false;
    }
    return false;
}

which is ≈160% faster.

Other answers have issues with some of these examples:

//Native code like
Math.min

//Misleading comments
f(/*()=>*/){}

//or this
async () => {
    function a() {};
}
Madalinemadalyn answered 20/6, 2024 at 16:27 Comment(0)
V
-1

Modern solution to validate arrow function:

const isArrowFunction = obj => typeof obj === 'function' && obj.prototype === undefined;
isArrowFunction(() => 10); // true
isArrowFunction(function() {}); // false
Violate answered 4/1, 2021 at 21:46 Comment(2)
Produces false-positives e.g. for Math.min, also see my previous commentArcheology
@Archeology you are right, this case is escaped.Violate

© 2022 - 2025 — McMap. All rights reserved.