虽然javascript是一门面向对象的编程语言,但这门语言同时也同时拥有许多函数式语言的特性。

函数式语言的鼻祖是LISP,javascript设计之初参考了LISP两大方言之一的Schenme,引入了Lambda表达式,闭包,高阶函数等特性。使用这些特性,我们就可以灵活的编写javascript代码。

一:闭包

对于javascript程序员来说,闭包(closure)是一个难懂又必须征服的概念。闭包的形成与变量作用域以及变量的声明周期密切相关。

1.变量作用域

变量的作用域就是指变量的有效范围,我们最常谈到的是在函数中声明的变量作用域。

当在函数中声明一个变量时,如果没有使用var关键字,这个变量就会变成全局变量(当然这是一种容易造成命名冲突的做法。)

另外一种情况是用var关键字在函数中声明变量,这时候的变量即局部变量,只有在函数内部才能访问到这变量,在函数外面是访问不到的,代码如下:

var func = function() {
    var a = 1;
    console.log(a)
}
func()
console.log(a);//Uncaught ReferenceError: a is not defined

下面这段包含了嵌套函数的代码,也许能帮助我们加深对遍历搜索过程中的理解

var a = 1;
var func = function() {
    var b = 2;
    var func2 = function(){
        var c = 3;
        console.log(b);
        console.log(a)
    }
    func2()
    console.log(c) //Uncaught ReferenceError: c is not defined
}
func()

2.变量的生成周期

var func = function(){
    var a =1;
    console.log(a) //退出函数后局部变量a将销毁
}
func()
var func2 = function(){
    var a = 2;
    return function() {
        a++;
        console.log(a)
    }
}
var f = func2();
f() //3
f() //4
f() //5
f() //6

func2根我们之前的推论相反,当退出函数后,局部变量a并没有消失,而是停留在某个地方。这是因为,当执行 var f = func2()时,f返回了一个匿名函数的引用,它可以访问到func()被调用时的所产生的环境,而局部变量a一直处在这个环境里。既然局部变量所在的环境还能被外界访问,这个局部的变量就有了不被销毁的理由。在这里产生了一个闭包环境,局部变量看起来被延续了。

利用闭包我们可以完成很多奇妙的工作,下面介绍一个闭包的经典应用。

假设页面上有5个div节点,我们通过循环给div绑定onclick,按照索引顺序,点击第一个时弹出0,第二个输出2,依次类推。

<div>div1</div>
<div>div2</div>
<div>div3</div>
<div>div4</div>
<div>div5</div>
<div>div6</div>

<script type="text/javascript">
var nodes = document.getElementsByTagName('div')
console.log(nodes.length)
for (var i = 0; i < nodes.length; i++) {
    nodes[i].onclick = function() {
        console.log(i)
    }
}
</script>

在这种情况下,发现无论点击那个div都输出6,这是因为div节点的onclick是被异步触发的,当事件被触发的时候,for循环早已经结束,此时的变量i已经是6。

解决的办法是,在闭包的帮助下,把每次循环的i都封闭起来,当事件函数顺着作用域链中从内到外查找变量i时,会先找到被封闭在闭包环境中的i,如果有6个div,这里的i就是0,1,2,3,4,5

var nodes = document.getElementsByTagName('div')
for (var i = 0; i < nodes.length; i++) {
    (function(i){
        nodes[i].onclick = function(){
            console.log(i+1)
        }
    })(i)
}

根据同样的道理,我们还可以编写如下一段代码

var Type = {};

for (var i = 0 , type; type = ['String','Array','Number'][i++];){
    (function ( type ){
        Type['is' + type] = function( obj ) {
            return Object.prototype.toString.call( obj ) === '[object '+ type +']'
        }
    })( type )
}

console.log( Type.isArray([]) ) //true
console.log( Type.isString('') )//true

3.闭包的更多的作用

在实际开发中,闭包的运用十分广泛

(1)封装变量

闭包可以帮助把一些不需要暴露在全局的变量封装成“私有变量”,假设一个计算乘积的简单函数。

    var mult = function(){
        var a = 1;
        for (var i = 0, l = arguments.length; i < l; i++) {
            a = a * arguments[i]
        }
        return a
    }

    console.log(mult(10,2,4)) //80

