闭包及高阶应用1(惰性函数和单例设计模式)

首先来看一道开放题:谈谈你对闭包的理解,以及在项目中的应用!

  1. 阐述闭包是什么?(引申:堆栈、EC、AO、VO、SCOPE…)
  2. 闭包的作用及在真实项目中的应用场景,以及所带来的问题!
  3. 由闭包引发的高阶编程技巧
  4. 突出自己在分析研究框架的源码(逼格更高的是自己写类库、插件的时候)是怎么应用这些东西的!
    建议:浏览器加载页面会把代码放到栈内存中执行(也就是ECStack),函数进栈执行会产生一个私有的上下文(也就是EC),此上下文能保护里面的私有变量(也就是AO)不受外界的干扰,并且如果当前上下文中的某些内容,被上下文以外的内容所占用,当前上下文是不会出栈释放的,这样可以保存里面的变量和变量值,所以我认为闭包是一种保存和保护内部私有变量的机制…在真实的项目中,其实我应用闭包的场景还是很多的,例如:
  1. 我会基于闭包把自己编写的模块内容包起来,这样自己编写的代码都是私有的,防止和全局变量或者别人的代码冲突,这一点利用的是闭包的保护机制
  2. 在没有用LET之前,我们循环处理事件绑定,在事件触发需要用到索引值的时候,我们基于闭包,把每一轮循环的索引值保存起来,这样来实现我们的需求,只不过现在都是基于LET来完成,因为LET会产生块级作用域来保存需要的内容(机制和闭包类似)
    但是不建议过多使用闭包,因为形成不被释放的上下文,是占用栈内存空间的,过多使用会导致页面渲染变慢,所以要合理应用闭包
    除了这些传统的业务开发中会应用闭包,我之前在研究别人源码和自己写一写插件的时候,往往会利用一些JS高阶编程技巧来实现代码的管理和功能的开发,他们的底层机制其实就是闭包,例如:
  • 惰性函数
  • 柯理化函数
  • compose函数
惰性函数

DOM0事件绑定:xxx.οnclick=function(){}
DOM2事件绑定:xxx.addEventListener(‘click’,function(){}),只不过不兼容IE6~8,IE低版本浏览器中是基于 xxx.attachEvent(‘onclick’,function(){}) 实现的

//实现可以兼容各种事件绑定的方法
function observerEvent(element, type, func) {
	if (element.addEventListener) {
		element.addEventListener(type, func);
	} else if (element.attachEvent) {
		element.attachEvent('on' + type, func);
	} else {//不支持DOM2事件绑定,用DOM0事件绑定
		element['on' + type] = func;
	}
} 
// observerEvent(xxx,'click',function(){});
// observerEvent(xxx,'click',function(){});
// observerEvent(xxx,'click',function(){});
// ...

上面这种写法虽然可以实现我们的需求和效果,但是在相同页面中,每一次执行函数,进来后都要重复的兼容判断(但是理论上第一次执行,我们就知道兼容性了,后期再执行,没必要每一次都判断兼容,也就是把兼容处理只处理一次 =>“懒”)

function observerEvent(element, type, func) {
// 第一次执行observerEvent,根据兼容判断,重构了这个函数,重构后的小函数是不需要做兼容验证处理的
	if (element.addEventListener) {
		observerEvent = function (element, type, func) {
			element.addEventListener(type, func);
		};
	} else if (element.attachEvent) {
		observerEvent = function (element, type, func) {
			element.attachEvent('on' + type, func);
		};
	} else {
		observerEvent = function (element, type, func) {
			element['on' + type] = func;
		};
	}
	// 第一次也需要执行重构后的方法,实现事件绑定
	observerEvent(element, type, func);
    //当前这个案例不是递归,因为递归是自己调用自己,但是当前这个例子是函数被重构后调用,不是执行的这个大函数。 
} 
// observerEvent(xxx,'click',function(){});

这个大函数的堆内存会被释放,但是形成的这个栈内存(即这个大函数的私有上下文)不会被释放,因为它内部的变量被外部的全局变量小函数给占用了。

单例设计模式

JS中一个最基础,但是也是业务逻辑开发中常用到的设计思想:“单例设计模式”(框架开发模式下无需用单例处理)最早的模块开发思想(AMD/CMD/CommonJS/ES6Module)

A开发天气版块

(function () {
   let index = 0;
   function queryData() {}
   function getElement() {}
/*
想让闭包之外的东西调用此方法getElement,可以基于WINDOW.xxx把其暴露到全局上
(如果向全局暴露的东西过多,也会存在冲突问题)
*/
   window.getElement = getElement
})();

B开发资讯版块

(function () {
	let index = 0;
	function queryData() {}
    //因为A中把getElement暴露在了全局上window,所以此处可以调用
	getElement();//与window.getElement()一样

})();

