JS进阶-理解闭包

定义

闭包是指函数声明时所处作用域外被调用的函数。所以闭包也是函数,只不过要满足3个条件才叫闭包:

  1. 访问函数所处作用域。
  2. 函数嵌套。因为只有函数嵌套才能创建不同的作用域。
  3. 函数所处作用域外被调用。
function foo() {
  var a = 1;
  function foo2() {
    console.log(a); // 1
  }
  return foo2;
}
foo()();

// 等价于
function foo() {
var a = 1;
  return function foo2() {
   console.log(a); // 1
  }
}
foo()();

示例中在全局作用域中被调用的foo2函数就是一个闭包。foo2声明时所处的作用域就是foo函数作用域,它在foo函数作用域外的全局作用域中被调用,并且foo2中的变量a访问了foo2所处的作用域,满足闭包的三个条件。

IIFE立即执行函数

函数定义后立即被调用的函数就是立即执行函数,全称是立即调用的函数表达式IIFE(Imdiately Invoked Function Expression)

严格来说IIFE不是闭包,因为不满足闭包的三个条件。但是如果在IIFE中嵌套了函数,那就另当别论了。

IIFE的常见形式

// 常用
(function(){ /* code */ }()); 
(function(){ /* code */ })();

// 其他
var i = function(){ /* code */ }();
true && function(){ /* code */ }();
0, function(){ /* code */ }();

!function(){ /* code */ }();
~function(){ /* code */ }();
-function(){ /* code */ }();
+function(){ /* code */ }();

new function(){ /* code */ };
new function(){ /* code */ }();  

注意: JS引擎规定,如果function关键字出现在首行,一律解释成函数声明。所以function关键字前面添加!、~、-、+符号,是为了让引擎把它解释成一个表达式。

// 函数声明必须要定义函数名称,所以报错
function(){ /* code */ }(); // Uncaught SyntaxError: Unexpected token (

// 这个并不是函数调用,而是函数声明和分组操作符的组合。由于分组操作符不能为空,所以报错。
function foo(){ /* code */ }(); // Uncaught SyntaxError: Unexpected token )

// 这样虽然不报错,但是没什么意义。
function foo(){ /* code */ }(1); 

// 函数表达式
!function(){ /* code */ }();

注意: 函数表达式和IIFE使用时,表达式后面的分号必须要有。

// 示例1
var a = function(){
   return 1;
}

(function(){
    console.log(1);// 报错
})();

// 示例2
var a = function(){
   return 1;
};

(function(){
    console.log(1);// 正常
})();

闭包的常见形式

返回值

var foo = function(){
  var a = 1;
  var foo2 = function() {
    return a;
  }
  return foo2;
}
var fn = foo();
console.log(fn()); // 1

函数赋值

把内部函数赋值给外部变量

var fn;
var foo = function() {
  var a = 1;
  var foo2 = function() {
    return a;
  }
  fn = foo2;
}
foo();
console.log(fn()); // 1

函数参数

通过把内部函数作为参数传递给外部函数的形式实现闭包。

var fn = function(f) {
  console.log(f()); // 1
};

var foo = function() {
  var a = 1;
  var foo2 = function() {
   return a;
  }
  fn(foo2);
};
foo();

上面的3种闭包形式,foo函数声明后是立即被调用的,所以上面的示例可以用IIFE表示。

var fn = (function(){
  var a = 1;
  var foo2 = function() {
   return a;
  }
  return foo2;
})();

console.log(fn()); // 1
var fn;
var foo = (function() {
  var a = 1;
  var foo2 = function() {
    return a;
  }
  fn = foo2;
})();

console.log(fn()); // 1
var fn = function(f) {
  console.log(f()); // 1
};

var foo = (function() {
  var a = 1;
  var foo2 = function() {
    return a;
  }
  fn(foo2);
})();

闭包的常见用途

循环赋值

function foo() {
  var arr = [];
  for(var i = 0; i < 3; i++) {
   arr[i] = function() {
     return i;
   }
  }
  return arr;
}

var ret = foo();
console.log(ret[0]()); // 3

上面的循环,没有出现预想的结果0,原因是在调用匿名函数时,通过作用域找到的不是循环时候的瞬间值,而是循环结束后的索引值。

正确写法:

function foo() {
  var arr = [];
  for(var i = 0; i < 3; i++) {
   arr[i] = (function(j) {
     return function() {
       return j;
     };
   })(i)
  }
  return arr;
}

var ret = foo();
console.log(ret[0]()); // 0

变量私有化

通过闭包把变量私有化,避免污染全局作用局。

var getVal, setVal;

(function(){
  var secret = 0;
  getVal = function() {
   return secret;
  }
  setVal = function(v) {
   secret = v;
  }
})()

setVal(3);
getVal(); // 3

迭代器

迭代器

function setup(arr) {
  var i = 0;
  return function() {
   return arr[i++];
  }
}
var next = setup(['a','b','c']);
next(); // 'a'
next(); // 'b'
next(); // 'c'

累加器

var add = (function(){
  var i = 0;
  return function() {
   return ++i;
  }
})();

console.log(add()) // 1
console.log(add()) // 2

闭包的缺点

使用闭包是有一定代价的,闭包函数所处作用域中的变量会一直存在于内存中,这是为了保证闭包在调用的时候,能够通过作用域链找到这个变量,直到页面关闭内存才会被释放。

由于闭包占用内存空间,所以使用闭包要尽量少用。并且使用结束后通过把值赋值为null解除引用,以便尽早释放内存。

var foo = function(){
  var a = 1;
  var foo2 = function() {
   return a;
  }
  return foo2;
}
var fn = foo();
fn();
fn = null;
posted @ 2021-09-29 11:19  wmui  阅读(53)  评论(0编辑  收藏  举报