JavaScript ES6:如何测试箭头函数、内置函数和普通函数?

34

有没有一种优雅的方式可以区分Harmony的箭头函数与普通函数内置函数?

Harmony wiki表示:

箭头函数类似于内置函数,两者都缺少.prototype和任何[[Construct]]内部方法。因此,new (() => {})会抛出TypeError,但除此之外,箭头函数就像函数一样。

这意味着,你可以像这样测试箭头函数:

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

但是该测试对于任何内置函数也会返回true,例如setTimeoutMath.min

如果你获取源代码并检查是否为"native code",它在Firefox中似乎有点可行,但它似乎不是很可靠也不太可移植(其他浏览器实现、NodeJS/iojs):

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

这个小 GitHub 项目 node-is-arrow-function 依赖于对函数源代码的正则表达式检查,这并不是很简洁。

编辑:我试着使用 JavaScript 解析器 acorn,它似乎工作得还不错——尽管有些过度设计了。

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;

5
出于好奇心,你为什么要首先这样做呢? - Alexis King
2
Firefox确实实现了Function.prototype.isGenerator。 - CodeManX
4
我对这个感兴趣是为了向图书馆的用户提供反馈。如果我将传递的callback绑定到某个对象并调用它,但无法将callback绑定到任何对象,我希望抛出一个错误。 - Aleksei Zabrodskii
@AlexisKing 我的使用案例是单元测试,在其中我需要检查类的所有原型函数是否为“普通”函数,而不是箭头函数,因为它们显然使用 this,如果它们是箭头函数,那么它们将无法正常工作。 - Nano Miratus
1
不适用于在对象上定义的方法简写。 var g = { f() { return 'x'; } }; g.f.hasOwnProperty('prototype') /* false */ - Mikal Madsen
显示剩余5条评论
10个回答

14

信不信由你...

测试函数的字符串表示中是否存在 "=>" 是最可靠的方法(但不是100%)。

显然,我们不能针对您提到的两个条件进行测试——原型属性的缺乏和 [[Construct]] 的缺乏,因为这可能会导致与缺乏 [[Construct]] 的主机对象或内置对象(例如 Math.floorJSON.parse 等)产生误报。

然而,我们可以使用老旧的 Function.prototype.toString 来检查函数表示是否包含 "=>"。

现在,我始终建议不要使用 Function.prototype.toString(所谓的函数反编译),因为它受实现依赖性和历史上的不可靠性影响(有关详细信息,请参见Javascript 中函数反编译的状态)。

但是 ES6 实际上试图强制实施规则,至少是对内置和“用户创建”的函数(缺乏更好的术语)的表示方式。

  
      
  1. 如果 Type(func) 是 Object,并且是内置函数对象或具有 [[ECMAScriptCode]] 内部插槽的函数,则

         

    a. 返回 func 的实现依赖性字符串源代码表示。表示必须符合以下规则

  2.   
     

...

     

toString 表示要求:

     
        
  • 字符串表示必须具有 FunctionDeclaration FunctionExpression、GeneratorDeclaration、GeneratorExpession、ClassDeclaration、ClassExpression、ArrowFunction、MethodDefinition 或 GeneratorMethod 的语法,具体取决于对象的实际特征。

  •     
  • 在表示字符串中使用和放置空格、行终止符和分号是实现相关的。

  •     
  • 如果对象是使用 ECMAScript 代码定义的,并且返回的字符串表示形式不是 MethodDefinition 或 GeneratorMethod 的形式,则表示必须是这样的,即如果使用与用于创建原始对象的词法上下文等效的词法上下文进行评估字符串将导致一个新的功能等效的对象。在这种情况下,返回的源代码不能自由地提到任何原始函数的源代码没有自由提到的变量,即使这些“额外”的名称最初是在作用域中的。

  •     
  • 如果实现不能生成符合这些标准的源代码字符串,则必须返回一个字符串,其中 eval 将抛出 SyntaxError 异常。

  •   

我强调了相关的部分。

箭头函数具有内部的 [[ECMAScriptCode]](可以从 14.2.17 — 箭头函数的评估 - 跟踪到 FunctionCreateFunctionInitialize),这意味着它们必须符合ArrowFunction 语法

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

