# 闭包及闭包的应用

前端开发人员对闭包这个知识点,一定都不陌生,我们都知道闭包的概念是指有权访问另一个函数作用域中的变量的函数。那么,闭包在js中的实际应用都有哪些呢,今天就一起来了解一下吧。

# 执行环境及作用域链

要理解闭包,首先要对js的执行环境和作用域有了解。

概念太长不想看系列:

执行环境定义了变量或函数有权访问的其他数据,决定了它们各自的行为。每个执行环境都有与之关联的变量对象集合[[scope]],环境中定义的所有变量和函数都保存在这个对象中,这个集合被称为该环境的作用域链。

js中,每个函数都有自己的执行环境。当执行流进入一个函数时,函数的环境就会被推入一个环境栈中。在函数执行之后,栈将其环境弹出,环境随之销毁,控制权返回给之前的执行环境。

当代码在一个函数的环境中执行时,会创建一个变量对象的作用域链。作用域链的前端,始终都是当前执行的代码所在的变量对象。如果这个环境是函数,则将其活动对象(active object)作为变量对象。活动对象在最开始时只包含一个变量,即arguments对象(这个对象在全局环境中是不存在的)。作用域链中的下一个对象来自包含(外部)环境,而再下一个变量则来自下一个变量环境。这样,一直延续到全局执行环境;全局执行环境的变量对象始终都是作用域链中的最后一个对象。 当运行期上下文被销毁,活动对象也随之销毁。

标识符解析是沿着作用域链一级一级搜索标识符的过程。搜索过程始终从作用域链的前端开始,然后逐级的向后回溯,直至找到标识符为止。

总结:

  • 每个函数有一个执行环境,一个执行环境关联一个变量对象,变量对象的集合叫做作用域链。
  • 作用域链的前端是当前的执行代码所在的变量对象,下一个对象是外部函数,一直延续到全局变量。
  • 标识符解析是沿着作用域链从前端开始逐级回溯的过程。
  • 代码执行完毕后,所在的环境会被销毁,web中全局执行环境是window对象,全局环境会在应用程序退出时被销毁。

# 闭包的定义

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

创建闭包的常见方式,就是在一个函数内部创建另一个函数。

在javascript语言中,闭包就是函数和该函数作用域的组合。从这个概念上来讲,在js中,所有函数都是闭包(函数都是对象并且函数都有和他们相关联的作用域链scope chain)。

大多数函数被调用时(invoked),使用的作用域和他们被定义时(defined)使用的作用域是同一个作用域,这种情况下,闭包神马的,无关紧要。但是,当他们被invoked的时候,使用的作用域不同于他们定义时使用的作用域的时候,闭包就会变的非常有趣,并且开始有了很多的使用场景,这就是你之所以要掌握闭包的原因。 用途:

  • 读取函数内部的变量。
  • 让这些变量的值始终保持在内存中。不会在f1调用后被自动清除。
  • 方便调用上下文的局部变量。利于代码封装。

# 面试题助消化

var scope = "window scope"; 
function checkScope() {
    var scope = "local scope";
    function f() {
        return scope;
    }
    return f();
}
checkScope();   //=> "local scope"
  • checkScope被invoke时,return f(),运行内部嵌套函数f,f沿着作用域链从内向外寻找变量scope,找到“local scope”,停止寻找,因此,函数返回 “local scope”;
var scope = "window scope"; 
function checkScope() {
    var scope = "local scope";
    function f() {
        return scope;
    }
    return f;
}
checkScope()();   //=> "local scope"
  • checkScope被invoke时,将内部嵌套的函数f返回,因此checkScope()()这句执行时,其实运行的是f(),f函数返回scope变量,在这种情况下,f会从哪个作用域里去寻找变量scope呢?
  • 词法作用域的基础规则:函数被执行时(executed)使用的作用域链(scope chain)是被定义时的scope chain,而不是执行时的scope chain。 嵌套函数f(), 被定义时,所在的作用域链中,变量scope是被绑定的值是“local scope”,而不是"window scope",因此,以上代码的结果是啥?没错,是"local scope"。 这就是闭包的神奇特性:闭包可以捕获到局部变量和参数的外部函数绑定,即便外部函数的调用已经结束。
var scope = "window scope"; 
function checkScope() {
    var scope = "local scope";
    function f() {
        return this.scope;
    }
    return f;
}
checkScope()();   //=> "window scope"
  • 闭包的this指向的是它定义的地方的this,非严格模式下,函数内部的this指向全局对象(严格模式下,this为undefined),函数 checkScope 的this指向的是window对象,所以返回了window scope

# 闭包的应用场景

# 为节点循环绑定click事件

当前页面有5个button,要求是点击每个button的时候弹出对应的编号

//html代码

<!DOCTYPE html>
<html>
<head>
     <meta charset="UTF-8">
</head>
<body>
    <button>Button0</button>
    <button>Button1</button>
    <button>Button2</button>
    <button>Button3</button>
    <button>Button4</button>
</body>
</html>

//js