mult函数每次都接受一些number类型的参数,并返回这些参数的乘积,现在我们觉得对于那些相同的参数来说,每次都进行一次计算是一种浪费,我们可以加入缓存机制来提高这个函数的性能。

var cache = {};

var mult = function(){
    var args = Array.prototype.join.call( arguments, ',' );
    if (cache[ args ]) {
        return cache[ args ]
    }

    var a = 1;
    for ( var i = 0, l = arguments.length; i<l;i++ ) {
        a = a * arguments[i]
    }

    return cache[ args ] = a;
}

console.log(mult(10,2,4)) //80

看到cache这个变量仅仅在mult函数中被使用,与其让cache变量跟mult函数一起暴露在全局作用域下,不如将它封装在mult内部,这样可以减少页面的全局变量,以避免在其它地方不小心修改而引发错误。

var mult = (function(){
    var cache = {};
    return function(){
        var args = Array.prototype.join.call( arguments, ',' );
        if (args in cache){
            return cache[ args ]
        }

        var a = 1;
        for ( var i = 0, l = arguments.length; i < l; i++ ){
            a = a * arguments[i]
        }

        return cache[ args ] = a;
    }
})()

console.log(mult(10,2,4,2)) //160

提炼函数是重构中一种常见的技巧。如果在一个大函数中有一些代码能独立出来,我们常常把这些小代码块封装在独立的小函数里面。独立的小函数有助于代码复用 ,如果这些小函数有一个良好的命名,它们本身起到了注释的作用,这些小函数不需要在程序的其它地方使用,最好是他们用闭包封闭起来。代码如下:

var mult = (function(){
    var cache = {};
    var calculate = function(){//封闭calculate函数
        var a = 1;
        for ( var i = 0, l = arguments.length; i < l; i++ ){
            a = a * arguments[i]
        }
        return a;
    }

    return function(){
        var args = Array.prototype.join.call( arguments, ',' );
        if ( args in cache ){
            return cache[ args ];
        }
        return cache[ args ] = calculate.apply( null, arguments )
    }
})()
console.log(mult(10,2,4,2,2)) //320

(2)延续局部变量的寿命

img对象经常用于数据的上报,如下所示

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

但是我们结果查询后,得知,因为一些低版本浏览器的实现存在bug,在这些浏览器下使用report函数数据的上报会丢失30%,也就是说,reprot函数并不是每次都发起了请求。
丢失的原因是img是report函数中的局部变量,当report函数的调用结束后,img局部变量随即被销毁,而此时或许还没有来的及发出http请求。所有此次的请求就会丢失掉。

现在我们将img变量用闭包封闭起来,便能解决请求丢失的问题。

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

4.闭包和面向对象设计

下面我们来看看跟闭包相关的代码:

var extent = function(){
    var value = 0;
    return {
        call : function(){
            value++;
            console.log(value)
        }
    }
};
var bb = extent();

bb.call() //1
bb.call() //2
bb.call() //3

如果换成面向对象的写法,就是:

var extent = {
    value : 0,
    call : function(){
        this.value++;
        console.log(this.value)
    }
}
extent.call();//1
extent.call();//2
extent.call();//3

或者,

var extent = function(){
    this.value = 0;
} 
extent.prototype.call = function(){
    this.value++;
    console.log(this.value)
}

var dd = new extent()
dd.call();//1
dd.call();//2
dd.call();//3

 二.高阶函数

高阶函数是至少满足以下两点的函数

  • 函数可以作为参数被传递
  • 函数可以作为返回值输出

1)。函数作为参数传递

1.回调函数

在ajax的请求应用中,回调函数使用的特别频繁,当我们想在ajax请求返回之后做一些事情。但又不知道确切的返回时间时,最常见的方案就是把callback函数作为参数传入发起ajax请求的方法中,待请求完成时执行callback函数

var getUserInfo = function( userid, callback) {
    $.ajax('http://xxx.com/getUserInfo?' + userid, function( data ){
        if (typeof callback === 'function') {
            callback( data )
        }
    });
}
getUserInfo(1233,function( data ){
    console.log( data )
});

