一文搞定闭包(附带高频面试题)

1,119 阅读5分钟

1.变量作用域

全局变量和局部变量 特殊:函数内部可以直接读取全局变量,函数外部自然无法读取函数内的局部变量 注意:是var ,let const 声明的变量 外部才无法读取,不然就算是全局变量

1.对于全局变量来说生存周期是永久的,除非主动销毁。 2.一般对于函数作用域或者局部作用域(let const),会随着函数调用的结束而被销毁。 3.当函数内部调用外部变量就产生了闭包 这时候变量的回收策略可以参考引用计数等垃圾回收策略。

 var n = 999;
 function f1() {
      t = 888;
      console.log(n)
 }
 f1(); // 999 函数内部可以直接读取全局变量
 console.log(t); //error 函数外部自然无法读取函数内的局部变量

2.什么是闭包

闭包是一个函数,是有权访问另外一个函数作用域中的变量的函数。

function outer() {
 var a = '1'
 var inner = function () {
 console.info(a)
 }
 return inner // inner 就是一个闭包函数,因为他能够访问到outer函数的作用域
 }
//也有说法说的是
    一个函数访问另一个函数的作用域,那被访问的变量的所在函数是闭包。
    因此outer是闭包

2.1闭包形成 

  • 形成:  函数中嵌套函数
  • 作用:  函数内部调用外部变量、构造函数的私有属性、延长变量生命周期
  • 优点:  希望一个变量长期存在内存中、模块化代码避免全局变量的污染、私有属性
  • 缺点:  无法回收闭包中引用变量,容易造成内存泄漏

2.2作用

  • 1.在外面的全局作用域可以访问里面的作用域(延长变量生命周期)
 function f3() {
   var n = "shuzi";
   function f4() { console.log(n); }
     return f4;
 }
 var fun = f3();
 fun();

3.闭包缺点

3.1 this指向问题

闭包函数是在window作用域下执行的,this指向windows

 var name = "The Window";
 var object = {
  name: "My Object",
 getNameFunc: function () {
  return function () {
   return this.name;
  };
 }
};
alert(object.getNameFunc()());//The Window

3.2 内存泄露问题

function  showId() {
    var el = document.getElementById("app")
    el.onclick = function(){
      aler(el.id)   // 这样会导致闭包引用外层的el,当执行完showId后,el无法释放
    }
}
// 改成下面
function  showId() {
    var el = document.getElementById("app")
    var id  = el.id
    el.onclick = function(){
      aler(id)   // 这样会导致闭包引用外层的el,当执行完showId后,el无法释放
    }
    el = null    // 主动释放el,解除引用避免内存泄露
}

4.闭包应用

简单的有:

  • ajax请求的成功回调
  • 事件绑定的回调方法
  • setTimeout的延时回调
  • 函数内部返回另一个匿名函数

我们详细讲下以下几个应用场景:

  • 构造函数的私有属性
  • 计算缓存
  • 函数节流、防抖

4.1构造函数的私有属性

某些不希望被外部修改的私有属性可以通过闭包的方式实现。 

function Person(param) {  
   var name = param.name; // 私有属性
   this.age = 18; // 共有属性


   this.sayName = function () { console.log(name)}
   this.sayAge = function () {  console.log(this.age)}
}
   const tom = new Person({ name: 'tom' });
   tom.age += 1; // 共有属性,外部可以更改
   tom.sayName(); // tom
   tom.sayAge();//19
   
   tom.name = 'jerry';// 共有属性,外部不可更改
   tom.sayName(); // tom

4.2计算缓存 

  var square = (function () {
      var cache = {};
      return function (n) {
           if (!cache[n]) {
               cache[n] = n * n;
            }
           return cache[n];
       }
    })();
  console.log(square(3))

4.3函数节流,防抖动

// 节流 
function throttle(fn, delay) { 
  var timer = null,
  firstTime = true; 
  return function () { 
    if (timer) { 
    return false;
  } 
  var that = this; 
  var args = arguments;
  fn.apply(that, args); 
  timer = setTimeout(function () {
    clearTimeout(timer); 
    timer = null; 
  }, delay || 500); }
}
 
