深入了解JavaScript中的匿名函数、立即执行函数和闭包
JS中函数两种命名方式:声明式和表达式
- 函数声明式
function fn(){
console.log("函数声明式");
}
- 函数表达式
var fn = function(){
console.log("函数表达式");
}
(function(){}())
,(function(){})()
这两种是js中立即执行函数的写法,函数表达式后加上()
可以被直接调用,但是把整个声明式函数用()包起来的话,则会被编译器认为是函数表达式,从而可以用()来直接调用,如(function fn(){/*...*/})()
。
但是如果这个括号加在声明式函数后面,如function fn(){/*...*/}()
,则会报错,很多博客说这种写法()
会被省略,但实际是会出错,因为不符合js的语法,所以想要通过浏览器的语法检查,就必须加点符号,比如()、+、!等,详细见下方。
匿名函数
JavaScript并不是面向对象的,所以不支持封装,但是可以 通过匿名函数实现封装。
匿名函数:没有函数名的函数,不能单独定义与使用,否则会js语法错误,至少用()包裹起来。
function(){
console.log("匿名函数");
}
匿名函数作用:
1.用作函数表达式
var sum = function(num1, num2){
return num1 + num2;
};
console.log(sum(2, 3));
2.作为返回值
function sum(num1, num2){
return function(){
return num1 + num2;
}
}
console.log(sum(2, 3)); //function
console.log(sum(2, 3)()); //5
3.用作定义对象方法
var obj = {
name: 'umaru',
age: 17,
fu: function() {
console.log(this.name + ' ' + this.age);
}
};
obj.fu();
4.作为回调函数
setTimeout(function() {
console.log('匿名函数作为回调函数');
}, 1000);
5.用于立即执行函数
在内容形成局部变量和局部函数,防止全局污染。
(function() {
console.log('立即执行函数是基于匿名函数创建的');
}());
6.用于DOM元素注册事件
<input type="button" value="Click me!" id="btn">
<script>
var btn = document.querySelector("#btn");
//给按钮注册点击事件
btn.onclick = function(){
console.log('Click event');
}
</script>
立即执行函数
立即执行函数(IIFE,Immediately-Invoked Function Expression),是一种在定义后就会立即执行的函数,其实质是一种语法。
立即执行函数形式
1. 将匿名函数包裹在一个括号运算符中,后面再跟一个括号。
(function () {
console.log('立即执行函数');
})();
// !!!特别说明:若此立即执行函数后面立马又跟着一个立即执行函数,一定要在结尾加分号,否则后面的立即执行函数会报错!
// 上一个立即执行函数不加分号,下行代码将报错:TypeError: (intermediate value)(...) is not a function
(function (a, b, c) { // 形参
console.log(a + b + c); // 6
})(1, 2, 3); // 实参
可以用!、+、-、~来代替常用第一个括号
!function (a, b, c) {
console.log(a + b + c); // 6
}(1, 2, 3);
+function (a, b, c) {
console.log(a + b + c); // 6
}(1, 2, 3);
-function (a, b, c) {
console.log(a + b + c); // 6
}(1, 2, 3);
~function (a, b, c) {
console.log(a + b + c); // 6
}(1, 2, 3);
var fn = function (a, b, c) {
console.log(a + b + c); // 6
}(1, 2, 3);
2. 匿名函数后面跟一个括号,再将整个包裹在一个括号运算符中。
(function () {
console.log('立即执行函数');
}());
(function (a, b, c) { // 形参
console.log(a + b + c); // 6
}(1, 2, 3)); // 实参
立即函数的作用
立即执行函数最本质的作用是:创建一个独立的作用域。
利用这一功能,可以
- 初始化数据和页面(只执行一次)
- 模块化开发中,定义私有变量,防止污染全局(独立作用域)
- 解决闭包中的状态保存问题;(常见的一个函数内部返回多个函数,调用这些函数,打印父函数内部变量的问题)
匿名函数特点:
- 页面加载时立即制行
- 获取到返回值
- 执行完成之后立即释放
闭包经典案例分析
<ul id=”test”>
<li>这是第一条</li>
<li>这是第二条</li>
<li>这是第三条</li>
</ul>
<script>
var liList=document.getElementsByTagName('li');
for(var i=0;i<liList.length;i++)
{
liList[i].onclick=function(){
console.log(i);
}
};
</script>
很多人觉得这样的执行效果是点击第一个li,则会输出1,点击第二个li,则会输出二,以此类推。但是真正的执行效果是,不管点击第几个li,都会输出3。
因为 i 是贯穿整个作用域的,而不是给每个 li 分配了一个 i,用户触发的onclick事件之前,for循环已经执行结束了,而for循环执行完的时候i=3。
但是如果我们用了__立即执行函数给每个 li 创造一个独立作用域,__就可以改写为下面的这样,这样就能实现点击第几条就能输出几的功能。
<script>
var liList=document.getElementsByTagName('li');
for(var i=0;i<liList.length;i++)
{
(function(ii) {
liList[ii].onclick=function(){
console.log(ii);
}
})(i)
};
</script>
其实ES6语法中的let也可以实现上述的功能。
<script>
var liList=document.getElementsByTagName('li');
for(let i=0;i<liList.length;i++)
{
liList[i].onclick=function(){
console.log(i);
}
}
</script>
闭包
闭包函数:声明在一个函数中的函数,叫做闭包函数
闭包:内部函数总是可以访问其所在外部函数中的声明的参数和变量,即使在其外部函数被返回(寿命终结)了之后。
创建闭包的常见方式:就是在一个函数内部创建另一个函数。闭包可以有函数名,也可以是匿名函数(没有函数名);
闭包特点
- 让外部访问函数内部变量成为可能
- 局部变量会常驻在内存中
- 可以避免使用全局变量,防止全局变量污染
- 会造成内存泄漏(有一块内存空间被长期占用,而不被释放)
闭包的创建
-
闭包就是可以创建一个独立的环境,每个闭包里面的环境都是独立的,互不干扰。闭包会发生内存泄漏,__每次外部函数执行的时候,外部函数的引用地址不同,都会重新创建一个新的地址。__但凡是当前活动对象中有被内部子集引用的数据,那么这个时候,这个数据不删除,保留一根指针给内部活动对象。
-
闭包内存泄漏为: key = value,key 被删除了 value 常驻内存中;
-
局部变量闭包升级版(中间引用的变量)=> 自由变量;
闭包形式
1.直接以匿名函数的形式作为外部函数的返回值(普遍用法)
function outer() {
var n = 1;
return function() {
n++; // 访问 outer 函数作用域中的变量 n,形成闭包
console.log(n);
}
}
outer()();
2.在外部函数内定义一个内部函数,并返回内部函数名
function outer() {
var n = 1;
function inner() {
n++; // 访问 outer 函数作用域中的变量 n,形成闭包
console.log(n);
}
return inner;
}
outer()();
3.在外部函数内定义一个立即执行函数
function outer() {
var n = 1;
(function() {
n++; // 访问 outer 函数作用域中的变量 n,形成闭包
console.log(n);
})();
}
outer();
闭包的应用场景
- 保护函数局部变量的安全
- 在内存中维持一个变量
- 通过保护变量的安全实现JS私有属性和私有方法(不能被外部访问)
使用闭包的注意点:
由于闭包会携带包含它的函数的作用域,因此会比其他函数占用跟多的内存。过度使用闭包可能会导致内存占用过多。
结论: 闭包找到的是同一地址中父级函数中对应变量最终的值。
示例:
- 在返回的闭包后面紧跟一个 () ,立即执行
function outer() {
var n = 1;
function inner() {
n++;
console.log(n);
}
return inner;
}
outer()(); // 2
outer()(); // 2
- 将返回的闭包赋值给一个全局变量,全局变量运行 () 操作,执行闭包
function outer() {
var n = 1;
function inner() {
n++;
console.log(n);
}
return inner;
}
var inner = outer();
inner(); // 2
inner(); // 3 (是在 2 的基础上加 1)
- 请继续看以下代码,同时使用了1和2的两种方式
function outer() {
var n = 1;
function inner() {
n++;
console.log(n);
}
return inner;
}
var inner = outer();
outer()(); // 2
outer()(); // 2 (Flag1)
inner(); // 2 (Flag2)
inner(); // 3
可以看到,Flag1 跟 Flag2 的值都是 2,假设 outer()() 这样的闭包执行方式会一直保存局部变量在内存的话,那 Flag2 处的值应该是 3。从而说明:闭包并非就一定可以一直保存局部变量在内存,还跟执行方式有关。
- 主动释放内存
function outer() {
var n = 1;
function inner() {
n++;
console.log('n = ', n);
}
return inner;
}
var inner = outer();
inner(); // 2
inner(); // 3
inner = null; // 解除对闭包的引用,以便释放内存
function foo() {
var a = 2;
return function fun1() {
console.log(a)
}
}
var fun2 = foo()
fun2() // 2
在上面的例子中,fun1能够访问foo的内部作用域,我们把fun1作为一个值返回。在foo()执行后,把foo()的返回值 fun1 赋值给fun2并调用fun2。打印出了结果2.
此时,我们可以说fun1记住并访问了所在的词法作用域 或者说 fun2访问了另一个函数作用域中的变量(fun2在全局作用域中声明,访问了foo的内部作用域)
由于引擎有自动的垃圾回收机制,在foo()执行后(不再使用),通常foo的整个内部作用域会被销毁,对内存进行回收。而闭包的神奇之处正是可以阻止这件事情的发生,因为fun1依然持有对该作用域的引用,这个引用就叫做闭包。
无论使用何种方式对函数类型的值进行传递,当函数在别处调用时,都可以看到闭包。
function outerFn(){
var i = 0;
function innerFn(){
i++;
console.log(i);
}
return innerFn;
}
var inner = outerFn(); //每次外部函数执行的时候,都会开辟一块内存空间,外部函数的地址不同,都会重新创建一个新的地址
inner();
inner();
inner();
var inner2 = outerFn();
inner2();
inner2();
inner2(); //1 2 3 1 2 3
var i = 0;
function outerFn(){
function innnerFn(){
i++;
console.log(i);
}
return innnerFn;
}
var inner1 = outerFn();
var inner2 = outerFn();
inner1();
inner2();
inner1();
inner2(); //1 2 3 4
function fn(){
var a = 3;
return function(){
return ++a;
}
}
alert(fn()()); //4
alert(fn()()); //4
function outerFn(){
var i = 0;
function innnerFn(){
i++;
console.log(i);
}
return innnerFn;
}
var inner1 = outerFn();
var inner2 = outerFn();
inner1();
inner2();
inner1();
inner2(); //1 1 2 2
(function() {
var m = 0;
function getM() { return m; }
function seta(val) { m = val; }
window.g = getM;
window.f = seta;
})();
f(100);
console.info(g()); //100 闭包找到的是同一地址中父级函数中对应变量最终的值
function a() {
var i = 0;
function b() { alert(++i); }
return b;
}
var c = a();
c(); //1
c(); //2
function f() {
var count = 0;
return function() {
count++;
console.info(count);
}
}
var t1 = f();
t1(); //1
t1(); //2
t1(); //3
- ES6柯里化
var add = function(x) {
var sum = 1;
var tmp = function(x) {
sum = sum + x;
return tmp;
}
tmp.toString = function() {
return sum;
}
return tmp;
}
alert(add(1)(2)(3)); //6
var lis = document.getElementsByTagName("li");
for(var i=0;i<lis.length;i++){
(function(i){
lis[i].onclick = function(){
console.log(i);
};
})(i); //事件处理函数中闭包的写法
}
function m1(){
var x = 1;
return function(){
console.log(++x);
}
}
m1()(); //2
m1()(); //2
m1()(); //2
var m2 = m1();
m2(); //2
m2(); //3
m2(); //4
var fn=(function(){
var i=10;
function fn(){
console.log(++i);
}
return fn;
})()
fn(); //11
fn(); //12
var fn=(function(){
var i=10;
function fn(){
console.log(++i);
}
return fn;
})()
fn(); //11
fn(); //12
function love1(){
var num = 223;
var me1 = function() {
console.log(num);
}
num++;
return me1;
}
var loveme1 = love1();
loveme1(); //输出224
function fun(n,o) {
console.log(o);
return {
fun:function(m) {
return fun(m,n);
}
};
}
var a = fun(0); //undefined
a.fun(1); //0
a.fun(2); //0
a.fun(3); //0
var b = fun(0).fun(1).fun(2).fun(3); //undefined 0 1 2
var c = fun(0).fun(1);
c.fun(2);
c.fun(3); //undefined 0 1 1
function fn(){
var arr = [];
for(var i = 0;i < 5;i ++){
arr[i] = function(){
return i;
}
}
return arr;
}
var list = fn();
for(var i = 0,len = list.length;i < len ; i ++){
console.log(list[i]());
} //5 5 5 5 5
function fn(){
var arr = [];
for(var i = 0;i < 5;i ++){
arr[i] = (function(i){
return function (){
return i;
};
})(i);
}
return arr;
}
var list = fn();
for(var i = 0,len = list.length;i < len ; i ++){
console.log(list[i]());
} //0 1 2 3 4