揭秘Babel-polyfill如何把普通函数伪装成内置函数?"function () { [native code] }"

1,508 阅读3分钟

揭秘Babel-polyfill如何把普通函数伪装成内置函数?

"function () { [native code] }"

最近在学习大前端的过程中,使用vue或者react开发前端项目时,为了兼容浏览器,一般都会添加babel-polyfill

在IE浏览器中,不支持 Promise,Symbol,Reflect等等内置函数和对象,添加babel-polyfill之后,控制台查看Promise函数,输出的竟然是

console.log(Promise)

function Promise() { [native code] }

要看Promise如何实现的可以看github这里的polyfill

github.com/stefanpenne…

github.com/then/promis…


顿时就震惊了,它竟然伪装成了内置函数?

研究了一下babel-polyfill的源码,找到了答案,有了一些心得,分享一下

文章的最后还有我总结的三种伪装内置函数的方法

由于ie浏览器的调试工具不好用, 在chrome浏览器中测试一下,

chrome浏览器中是没有内置setImmediate函数的,看一下babel-polyfill如何实现内置函数的伪装的

import('https://cdn.bootcss.com/babel-polyfill/7.4.4/polyfill.js')
.then(()=>{console.log(window.setImmediate)})

ƒ setImmediate() { [native code] }

查看一下它的属性

console.log([setImmediate])


[ƒ]
0: ƒ setImmediate()
Symbol(src)_1.rlpsogvo8a: "function setImmediate() { [native code] }"
arguments: (...)
caller: (...)
length: 1
name: "setImmediate"
prototype: {constructor: ƒ}
__proto__: ƒ ()
[[FunctionLocation]]: polyfill.js:2434
[[Scopes]]: Scopes[2]
length: 1
__proto__: Array(0)

发现了Symbol(src)_1.rlpsogvo8a这个属性,

这是用Symbol的polyfill生成的Symbol(src)对象

在源代码中找到了这么一段

{
118: [function(_dereq_, module, exports) {
        var global = _dereq_(70);
        var hide = _dereq_(72);
        var has = _dereq_(71);
        var SRC = _dereq_(147)('src');
        var $toString = _dereq_(69);
        var TO_STRING = 'toString';
        var TPL = ('' + $toString).split(TO_STRING);

        _dereq_(52).inspectSource = function(it) {
            return $toString.call(it);
        }
        ;

        (module.exports = function(O, key, val, safe) {
            var isFunction = typeof val == 'function';
            if (isFunction)
                has(val, 'name') || hide(val, 'name', key);
            if (O[key] === val)
                return;
            if (isFunction)
                has(val, SRC) || hide(val, SRC, O[key] ? '' + O[key] : TPL.join(String(key)));
            if (O === global) {
                O[key] = val;
            } else if (!safe) {
                delete O[key];
                hide(O, key, val);
            } else if (O[key]) {
                O[key] = val;
            } else {
                hide(O, key, val);
            }
            // add fake Function#toString for correct work wrapped methods / constructors with methods like LoDash isNative
        }
        )(Function.prototype, TO_STRING, function toString() {
            return typeof this == 'function' && this[SRC] || $toString.call(this);
        });

    }
    , {
        "147": 147,
        "52": 52,
        "69": 69,
        "70": 70,
        "71": 71,
        "72": 72
    }],
    }
  { 
  147: [function(_dereq_, module, exports) {
        var id = 0;
        var px = Math.random();
        module.exports = function(key) {
            return 'Symbol('.concat(key === undefined ? '' : key, ')_', (++id + px).toString(36));
        }
        ;

    }, {}],
    }
{
 69: [function(_dereq_, module, exports) {
        module.exports = _dereq_(126)('native-function-to-string', Function.toString);

    }
    , {
        "126": 126
    }],
    }
{
 30: [function(_dereq_, module, exports) {
        module.exports = function(bitmap, value) {
            return {
                enumerable: !(bitmap & 1),
                configurable: !(bitmap & 2),
                writable: !(bitmap & 4),
                value: value
            };
        }
        ;

    }
    , {}],
    }

看到这里明白了Function.prototype.toString被修改了,先查找函数的Symbol(src)属性,如果存在就返回这个值,否则返回函数原先的toString方法的返回值,