回调函数的应用不仅只在异步请求中,当一个函数不适合执行一些请求时,我们也可以把一些请求封装成一个函数,并把它作为参数传递给另外一个函数,“委托”给另外一个函数来执行。

比如,我们想在页面中创建100个DIV节点,然后把这些DIV节点都设置为隐藏。下面是一种编写代码的方式:

var appendDiv = function(){
    for (var i = 0; i < 100; i++){
        var div = document.createElement('div');
        div.innerHTML = i;
        document.body.appendChild( div )
        div.style.display = 'none'
    }
}
appendDiv()

将 div.style.display = 'none' 的逻辑编码在appendDiv里面是不合理的,appendDiv未免有点个性化,成为了一个难以复用的的函数,并不是每人创建了节点之后就希望它们立即隐藏。

于是我们将div.style.display = 'none'这行代码抽出来,用回调函数传入appendDiv方法:

var appendDiv = function( callback ){
    for (var i = 0; i < 100; i++){
        var div = document.createElement('div');
        div.innerHTML = i;
        document.body.appendChild( div );
        if (typeof callback === 'function'){
            callback( div )
        }
    }
}
appendDiv( function( node ){
    node.style.display = 'none'
});

可以看到,隐藏节点的请求实际上是由客户端发起的,但是客户并不知道节点什么时候会创建好,于是把隐藏节点的逻辑放在回调函数中,“委托”给appendDiv方法。appendDiv方法方然知道节点什么时候创建好,所以在节点创建好的时候,appendDiv会执行之前客户传入的回调函数。

2.Array.prototype.sort

Array.prototype.sort接受一个函数当做参数,这个函数里面封装了数组元素的排序规则。从Array.prototype.sort的使用可以看出,我们的目的是对数组进行排序,这是不变的部分;而使用什么规则去排序,则是可变的部分,把可变的部分封装在函数参数里,动态传入Array.prototype.sort,使Array.prototype.sort方法编程一个非常灵活的方法,代码如下:

    //从小到大
    var cc = [1,4,3].sort(function( a, b ){
        return a - b;
    });
    console.log(cc);//[1, 3, 4]

    //从大到小
    var dd = [1,5,2,57,22].sort(function( a, b){
        return b - a;
    })
    console.log(dd) ;//[57, 22, 5, 2, 1]

2)。函数作为返回值输出

相比把函数当做参数传递,函数当返回作返回值输出的应用场景或更多,也能更体现函数式编程的巧妙。让函数继续返回一个可执行的函数,意味着运算过程是可延续的。

1.判断数据的类型。

var isString = function ( obj ){
    return Object.prototype.toString.call( obj ) === '[object string]';
}

var isArray = function( obj ){
    return Object.prototype.toString.call( obj ) === '[object Array]';
}

var isMumber = function( obj ){
    return Object.prototype.toString.call( obj ) === '[object Number]'
}

我们发现,这些函数的大部分都是相同的,不同的只是Object.prototype.toString.call( obj )返回的字符串。,为了避免多余的代码,我们尝试把这些字符串作为参数提前值入isStype函数。代码如下:

var isType = function( type ){
    return function( obj ){
        return Object.prototype.toString.call( obj ) === '[object ' + type +']';
    }
}

var isString = isType( 'String' );
var isArray = isType( 'Array' );
var isNumer = isType( 'Number' );

console.log( isArray([1,2,3]) );

2.getSingle

下面是一个单例模式的例子,在后面的设计模式的学习中,我们将更深入的讲解,这是暂且只理解其代码的实现

var getSingle = function( fn ){
    var ret;
    return function(){
        return ret || ( ret = fn.apply( this, arguments) );
    }
};

3)高阶函数实现AOP

AOP(面向切面编程)的主要作用是把一些跟核心业务逻辑模块无关的功能抽离出来,这些跟业务逻辑无关的功能通常包括日志的统计,安全控制,异常处理等。
这些功能抽离出来之后,再通过“动态织入”的方式掺入业务逻辑模块中。这样做的好处首先是可以保持业务逻辑模块的纯净和高内聚性,其次是很方便的复用日志统计等功能模块。