// 防抖 
 function debounce(fn, delay) { 
   var timer = null; 
   return function () { 
   var that = this; 
   var args = arguments; 
   clearTimeout(timer);// 清除重新计时
   timer = setTimeout(function () { 
    fn.apply(that, args); }, delay || 500); }
 }

4.4 for循环打印当前索引号

 <ul> <li>1</li>
       <li>2</li>
       <li>3</li>
       <li>4</li>
  </ul>
 var li = document.getElementsByTagName('li');
        var len = li.length;
        for (var i = 0; i < len; i++) {
            li[i].onclick = function () {
                console.log(i); //此处没有变量i,因此需要向逐级向上寻找。
     }
  }
当我们触发点击事件的时候,for循环已经执行完毕,
此时i=4,因此无论我们点击那个li标签都只会打印出4

(function(){ })()立即执行函数 ,立即执行函数也是闭包

解决方法:再加一个函数形成闭包,使用立即执行函数,将i套现

var li = document.getElementsByTagName('li');
    var len = li.length;
    for (var i = 0; i < len; i++) {
        li[i].onclick = function (i) {
         return function () {
          console.log(i) //此处没有变量i,因此需要向逐级向上寻找。
         }
       }(i);
    }
    
   var li = document.getElementsByTagName('li');
    var len = li.length;
    for (var i = 0; i < len; i++) {
       (function (i) {
          li[i].onclick = function () {
          console.log(i);
       })(i)
    }
匿名函数1里面再写入一个匿名函数2,
这个匿名函数2需要的num值会在他的父级函数匿名函数1里面去寻找,
而匿名函数1里面的num值就是传入的这个参数i,和上面例子中的i是一样的,
  function box(){
    var arr = [];
    for(var i=0;i<5;i++){
        arr[i] = (function(num){
            return num;                  
        })(i);  
    }                                            
    return arr;
}
//alert(box()

5.闭包高频题

闭包找到的是同一地址中父级函数中对应变量最终的值

1.
for ( var i = 0 ; i < 5; i++ ) {
    setTimeout(function(){
        console.log(i);
    }, 0);
}
setTimeout事件的时候,for循环已经执行完毕
输出: 55555




2.
for ( var i = 0 ; i < 5; i++ ) { 
   (function(j){ 
   setTimeout(function(){ 
    console.log(j); 
    }, 0)})(i);
} 
输出:0 1 2 3 4


3.
for ( let i = 0 ; i < 5; i++ ) {
    setTimeout(function(){
        console.log(i);
    },0);
}
输出: 0 1 2 3 4


4.
 var scope = 'global scope';
 function checkscope() {
   var scope = 'local scope';
   return function f() {
       console.log(scope);
    };
 }
checkscope()();
输出:local scope


5.
 var scope = 'global scope';
 function checkscope() {
   return function f() {
       console.log(scope);
    };
 }
checkscope()();
输出:global scope


6.
var obj = { 
 name: 'tom', 
 sayName() {
  var name = 'alan';
  console.log(this.name); 
  }
 } 
obj.sayName();// 'tom'


7.
 var obj = {
  name: 'tom',
  sayName() { console.log(this.name)}
 }
 obj.sayName(); // tom


8.
 var name = 'jerry';
 var obj = {
   name: 'tom',
   sayName() {
    return function () { console.log(this.name) }
  }
}
obj.sayName()(); // jerry


9.
function fun(a, b) {
   console.log(b)
   return {
     fun: function (c) { return fun(c, a)}
     };
 }
 var d = fun(0); // undefined
 d.fun(1); //0
 d.fun(2); //0
 d.fun(3); //0

闭包找到的是同一地址中父级函数中对应变量最终的值

1.
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


2.
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


3.
function fn(){
	var a = 3;
	return function(){
		return  ++a;                                     
	}
}
alert(fn()());  //4
alert(fn()());  //4


4. 与区别在于是不是同一块地址
function a() { 
  var i = 0; 
  function b() { alert(++i); } 
  return b; 
} 
var c = a(); 
c();      //1 
c();      //2