控制台打印一个函数时,会去寻找函数的toString方法的返回值

而在定义setImmediate函数时,先定义了它的Symbol(src)属性,然后隐藏这个属性,并且这个属性值不可修改和删除


接下来看看setImmediate是如何实现的吧?

先是判断当前的环境是哪种浏览器环境还是nodejs环境还是webworker环境,

使用process.nextTick或者Dispatch或者MessageChannel或者addEventListenerpostMessage实现

或者创建script标签设置onreadystatechange事件监听,

最后使用setTimeout实现

{ 
136: [function(_dereq_, module, exports) {
        var ctx = _dereq_(54);
        var invoke = _dereq_(76);
        var html = _dereq_(73);
        var cel = _dereq_(59);
        var global = _dereq_(70);
        var process = global.process;
        var setTask = global.setImmediate;
        var clearTask = global.clearImmediate;
        var MessageChannel = global.MessageChannel;
        var Dispatch = global.Dispatch;
        var counter = 0;
        var queue = {};
        var ONREADYSTATECHANGE = 'onreadystatechange';
        var defer, channel, port;
        var run = function() {
            var id = +this;
            // eslint-disable-next-line no-prototype-builtins
            if (queue.hasOwnProperty(id)) {
                var fn = queue[id];
                delete queue[id];
                fn();
            }
        };
        var listener = function(event) {
            run.call(event.data);
        };
        // Node.js 0.9+ & IE10+ has setImmediate, otherwise:
        if (!setTask || !clearTask) {
            setTask = function setImmediate(fn) {
                var args = [];
                var i = 1;
                while (arguments.length > i)
                    args.push(arguments[i++]);
                queue[++counter] = function() {
                    // eslint-disable-next-line no-new-func
                    invoke(typeof fn == 'function' ? fn : Function(fn), args);
                }
                ;
                defer(counter);
                return counter;
            }
            ;
            clearTask = function clearImmediate(id) {
                delete queue[id];
            }
            ;
            // Node.js 0.8-
            if (_dereq_(48)(process) == 'process') {
                defer = function(id) {
                    process.nextTick(ctx(run, id, 1));
                }
                ;
                // Sphere (JS game engine) Dispatch API
            } else if (Dispatch && Dispatch.now) {
                defer = function(id) {
                    Dispatch.now(ctx(run, id, 1));
                }
                ;
                // Browsers with MessageChannel, includes WebWorkers
            } else if (MessageChannel) {
                channel = new MessageChannel();
                port = channel.port2;
                channel.port1.onmessage = listener;
                defer = ctx(port.postMessage, port, 1);
                // Browsers with postMessage, skip WebWorkers
                // IE8 has postMessage, but it's sync & typeof its postMessage is 'object'
            } else if (global.addEventListener && typeof postMessage == 'function' && !global.importScripts) {
                defer = function(id) {
                    global.postMessage(id + '', '*');
                }
                ;
                global.addEventListener('message', listener, false);
                // IE8-
            } else if (ONREADYSTATECHANGE in cel('script')) {
                defer = function(id) {
                    html.appendChild(cel('script'))[ONREADYSTATECHANGE] = function() {
                        html.removeChild(this);
                        run.call(id);
                    }
                    ;
                }
                ;
                // Rest old browsers
            } else {
                defer = function(id) {
                    setTimeout(ctx(run, id, 1), 0);
                }
                ;
            }
        }
        module.exports = {
            set: setTask,
            clear: clearTask
        };

    }
    , {
        "48": 48,
        "54": 54,
        "59": 59,
        "70": 70,
        "73": 73,
        "76": 76
    }],
    
}

还有其他方法伪装内置函数吗?

有以下几种方法实现

1.给函数绑定this


 (()=>{}).bind().toString()
 
"function () { [native code] }"

或者

2.用Proxy代理函数

new Proxy(()=>{},{}).toString()

"function () { [native code] }"

3.修改函数的toString方法

function testa(){return 'aaaaaaaa'}

testa.toString=(()=>{}).toString.bind((()=>{}).bind())


console.log(testa)

ƒ () { [native code] }

testa.toString()

"function () { [native code] }"

或者简单粗暴的设置返回一个字符串也行

function testa(){return 'aaaaaaaa'}

testa.toString=()=>"function () { [native code] }"