精读JavaScript模式(五),函数的回调、闭包与重写模式
一、前言
今天地铁上,看到很多拖着行李箱的路人,想回家了。
在上篇博客结尾,记录到了函数的几种创建方式,简单说了下创建差异,以及不同浏览器对于name属性的支持,这篇博客将从第四章函数的回调模式说起。我想了想,还是把一篇博客的知识点控制在五个以内,太长了我自己都懒得看,而且显得特别混杂。标题还是简要说下介绍了哪些知识,也方便自己以后查阅,那么开始。
二、函数的回调模式
1.什么是函数回调模式?
当调用函数时,我们可以将函数作为参数传入到需要调用的函数中,例如我们为函数A传入一个函数B,当函数A执行时调用了函数B,那么我们可以说函数B是一个回调函数,简称回调。
function A(data){ data(); }; function B(){ console.log(1); }; A(B);//将函数B作为参数传入到A函数中进行调用
当B函数作为参数传入A函数时,此时的B函数是不带括号的,因为函数名带括号时表示立即执行,这点大家应该都知道。
回调函数可以是一个匿名函数,其实这种写法在编程中反而更为常见。
function A(data){ data(); }; A(function (){ console.log(2); });//2
2.回调函数作为对象的方法时this指向问题
回调函数通常的写法是callback(parameters),通常parameters是一个匿名函数,或者一个可调用的全局函数。但当函数是某个对象的方法时,常规的回调执行会出现问题。
//回调函数sayName是obj的一个方法 let obj = { name : 'echo', sayName : function () { console.log(this.name); } }; let func = function (callback) { callback() }; func(obj.sayName);//并不会输出echo
我们原本预期是输出echo,但实际执行时this指向了全局window而非obj,所以不能拿到name属性,如何解决呢,在传递回调函数时,我们也可以把回调函数所属对象也作为参数传进去。调用方式改为如何即可,通过call或者apply改变this指向。
//回调函数sayName是obj的一个方法 let obj = { name : 'echo', sayName : function () { console.log(this.name); } }; let func = function (callback, obj) { callback.call(obj) }; func(obj.sayName, obj);//echo
在上述代码中,回调函数作为参数的写法是obj.sayName,其实也可以直接传一个字符串sayName进去,通过obj[sayName]执行,这样做的好处是,函数执行时this直接指向了调用的obj,所以就不需要call额外修改this指向了。
//回调函数sayName是obj的一个方法 let obj = { name : 'echo', sayName : function () { console.log(this.name); } }; let func = function (callback, obj) { obj[callback](); }; func('sayName', obj);//echo
3.回调模式的使用价值
在浏览器中大部分编程都是事件驱动的,例如页面加载完成触发load事件,用户点击触发click事件,也真是因为回调模式的灵活性,才让JS事件驱动编程如此灵活。回调模式能让程序'异步'执行,也就是不按代码顺序执行。
我们可以在程序中定义多个回调函数,但它们并不是在代码加载就会执行,等到时间成熟,例如用户点击了某个元素,回调函数才会根据开始执行。
document.addEventListener("click", console.log, false);
除了事件驱动,另一个常用回调函数的情景就是结合定时器,setTimeout()与setInterval(),这两个方法的参数都是回调模式。
let func = function () { console.log(1); }; setTimeout(func, 500);//500ms后执行一次func函数 setInterval(func, 500)//每隔500ms执行一次func函数
再次强调的是,定时器中回调函数的写法是func,并未带括号,如果带了括号就是立即执行。另一种写法是setTimeout('func()', 500),但这是一种反模式,并不推荐。
三、返回函数作为返回值
函数是对象,除了可以作为参数同样也能作为返回值,也就是说函数的返回值也能是一个函数。
function demo () { console.log(1); return function () { console.log(2); }; }; let func1 = demo();//1 func1()//2
在上述代码中函数demo将返回的函数包裹了起来,创建了一个闭包,我们可以利用闭包存储一些私有属性,而私有属性可以通过返回的函数操作,且外部函数不能直接读取这些私有属性。
const setup = function () { let count = 0; return function () { return (count += 1); }; }; let next = setup(); next();//1 next();//2 next();//3
四、函数重写
当我们希望一个函数做一些初始化操作,并且初始化的操作只执行一次时,面对这种情况我们就需要使用函数重写。
let handsomeMan = function () { console.log('echo is handsome man!'); handsomeMan = function () { console.log('Yes,echo is handsome man!'); }; }; handsomeMan()//echo is handsome man! handsomeMan()//Yes,echo is handsome man!
上方代码中,第一次调用的输出只会执行一次,这是因为新的函数覆盖掉了旧函数,虽然一直都是调用handsomeMan,但前后执行函数完全不同。
这种模式的另一名字是函数的懒惰定义,因为函数是执行一次后才重新定义,相比分开两个函数来写,这样的执行效率更为高效。
此模式有个明显的问题就是,一旦重新函数被重写,最初函数的所有方法属性都将丢失。
let handsomeMan = function () { console.log('echo is handsome man!'); handsomeMan = function () { console.log('Yes,echo is handsome man!'); }; }; handsomeMan.property = "properly"; let boy = handsomeMan; boy();//echo is handsome man! boy();//echo is handsome man! console.log(boy.property);//properly //property属性已丢失 handsomeMan()//Yes,echo is handsome man! handsomeMan()//Yes,echo is handsome man! console.log(handsomeMan.property);//undefined
五、立即执行函数
所谓立即执行函数就是一个在创建后就会被立即执行的函数表达式,也可以叫自调用函数(IIFE)。
//调用括号在里面 (function () { console.log(1); }()); //调用括号在外面 (function () { console.log(2); })();
它主要由三部分组成,一对括号(),里面包裹着一个函数表达式,以及一个自调的括号(),这个括号可紧跟函数,也可以写在外面。我个人常用第二种写法,但JSLint更倾向于第一种写法。
立即执行函数长用于处理代码初始化的工作,因为它提供了一个独立的作用域,所有初始化中存在的的变量都不会污染到全局环境。
立即执行函数也可以传递参数,像这样
(function (data) { console.log(data); })(1);
除了传参,立即执行函数也可以返回值,并且这些返回值可以赋值给变量。
var result = (function () { return 2 + 2; })(); console.log(result);//4
在对应一个对象的属性时,也可以使用立即执行函数,假设对象的某个属性是一个待确定的值,那我们就可以使用此模式来初始化该值。
let o = { message: (function () { return 2+2 })(), getMsg: function () { return this.message; } }; o.message;//4 o.getMsg()//4
需要注意的是,此时的o.message是一个字符串,并非一个函数。
但是伴随着ES6中块级作用域的出现,利用自执行函数保护全局作用域免受初始化变量污染的做法已经没有必要了。
{ let a = 1; } console.log(a);//无权访问
六、代码初始化的意义
在函数重写和自调用函数模式中多次提到了代码初始化,为什么要做代码初始化,简单举例说下。
JS的函数监听大家都不会陌生,而早期IE与大部分浏览器提供的监听绑定方法不同,如果不使用初始化,可能是这样
let o = { addListener : function (el, type ,fn) { if(typeof window.addEventListener === 'function') { el.addEventListener(type, fn, false); }else if (typeof document.attachEvent === 'function') { el.attachEvent('on' + type, fn); }else{ el['on' + type] = fn; } } }; o.addListener();
当我们调用o.addListener()方法时,很明显效率不高,每次调用都要把各浏览器判断走一遍,才能确定最终的监听绑定方式;我们初始化监听方式。
let o = { addListener: null }; if (typeof window.addEventListener === 'function') { o.addListener = function (el, type, fn) { el.addEventListener(type, fn, false); }; }else if (typeof document.attachEvent === 'function') { o.addListener = function (el, type, fn){ el.attachEvent('on' + type, fn); } }else{ o.addListener = function () { el['on' + type] = fn; } };
在当我们调用o.addListener()方法时,此时addListener已经初始化过了,不用反反复复走监听绑定判断,这就是代码初始化的意义,把那些你能确定下来,但需要繁琐执行的逻辑一次性确定好,之后就是直接使用的操作了,就是这么个意思。
这篇就记录这么多吧,还有五天回家过年了!