为了前端的深度-闭包概念与应用

总结

定义:闭包可让一个函数访问并操做其声明时的做用域中的变量和函数,而且,即便声明时的做用域消失了,也能够调用javascript

应用:java

  1. 私有变量
  2. 回调与计时器
  3. 绑定函数上下文
  4. 偏应用函数
  5. 函数重载:缓存记忆、函数包装
  6. 即时函数:独立做用域、简洁代码、循环、类库包装、经过参数限制做用域内的名称

前言

最近忙着公司的项目,没有时间去继续面试受虐,只抽空读了一遍《javascript 忍者秘籍》。jquery

今天晚上有点焦虑失眠,就干脆写一篇本身总结的闭包知识。面试

内容基本所有来自忍者秘籍,以为写的好的话,能够仔细再看一遍书;以为写的很差的,多是由于我理解不到位,致使文中本身思考的地方出了差错,也多是我省略了书中的按部就班,致使漏掉一些知识点。各类缘由,都请指正。缓存

正文

看了不少文章,都在说闭包的定义和闭包的优缺点。我呢,再加上闭包的应用吧。bash

闭包的定义不少文章里都有,我记得有一种角度说只要能访问外部变量的就是闭包,还有一种角度全部函数都是闭包。闭包

我以为这些回答是正确的,可是不太方便面试官继续问下去,或者说是很差引导面试官。因此,若是是我在面试,我会用忍者秘籍里的定义:闭包是一个函数在建立时容许该自身函数访问并操做该自身函数以外的变量时所建立的做用域。这个还有点绕口,更清晰的版本是:闭包可让一个函数访问并操做其声明时的做用域中的变量和函数,而且,即便声明时的做用域消失了,也能够调用。要注意的是:闭包不是在建立的那一时刻点的状态的快照,而是一个真实的封装,只要闭包存在,就能够对其进行修改。app

最简单的闭包:异步

// 全局做用于就是一个闭包
var outerVal = 'lionel';
function outerFn(){
  console.log(outerVal)
}
outerFn() // lionel
复制代码

复杂点的,也是咱们印象中的:函数

var outerVal = 'lionel';
var later;
function outerFn(){
  var innerVal = 'karma';
  function innerFn(){
    console.log(outerVal, innerVal);
  }
  later = innerFn;
}
outerFn();  // 此时outerFn的做用域已经消失了
later();  // lionel karma
复制代码

难以理解的,这个例子咱们能够理解到,闭包不是快照:

var later;
function outerFn(){
  function innerFn(){
    console.log(lateVal)
  }
  later = innerFn;
}
console.log(lateVal); // undefined
var lateVal = 'lionel'; // 变量提高,闭包声明的那一刻存在这个变量
outerFn();
later(); // lionel
复制代码

缺点你们很熟悉了,闭包里的信息会一直保存在内存里。解决方法是,在你以为能够的地方,清除引用,像上面的例子中,使用 later = null 便可,这样就能够在下次垃圾回收中,清除闭包。

下面咱们重点来看一下闭包的实际应用

1、私有变量

闭包常见的用法,封装私有变量。用户没法直接获取和修改变量的值,必须经过调用方法;而且这个用法能够建立只读的私有变量哦。咱们从下面的例子来理解:

function People(num) { // 构造器
  var age = num;
  this.getAge = function() {
    return age;
  };
  this.addAge = function() {
    age++;
  };
}
var lionel = new People(23); // new方法会固化this为lionel哦
lionel.addAge();
console.log(lionel.age);      // undefined
console.log(lionel.getAge()); // 24
var karma = new People(20);
console.log(karma.getAge()); // 20
复制代码

以下图,lionel中并不存在age属性,age只存在new的那个过程的做用域中,而且,getAge和addAge中,咱们能够看到他们的做用域中都包含一个People的闭包。

alt

2、回调和计时器

这部分我没有多聊的,

3、绑定函数上下文

刚看到这个应用可能有点懵,仔细想一想其实咱们看到不少次了,那就是bind()函数的实现方式,这里再贴一次简单实现的代码:

Function.prototype.myBind = function() {
  var fn = this,
      args = [...arguments],
      object = args.shift();
  return function() {
    return fn.apply(object, args.concat(...arguments))
  }
}
复制代码

这里要注意的是:bind()并非apply和call的替代方法。该方法的潜在目的是经过匿名函数和闭包控制后续执行上下文。

