JS 我只是想创建一个函数(声明函数 函数表达式 构造函数)

1,322 阅读6分钟

JS作为一种弱类型语言,在定义函数时相对于其他语言比较灵活,有多种声明函数的方式,方法不同,自然会出现一些细微的差别,本篇内容就一起来探探坑。

一、 常用的function语句(声明函数)

// 语法格式:fn1 - 函数名, console语句 - 函数体
    function fn1(name) {
        console.log(`${name}的函数体`);
    }

二、 函数表达式(函数字面量)

// 语法格式:fn2 - 变量名, function - 匿名函数, console语句 - 函数体
    var fn2 = function(name) {
        console.log(`${name}的函数体`);
    }

三、 Function() 构造函数

// 语法格式:fn3 - 变量名, Function - 构造函数
    var fn3 = new Function('name','console.log(`${name}的函数体`)');

四、执行结果

    fn1('fn1'); // fn1的函数体
    fn2('fn2'); // fn2的函数体
    fn3('fn3'); // fn3的函数体

五、 你已经达到目的,成功的声明了函数

我只是想声明一个函数啊,怎么有点懵,一下子整出来三个,看起来都很好用完全没问题了啊。

接下来我们放入一个简单场景,求和。

好吃的栗子:

 // 简单,求两个数的和
    console.log(fn1(10, 10)); // 20
    console.log(fn2(10, 10)); // Uncaught TypeError: fn2 is not a function

    function fn1(a, b) {
        return a + b;
    }
    
    var fn2 = function 函数名(a, b) {
        return a + b;
    }

不对啊,明明刚才都好用...

    console.log(typeof fn1); // function
    console.log(typeof fn2); // undefined

看一眼fn1fn2是啥

    console.log(fn1); // 'ƒ fn1(a, b) {return a + b;}'
    console.log(fn2); // 'ƒ (a, b) {return a + b;}'

函数位于一个初始化语句中,不是一个函数声明,不会被提前,而只会把var fn2提前,用typeof操作符显示fn2undefined,所以报错(fn3同理)。

注意: 在fn2中出现的函数名同样不能够被访问,存在的原因是 - 如果该函数体内的代码报错,会提示该函数名; 递归匿名函数时你需要它。(手动滑稽)

原理: 解释器会率先读取函数声明,并使其在执行之前可以访问,而使用表达式则必须等到解析器执行到它所在的代码行,才会真正被解释执行,函数声明会正常执行

但是....既然是匿名函数,那完全可以这么写:

// 声明后立即执行
    var fn2 = function(a, b) {
        return a + b;
    }(10, 25)

    console.log(fn2); // 35
    function fn1(a, b) {
        return a + b;
    }(10, 25)
  // Uncaught SyntaxError: Unexpected token )

补1(如果我就想让声明函数自调用怎么办呢)

我只是想声明一个函数啊,为什么要搞这么多事情,那new Function()总是没毛病的吧,大家再怎么牛,都是继承这位来的。

通过函数表达式定义的函数和通过函数声明定义的函数只会被解析一次,而Function构造函数定义的函数却不同。也就是说,每次构造函数被调用,传递给Function构造函数的函数体字符串都要被解析一次 。虽然函数表达式每次都创建了一个闭包,但函数体不会被重复解析,因此函数表达式仍然要快于new Function(...)。 所以Function构造函数应尽可能地避免使用。 来源:MDN

汉译汉: 简单粗暴的翻译一下,同内容下,声明函数最快,直接调用,函数表达式每次调用都会创建一个闭包,但是函数体不会再次解析,new表达式每次调用函数体都要解析一遍。

为什么会每次都解析一遍函数体呢? 因为JS解释器在解析代码时是分块执行,无论声明还是还是函数表达式,在第一次被解析完成后,都被看做'一块代码',构造函数采用'动态解析',每次都认为是全新的字符串,重新解析。 (但可是,如果构造函数中含有function,那么这里的function不会被再次解析)

六、道理我都懂了,别问,问就声明函数创建起来

好的,那么看一下这段代码 可怕的栗子:

    var isSay= true;
    if (isSay) {
        function say() {
            console.log('Hello')
        }
    }
    say(); // Hello

完全没毛病,你想干啥?(声明函数可以在执行代码前声明,函数表达式要执行到才可以赋值函数体)

那么。。。

    say(); // Uncaught TypeError: say is not a function
    var isSay= true;
    if (say) {
        function say() {
            console.log('Hello')
        }
    }

不对啊亲,我老老实实凭实力声明的函数,怎么就不好用了啊。 当然还有。。。

    play(); // Uncaught TypeError: play is not a function
    for( let item of new Array(10)) {
        function play() {
            console.log('happy')
        }
    }

函数声明非常容易(经常是意外地)转换为函数表达式。当它不再是一个函数声明: 成为表达式的一部分,不再是函数或者脚本自身的“源元素” (source element)。“源元素”是脚本或函数体中的非嵌套语句。 来源:MDN

有多容易转变呢,基本上除了脚本或函数体以外,嵌套进去了自己就变了。。。

七、最后一个坑了,填完就创建

上面简单而艰难的创建过程,我们似乎忽略了一个情况,那就是同名函数不同声明,到底谁是老大(优先级);

眼花的栗子:

    // fn1(); // fn1声明2

    var fn1 = function() {
        console.log('fn1 表达式1')
    }

    function fn1() {
        console.log('fn1声明1')
    }

    var fn1 = function() {
        console.log('fn1 表达式2')
    }

    function fn1() {
        function fn1() {
            console.log('fn1声明3')
        }
        console.log('fn1声明2')
    }

    fn1(); // fn1 表达式2

总结:

  • 最上方调用fn1时,函数表达式未执行,var变量提升但未报错,说明函数声明的提升比var要高。(说了解析前就声明了,var也只能算解析中)
  • 第二个同名fn1声明函数的函数体覆盖第一个;
  • 最下方调用fn1时,声明函数已经优先声明fn1后被赋值,最终赋值有效;
  • 什么?你说构造函数?不是告诉你不要用了吗。(应用场景决定它不会被如此错误的使用;补2)

终于,可以安心 不报错 创建函数了

补1:

  • 声明函数无法直接通过()自调用是因为解析器找不到函数名,又没有对应的函数表达式,所以可以通过将声明函数全部用()括起来或者以+ ~ !等特殊符号加在function关键字前,将声明函数转为表达式即可。

补2:

  • new Function通过实例化一个Function原型,得到一个数据类型为function的对象,也就是一个函数,而该变量就是函数名,这本来和我们最初的目标创建一个函数时符合的,但是new操作符本身会改变关键字this指向,会对创建的函数造成未知影响。
  • new Function时,函数体内的字符串会被解析为脚本语言并执行,也是有可能对日常开发产生影响的原因之一,因此在日常开发中不建议使用构造函数

引用:

  1. MDN