检查函数是否为生成器

68

我在Nodejs v0.11.2中尝试了生成器(generators),我想知道如何检查传递给我的函数的参数是否是生成器函数。

我发现这种方法 typeof f === 'function' && Object.getPrototypeOf(f) !== Object.getPrototypeOf(Function),但我不确定这是一个好方法(并且在未来是否有效)。

你对这个问题有什么看法?


2
根据当前ES6草案的15.19.3.1生成器函数构造函数,我相当确定f instanceof GeneratorFunction应该可以工作。 - user1106925
2
那个构造函数似乎是最新草案的新添加。在搜索之前的版本时,我没有找到那段文字。假设它保留在规范中,我想它会在某个时候出现。编辑: ...啊,是的,我在更改说明中看到了它“为生成器函数和生成器方法定义添加语义”...所以看起来它只是在大约10天前发布。 - user1106925
2
我看到这个更改被从v8中移除了,因为存在一些测试问题 https://github.com/v8/v8/commit/a5d33556b2893a594a26c91fdc74986369be4ba0 - Dima Vidmich
@Funkodebat:你把返回值(一个迭代器)和创建返回值的机制(生成器函数与其他几种从非生成器函数返回的迭代器创建方式)混淆了。还有那些毫无建设性的“粉丝”胡言乱语。我真的不应该在上面说“只是一个返回迭代器的函数”,这是错误的,我已经删除了。但是Erik的观点仍然有效:除了说“是的,这是一个生成器函数”之外,你无法从这些信息中获得任何有用的东西。 - T.J. Crowder
由于此问题特定于node v0.11.2,我不知道答案。但是,更新的版本可以表现得很好(即使绑定)。https://dev59.com/KoTca4cB1Zd3GeqPA-2p#36972162 - niry
显示剩余3条评论
13个回答

59

我们在 TC39 的面对面会议上讨论过这个问题,故意不公开一种检测函数是否为生成器的方法。原因是任何函数都可以返回一个可迭代对象,因此它是函数还是生成器函数并不重要。

var iterator = Symbol.iterator;

function notAGenerator() {
  var  count = 0;
  return {
    [iterator]: function() {
      return this;
    },
    next: function() {
      return {value: count++, done: false};
    }
  }
}

function* aGenerator() {
  var count = 0;
  while (true) {
    yield count++;
  }
}

这两个行为是相同的(减去 .throw(),但它也可以添加)


