虽然 JavaScript 天生就是一副随随便便的样子 但是随着浏览器能够完成的事情越来越多 这门语言也也越来越经常地摆出正襟危坐的架势 在复杂的逻辑下 JavaScript 需要被模块化 模块需要封装起来 只留下供外界调用的接口 闭包是 JavaScript 中实现模块封装的关键 也是很多初学者难以理解的要点 最初 我也陷入迷惑之中 现在 我自信对这个概念已经有了比较深入的理解 为了便于理解 文中试图 封装一个比较简单的对象
我们试图在页面上维护一个计数器对象 ticker 这个对象维护一个数值 n 随着用户的操作 我们可以增加一次计数(将数值 n 加上 ) 但不能减少 n 或直接改变 n 而且 我们需要时不时查询这个数值
门户大开的 JSON 风格模块化
一种门户大开的方式是
复制代码 代码如下: var ticker = { n: tick:function(){ this n++ } }这种方式书写自然 而且确实有效 我们需要增加一次计数时 就调用 ticker tick() 方法 需要查询次数时 就访问 ticker n 变量 但是其缺点也是显而易见的 模块的使用者被允许自由地改变 n 比如调用 ticker n 或者 ticker n= 我们并没有对 ticker 进行封装 n 和 tick() 看上去是 ticker 的“成员” 但是它们的可访问性和 ticker 一样 都是全局性的(如果 ticker 是全局变量的话) 在封装性上 这种模块化的方式比下面这种更加可笑的方式 只好那么一点点(虽然对有些简单的应用来说 这一点点也足够了)
复制代码 代码如下: var ticker = {}var tickerN = var tickerTick = function(){ tickerN++}
tickerTick()
值得注意的是 在 tick() 中 我访问的是 this n ——这并不是因为 n 是 ticker 的成员 而是因为调用 tick() 的是 ticker 事实上这里写成 ticker n 会更好 因为如果调用 tick() 的不是 ticker 而是其他什么东西 比如
复制代码 代码如下: var func = ticker tickfunc()这时 调用 tick() 的其实是 window 而函数执行时会试图访问 window n 而出错
事实上 这种“门户大开”型的模块化方式 往往用来组织 JSON 风格的数据 而不是程序 比如 我们可以将下面这个 JSON 对象传给 ticker 的某个函数 来确定 ticker 从 开始计数 每次递进
复制代码 代码如下: var config = { nStart: step: }作用域链和闭包 来看下面的代码 注意我们已经实现了传入 config 对 ticker 进行自定义
复制代码 代码如下: function ticker(config){ var n = config nStart function tick(){ n += config step } } console log(ticker n)// >undefined你也许会疑惑 怎么 ticker 从对象变成了函数了?这是因为 JavaScript 中只有函数具有作用域 从函数体外无法访问函数内部的变量 ticker() 外访问 ticker n 获得 undefined 而 tick() 内访问 n 却没有问题 从 tick() 到 ticker() 再到全局 这就是 JavaScript 中的“作用域链”
可是还有问题 那就是——怎么调用 tick() ? ticker() 的作用域将 tick() 也掩盖了起来 解决方法有两种
• )将需要调用方法作为返回值 正如我们将递增 n 的方法作为 ticker() 的返回值 • )设定外层作用域的变量 正如我们在 ticker() 中设置 getN
复制代码 代码如下: var getNfunction ticker(config){ var n = config nStart getN = function(){ return n } return function(){ n += config step }}var tick = ticker({nStart: step: })tick()console log(getN())// >
请看 这时 变量 n 就处在“闭包”之中 在 ticker() 外部无法直接访问它 但是却可以通过两个方法来观察或操纵它
在本节第一段代码中 ticker() 方法执行之后 n 和 tick() 就被销毁了 直到下一次调用该函数时再创建 但是在第二段代码中 ticker() 执行之后 n 不会被销毁 因为 tick() 和 getN() 可能访问它或改变它 浏览器会负责维持n 我对“闭包”的理解就是 用以保证 n 这种处在函数作用域内 函数执行结束后仍需维持 可能被通过其他方式访问的变量 不被销毁的机制
可是 我还是觉得不大对劲?如果我需要维持两个具有相同功能的对象 ticker 和 ticker 那该怎么办? ticker() 只有一个 总不能再写一遍吧?
new 运算符与构造函数 如果通过 new 运算符调用一个函数 就会创建一个新的对象 并使用该对象调用这个函数 在我的理解中 下面的代码中 t 和 t 的构造过程是一样的
复制代码 代码如下: function myClass(){} var t = new myClass()var t = {}t func = myClasst func()t func = undefinedt 和 t 都是新构造的对象 myClass() 就是构造函数了 类似的 ticker() 可以重新写成
复制代码 代码如下: function TICKER(config){ var n = config nStart this getN = function(){ return n } this tick = function(){ n += config step } }var ticker = new TICKER({nStart: step: })ticker tick()console log(ticker getN())// >var ticker = new TICKER({nStart: step: })ticker tick()ticker tick()console log(ticker getN())// >
习惯上 构造函数采用大写 注意 TICKER() 仍然是个函数 而不是个纯粹的对象(之所以说“纯粹” 是因为函数实际上也是对象 TICKER() 是函数对象) 闭包依旧有效 我们无法访问 ticker n
原型 prototype 与继承 上面这个 TICKER() 还是有缺陷 那就是 ticker tick() 和 ticker tick() 是互相独立的!请看 每使用 new 运算符调用 TICKER() 就会生成一个新的对象并生成一个新的函数绑定在这个新的对象上 每构造一个新的对象 浏览器就要开辟一块空间 存储 tick() 本身和 tick() 中的变量 这不是我们所期望的 我们期望 ticker tick 和 ticker tick 指向同一个函数对象
这就需要引入原型
JavaScript 中 除了 Object 对象 其他对象都有一个 prototype 属性 这个属性指向另一个对象 这“另一个对象”依旧有其原型对象 并形成原型链 最终指向 Object 对象 在某个对象上调用某方法时 如果发现这个对象没有指定的方法 那就在原型链上一次查找这个方法 直到 Object 对象
函数也是对象 因此函数也有原型对象 当一个函数被声明出来时(也就是当函数对象被定义出来时) 就会生成一个新的对象 这个对象的 prototype 属性指向 Object 对象 而且这个对象的 constructor 属性指向函数对象
通过构造函数构造出的新对象 其原型指向构造函数的原型对象 所以我们可以在构造函数的原型对象上添加函数 这些函数就不是依赖于 ticker 或 ticker 而是依赖于 TICKER 了
你也许会这样做
复制代码 代码如下: function TICKER(config){ var n = config nStart} TICKER prototype getN = function{ // attention : invalid implementation return n}TICKER prototype tick = function{ // attention : invalid implementation n += config step}请注意 这是无效的实现 因为原型对象的方法不能访问闭包中的内容 也就是变量 n TICK() 方法运行之后无法再访问到 n 浏览器会将 n 销毁 为了访问闭包中的内容 对象必须有一些简洁的依赖于实例的方法 来访问闭包中的内容 然后在其 prototype 上定义复杂的公有方法来实现逻辑 实际上 例子中的 tick() 方法就已经足够简洁了 我们还是把它放回到 TICKER 中吧 下面实现一个复杂些的方法 tickTimes() 它将允许调用者指定调用 tick() 的次数
复制代码 代码如下: function TICKER(config){ var n = config nStart this getN = function(){ return n } this tick = function(){ n += config step }} TICKER prototype tickTimes = function(n){ while(n>){ this tick() n } }var ticker = new TICKER({nStart: step: })ticker tick()console log(ticker getN())// >var ticker = new TICKER({nStart: step: })ticker tickTimes( )console log(ticker getN())// >这个 TICKER 就很好了 它封装了 n 从对象外部无法直接改变它 而复杂的函数 tickTimes() 被定义在原型上 这个函数通过调用实例的小函数来操作对象中的数据
所以 为了维持对象的封装性 我的建议是 将对数据的操作解耦为尽可能小的单元函数 在构造函数中定义为依赖于实例的(很多地方也称之为“私有”的) 而将复杂的逻辑实现在原型上(即“公有”的)
最后再说一些关于继承的话 实际上 当我们在原型上定义函数时 我们就已经用到了继承! JavaScript 中的继承比 C++ 中的更……呃……简单 或者说简陋 在 C++ 中 我们可能会定义一个 animal 类表示动物 然后再定义 bird 类继承 animal 类表示鸟类 但我想讨论的不是这样的继承(虽然这样的继承在 JavaScript 中也可以实现) 我想讨论的继承在 C++ 中将是 定义一个 animal 类 然后实例化了一个 myAnimal 对象 对 这在 C++ 里就是实例化 但在 JavaScript 中是作为继承来对待的
JavaScript 并不支持类 浏览器只管当前有哪些对象 而不会额外费心思地去管 这些对象是什么 class 的 应该具有怎样的结构 在我们的例子中 TICKER() 是个函数对象 我们可以对其赋值(TICKER= ) 将其删掉(TICKER=undefined) 但是正因为当前有 ticker 和 ticker 两个对象是通过 new 运算符调用它而来的 TICKER() 就充当了构造函数的作用 而 TICKER prototype 对象 也就充当了类的作用
lishixinzhi/Article/program/Java/JSP/201311/20429
在A函数中嵌套B函数,B函数访问A函数中的变量。将A函数复制给C函数并执行。
那么在大多数的理解中,包括许多著名的书籍,文章里都以函数C的名字代指这里生成的闭包。而在chrome中,则以执行上下文A的函数名代指闭包。
而我的理解是:闭包更准确的说是一项技术或者一个特性:只要运用具备阻止垃圾回收机制回收和突破作用域链限制的技术,就是闭包。像是《JavaScript权威指南》打的比方,像是把变量包裹了起来,形象的称为“闭包”。
如果非要指明哪个函数是闭包的话,我愿意将A函数称为定义闭包的函数,C函数为执行闭包的函数。
a. 在函数内部创建新的函数;
b. 新的函数在执行时,访问了函数的变量对象。
c. 闭包是在函数被调用执行的时候才被确认创建的。
一句话总结:==函数中闭包判定的准则,即执行时是否在内部定义的函数中访问了上层作用域的变量。==
闭包,阻止垃圾回收机制。
闭包,突破作用域链接。
《你不知道的JS中》的示例:
模块模式需要具备两个必要条件:
1.必须有外部的封闭函数,该函数必须至少被调用一次(每次调用都会创建一个新的模块实例)。
2.封闭函数必须返回至少一个内部函数,这样内部函数才能在私有作用域中形成闭包,并且可以访问或者修改私有的状态。
什么时候使用模块模式?
如果必须创建一个对象并一某些数据对其进行初始化,同时还要公开一些能够访问这些私有数据的方法,那么就可以使用模块模式。
哪些地方有用到模块模式?
最典型的就是JQuery库,jQuery和$标识符就是JQuery模块的公共API,但它们本身都是函数(由于函数也是对象,它们本身也可以拥有属性)
以后单独拿一个章节,来具体讲讲现在的模块化和未来的模块化机制。
具体的内容在函数的柯里化的章节中详细分析。
戳此传送门
输出6的原因是:被封闭在一个共享的全局作用域中,实际上只有一个i。
依次输出1-5的原因是:在迭代内部使用IIFE(立即表达函数)为每个迭代生成一个新的作用域,将外部变量的值传递进去并被引用(阻止垃圾回收机制回收),使得每个回调函数都能访问到正确值的变量。
戳此传送门
我们在开发及接触新技术的时候,总会接触到该技术以何种规范编写,为了更加系统了解这些规范,本文总结了AMD,CMD,CommonJs,UMD,ESM几种规范,供大家学习了解。
AMD 是 RequireJS 在推⼴过程中对 模块定义的规范化 产出,它是⼀个概念,RequireJS是对这个概念的实现,就好⽐JavaScript语⾔是对ECMAScript规范的实现。AMD是⼀个组织,RequireJS是在这个组织下⾃定义的⼀套脚本语⾔
RequireJS:是⼀个AMD框架,可以异步加载JS⽂件,按照模块加载⽅法,通过define()函数定义。第⼀个参数是⼀个数组,⾥⾯定义⼀些需要依赖的包,第⼆个参数是⼀个回调函数,通过变量来引⽤模块⾥⾯的⽅法,最后通过return来输出()。
是⼀个 依赖前置 、 异步定义 的AMD框架(在参数⾥⾯引⼊js⽂件),在定义的同时如果需要⽤到别的模块,在最前⾯定义好即在参数数组⾥⾯进⾏引⼊,在回调⾥⾯加载
AMD 定义了一套 JavaScript 模块依赖异步加载标准,来解决同步加载的问题。模块通过 define 函数定义在闭包中,格式如下:
define(id?: String, dependencies?: String[], factory: Function|Object)
一些栗子:
注意:在 webpack 中,模块名只有局部作用域,在 Require.js 中模块名是全局作用域,可以在全局引用。
定义一个没有 id 值的匿名模块,通常作为应用的启动函数:
依赖多个模块的定义:
模块输出:
在模块定义内部引用依赖:
SeaJS 在推⼴过程中对模块定义的规范化产出,是⼀个同步模块定义,是SeaJS的⼀个标准,SeaJS是CMD概念的⼀个实现,SeaJS是淘宝团队提供的⼀个模块开发的js框架.
通过define()定义, 没有依赖前置 ,通过require加载模块,CMD是 依赖就近 ,在什么地⽅使⽤到模块就在什么地⽅require该模块,即⽤即返,这是⼀个 同步 的概念
在前端浏览器⾥⾯并不⽀持module.exports,CommonJS 是以在浏览器环境之外构建 JavaScript 生态系统为目标而产生的项目,比如在服务器和桌面环境中。
Nodejs端是使⽤CommonJS规范的,前端浏览器⼀般使⽤AMD、CMD、ES6等定义模块化开发的。输出⽅式有2种:默认输出module export 和带有名字的输出exports.area
CommonJS 规范是为了解决 JavaScript 的作用域问题而定义的模块形式,可以使每个模块它自身的命名空间中执行。该规范的主要内容是,模块必须通过 module.exports 导出对外的变量或接口,通过 require() 来导入其他模块的输出到当前模块作用域中。
兼容AMD和commonJS规范的同时,还兼容全局引⽤的⽅式