for(var i = 0;i<btnList.length;i++){
	//错误的代码 onclick是异步触发的,
	btnList[i].onclick = function(){
		console.log(i)
	}
 
	//正确的代码
	//采用“立即执行函数Immediately-Invoked Function Expression (IIFE)”的方式创建作用域
	(function(i){
		btnList[i].onclick = function(){
			console.log(i)
		}
	})(i);
}

todo 立即执行函数不传i

# 延续局部变量的寿命

img对象经常用于数据上报,如下:

var report = function(src) {
    var img = new Image();
    img.src = src;
}
report('http://xxx.com/getUserInfo');

这段代码在运行时,发现在一些低版本浏览器上存在bug,会丢失部分数据上报,原因是img是report函数中的局部变量,当report函数调用结束后,img对象随即被销毁,而此时可能还没来得及发出http请求,所以此次请求就会丢失。

因此,我们使用闭包把img对象封闭起来,就可以解决数据丢失的问题:

var report = (function() {
    var imgs = [];
    return function(src) {
        var img = new Image();
        imgs.push(img);
        img.src = src;
    }
})()


 (function(){
        //i在外部就不认识啦
        for(var i=0;i<count;i++){}
  })();
  console.log(i);//报错,无法访问

# 对结果进行缓存

todo 传参复杂数据 react vue 的实现 tgou https

var fn=function(){
    var sum=0;
    for(var i=0;i<arguments.length;i++){
        sum+=arguments[i];
    }
    return sum;
}
console.log(fn(1,2));//3
 
//优化版本
var fn=(function(){
    var cache={}//将结果缓存到该对象中
    return function(){
        var str=JSON.stringify(arguments);
        if(cache[str]){//判断缓存中是否存在传递过来的参数,存在直接返回结果,无需计算
            return cache[str];
        }else{//进行计算并返回结果
            var sum=0;
            for(var i=0;i<arguments.length;i++){
                sum+=arguments[i];
            }
            return cache[str]=sum;
        }
    }
})()

# 设计模式之 构造器模式

在经典面向对象的编程语言中,Constructor是一种在内存已分配给该对象的情况下,用于初始化新创建对象的方法。 在JavaScript中,几乎所有的东西都是对象,我们通常最感兴趣的是object构造器。

Object构造器用于构建特定类型的对象--准备好对象以备使用,同时接收构造器可以使用的参数,以在第一次创建对象时,设置成员属性和方法的值。

// 构造器模式
function Car(model, year, miles){
	this.model = model;
	this.year = year;
	this.miles = miles;

	Car.prototype.toString = function(){
		return this.model + 'has done ' + this.miles + ' miles';
	}
}
 
var civic = new Car('honda civic', 2019, 2000);
console.log(civic.toString()); 
  • avaScript不支持类的概念,但支持与对象一起用的特殊constructor(构造器)函数,通过在构造器前面加new关键字,告诉JavaScript像使用构造器一样实例化一个新对象,并且对象成员由该函数定义。
  • toString这样的函数在这里在每次创建新的实例的时候都被重新定义,这不是最理想的,toString应该在所有的Car构造的实例之间共享。所以把toString放在Car的prototype(原型对象)上,Car构建的所有实例,都会访问同一个原型对象并获取到toString方法。 这里toString就是一个闭包,function可以访问实例中的model,miles和year变量。

todo prototype位置

# Module(模块)模式

在JavaScript中,Module模式用于进一步模拟类的概念。通过这种方式,能够使一个单独的对象拥有公有/私有方法和变量,从而屏蔽来自全局作用域的特殊部分。 产生的结果是:函数名与在页面上其他脚本定义的函数冲突的可能性降低。

var myNamespace = (function(){
    var obj = {};
    // 私有计数器变量
    var myPrivateVar = 0;
    
    // 记录所有参数的私有函数
    var myPrivateMethods  = function(bar){
        console.log('my privateVar in private = '+ bar);
    }
    
    //公有变量
    var myPublicVar = 'foo';
    // 调用私有变量和函数的公有函数
    function myPublicMethods(bar){
        // 增加私有计数器值
        myPrivateVar = bar;
        console.log('my privateVar in public = '+ myPrivateVar);
        // 调用私有函数 并传入参数
        myPrivateMethods(bar);
    }
    
    obj.myPublicVar = myPublicVar;
    obj.myPublicMethods = myPublicMethods;
    
    return obj;
    
})();

// myNamespace.myPrivateVar;
// 用户可以调用公有函数,访问和变更私有函数及私有变量
myNamespace.myPublicMethods('user input');

Module模式使用闭包封装“私有”状态和组织。它提供了一种包装混合共有/私有方法和变量的方式,防止其泄漏至全局作用域,并与别的开发人员的接口发生冲突。通过该模式,只需返回一个共有API,而其他的一切都维持在私有闭包里。 在Module模式中,共有部分(闭包)可以接触私有部分,然而外界无法接触类的私有部分,模块中的 myPrivateVar 和 myPrivateMethods 是私有的,因此应用程序的其他部分无法直接读取它。它只与模块的闭包一起存在。