5
哇……太糟糕了:( 无法确定是生成器函数还是简单函数将不允许像集成 Primise 库(如 Q.async)这样的好东西自动检测生成器并获取/推送值,以便基于生成器拥有漂亮干净的“Primise”API。 - Valentyn Shybanov
2
@Erik Arvidsson,我们在哪里可以找到Symbol函数的文档? - Lex Podgorny
3
需要说明的是,即使使用最新的Node.js开发版本,这段代码仍然无法工作,我会收到一个Unexpected token [错误提示在[iterator]: function() {处。这是怎么回事? - Yanick Rochon
@YanickRochon 这是新的对象表示法,你可以构造一个对象并使用方括号表示法 - 例如 var obj = {}; obj.next = function(){...}; obj[iterator] = () => this; - Benjamin Gruenbaum
1
@Erik,你是说生成器函数只是函数的一种特殊类别,而不是什么不同的东西?那么,也许我们可以通过检查一个函数是否具有所有生成器的特征(包含next[iterator]的对象返回,next返回valuecount等),来判断它是否为生成器。这样做能够在可预见的未来始终有效吗? - trysis
显示剩余5条评论

53
在最新版本的nodejs中(我已验证v0.11.12),您可以检查构造函数名称是否等于GeneratorFunction。我不知道这是在哪个版本中推出的,但它有效。
function isGenerator(fn) {
    return fn.constructor.name === 'GeneratorFunction';
}

你可以使用 obj.constructor.name 来检查一个对象的“类”,参见 https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/Function/name,但需要注意一些限制,请参考 https://dev59.com/6F0b5IYBdhLWcg3wVv-9。 - user5321531
2
这仅适用于函数声明和匿名函数,不适用于命名函数表达式。 - trysis
5
很棒的解决方案,感谢!根据今天的 JS 进行更新:const isGenerator = fn => ['GeneratorFunction', 'AsyncGeneratorFunction'].includes(fn.constructor.name)。异步生成器是 ES2018 的一部分,在 node v10 中可用,参见 node.green - tleb
请参考以下答案:“我们在TC39面对面的会议中讨论过这个问题,故意不提供一种方法来检测一个函数是否是生成器。”你的解决方案是滥用和特定实现。 - snnsnn

15

这在 node 和 Firefox 中都可以运行:

var GeneratorFunction = (function*(){yield undefined;}).constructor;

function* test() {
   yield 1;
   yield 2;
}

console.log(test instanceof GeneratorFunction); // true

jsfiddle

但是如果你绑定了一个生成器,它就不起作用了,例如:

foo = test.bind(bar); 
console.log(foo instanceof GeneratorFunction); // false

对我来说,在Chromium 76和node 10中,绑定的生成器也可以工作。 - jchook

9
我正在使用这个:

var sampleGenerator = function*() {};

function isGenerator(arg) {
    return arg.constructor === sampleGenerator.constructor;
}
exports.isGenerator = isGenerator;

function isGeneratorIterator(arg) {
    return arg.constructor === sampleGenerator.prototype.constructor;
}
exports.isGeneratorIterator = isGeneratorIterator;

2
我将其简化为 Generator = (function*(){}).constructor; g instanceof Generator,不幸的是 (function*(){}).prototype.constructor 不是检查生成器迭代器的 instanceof 的有效参数。 - Nick Sotiros

7

在 Node 7 中,您可以使用 instanceof 对构造函数进行检测,以便检测生成器函数和异步函数:

const GeneratorFunction = function*(){}.constructor;
const AsyncFunction = async function(){}.constructor;

function norm(){}
function*gen(){}
async function as(){}

norm instanceof Function;              // true
norm instanceof GeneratorFunction;     // false
norm instanceof AsyncFunction;         // false

gen instanceof Function;               // true
gen instanceof GeneratorFunction;      // true
gen instanceof AsyncFunction;          // false

as instanceof Function;                // true
as instanceof GeneratorFunction;       // false
as instanceof AsyncFunction;           // true

在我的测试中,这适用于所有情况。上面的评论说它不适用于命名生成器函数表达式,但我无法复现:

const genExprName=function*name(){};
genExprName instanceof GeneratorFunction;            // true
(function*name2(){}) instanceof GeneratorFunction;   // true

唯一的问题是实例的.constructor属性可以被更改。如果有人下定决心给你制造麻烦,他们可以打破它:
// Bad people doing bad things
const genProto = function*(){}.constructor.prototype;
Object.defineProperty(genProto,'constructor',{value:Boolean});

// .. sometime later, we have no access to GeneratorFunction
const GeneratorFunction = function*(){}.constructor;
GeneratorFunction;                     // [Function: Boolean]
function*gen(){}
gen instanceof GeneratorFunction;      // false

对我有用。思路很棒!当然,还有Nick Sotiros的答案,比你早2年。 - MasterBob
1
如果您使用异步生成器函数,则此答案将失败,只会返回1个true:async function*asgen(){}。 - Ferrybig

6

TJ Holowaychuk的co库拥有最佳函数来检查是否为生成器函数。以下是源代码:

function isGeneratorFunction(obj) {
   var constructor = obj.constructor;
   if (!constructor) return false;
   if ('GeneratorFunction' === constructor.name || 'GeneratorFunction' === constructor.displayName) return true;
   return isGenerator(constructor.prototype);
}

参考:https://github.com/tj/co/blob/717b043371ba057cb7a4a2a4e47120d598116ed7/index.js#L221

这是一个关于it技术的参考链接。请点击上方的链接进行查看。

5

正如 @Erik Arvidsson 所述,目前并没有标准的方法来检查一个函数是否为生成器函数。但是你可以确定地检查它是否符合生成器函数的接口:

function* fibonacci(prevPrev, prev) {

  while (true) {

    let next = prevPrev + prev;

    yield next;

    prevPrev = prev;
    prev = next;
  }
}

// fetch get an instance
let fibonacciGenerator = fibonacci(2, 3)

// check the interface
if (typeof fibonacciGenerator[Symbol.iterator] == 'function' && 
    typeof fibonacciGenerator['next'] == 'function' &&
    typeof fibonacciGenerator['throw'] == 'function') {

  // it's safe to assume the function is a generator function or a shim that behaves like a generator function

  let nextValue = fibonacciGenerator.next().value; // 5
}

这就是它。


我本来想检查 fn.constructor.name,但由于该函数是通过 Proxy 传递的,它被报告为常规函数...所以我不得不按照你建议的方法进行协同处理。 - Endless
2
如果它 Symbol.iterator 看起来像鸭子,next 像鸭子,throw 像鸭子,那么...... - Seth

3
function isGenerator(target) {
  return target[Symbol.toStringTag] === 'GeneratorFunction';
}

或者
function isGenerator(target) {
  return Object.prototype.toString.call(target) === '[object GeneratorFunction]';
}

3

传统的方法Object.prototype.toString.call(val)也可以使用。在Node版本11.12.0中,它返回[object Generator],但是最新版的Chrome和Firefox返回[object GeneratorFunction]

因此,可能会像这样:

function isGenerator(val) {
    return /\[object Generator|GeneratorFunction\]/.test(Object.prototype.toString.call(val));

}

2

我查看了一下 koa 是如何做到的,他们使用了这个库:https://github.com/ljharb/is-generator-function

你可以像这样使用它:

const isGeneratorFunction = require('is-generator-function');
if(isGeneratorFunction(f)) {
    ...
}

我将添加一行代码来展示库的实用性,但我仍然认为在这里提到解决所述问题的可重用库是有意义的。 - kraf

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