在java中,可以通过反射和动态代理机制来实现AOP技术。而在javascript这种动态语言中,AOP的实现更加简单。这是javascript与生俱来的能力。

通常,在javascript中实现AOP,都是指把一个函数“动态织入”到另外一个函数中,具体的实现方式有很多,本书我们将通过扩展Function.prototype来做到这一点,代码如下:

Function.prototype.before = function( beforefn ) {
    var _self = this;//保存对原函数的引用
    return function(){ //返回包含了原函数和新函数的“代理”的函数
        beforefn.apply( this, arguments ); //执行新函数,修正this
        return _self.apply( this, arguments );//执行原函数
    }
}

Function.prototype.after =  function( afterfn ) {
    var _self = this;
    return function(){
        var ret = _self.apply( this, arguments );
        afterfn.apply( this, arguments );
        return ret;
    }
}

var func = function() {
    console.log(2)
}

func = func.before(function(){
    console.log( 1 );
}).after(function(){
    console.log(3)
})

func()

我们把负责打印数字1和打印数字3的两个函数通过AOP的方式动态植入func函数。通过执行,我们看到控制台返回1 2 3

三:函数节流


javascript中的函数大部分的情况是由用户主动是触发的,除非函数本身不合理,否则我们一般不会遇到跟性能相关的问题。但在一些少数的情况下,函数的触发不是由用户直接控制的。在这些场景下,函数有可能非常频繁的被调用,从而造成大的性能问题,下面将举例说明下这个问题。

(1)函数被频繁调用。

  • window.onresize事件。我们给window绑定了resize事件,当浏览器的大小窗口被改变时,这个事件的触发频率非常高。如果我们在window.resize事件函数里有一些跟DOM相关的节点操作,往往是非常消耗性能的。这个时候浏览器有可能吃不消,或者卡顿。
  • mosemove事件。 同样,如果我们给一个div绑定了拖拽事件(主要是mousemove),当div节点被拖动时,也会频繁的触发该拖拽事件。
  • 上传进度。在一个文件被浏览器扫描并上传文件之前,会对文件进行扫描并随时通知javascript函数,以便在当前页面中显示当前正真的进度。但通知频率非常高,大概一秒种10次,很显然我们在页面中没有必要这么频繁的去通知。

(2)函数节流的原理
我们通过整理上面提到的是哪个场景,发现它们共同面临的问题是函数触发的频率太高。

(3)函数节流的实现。

关于函数节流的实现代码有很多种,下面的throttle函数的原理是,将即将被执行的函数用setTimeout延迟一段时间执行。如果该执行延迟还没有完成,则忽略接下来调用该函数的请求。throttle函数接受两个参数,第一个参数需要被延迟执行的函数,第二个参数为延迟执行的时间。具体代码如下:

var throttle = function( fn, interval ){
    var __self = fn, //保存需要被延迟执行的函数引用
    timer, //定时器
    firstTime = true; //是否第一次调用

    return function() {
        var args = arguments,
        __me = this;

        if( firstTime ) { //如果是第一次调用,不需要延迟执行
            __self.apply( __me, args );
            return firstTime = false;
        }
        if (timer) { //如果定时器还在,说明潜一次延迟执行还没有完成
            return false;
        }

        timer = setTimeout(function(){//延迟一段时间执行
            clearTimeout(timer);
            timer = null;
            __self.apply(__me, args)
        }, interval || 500)
    } 
}

window.onresize = throttle(function(){
    console.log( 1 )
},500)

 (4)分时函数


在前面的函数节流的讨论中,我们提供了一种限制函数被频繁调用的解决方案,下面我们将遇到另外一个问题,某些函数确实是用户主动调用的,但是因为一些客观的原因,这些函数会严重的影响页面的性能。

一个列子是webQQ创建QQ好友列表,列表中会有成千上万的好友,如果一个节点一个节点的表示,当我们渲染这个列表时,可能一次要往一个页面中创建成千上万的节点。

在短时间内往页面中添加大量DOM节点,会让浏览器吃不消,造成假死。

