从闭包到做用域再到具体的应用

前言

该篇文章是我我的《JavaScript高级程序设计》精读笔记系列中节选的文章,感受此次系统基础的学习JS基础知识确实学到了不少,特此来跟你们分享,勿怪,勿笑 ╰( ̄▽ ̄)╭ ,有错误或者是建议的小伙伴请踊跃发言,谢谢你们!前端

那么,本篇文章你能学到什么?webpack

  • 闭包的概念
  • 词法做用域/静态做用域/动态做用域
  • 做用域与做用域链
  • 标识符在做用域链上的解析
  • 换个角度,经过做用域与做用域链来看待 this 指向和函数的 arguments
  • 经过闭包实现对象的私有成员变量
  • 经过闭包实现模块模式

闭包

“闭包(closure)”实际上就是被返回的函数有权访问其包含函数中定义的变量和标识符。 “闭包”能够访问其包含函数中的变量标识符,原理在于闭包函数的做用域链中引用了包含函数的做用域。web

function debounce(fn, wait) {
  var timeout = null;
  return function() {
    var args = arguments;
    var that = this;
    if (timeout) clearTimeout(timeout);
    timeout = setTimeout(function() {
      fun.apply(that, args);
    }, wait);
  };
}
复制代码

在这个“防抖”的示例中,debounce 就是“包含函数”,而 debounce 函数内部经过 return 语句返回的匿名函数就是前面咱们所说的 —— “闭包函数”。 对于“包含函数”、“匿名闭包函数” 以及全局对象 window,它们之间的包含关系,相信很快就会联想到下图。express

在了解了关于“闭包”的大体概念后,如今咱们来继续了解“做用域”、“做用域链”的相关知识,由于这些才是闭包的底层原理。 首先、“做用域”指的是变量的访问范围,由于 JS 是“词法做用域”(lexical scoping),即“静态做用域”而非“动态做用域”,因此函数的做用域范围在函数定义的时候便已经确立,在 JS 中每一个函数都会有独立的局部做用域,因此咱们要说的“做用域链”实际上就是多个函数做用域的串联,这一点很是相似与对象继承关系的“原型链”。但与原型链中原型属性 [[Prototype]] 属性与原型对象 prorotype 的关联关系又有所不一样,“做用域链”其实是多层级函数嵌套的过程,即做用域链内的每个环节(做用域)都对应者嵌套的每一层函数,关于这点的理解的图示,可见读书笔记的第四章。浏览器

“做用域”与“做用域链”听上去很虚,难以理解,那么咱们就寻找它们存在的实物而后参照着去理解,首先咱们是否还记得第五章函数类型时咱们分析过,当一个函数被执行的时候,首先会建立该函数的执行上下文 EC,而后将该上下文加入执行栈中等待执行,实际中当函数的执行上下文建立好后,分别会使用 this 以及函数的 arguments 和其它函数的形参来初始化执行环境的 “变量对象”,因为这个变量对象只在函数执行的时候才被建立,所以也称之为“活动对象”,同时初始化的还有“做用域链”对象并将其保存在函数内部的 [[scope]] 属性中。缓存

function fn() {}
console.dir(fn);
/* arguments: null caller: null length: 0 name: "fn" prototype: {constructor: ƒ} __proto__: ƒ () [[Scopes]]: Scopes[1] */
复制代码

抛开“原型链”对象,咱们知道“变量对象”其实是一个 MAP 表结构,存放了当前函数内的全部标识符(私有变量、函数的形参、arguments),当在函数内部去访问这些私有的标识符的时候,都会前往该函数的活动对象中去查找,所以“活动对象”就决定了某个标识符是否能解析获取成功,即“变量对象”就是“做用域”,由于它决定了当前函数内的标识符解析。安全

每一个函数都有本身的”活动对象“,因此每一个函数都有本身的独立做用域,而“做用域链”则是指向这些变量对象的指针列表,它只引用但不实际包含变量对象,所以每一个函数的做用域链对象中都会有一个引用自身的“变量对象”,若是这个函数还被嵌套在其它函数中,那么其做用域链表的第二个位置就是其嵌套函数“活动对象”,依次类推做用域链表的最顶端必定是全局环境 window 的“变量对象” —— 即全局做用域,由于全部的局部代码都是嵌套在全局代码内的,这也很好的说明了为何咱们不论在那里编写代码,总能访问到全局变量。闭包

总结下,“变量对象”或者是“活动对象”就是“做用域”,它们决定了某个变量或标识符可否查询到,而“做用域链”对象则是保存了一张做用域链表,这张表里除了保存当前做用域的引用,还保存了当前做用域上层以及上上层等做用域的引用(若是有的话),最终到达“全局做用域”。app

与原型链中原型属性的访问机制相同,若是要解析的标识符没有在当前的做用域对象中查询到,则会沿着做用域链表的顺序依次在每一个做用域中进行查找,若是查询到了则会中止解析,哪怕再上层具备同名的标识符也不会去解析 —— 这就是标识符在做用域链中的解析过程。异步

闭包与变量

