3. JavaScript 闭包和高阶函数

本文源于本人关于《JavaScript设计模式与开发实践》(曾探著)的阅读总结。想详细了解具体内容建议阅读该书。

1. 闭包

首先我们需要了解变量的作用域以及变量的生存周期。

1.1 变量的作用域

  • 在函数中声明的变量作用域 —— 局部变量。
  • 未在任何函数中声明的变量 —— 全局变量。
  • 如果在函数中未用var等声明变量,这个变量就会变成全局变量。
  • 函数外不能访问函数内的变量,函数内可以访问函数外的变量。
var a = 1;
var func = function(){
    var b = 2;
    var func2 = function(){
        var c = 3;
        console.log(b); // 2
        console.log(a); // 1
    }
    console.log(c); // undefined
}

func();
console.log(b); // undefined

1.2 变量的生存周期

对于存在函数内用var声明的局部变量而言,当函数推出时,这些局部变量就失去了它们的价值,它们都会随着函数调用的结束而被销毁。

现在看看这段代码:

var func = function() {
    var a = 0;
    return function(){
        console.log(a++);
    }
}

var f = func();

f() // 0
f() // 1
f() // 2
f() // 3

跟我们之前的推论相反,函数退出后,局部变量a并没有消失。这是因为执行f=func()时,f返回了一个匿名函数的引用,它可以访问到func被调用时产生的环境,而局部变量a一直处于这个环境里。既然局部变量所在环境还能被外界访问,这个变量就有了不被销魂的理由。在这里产生了一个闭包结构。

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

console.log(Type.isArray([]));
console.log(Type.isString(''));

利用闭包保存每次传入的type。

1.3 闭包作用

封装变量

计算阶乘:

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(1, 2, 3, 4, 5, 5, 5));

var mult_closure = (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;
  }
})()

t1 = new Date();  
console.log(mult_closure(1, 2, 3, 4, 5, 5, 5));
console.log(new Date() - t1);  // 花费4ms
t2 = new Date();
console.log(mult_closure(1, 2, 3, 4, 5, 5, 5));
console.log(new Date() - t2);  // 花费0ms

第二个用了闭包封装了缓存,当计算相同参数时,可以直接返回结果。

闭包合面向对象设计

闭包能够实现的,面向对象也可以实现,反之依然。

闭包:

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

var extent = extent();
extent.call(); // 0
extent.call(); // 1
extent.call(); // 2

面向对象:

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

extent.call(); // 0
extent.call(); // 1
extent.call(); // 2
闭包实现命令模式

命令模式意图把请求封装为对象, 从而分离请求的发起者和请求的接受者。在命令被执行前预先植入命令的接收者。

    var Tv = {
      open: function () {
        console.log('open');
      },
      close: function () {
        console.log('close');
      }
    }

    var createCommand = function (receiver) {
      var execute = function () {
        receiver.open();
      },
        undo = function () {
          receiver.close();
        }
      return {
        execute: execute,
        undo: undo
      }
    }

    var setCommond = function (commond) {
      document.getElementById('execute').onclick = commond.execute;
      document.getElementById('undo').onclick = commond.undo;
    }

    setCommond(createCommand(Tv));

把命令封存在了createCommond中。

高阶函数

  • 函数可以作为参数被传递
  • 函数可以作为返回值输出
函数作为参数传递
  • 回调函数