var ary = [];

for (var i = 1; i <= 1000; i++){
    ary.push(i); //假设ary装在了1000个好友
}

var renderList = function( data ){
    for (var i = 0, l = data.length; i < l; i++){
        var div = document.createElement('div');
        div.innerHTML = i;
        document.body.appendChild( div )
    }
}

renderList( ary )

下面这个问题的解决方案之一是下面的timeChunk函数,timeChunk函数让创建节点工作分批进行,比如1秒钟创建1000个节点变为每隔200秒创建8个节点。

timeChunk函数接受3个参数,第一个参数是创建节点时需要用到的数据,第2个参数是封装了创建节点逻辑的函数,第3个参数表示每一批创建节点的数量。
代码如下:

var timeChunk = function( ary, fn, count ){
    var obj,
    t;
    var len = ary.length;
    var start = function(){
        for (var i = 0; i < Math.min( count || 1, ary.length ); i++) {
            var obj = ary.shift();
            fn( obj )
        }
    };

    return function(){
        t = setInterval(function(){
            if ( ary.length === 0 ){//如果全部节点以及都已经创建好
                return clearInterval(t)
            }
            start()
        },200)
    };
};

最后我们进行一些小测试,假设我们有1000个好友的数据,我们利用timeChunk函数,每一批只往页面中创建8个节点:

var ary = [];

for (var i = 1; i <= 1000; i++){
    ary.push(i); //假设ary装在了1000个好友
}

var renderList = timeChunk( ary, function( n ){
    var div = document.createElement('div');
    div.innerHTML = n;
    document.body.appendChild( div );
}, 8 );

renderList()

(5)惰性加载函数

在web开发中,因为浏览器的实现差异,一些嗅探工作总是不可避免。比如我们需要在一个各个浏览器中都能够通用的事件绑定函数addEvent,常见写法是如下:

var addEvent = function( elem, type, hander ){
    if ( window.addEventListener ){
        return elem.addEventListener( type, hander, false );
    }
    if (window.attachEvent) {
        return elem.attachEvent( 'on' + type, hander )
    }
};

这个函数的缺点是,当它每次被调用时都会执行里面的if条件分支,虽然执行这些if分支的开销不算大,但也许有一些方法可以让程序避免这些重复执行的过程。

第二种方案是这样 ,我们把嗅探浏览器的操作提前到代码加载的时候,在代码加载的时候就进行一次判断,以便让addEvent返回一个包裹了正确的逻辑函数,代码如下:

var addEvent = (function(){
    if ( window.addEventListener ){
        return function( elem, type, hander ){
            elem.addEventListener( type, hander, false );
        }
    }
    if ( window.attachEvent ){
        return function( elem, type, hander ){
            elem.attachEvent( 'on' + type, hander )
        }
    }
})();

目前addEvent函数依然有个缺点,也许我们从头到尾都没有使用过addEvent函数,这样看来,前一次的浏览器嗅探就是完全多余的操作,而且这样也会稍微延长页面的ready时间。

第三种方法我们将要讨论惰性载入函数方案。此时addEvent依然被声明为一个普通函数,在函数里依然有一些分支判断,但是在第一次进入条件分支后,在函数内部会重写这个函数,重写之后的函数就是我们期望的addEvent函数,在下一次几个人addEvent函数的时候,addEvent函数里不再存在条件分支语句。

var addEvent = function( elem, type, handler ){
    if ( window.addEventListener ){
        addEvent = function( elem, type, handler ){
            elem.addEventListener( type, handler, false )
        }
    } else if ( window.attachEvent ){
        addEvent = function( elem, type, handler ){
            elem.attachEvent( 'on' + type, handler);
        }
    }
    addEvent( elem, type, handler )
}

var div = document.getElementById('div1');
addEvent( div, 'click', function(){
    alert('1')
});

addEvent( div, 'click', function(){
    alert('2')
})

 

本文完结

 

 上一篇文章: (二)this、call和apply 下一篇文章 (四)设计模式

posted on 2016-01-09 23:42  村长很忙  阅读(1906)  评论(2编辑  收藏  举报