精读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()//echo is handsome man!
handsomeMan()//Yes,echo is handsome man!
console.log(handsomeMan.property);//undefined

 五、立即执行函数

所谓立即执行函数就是一个在创建后就会被立即执行的函数表达式,也可以叫自调用函数。

//调用括号在里面
(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是一个字符串,并非一个函数。

六、代码初始化的意义

在函数重写和自调用函数模式中多次提到了代码初始化,为什么要做代码初始化,简单举例说下。

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已经初始化过了,不用反反复复走监听绑定判断,这就是代码初始化的意义,把那些你能确定下来,但需要繁琐执行的逻辑一次性确定好,之后就是直接使用的操作了,就是这么个意思。

这篇就记录这么多吧,还有五天回家过年了!

猜你喜欢

转载自www.cnblogs.com/echolun/p/10285380.html