因为“闭包函数”是在上一层执行环境执行完成后返回的,因此若是闭包函数内部经过做用域链去查询上层活动对象中的标识符,那么只会得到上层活动对象中全部变量的最后阶段的值。

function getIndex() {
  var clouser = [];
  for (var i = 0; i < 10; i++) {
    clouser.push(function() {
      console.log(i); //每次都会是10
    });
  }
  return clouser;
}
复制代码

缘由很简单,那就是闭包返回的时候上层函数 getIndex 已经执行完了,其活动对象中保存的 i 变量的值已是 10,此时闭包再去获取总会固定返回 10。 若是想每次执行闭包都返回对应的索引,咱们能够在闭包函数外面再套一层“当即执行函数表达式”,套的缘由在于每次循环的时候都会建立一个匿名函数,而后把循环的索引值做为参数传递这个匿名函数,让每次经过循环生成的当即执行函数使用本身的活动对象来保存每一个阶段循环的索引值,而后当即执行函数中再返回闭包,这样闭包就能够经过上层当即执行函数的活动对象来读取循环的每一个阶段的值,返回的也总会是每次循环的索引值。

function getIndex() {
  var clouser = [];
  for (var i = 0; i < 10; i++) {
    clouser.push(
      (function(index) {
        return function() {
          console.log(index); //每次都会是10
        };
      })(i)
    );
  }
  return clouser;
}
复制代码

而后调用每次循环生成的函数

getIndex()[0](); //0
getIndex()[1](); //1
//....
getIndex()[9](); //9
复制代码

闭包与 this

其实应该说的是“活动对象”与 this ,因为每一个活动对象都会存在 this 对象,而且当函数或方法不属于某个对象的成员属性,那么其活动对象中 this 默认指向的都是 window,所以就会形成内部函数每次搜索 this 的时候,只会搜索到当前活动对象为止。即永远不会再访问外部函数活动对象的 this,并且固定的指向 window

若是内部函数想引用外部函数的 this 其实很简单,只须要在外部函数的做用域中在定义一个私有变量,而后保存当前活动对象中的 this,那么,当内部函数去解析这个变量标识符的时候,便会沿着做用域链到上层的做用域对象中去取这个变量所引用的 this

function Fn() {
  var that = this;
  return function() {
    console.log(that);
  };
}
复制代码

闭包与 arguments

this 的原理相同,由于每一个函数的“活动对象”中都存在 arguments 对象,因此当内部函数每次搜素 arguments 的时候,都只会搜索到当前的活动对象为止,即不会超出当前做用域的范围。

闭包回收

对于主流浏览器而言闭包的回收很简单,只须要将引用闭包的变量置为 null 便可,但值得咱们注意的一点是,若是一个闭包没有被回收,并且这个闭包内部还访问了其它做用域中的标识符,那么保存这些标识符的执行环境以及执行环境中的做用域链对象在执行完成后确实会被销毁,可是这些执行环境中的“活动对象”却永远不会被销毁,由于闭包的做用域链还对这些活动对象具备引用关系。

固然销毁闭包等于销毁一切,但这只是针对采用“标记清除”垃圾回收机制的现代浏览器而言的,对于采用“引用计数”机制的老版本 IE(IE8-),咱们在编码的时候就须要具体的考量,以免循环引用的状况发生。

var handleClick = function() {
  var elem = document.getElementById("btn");
  elem.onclick = function() {
    return elem.id;
  };
};
复制代码

本来的本意是定义一个 handleClick 的函数用于封装 btn 元素的点击事件,可是因为 elem 这个 DOM 对象经过 onclick 引用了一个匿名函数,而这个匿名函数中又由于返回 elem.id 的值,再次引用了 elem DOM 元素,所以就产生了循环引用的状况,若是在老版本的 IE,那么这里的标识符将没法被正常的回收。

此时,咱们就须要在编码的过程当中仔细的考量,谨慎的定义多个对象之间的引用关系。

var handleClick = function() {
  var elem = document.getElementById("btn");
  var id = elem.id;

  elem.onclick = function() {
    return id;
  };

  elem = null;
};
复制代码

对象的私有变量

对象不像函数,具备独立的做用域且能够定义私有的变量或方法,对象的成员属性具备透明性,即全部的方法均可以访问对象中的任何成员属性或成员方法,对没有进行不可扩展、密封、冻结的对象还能够进行添加、修改、删除该对象成员的操做。 若是要让对象也具备函数私有变量同样性质的私有成员,解决的办法就是经过“对象”与“闭包”来模拟实现,为何说是“模拟”由于咱们所认为的私有成员实际上仍是闭包方法的上一层做用域的私有变量,只是该私有变量能够被咱们所建立的对象进行引用,并且还只能经过对象上特定的方法才能访问,但本质上该变量并不是是对象的实际成员。

咱们先从一个简单的全局变量入手,咱们定义一个全局的变量 _g,而后在一个当即执行函数中为这个全局变量赋值一个匿名函数表达式,这个匿名函数中存在着对上层做用域的变量进行引用。

