文章已同步至掘金:https://juejin.cn/post/6844903929633832974
欢迎访问😃,有任何问题都可留言评论哦~
在JS中,如果我们定义了一个函数如下:
function fn(){ /* code */ }
或者
let fn = function(){ /* code */ }
当我们在调用时,都需要在后面加上一对圆括号,像这样:fn()
正如上面所写的那样,fn
相对于函数表达式function(){ /* code */ }
只是引用的变量
那么我们可以可以这样写吗?
function(){ /* code */ }()
如果这样的话,是会报错的,因为当圆括号为了调用函数而出现在函数后面时,无论在全局环境或者局部环境里遇到了这样的function
关键字。
默认的,他会将他当作是一个函数声明,而不是函数表达式,如果你不明确的告诉圆括号他是一个表达式,他会将其当作没有名字的函数声明并且抛出一个错误,因为函数声明需要一个名字。
那么如果我们加上函数名呢?
function fn(){ /* code */ }()
依然报错,因为这对圆括号和前面的声明语句没有任关系,而只是一个分组操作符,用来控制运算的优先级,这里的意思是小括号里面优先计算,所以上面代码等同于:
function fn(){ /* code */ }
()
立即执行函数(IIFE)
当我们声明了一个函数,可能不需要调用多次,并且可以只调用一次得到一个单一的值
显然上面的方法是不行的,那么怎么办呢?其实我们可以这样写:
(function () { /* code */ }());
或者这样
(function () { /* code */ })();
那么这两者又有什么区别呢?
其实这两者形式就是最开始写的那两种函数的形式:
- 函数声明:
function fn(){ /* code */ }
- 函数表达式:
let fn = function(){ /* code */ }
函数表达式后面可以加括号立即调用该函数,函数声明不可以,只能以 fn() 形式调用
所以我们可以这样写立即执行函数
写法
(function () { /* code */ }());
(function () { /* code */ })();
let i = function(){ /* code */ }();
let j = (function(){ /* code */ }());
true && function () { /* code */ }();
0, function(){ /* code */ }();
!function () { /* code */ }();
~function () { /* code */ }();
-function () { /* code */ }();
+function () { /* code */ }();
new function(){ /* code */ };
new function(){ /* code */ }(); // 带参数
可以看到,在 function 前面加 ! 、+ 、- 甚至是逗号等都可以起到立即执行的效果,而 () 、! 、+ 、- 、= 等运算符,都将函数声明转换成函数表达式,消除了 javascript 引擎识别函数表达式和函数声明的歧义,告诉 javascript 引擎这是一个函数表达式,不是函数声明,可以在后面加括号,并立即执行函数的代码。
其实加括号是最安全的做法,因为 ! 、+ 、- 等运算符还会和函数的返回值进行运算,有时造成不必要的麻烦。
IIFE 与 闭包
说到立即执行函数的话,顺便扯一下闭包
和普通函数传参一样,立即执行函数也可以传递参数。如果在函数内部定一个函数,而里面的那个函数能引用外部的变量和参数(闭包),我们就能用立即执行函数锁定变量保存状态。
下面用代码来做测试:
<div>
<ul>
<li><a>第一个超链接</a></li>
<li><a>第二个超链接</a></li>
</ul>
</div>
var elems = document.getElementsByTagName('a');
for(var i=0; i<elems.length; i++) {
elems[i].addEventListener('click', function (e) {
e.preventDefault();
alert('I am click Link ' + i);
}, 'false')
}
这段代码意图是点击第一个超链接提示“I am click Link 0”,点击第二个提示“I am click Link 1”。真的是这样吗? 不是,每一次都是“I am click Link 2”
为什么?因为i
的值没有被锁住,当我们点击链接的时候其实for
循环早已经执行完了,于是在点击的时候i的值已经是elems.length
了。
修改代码:
var elems = document.getElementsByTagName('a');
for(var i=0; i < elems.length; i++){
(function (LockedInIndex) {
elems[i].addEventListener('click', function (e) {
e.preventDefault();
alert('I am cliick Link ' + i);
}, 'false')
})(i)
}
这次可以正确的输出结果,i
的值被传给了LockedIndex
,并且被锁定在内存中,尽管for
循环之后i
的值已经改变,但是立即执行函数内部的LockedIndex
的值并不会改变。
当然也可以这样写:
var elems = document.getElementsByTagName('a');
for ( var i = 0; i < elems.length; i++ ) {
elems[ i ].addEventListener( 'click', (function( lockedInIndex ){
return function(e){
e.preventDefault();
alert( 'I am link ' + lockedInIndex );
};
})( i ), 'false' );
}
最好的方法就是用let
,如下:
var elems = document.getElementsByTagName('a');
for(let i=0; i<elems.length; i++) {
elems[i].addEventListener('click', function (e) {
e.preventDefault();
alert('I am click Link ' + i);
}, 'false')
}
关于let
具体的使用,请参考我的文章:
《JavaScript 变量的使用》
《ES6 新增内容总结》
模块模式
立即执行函数在模块化的时候也有用,用立即执行函数处理模块可以减少全局变量造成的空间污染,而是使用私有变量。
如下创建一个立即执行的匿名函数,该函数返回一个对象,包含要暴露给外部的属性i
,如果不实用立即执行函数就要多定义一个属性i
了,这个i
就会显示的暴露给外部,这样:counter.i
,这种方式明显不太安全。
var counter = (function(){
var i = 0;
return {
get: function(){
return i;
},
set: function(val){
i = val;
},
increment: function(){
return ++i;
}
}
}());
counter.get();//0
counter.set(3);
counter.increment();//4
counter.increment();//5
conuter.i;//undefined (`i` is not a property of the returned object)
i;//ReferenceError: i is not defined (it only exists inside the closure)
这里如果使用counter.i
来访问这个内部变量,会报错undefined
,因为i
并不是counter
的属性。
模块模式方法不仅相当的厉害而且简单。非常少的代码,你可以有效的利用与方法和属性相关的命名,在一个对象里,组织全部的模块代码,即最小化了全局变量的污染也创造了使用变量。
评论区