这意味着它们必须在Function.prototype.toString的输出中出现"=>"。

显然,您需要确保ArrowParameters后跟"=>”,而不仅仅是在FunctionBody中存在此内容:

function f() { return "=>" }

至于可靠性——请记住,当前/可能不是所有引擎都支持此行为,并且主机对象的表示可能会因各种原因而存在误差(尽管规格正在努力改进)。


我不确定为什么内置函数一定会是误报 - 也许它在引擎源代码中确实是箭头函数? - Bergi
不一定,但是有可能。我指的是那些没有[[Construct]]的函数。至于在引擎中表示为箭头函数的内置函数......那是一个有趣的想法。我并没有真正看到其中太大的好处;箭头函数似乎更像是语法糖(引擎通常不关心)而不是运行时优化。 - kangax
就运行时语义而言,箭头函数和普通函数声明都使用_FunctionCreate_(只是具有不同的“kind”参数),只有箭头函数不会创建[[Construct]](在_FunctionAllocate_中)并将[[ThisMode]]设置为“词法”(在_FunctionIntialize_中)。因此理论上它们更“轻”,但仍然不太可能在内部使用。特别是因为大多数内置函数可能需要“重”逻辑。 - kangax
Math.min 可能很容易想使用一个“轻量级”的函数,我认为(当然我并不是说它们一定是用箭头语法构建的)。我只是想知道为什么 OP 要区分“箭头函数”和“内置/宿主函数”,当它们的行为可能没有任何区别。 - Bergi
我使用了这个用JS编写的JS解析器,以确定函数中是否包含箭头表达式节点,取得了一些成功:https://github.com/marijnh/acorn - CodeManX
你写道:“在函数的字符串表示中测试是否存在“=>”可能是最可靠的方法(但不是100%准确的)。但是,在我的代码库中,箭头函数通常存在于其他非箭头函数内部。因此,如果我测试外部的非箭头函数是否在其源代码中存在'=>',通常会存在这样的情况,因此至少在我的代码库中会有很多误报。请查看下面的答案,提供另一个方案:测试函数的源代码是否以“function ”开头。” - Panu Logic

11

更新

最初,我使用正则表达式实现了Kangax的解决方案,但是正如一些人指出的那样,存在一些误报和注意事项,表明我们需要一个稍微更全面的方法。

考虑到这一点,我花了一些时间查阅了最新的ES规范,以便找出一个完整的方法。在以下排除性解决方案中,我们检测所有非箭头函数的语法,并具有function JS类型。我们还忽略注释和换行符,这占据了正则表达式的大部分内容。

如果JS引擎符合ES规范,则以下内容应在所有情况下均可正常工作:

/** 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

问题?

如果您有任何问题,请在评论中留言,我会解决并提供更新的解决方案。请确保在答案下留言。我不会监控这个页面,所以如果您将问题作为单独的答案回复,我将无法看到它。


<code>function(arg="=>"){ }</code>和<code>function(arg/=>/){ }</code>在Chrome上测试出现了误报。 - Jens
感谢@Jens。已更新。 - Ron S.
@RonS。对于 isArrowFn({["=>"](){}}["=>"]),出现了误报。 - Harry
1
谢谢@Harry!看起来我错过了计算方法的名称。我已经更新了答案以涵盖该情况。 - Ron S.

6

我为 Node 写了这个,Chrome 也应该可以用。

“Boundness”(绑定状态)被检测到(显然,只有在 ES6 中),并报告为 native && bound。根据您使用此信息的目的,这可能是一个问题或不是一个问题。

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;

那真是个好主意。我讨厌我们必须将函数反编译成字符串的事实 - 这可能非常长,但我接受这个判决...啊 - 你也可以写一个不需要两次分配到除了 flags.function 之外的每个标志的版本。我会声明6个常量,只分配一次,并返回所有6个的对象字面量。这样可以减少代码行数的一半 :) - Radagast the Brown
只需对类、异步和生成器函数添加测试。 - Monday Fatigue
唯一有效的解决方案,我测试了所有的方案,其他都有误报。 - Heroselohim

1
ECMAScript放弃了很多对宿主对象的保证,因此也同样放弃了对宿主函数的保证。这使得通过反射访问属性在很大程度上取决于实现,至少在ecmascript规范中是如此,W3C规范可能更具体地针对浏览器宿主对象。

例如,请参见

8.6.2 Object Internal Properties and Methods

表9总结了本规范仅适用于某些ECMAScript对象的内部属性。[...]主机对象可以使用这些内部属性,只要其行为与本文档中指定的特定主机对象限制一致即可。

因此,内置函数可能是可调用的,但没有原型(即不继承自函数)。或者它们可能有一个。

规范说它们可能会有不同的行为。但它们也可能实现所有标准行为,使它们与普通函数无法区分。

请注意,我引用的是ES5规范。ES6仍在修订中,本地和主机对象现在被称为异类对象。但是规范基本上是相同的。它提供了一些不变量,即使它们必须满足,但除此之外,它只说它们可能或可能不满足所有可选行为。

1
据我所知,这应该是有效的:
所有非箭头函数在转换为字符串后都以“function”开头。箭头函数不会。
试图测试“=>”是否存在并不是测试函数是否为箭头函数的可靠方式,因为任何非箭头函数都可以包含箭头函数,因此“=>”可能出现在它们的源代码中。

1
我查看了标准(ES6和ES2017),似乎你可以依赖于!func.toString().startsWith('function ')来测试箭头函数,因为语法不允许它们有前导的“function”关键字,并且Function.prototype.toString()应该能够重现该语法。但我没有检查字符序列“function ”是否必须从字符串化的函数的位置0开始(也许允许注释或空格?)。 - CodeManX
就我所知,当非箭头函数转换为字符串时,在单词“function”之前没有空格,因为为什么会有呢?如果在函数之前有注释,那么没有理由认为它是后面函数的一部分,对吧?当然,现在有可能由于某种原因替换了一个或多个函数的toString()。 - Panu Logic
1
我的担忧是,一些引擎可能会以注释的形式向字符串化的函数代码中添加信息,例如用于调试目的。这似乎是符合标准的,因为它不会影响代码并且在语法上也是合法的(然而,尚未发现执行此操作的引擎)。我记得有一个略微相关的问题,即某些框架中的不准确的回溯,因为开发人员没有考虑到被调用的函数被包装在IIFE中,这会在起始位置添加两行。 - CodeManX
3
如果使用方法简写定义函数,则此方法不起作用。var g = { f() { return 'x'; } } g.f.toString() /* "f() { return 'x'; }" */ - Mikal Madsen
@Mikal Madsen。说得好。它似乎也不适用于类的方法。例如:(class temp { static test () {} }).test + ""。但是,原始问题没有提到方法。方法简写就像方法一样,被称为“方法简写”。 - Panu Logic

1

基于mozilla.org 文档,并考虑到使用 new 操作符的副作用和该页面,我们可以尝试做以下事情:

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) 遗憾地也是正确的,参见 https://stackoverflow.com/questions/28222228/javascript-es6-test-for-arrow-function-built-in-function-regular-function/62073784#comment107716973_60881334 - CodeManX
要检测函数是否为箭头函数,首先需要调用它。因此,这对于许多情况来说并不是一个非常实用的技巧。 - Heroselohim

0

Ron S的解决方案非常好,但可能会检测出错误的结果:

/** 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
)


谢谢您指出这个问题。我已经更新了我的答案。 - Ron S.

0

对我来说,这个在给定的测试中完美运行:

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

0

验证箭头函数的现代解决方案:

const isArrowFunction = obj => typeof obj === 'function' && obj.prototype === undefined;
isArrowFunction(() => 10); // true
isArrowFunction(function() {}); // false

产生误报,例如对于 Math.min 函数,也请查看我的先前评论。 - CodeManX
@CodeManX 你说得对,这个情况已经被排除了。 - htndev

0

我找不到假阳性。对Ron S的方法进行了小改动:

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 的回答,这个正则表达式的目的是什么? - Akshay Vijay Jain
2
这会为 function f(/*()=>*/){} 产生一个错误的正面结果。 - EnderShadow8

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接