var getUserInfo = function(userId, callback){
    $.ajax('http://xxx.com/getUserInfo?' + userId, function({
      if(typeof callback === 'function'){
        callback(data)
      }
    })
}

getUserInfo(123, function(data){
    console.log(data.userName);
})

该例子和第一章的一致, 把变化的地方抽离了出来,以回调函数的形式传入。

  • Array.prototype.sort:接受一个函数作为参数。这个函数里面封装了数组元素的排序规则。我们目的对数组进行排序,这是不变的部分,而使用什么规则去排序是可变的部分。
函数作为返回值输出
  • 判断数据类型:
var Type = (function () {
  var Type = {};
  for (var i = 0, type; type = ['String', 'Number', 'Array'][i++];) {
    (function (type) {
      Type['is' + type] = function (obj) {
        return Object.prototype.toString.call(obj) === '[object ' + type + ']';
      }
    })(type)
  }
  return Type;
})();

console.log(Type.isArray([]));
console.log(Type.isString(''));

这个函数把返回的函数都赋予给了Type对象,返回的都是函数,故也为高阶函数。

  • getSingle:单例模式
var getSingle = function (fn) {
  var ret;
  return function () {
    return ret || (ret = fn.apply(this, arguments));
  }
}

var getObj = getSingle(function () {
  return {};
})

var obj1 = getObj();
var obj2 = getObj();

console.log(obj1 === obj2); // true

既把函数作为了参数也作为了返回值,利用了闭包保存了一个单一不变的饮用,如果存在引用则直接返回,不存在才调用函数创建单例。

高阶函数实现AOP

AOP指面向切面编程,主要作用把一些跟核心业务逻辑模块无关的功能抽离出来,再通过“动态织入”的方式渗入业务模块中。通常js中实现AOP都是只把一个函数织入到另一个函数之中,实现方法之一:

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

Function.prototype.after = function (fn) {
  var _self = this; // 保存原函数的引用
  return function () {
    var ret = _self.apply(this, arguments);
    fn.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, 2, 3
其他应用
  • currying:柯里化,函数首先会接受一些参数,接受了参数并不会马上求值,而是继续返回另一个函数,待真正需要求值时,之前传入的所有参数都会被计算。
function curring(fn) {
  var args = []; // 保存不计算的值

  return function () {
    if (arguments.length === 0) {
      return fn.apply(this, args);
    } else {
      [].push.apply(args, arguments);
    }
  }
}

var cost = (function () {
  var money = 0; // 用于记录累加的值

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

var cost = curring(cost);
cost(100);
cost(200);
cost(300);
console.log(cost()); // 600
  • uncurrying:我们通常可以使用call和apply去借用其他对象的方法, 但是有没有办法把泛化this的过程提取出来呢?以下代码是uncurrying的实现方式之一:
Function.prototype.uncurrying = function () {
  var self = this;
  return function () {
    var obj = Array.prototype.shift.call(arguments);
    return self.apply(obj, arguments);
  }
}

for (var i = 0, fn, ary = ['push', 'shift', 'forEach']; fn = ary[i++];) {
  Array[fn] = Array.prototype[fn].uncurrying();
};

var obj = {
  length: 3,
  0: 1,
  1: 2,
  2: 3
};

Array.push(obj ,4);
console.log(obj.length); // 4

通过uncurrying的方式,Array的push就变成了一个普通的push函数,这样push函数的作用也与Array的效果一样。同样不仅仅局限于操作array对象,其他对象也可以。

  • 函数节流:函数被频繁调用 ,严重影响性能:
    • window.onresize
    • mousemove事件
    • 上传进度

节流原理:比如window.onresize,我们在改变窗口大小时,打印窗口大小的工作1s进行了10次,而我们实际上只需要2次或者3次,这就需要我们按时间段来忽略掉一些事件请求。

    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; // 定时器还在 说明还在上一次执行后的暂停时间中
        } else {
          timer = setTimeout(function () {
            clearTimeout(timer);
            timer = null;
            _self.apply(_me, args);
          }, interval || 500);
        }
      }
    }

    window.onresize = throttle(function () {
      console.log(1);
    }, 500);
  • 分时函数: 比如qq渲染好友列表,一次性渲染1000个好友会让浏览器吃不消,故可以改为每隔200毫秒创建8个节点。
    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);
      };
    };

    var ary = [];
    for (var i = 1; i <= 1000; i++) {
      ary.push(i);
    }

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

    renderFriendList();
  • 惰性加载函数:比如判断浏览器类型而采用不同的添加事件方法,如果每次执行添加事件时都要判断,那么效率很低,因为毕竟判断过了一次之后,代码一直都是在相同的环境中运行。
    • 解决办法:提前判断浏览器类型,并选定使用的方法。
    • 缺点:如果我们整个业务下来,并没有用到事件添加函数,那么这次计算就白计算了。
    • 最终:调用了 事件添加函数后,才进行第一次判断,并且利用这个判断的返回值,以后不再继续做判断。
    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);
    }

第一次调用后,根据浏览器类型选定一个固定的事件添加函数赋值给addEvent。 之后就一直使用该方法调用,如果整个流程不使用到事添加函数,则不发生判断。

你可能感兴趣的:(3. JavaScript 闭包和高阶函数)