var _g;

(function() {
  var value = "IIFE";
  _g = function() {
    return value;
  };
})();

_g();
复制代码

在这个简单的示例中咱们要学会两个概念的定义,例如当即执行函数中的 value私有的变量,可以访问这个私有的变量只有 _g 这个方法,所以咱们把有权访问私有变量和私有函数的公有方法称之为 特权方法。 继续深刻,如今若是如今这个全局变量 _g 是一个对象会怎么样呢?

var _g = {};

(function() {
  var value = "function expression";
  _g.getValue = function() {
    return value;
  };
  _g.setValue = function(v) {
    value = v;
  };
})();

_g.getValue(); //function expression
复制代码

如今咱们就为变量 _g 定义了一个模拟的私有成员属性 value,这个属性直接经过对象是没法获取到的,只有使用两个特权方法 getValuesetVlaue 才可以获取以及操做。 可是示例的方式都是经过全局变量来引用当即执行函数中的私有函数,并且全局变量与当即执行函数也是分开定义的,缺乏总体性与封装性,所以咱们还能够继续更一步的改进,即在对象建立的时候就定义私有成员以及特权方法。

对象的静态私有变量

“对象的静态私有变量”指的是在对象的构造函数中定义私有变量和特权方法,而后将特权方法做为对象的实例成员随同返回。

function Person(name) {
  var name = "blob";
  this.getName = function() {
    return name;
  };
  this.setName = function(n) {
    name = n;
  };
}
复制代码

根据构造函数的特性,每次执行构造函数返回的实例对象都具备独立的私有变量与特权方法,多个实例对象同时对 name 进行设置与获取,都是独立的操做。

对象的共享私有变量

“对象的共享私有变量” 这种模式下对象的私有成员其实是一个当即执行函数的私有变量,而后特权方法则定义在构造函数的原型上,这样全部该构造函数的实例对象均可以操做读取这个公共的私有成员属性。

(function() {
  var name = "";
  Person = function(n) {
    name = n;
  };
  Person.prototype.getName = function() {
    return name;
  };
  Person.prototype.setName = function(n) {
    name = n;
  };
})();
复制代码

注意:只要没有使用关键字声明的变量都是全局变量。

在这种模式下,咱们 setNamegetName 实际上都是对同一个私有变量进行操做。

小结

“对象的静态私有变量” 与 “对象的共享私有变量”的优势 相比普通离散的方式,“对象的静态私有变量” 与 “对象的共享私有变量”更具备可封装性,同时还能够定义特定引用类型且具备私有成员变量的对象。

“对象私有成员变量”的做用? 例如,咱们须要对象的某个成员属性做为缓存空间,来保存特权方法处理的结果,可是又不想让这个成员属性直接暴漏出来,能够被任何的方法进行修改读取等。

思考 我的认为使用对象的扩展性、密封性、冻结性等彻底能够做为“对象私有成员变量”的替代方案。

模块模式

“模块模式” 中的模块实际上就是指函数做用域的封闭性与独立性。 “模块模式” 实际上就是对闭包的一种运用,它定义了一个封闭的空间,而后暴漏出一个接口,只有经过这个接口,才能访问这个模块的内部的私有变量和标识符。

模块模式的做用

上面都是对“模块模式”的定义以及模块模式的性质进行说明,而实际运用上,模块模式的最大价值在于保持代码块之间相互独立,清晰地分离和组织项目中的代码单元,而这一点的具体体现就是知名的前端打包工具 webpack ,它就是利用了 JS 函数具备模块的封闭性来模拟其它面向对象语言中的 Package 概念,除此以外,AMD 模式、CommonJS 模块、IEFF 模块等都是模块模式的典型运用。

基本模块模式

就是具备独立做用域的普通声明函数和函数表达式。

function applaction() {
    return {}
}

var applaction = function(){ return {}}
复制代码

经典模块模式

var applaction = function (component) {
    var components = [];
    if (typeof components === 'object') {
        components.push(component)
    }
    return {
        getComponentCount: function () {
            return components.length;
        },
        registerComponent: function (component) {
            if (typeof components === 'object') {
                components.push(component)
            }
        }
    }
}
复制代码

实际到了这里咱们“面向对象程序设计 - 建立对象”中所说的“稳妥构造函数模式”(我我的喜欢称呼的简单的安全工厂模式)就是对“模块模式”结合对象的建立返回。

当即执行的模块模式(IEFF)

var applaction = (function() {
  return {};
})();
复制代码

可扩展的模块模式

(function($) {
  // 获取 jQuery
})(jQuery);
复制代码

高级的可扩展模块模式

var applaction = (function(module) {
  //add method or add property
  return module;
})(module || {});
复制代码

这种方式的优势在于能够进行异步脚本的执行,好比这个 module 对象尚未获取到的状况。

Sub-modules

能够基于 Module 创建 Sub Module

applaction.sub = (function() {
  return {};
})();
复制代码

PS: 最后自荐下个人 《JavaScript 高级程序设计》读书笔记

相关文章
相关标签/搜索