因为想让闭包之外的东西调用此方法getElement,可以基于WINDOW.xxx把其暴露到全局上(如果向全局暴露的东西过多,也会存在冲突问题,所以建议使用return方式,如下:

  • A开发天气版块
  1. 单例设计模式:利用闭包机制,首先将所有的内容私有化,需要暴露给外面的通过return返回一个对象,赋值(暴露)给当前这个模块名,以后想调用这个模块中的方法时直接通过:模块名.XXX 调用。
  2. 此时的weatherModule除了是全局变量、模块名称、对象名,更专业的叫法是“命名空间”,这样单例设计模式就是把描述相同事务(相同版块)中的属性和方法,归拢到相同的命名空间下,实现分组管理(既可以避免全局变量污染、也可以实现模块之间的相互调用)
  3. 堆内存也是一个空间,把这个堆内存的地址赋值给这个变量名,以后这个变量名代表的就是这个堆。给当前这个堆取的名字就叫命名空间,所以所有的对象名都是命名空间。let obj={},这个obj既是对象名也是命名空间,因为{}是一个堆内存空间,把这个空间地址赋值给obj,obj代表的就是这个堆内存,给这个空间取了一个变量名,所以把这个变量名叫做命名空间。所以单例设计模式下都叫做“命名空间”。
let weatherModule = (function () {
   let index = 0;
   function queryData() {}
   function getElement() {}
   //建议使用return的方式,返回一个对象,这样的优点就是以后想暴露什么就直接写在这个对象里。
   return {weatherModule就是这个对象,通过这个对象可以调用其中的方法,最早期的模块开发思想就是基于这个思想实现的
       //getElement:getElement 下面为简写的方式
       getElement
   }
})();

上述A代码的解说:当前一个自执行函数执行, 执行的时候形成一个私有的上下文,这个上下文中有一个堆,作为值返回给外界的变量,相当于全局变量下的weatherModule占用来这个私有上下文中的堆,那么当前这个上下文就不会被释放,当前上下文中的私有变量和值也就被保存下来了。其他的模块想要调用此模块中的方法时就可以同点的方式直接调用了。这就是闭包机制的应用。——单例设计模式,最早的模式化开发管理思想

  • B开发资讯版块中调用
(function () {
	let index = 0;
	function queryData() {}
    //因为A中把getElement返回一个对象中,所以可以通过 模块名.xxx来调用
	weatherModule.getElement();

})();

这样一来暴露到全局的就只有“weatherModule”这个命名空间名或者模块名,命名空间
B开发资讯版块中

let informationModule = (function () {
	let index = 0;
	function queryData() {}
    //因为A中把getElement返回一个对象中,所以可以通过 模块名.xxx来调用
	weatherModule.getElement();
    return {
        //想暴露就写在这里,不想暴露就不写
    }
})();

在真实项目中除了按照单例模式管理外,一般还会在return里添加命令模式 init(){}
B开发资讯版块中

let informationModule = (function () {
	let index = 0;
    //要想实现资讯这个B模块,第一步:获取数据
	function queryData() {}
    //第二步:绑定内容
    function queryData() {}
    //第三步:处理事件
    function handleEvent() {}
    //······ 真实项目中想要实现一个模块需要写很多方法

 //当我们想依次执行这些方法的时候,不可以将它们都写在返回对象中,然后在外面通过点方法一一调用,像一下的代码不可取   
    return {
        //init:function(){}
        init(){//ES6中这样简写
        // 在单例设计模式的基础上,增加一个命令模式,init作为当前模块业务的入口
        }

        //不可以将它们都写在返回对象中
        queryData,
        queryDat,
        handleEvent
    }
})();
//然后外界一一调用
informationModule.queryData();
informationModule.queryDat();
informationModule.handleEvent();

真实项目中用以下的代码:在单例设计模式的基础上,增加一个命令模式,init作为当前模块业务的入口,以后只需要执行informationModule.init(),我们在init中根据业务需求,把编写的方法按照顺序依次调用执行即可

let informationModule = (function () {
	let index = 0;
	function queryData() {}
    function queryData() {}
    function handleEvent() {}
    //······ 

    return {
        init(){
// 在单例设计模式的基础上,增加一个命令模式,init作为当前模块业务的入口,以后只需要执行informationModule.init(),我们在init中根据业务需求,把编写的方法按照顺序依次调用执行即可
			queryData();
			bindHTML();
			handleEvent();

        }
    }
})();

informationModule.init();

这就是结合init命令模式,如何利用单例模式去规划我们的业务开发,在不用框架之前的案例都按照这种方式来写,这也是利用闭包机制。
闭包的技巧中第一个是惰性思想,惰性思想除了函数重构赋值之外,另外一种方式就是单例设计模式,也是利用闭包机制,也是一种惰性思想:第一次执行函数形成闭包,将这个上下文中的变量和值都保存下来,以后调用其中的方法时直接调用,不需要重复创建了。其实这样将单例设计模式理解为惰性思想还是有一点牵强,真正的惰性思想还是函数重构。

你可能感兴趣的:(javascript)