4、偏应用函数

偏应用函数返回了一个含有预处理参数的函数,以便后期能够调用。具体仍是看代码吧

Function.prototype.partial = function() {
  var fn = this,
      args = [...arguments];
  return function() {
    var arg = 0;
    var argsTmp = [...args]
    for (var i=0; i<argsTmp.length && arg < arguments.length; i++) {
      if (argsTmp[i] === undefined) {
        argsTmp[i] = arguments[arg++]
      }
    }
    return fn.apply(this, argsTmp)
  }
}
function addAB(a ,b) {
  console.log( a + b);
}
var hello = addAB.partial('hello ', undefined);
hello('lionel'); // hello lionel
hello('karma'); // hello karma
var bye = addAB.partial(undefined, ' bye')
bye('lionel'); // lionel bye
bye('karma'); // karma bye
复制代码

上面的例子可能有点难以理解,下面是一个简化版的例子:

function add(a) {
  return function(b) {
    console.log( a + b);
  };
}
var hello = add('hello ')
hello('lionel'); // hello lionel
hello('karma'); // hello karma
复制代码

emmm... 写到这里去研究了半天柯里化和偏函数的区别,最终找到一篇文章符合个人想法:偏函数与函数柯里化,不对的地方请指正。

5、函数重载

1 缓存记忆

咱们能够经过闭包来包装一个函数,,从而让调用咱们函数的人,不知道咱们采用了缓存的方法,或者说,不须要调用者额外作什么,就能够缓存计算结果,以下代码

Function.prototype.memoized = function(key) {
  this._values = this._values || {};
  return this._values[key] !== undefined ?
    this._values[key] + ' memoized' :
    this._values[key] = this.apply(this, arguments);
}
Function.prototype.memoize = function() {
  var fn = this;
  return function() {
    // return fn.memoized.apply(fn, arguments);
    console.log(fn.memoized.apply(fn, arguments))
  }
}
var computed = (function(num){
  // 这里有超级超级复杂的计算,耗时特别久
  console.log('----计算了好久-----')
  return 2
}).memoize();
computed(1); // ----计算了好久-----     2
computed(1); // 2 memoized
复制代码

2 函数包装

下面的这个例子写的没有书里的好。

function wrap(object, method, wrapper){
  var fn = object[method];
  return object[method] = function() {
    return wrapper.apply(this, [fn.bind(this)].concat(...arguments))
  }
}
let util = {
  reciprocal: function(tag){
    console.log(1 / tag)
  }
}

wrap(util, 'reciprocal', function(original, tag){
   return tag == 0 ? 0 : original(tag)
})

util.reciprocal(0);  // 0
复制代码

6、即时函数

针对为何即时函数会放在闭包里介绍,下图是一个很好的说明:

alt

1 独立做用域

var button = $('#mybtn');
(function(){
  var numClicks = 0;
  button.click = function(){
    alert(++numClicks)
  }
})
复制代码

2 简洁代码

// 例若有以下data
data = {
  a: {
    b: {
      c: {
        get: function(){},
        set: function(){},
        add: function(){}
      }
    }
  }
}
// 第一种调用这三个方法的代码以下, 繁琐
data.a.b.c.get();
data.a.b.c.set();
data.a.b.c.add();
// 第二种方法以下, 引入多余变量
var short = data.a.b.c;
short.get();
short.set();
short.add();
// 第三种使用即时函数 优雅
(function(short){
  short.get();
  short.set();
  short.add();
})(data.a.b.c)
复制代码

3 循环

这部分是经典的for循环中调用setTimeout打印i,之因此打印i为固定值,是由于闭包并非快照,而是变量的引用,在执行到异步队列时,i已经改变。

解决方法就是再用一个闭包和即时函数。

4 类库包装

// 下方的代码展现了,为何jquery库中,它能够放心的用jquery而不担忧这个变量被替换
(function(){
  var jQuery = window.jQuery = function() {
    // Initialize
  };
  // ...
})()
复制代码

5 经过参数限制做用域内的名称

// 当咱们担忧jquery中的$符号,被其余库占用,致使咱们代码出问题的时候,
// 用下面的方法,就能够放心大胆的用啦(不过要注意:若是jQuery也被占用的话就...)
(function($){
  $.post(...)
})(jQuery)
复制代码
相关文章
相关标签/搜索