Advertisement

探索在 JS 中,为什么要在函数前面加

阅读量:

简介

我们基本都知道,函数的声明方式有这两种

复制代码
    function msg(){alert('msg');}//声明式定义函数
复制代码
复制代码
    var msg = function(){alert('msg');}//函数赋值表达式定义函数
复制代码

但其实还有第三种声明方式,Function构造函数

var msg = new function(msg) {

alert('msg')

}

复制代码

等同于

function msg(msg) {

alert('msg')

}

复制代码

函数的调用方式通常是方法名()

但是,如果我们尝试为一个“定义函数”末尾加上(),解析器是无法理解的。

function msg(){`` alert('message');``}();//解析器是无法理解的

复制代码

说明函数的调用方式应该是print(); 那么是否仅仅通过将函数体内容置于括号内就能完成调用了呢?

最初采用将定义的函数体包裹在括号内的方法时,并不会影响到解析器的行为。实际上,在这种情况下, 解析器会采用基于标准程序处理机制的方式来执行对定义功能体的调用. 这意味着, 任何能够将某个功能转化为一个可表示为标准程序处理形式的做法都会使解析器正确地执行相关功能. 其中就包括!这一类操作符, 而像+/-||~这类符号也具有类似的特性.

但是请注意,在将函数体放入括号并立即执行的情况下,则只能在单次调用该函数时使用这种方法;这涉及到作用域相关的问题。当试图将该函数重复使用时,则会遇到相应的作用域限制问题:

如果需要调用该函数的话,则可以在先声明后调用,在同一个作用域内从而可以在同一作用域内再次使用它。

​​​​​​​var msg = function(msg) {}``msg();

复制代码

关于这个问题,后面会进一步分析

function前面加 ! ?

自执行匿名函数:

在很多js代码中我们常常会看见这样一种写法:


复制代码
    (function( window, undefined ) {    // code})(window);
复制代码

我们将其称为自执行型匿名函数。其名称即反映了这一特性:它自身即可完成操作。在该表达式中,默认情况下会将前一个括号定义的一个嵌套匿名函数立即触发;而后一个括号则用于立即触发该操作。

前面也提到 + - || ~这些运算符也同样有这样的功能


复制代码
    (function () { /* code */ } ()); !function () { /* code */ } ();  ~function () { /* code */ } (); -function () { /* code */ } ();+function () { /* code */ } ();
复制代码

① ( ) 没什么实际意义,不操作返回值

② ! 对返回值的真假取反

对返回值施以取反运算(其中一项操作)。具体而言,在处理正整数值时会将其转换为与其加一后的负数值相对应;而对于负整数值,则会将其转换为其加一后的绝对数值;对于零,则会直接变为-1。此外,在执行此操作前会对输入进行强制类型转换:若输入为字符串形式(如"5"),则将其解析为对应的数字5;若输入为布尔型变量,则分别映射false为0和true为1;对于无法解析为数字或其他非数字类型的变量,则默认视为0处理)

~运算符+和-执行数值运算于返回值上(可以看出,在返回值为非数字类型时,+和-会将返回值被强制转换为数值)。

先从IIFE开始介绍 (注:这个例子是参考网上)

IIFE(Imdiately Invoked Function Expression 立即执行的函数表达式)


复制代码
    function(){    alert('IIFE');}
复制代码

把这个代码放在console中执行会报错

由于这是一个无名函数,在没有指定名称的情况下,默认行为可能导致问题;因此必须为其指定一个名称,并通过该名称进行调用。

在匿名函数前面添加一些符号后,并将一个函数声明语句转换为一个函数表达式,在script标签中就会自动执行

所以现在很多对代码压缩和编译后,导出的js文件通常如下:

复制代码
    (function(e,t){"use strict";function n(e){var t=e.length,n=st.type(e);return st.isWindow(e)?!1:1===e.nodeType&&t?!0:"array"===n||"function"!==n&&(0===t||"number"==typeof t&&t>0&&t-1 in e)}function r(e){var t=Tt[e]={};return st.each(e.match(lt)||[],function(e,n){t[n]=!0}),t}function i(e,n,r,i){if(st.acceptData(e)){var o,a,s=st.expando,u="string"==typeof n,l=e.nodeType,c=l?st.cache:e,f=l?e[s]:e[s]&&s;if(f&&c[f]&&(i||c[f].data)||!u||r!==t)return f||(l?e[s]=f=K.pop()||st.guid++:f=s),c[f]||(c[f]={},l||(c[f].toJSON=st.noop)),("object"==typeof n||"function"==typeof n)&&(i?c[f]=st.extend(c[f],n):c[f].data=st.extend(c[f].data,n)),o=c[f],i||(o.data||(o.data={}),o=o.data),r!==t&&(o[st.camelCase(n)]=r),u?(a=o[n],null==a&&(a=o[st.camelCase(n)])):a=o,a}}function o(e,t,n){if(st.acceptData(e)){var r,i,o,a=e.nodeType,u=a?st.cache:e,l=a?e[st.expando]:st.expando;if(u[l]){if(t&&(r=n?u[l]:u[l].data)){st.isArray(t)?t=t.concat(st.map(t,st.camelCase)):t in r?t=[t]:(t=st.camelCase(t),t=t in r?[t]:t.split(" "));for(i=0,o=t.length;o>i;i++)delete r[t[i]];if(!(n?s:st.isEmptyObject)(r))return}(n||(delete u[l].data,s(u[l])))&&(a?st.cleanData([e],!0):st.support.deleteExpando||u!=u.window?delete u[l]:u[l]=null)}}}function a(e,n,r){if(r===t&&1===e.nodeType){var i="data-"+n.replace(Nt,"-$1").toLowerCase();if(r=e.getAttribute(i),"string"==typeof r){try{r="true"===r?!0:"false"===r?!1:"null"===r?null:+r+""===r?+r:wt.test(r)?st.parseJSON(r):r}catch(o){}st.data(e,n,r)}else r=t}return r}function s(e){var t;for(t in e)if(("data"!==t||!st.isEmptyObject(e[t]))&&"toJSON"!==t)return!1;return!0}function u(){return!0}function l(){return!1}function c(e,t){do 
复制代码

运算符

可能会有疑问的是,运算符如何将声明式函数转换为函数表达式?这涉及到一个概念解析器

该系统之前的开发流程较为复杂,在设计阶段就需要完成对原始代码进行分析并生成中间代码这一重要环节。具体而言,在进行代码转换时需要先对原始编码进行语义分析并生成相应的中间表示形式;随后再将其转化为机器可执行的形式即目标代码

那么什么是解析器?

通常来说,在计算机科学中,所谓解析器(Parser)是一种将特定格式的输入(如字符串)转化为可执行操作的机制。最常见的应用场景是将程序文本转化为编译器内部的一种信息组织形式——抽象语法树(AST)。这种机制有时也被称为语法分析器或词法分析器。此外,在实际应用中还存在一些基础的解析工具,它们主要负责处理常见的数据格式如CSV文件、JSON对象以及XML文档等类型的信息处理任务

在JS解析器执行第一步预解析的过程中(...),它会从代码的开头到末尾进行扫描(...),只检测出var、function以及参数的相关信息(...)。通常我们会将这一阶段统称为('JavaScript' 的预处理阶段)。具体来说,在这一阶段结束时('end of this phase'),所有被检测到的变量都会被预先赋值为"undefined"状态;而所有被检测到的功能则会在程序运行开始前完整地包含起始点至结束点之间的全部内容('entire function body')。为了使程序能够正确识别并处理后续出现的各种表达式('expression'),我们通常需要在此基础上添加一些特殊的标记或符号('special operator')。例如,在前面所提到的例子中就使用了这种特殊的运算符来实现功能

解析过程大致如下:

1、“找一些东西”: var、 function、 参数;(也被称之为预解析)

注释:当出现变量与函数同名时,请仅保留其对应的唯一标识符;若在同一个命名空间内存在多个具有相同名称的两个或多个目标类型,则按照该命名空间中实际运行时的目标类型调用顺序确定最后被调用的目标类型,并仅保留其对应的目标类型标识符。

2、逐行解读代码。

备注:该表达式的值可以在预计算阶段进行调整(个人根据需求进行优化)。(这将是后续部分会涉及的内容)

函数声明与函数定义

函数声明 一般相对规范的声明形式为:fucntion msg(void) 注意是有分号

复制代码
    function msg() 
复制代码

函数定义 function msg()注意没有分号


复制代码
    {    alert('IIFE');}
复制代码

函数调用

这样是一个函数调用

复制代码
    msg();
复制代码

函数声明加一个()就可以调用函数了


复制代码
    function msg(){    alert('IIFE');}()
复制代码

就这样
但是我们按上面在console中执行发现出错了

由于这种代码会混淆函数声明与函数调用,在这种情况下定义的函数 msg 必须以 msg() 的方式被调用。

若采用(function msg())()的方式构造这样一个实体:(函数体)(IIFE),JavaScript解析器能够正确识别并正常执行该结构。

从Js解析器的预解析过程了解到:

大多数解析器都有能力识别特定模式,并通过将函数封装在括号内来实现这一目标。通常情况下,在这种情况下对程序分析非常有利。也就是说,在这种情况下要求相应的函数必须立即被执行。当一个解析器遇到一个左括号时(即遇到一对连续的左右括号),它会立刻开始处理紧跟其后的函数声明部分。为了优化程序分析的速度并提高效率,在处理这类情况时建议明确地定义这些必须立即执行的函数

这表明括号的功能是定义一个函数声明,并使解析器将其视为一个表达式。最终由程序运行该函数。

总结

所有用于消除函数声明与表达式间歧义的方法都能够被JavaScript解析器准确处理。

赋值操作不仅仅是简单的赋值和逻辑运算,并且还包含多种运算符。尤其在编程中使用解析器时,在处理语法结构时会遇到逗号和其他特殊符号的问题。一种情况是使用!function() 来表示函数调用的否定;另一种情况则是将(function())转换为表达式。

测试

在选择优先级时, 采用该方法, 而其他运算符则需多执行一步计算过程, 即意味着相较于而言,则需额外增加一个计算步骤., 例如,在进行类似+(表达式)的操作时, 直接完成加法操作的过程., 经过测试发现:

结论

通过截图分析实验结果可以看出,在速度上高出一个数量级的是IIFE方式。已知立即执行的时间复杂度为O(n),而运算符的时间复杂度则为O(10n),这一结论基于初步实验数据得出。需要注意的是,在现有的浏览器解析速度下(时间基数小到可忽略),这种差异表现得更为明显。根据不同的使用场景和开发者偏好(萝卜白菜各有各的好处),选择哪种实现方式并无绝对。

全部评论 (0)

还没有任何评论哟~