JavaScript ES6:测试箭头函数、内置函数、常规函数?

IT技术 javascript function ecmascript-6 ecmascript-harmony arrow-functions
2021-01-14 10:28:46

除了常规函数内置函数之外,有没有一种优雅的方式来区分 Harmony 的细长箭头函数?

和谐维基指出:

箭头函数就像内置函数一样,都缺少 .prototype和任何 [[Construct]] 内部方法。所以 new (() => {}) 抛出一个 TypeError 但否则箭头就像函数

这意味着,您可以测试箭头函数,例如:

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

但是测试也将返回true任何内置函数,例如setTimeoutor Math.min

如果您获取源代码并检查它是否在 Firefox 中,它可以在 Firefox 中工作"native code",但它似乎不太可靠或可移植(其他浏览器实现,NodeJS / iojs):

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

GitHub 小项目node-is-arrow-function依赖于针对函数源代码的 RegExp 检查,这不是那么整洁。

编辑:尝试了 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;
6个回答

信不信由你...

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

很显然,我们可以对任何一个你所提到的2个条件不是测试-缺乏原型属性和缺乏[[Construct]]作为唯一可能产生误报或者主机对象或内置那些缺乏[[Construct]]Math.floorJSON.parse,等)

但是,我们可以使用 good oldFunction.prototype.toString来检查函数表示是否包含“=>”。

现在,我一直建议不要使用Function.prototype.toString(所谓的函数反编译),因为它依赖于实现并且在历史上不可靠(更多细节在 Javascript 中的函数反编译状态)。

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

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

    一个。返回 func 的依赖于实现的 String 源代码表示。表示必须符合以下规则

...

toString 表示要求:

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

  • 表示字符串中空格、行终止符和分号的使用和放置取决于实现。

  • 如果对象是使用 ECMAScript 代码定义的,并且返回的字符串表示不是 MethodDefinition 或 GeneratorMethod 的形式,则表示必须是这样的:如果对字符串进行求值,则在与所使用的词法上下文等效的词法上下文中使用 eval要创建原始对象,它将产生一个新的功能等效的对象。在这种情况下,返回的源代码不得随意提及原始函数的源代码未自由提及的任何变量,即使这些“额外”名称最初在范围内。

  • 如果实现无法生成满足这些条件的源代码字符串,则它必须返回一个字符串,eval 将针对该字符串抛出 SyntaxError 异常。

我强调了相关的块。

箭头函数具有内部[[ECMAScriptCode]](您可以从 14.2.17 — 箭头函数的评估 — 到FunctionCreateFunctionInitialize进行跟踪)。

这意味着它们必须符合ArrowFunction 语法

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

..这意味着他们必须有 => inFunction.prototype.toString的输出。

显然,您需要确保“=>”跟在 ArrowParameters 之后,而不仅仅是 FunctionBody 中存在的东西:

function f() { return "=>" }

至于可靠性——请记住,目前任何/所有引擎都/可能不支持这种行为,并且无论出于何种原因,宿主对象的表示都可能撒谎(尽管规范努力)。

我用这个用 JS 编写的 JS 解析器取得了一些成功,以确定函数是否包含箭头表达式节点:github.com/marijnh/acorn
2021-03-19 10:28:46
不,不一定,但可能。我说的是那些没有[[Construct]]. 至于引擎中用箭头表示的内置函数……这是一个有趣的想法。我真的看不出有什么好处。箭头函数似乎更像是一种语法糖(引擎通常不关心)而不是运行时优化。
2021-04-05 10:28:46
我不确定为什么内置函数一定是误报 - 它可能引擎源中的箭头函数吗?
2021-04-07 10:28:46
至于运行时语义,箭头和常规函数声明都使用FunctionCreate(只是具有不同的“种类”参数),只有箭头不创建[[Construct]](在FunctionAllocate 中)并设置[[ThisMode]]为“词法”(在FunctionIntialize 中)。所以理论上它们“更轻”,但仍然感觉它们不太可能在内部使用。特别是因为大多数内置程序可能需要“重”逻辑。
2021-04-07 10:28:46
Math.min可能很容易想使用我认为的“更轻”的函数(当然我并不是说必须用箭头语法构造)。我只是想知道为什么 OP 想要区分“箭头函数”和“内置/主机函数”,而它们的行为可能没有任何区别。
2021-04-13 10:28:46

更新

最初,我使用正则表达式实现了 Kangax 的解决方案,但是,正如一些人指出的那样,存在一些误报和陷阱情况,表明我们需要更彻底的方法。

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

如果 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*\())/.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 上测试)
2021-03-27 10:28:46
谢谢@Jens。更新
2021-04-06 10:28:46

我是为 Node 写的,应该也适用于 Chrome。

检测到“边界”(显然,仅在 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 个的对象字面量。代码行数也减半:)
2021-04-04 10:28:46
只需要添加针对类、异步、生成器函数的测试
2021-04-08 10:28:46

ECMAScript 放弃了对宿主对象的很多保证,因此扩展了宿主函数。这使得通过反射访问的属性主要依赖于实现,几乎没有一致性保证,至少就 ecmascript 规范而言,W3C 规范可能更具体地针对浏览器主机对象。

例如看

8.6.2 对象内部属性和方法

表 9 总结了本规范使用的仅适用于某些 ECMAScript 对象的内部属性。[...] 宿主对象可以支持具有任何依赖于实现的行为的这些内部属性,只要它与本文档中规定的特定宿主对象限制一致。

所以内置函数可能是可调用的,但没有原型(即不是从函数继承)。或者他们可以拥有一个。

规范说他们的行为可能不同。但它们也可能实现所有标准行为,使它们与正常功能无法区分。

请注意,我引用的是 ES5 规范。ES6 仍在修订中,本机和宿主对象现在被称为外来对象。但规范几乎是一样的。它提供了一些甚至它们必须满足的不变量,但除此之外只说明它们可能会或可能不会满足所有可选行为。

基于文件的mozilla.org,并要考虑到以下副作用,Use of the new operator并且该页面,我们可以试着这样做:

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)不幸的是也是如此,另见stackoverflow.com/questions/28222228/...
2021-03-27 10:28:46