源码系列—is-generator-function

1,126 阅读3分钟

这是我参与更文挑战的第9天,活动详情查看:更文挑战

is-generator-function 是 Koa 的一个依赖包,用于判断是否是一个 Generator 函数,源码总共 38 行,逻辑并不复杂。 核心代码是下面这一块:

module.exports = function isGeneratorFunction(fn) {
    if (typeof fn !== 'function') {
        return false;
    }
    if (isFnRegex.test(fnToStr.call(fn))) {
        return true;
    }
    if (!hasToStringTag) {
        var str = toStr.call(fn);
        return str === '[object GeneratorFunction]';
    }
    if (!getProto) {
        return false;
    }
    if (typeof GeneratorFunction === 'undefined') {
        var generatorFunc = getGeneratorFunc();
        GeneratorFunction = generatorFunc ? getProto(generatorFunc) : false;
    }
    return getProto(fn) === GeneratorFunction;
};

首先使用 typeof 判断目标函数是否是函数类型,不是函数类型直接确定不是一个 Generator 函数。

是函数类型的话将该函数转成字符串,通过正则匹配判断,isFnRegexfnToStr 的定义如下所示:

var fnToStr = Function.prototype.toString;
var isFnRegex = /^\s*(?:function)?\*/;

Function.prototype.toString用于将整个函数的代码块转成字符串,我们做个简单测试如下所示:

let func1 = function* () {}
let func2 = function * () {}
let obj = {
    *fn() {}
}

console.log(fnToStr.call(func1)) // function* () {}
console.log(isFnRegex.test(fnToStr.call(func1))) // true
console.log(fnToStr.call(func2)) // function * () {}
console.log(isFnRegex.test(fnToStr.call(func2)))  // false
console.log(fnToStr.call(obj.fn)) // *fn() {}
console.log(isFnRegex.test(fnToStr.call(obj.fn))) // true

可以看到该正则表达式可以匹配出function**func方式定义的 Generator 函数,而function *方式定义的则无法判断。

所以如果正则匹配失败,就通过Object.prototype.toString方式判断。hasToStringTagtoStr 代码如下所示:

var toStr = Object.prototype.toString;
var hasToStringTag = typeof Symbol === 'function' && typeof Symbol.toStringTag === 'symbol';

由于 ES6 增加了 Symbol.toStringTag,通过该属性能够修改Object.prototype.toString返回值,如下所示:

let user = {
    [Symbol.toStringTag]: "User"
};
alert( {}.toString.call(user) ); // [object User]

所以只有在明确不存在 Symbol.toStringTag 的情况下,以 Object.prototype.toString得到的类型判断结果才可靠。

module.exports = function isGeneratorFunction(fn) {
    // ......
    if (!hasToStringTag) {
        var str = toStr.call(fn);
        return str === '[object GeneratorFunction]';
    }
    // ......
}

以上条件都不满足的情况下,进入最后一个步骤,判断该函数的 prototype 属性是否与 GeneratorFunction 的 prototype 属性一致。

首先判断环境是否存在Object.getPrototypeOf方法,如果不存在就直接返回false,如果存在Object.getPrototypeOf,就创建一个空的 Generator 函数,通过判断这个 Genertator 函数的 ptototype 与目标函数的 prototype 是否相等来判定目标函数是否是一个 Generator 函数。

var GeneratorFunction;  // 用于存储 Generator 函数的 prototype
var getProto = Object.getPrototypeOf;
var getGeneratorFunc = function () { // 该函数用于创建一个空的 Generator 函数
    if (!hasToStringTag) {
        return false;
    }
    try {
        return Function('return function*() {}')();
    } catch (e) {
    }
};

module.exports = function isGeneratorFunction(fn) {
    //......
    if (typeof GeneratorFunction === 'undefined') {
        // 创建一个空的 Generator 函数
        var generatorFunc = getGeneratorFunc(); 
        // 获取到 Generator 函数的 prototype
        GeneratorFunction = generatorFunc ? getProto(generatorFunc) : false;  
    }
    // 根据 prototype 判断目标函数是否是 Generator 函数
    return getProto(fn) === GeneratorFunction;  
}

总结

Generator 函数的判断逻辑如下:

  1. 通过 typeof 判断是否是函数类型,符合则进行步骤 2;
  2. 转成字符串,通过正则匹配,判断是否符合 Generator 函数定义方式,符合返回 true,不符合进行步骤 3;
  3. 判断当前环境是否具有Symbol.toString,没有就根据Object.prototype.toString方式判定是否是 Generator 函数,有Symbol.toString的话进行步骤 4;
  4. 判断环境是否具有Object.getPrototypeOf,没有就返回 false,有则通过当前函数的 prototype 是否与 Generator 函数的 prototype 一致来判定目标函数是否是 Generator 函数

对比

昨天写了co 源码解析一文,在 co 中也存在 Generator 函数的判断逻辑,实现思路与本文的思路还是不太一样,有兴趣的朋友可以对比看看。

// 判断 obj 是否是一个 Generator 函数
function isGeneratorFunction(obj) {
    var constructor = obj.constructor;
    if (!constructor) return false;
    if ('GeneratorFunction' === constructor.name || 'GeneratorFunction' === constructor.displayName) 
        return true;
    return isGenerator(constructor.prototype);
}

// 判断 obj 是否是一个 Generator 对象
function isGenerator(obj) {
    return 'function' == typeof obj.next && 'function' == typeof obj.throw;
}