《你不知道的JavaScript》梗概
⌨️

《你不知道的JavaScript》梗概

Author
Tags
技术
前端
Published
Mar 16, 2016

作用域和闭包

作用域是什么

编译原理

  • 分词/词法分析
    • 例:var a = 2;对这段代码进行词法分析,通常会分解成var a = 2 ;
  • 解析/语法分析
    • 生成“抽象语法树(Abstract Syntax Tree, AST)”
  • 代码生成
    • 通过AST编译成一组机器指令
注:JavaScript引擎的编译要比以上复杂的多。例如:在语法分析和代码生成阶段有特定的步骤来对运行性能进行优化,包括对冗余元素进行优化。

理解作用域

  • 引擎
    • 从头到尾负责整个JavaScript程序的编译及执行过程。
  • 编译器
    • 引擎的好朋友之一,负责语法分析及代码生成等脏活累活(详见前一节的内容)。
  • 作用域
    • 引擎的另一位好朋友,负责收集并维护由所有声明的标识符(变量)组成的一系列查询,并实施一套非常严格的规则,确定当前执行的代码对这些标识符的访问权限。
接下来看一段代码执行过程中他们是如何进行交流
function foo(a) { console.log( a ); // 2 } foo( 2 );
对话:
“引擎:我说作用域,我需要为foo进行RHS引用。你见过它吗? 作用域:别说,我还真见过,编译器那小子刚刚声明了它。它是一个函数,给你。 引擎:哥们太够意思了!好吧,我来执行一下foo。 引擎:作用域,还有个事儿。我需要为a进行LHS引用,这个你见过吗? 作用域:这个也见过,编译器最近把它声名为foo的一个形式参数了,拿去吧。 引擎:大恩不言谢,你总是这么棒。现在我要把2赋值给a。 引擎:哥们,不好意思又来打扰你。我要为console进行RHS引用,你见过它吗? 作用域:咱俩谁跟谁啊,再说我就是干这个的。这个我也有,console是个内置对象。给你。 引擎:么么哒。我得看看这里面是不是有log(..)。太好了,找到了,是一个函数。 引擎:哥们,能帮我再找一下对a的RHS引用吗?虽然[…]”
摘录来自: Kyle Simpson、赵望野、梁杰. “你不知道的JavaScript(上卷)”。 iBooks.

作用域嵌套

在当前作用域中无法找到某个变量时,引擎就会在外层嵌套作用域中继续查找,知道找到该变量,或抵达最外层的全局作用域为止。

异常

如果在全局作用域中也无法找到目标变量,全局作用域中就会创建一个具有该名称的变量,并将其返还给引擎,前提是程序运行在非“严格模式”下
如果ES5中引入了“严格模式”,那么严格模式会禁止自动的或隐式的创建全局变量。因此在查询失败的时候会抛出类似ReferenceError异常。
如果你对一个已查询到的变量进行不合理的操作时,比如对一个非函数类型的值进行函数调用,或者引用null undefined类型的值中的属性,那么引擎会抛出另外一种类型的异常,叫做TypeError

小结

作用域是一套规则,用于确定在何处以及如何查找变量(标识符)。
var a = 2
首先,var a 在其作用域中声明新变量。这会在最开始的阶段,也就是代码执行前进行。接下来,a = 2会查询(LHS查询)变量a并对其进行赋值。
摘录来自: Kyle Simpson、赵望野、梁杰. “你不知道的JavaScript(上卷)”。 iBooks.

词法作用域

词法阶段

气泡1包含着整个全局作用域,其中只有一个标识符:foo。 气泡2包含着foo所创建的作用域,其中有三个标识符:s bar b。 气泡3包含着bar所创建的作用域,其中只有一个标识符:c
作用域查找会在找到第一个匹配的标识符时停止,在多层的嵌套作用域中可以定义同名的标识符,这叫做“遮蔽效应”(内部的标识符“遮蔽”了外部的标识符)
全局变量会自动成为全局对象(比如浏览器中的window对象)的属性。
var a = 1; function fun1() { var a = 2; console.log(a)//输出2 } function fun2() { var a = 2; console.log(window.a)//输出1 }
对浏览器中window全局对象的解释可以进一步到以下网址了解:

欺骗词法

eval with 等语法会在运行时修改或创建新的作用域,以此来欺骗其他在书写时定义的词法作用域。而且会导致JavaScript引擎在编译阶段无法对它们做任何的优化处理。 简单介绍一下这两个语法:
  • eval
    • 可以对一段包含一个或多个声明的“代码”字符串进行演算,并借此来修改已经存在的词法作用域(在运行时)。
  • with
    • 本质上是通过将一个对象的引用当作作用域来处理,将对象的属性当作作用域中的标识符来处理,从而创建了一个新的词法作用域(同样是在运行时)
这两个机制的副作用会导致代码运行变慢。不要使用它们。

函数作用域和块作用域

  • 每一个函数都会创建一个自己的作用域,如果你在该作用域中声明了变量,且该变量存在于外部作用域,那么它会遮蔽外部作用域中的变量。
  • 为了不污染全局作用域,可以将代码写入一个匿名函数中调用
var a = 2; (function() { var a = 3; console.log( a ); // 3 })(); console.log( a ); // 2
  • 变量声明应该距离使用的地方越近越好,并最大限度的本地化
  • let
{ let a = 10; var b = 1; } a // ReferenceError: a is not defined. b // 1
  • const
var foo = true; if (foo) { var a = 2; const b = 3; // 包含在if中的块作用域常量 a = 3; // 正常! b = 4; // 错误! } console.log( a ); // 3 console.log( b ); // ReferenceError!

this和对象原型

第一章 关于this

为什么要用this

全面解析

调用位置

什么是调用栈和调用位置,我们有一段代码来解释这个问题:
function baz(){ //当前调用栈是:baz //因此当前调用位置是全局作用域 console.log("baz"); bar();//<--bar的调用位置 } function bar() { // 当前调用栈是baz -> bar // 因此,当前调用位置在baz中 console.log( "bar" ); foo(); // <-- foo的调用位置 } function foo() { // 当前调用栈是baz -> bar -> foo // 因此,当前调用位置在bar中 console.log( "foo" ); } baz(); // <-- baz的调用位置
你可以把调用栈想象成一个函数调用链,就想代码中注释写的那样。

绑定规则

默认绑定

独立函数调用。可以把这条规则看做是无法应用其他规则时的默认规则。
function foo(){ console.log(this.a); } var a = 1; foo();
当我们调用foo()的时候,this.a被解析成了全局变量a。因为在函数调用时,应用了this的默认绑定,因此this指向全局对象。
但是如果我们使用了严格模式(strict mode),那么全局对象将无法使用默认绑定,因此,this会被绑定到undefined
严格模式下与foo()的调用位置无关
function foo(){ console.log(this.a); } var a =2; (function(){ "use strict"; foo();//2 })()
function foo(){ "use strict"; console.log(this.a); } var a = 1; foo();//TypeError:this is undefined

隐式绑定

如果调用位置有上下文对象,或者说被某个对象拥有或者包含。那么这时候我们需要考虑一些问题。
function foo(){ console.log(this.a); } var obj = { a:2, foo:foo }; obj.foo();//2
由于调用位置会使用obj上下文来引用函数,因此你可以说函数被调用时obj对象“拥有”或者“包含”它。也就是说,在foo()被调用时,它的落脚点确实指向obj对象。而函数调用中的this会绑定到这个上下文对象obj
隐式丢失 有些情况下this会丢失绑定对象,然后应用默认绑定,从而把this绑定到全局对象或者undefined上。
function foo(){ console.log(this.a); } var obj = { a:2, foo:foo }; var bar = obj.foo;//函数别名 var a = "oops,global";//a是全局对象的属性 bar();//"oops,global"
虽然barobj.foo的一个引用,但是他引用的确实foo()本身,所以此时bar()其实是一个不带任何修饰的函数调用,所以会应用默认绑定。

显示绑定

这里就涉及到了函数的两个方法:call(...)apply(...)。它们的第一个参数是一个对象,它们会把函数的this绑定到这个对象上,因此我们称之为显示绑定。
function foo(){ console.log(this.a); } var obj = { a:2 }; foo.call(obj);//2
通过foo.call(..)我们把this绑定到了obj
硬绑定
function foo(something) { console.log( this.a, something ); return this.a + something; } var obj = { a:2 }; var bar = function() { return foo.apply( obj, arguments ); }; var b = bar( 3 ); // 2 3 console.log( b ); // 5
bar内部手动调用了foo.apply(obj,arguments),因此强制把foothis绑定到了obj。无论如何调用bar都会手动在obj上调用foo
由于硬绑定是一种常用的模式,所以ES5中提供了内置的方法Function.prototype.bind,他的用法如下:
function foo(something){ console.log(this.a,something); return this.a + something; } var obj = { a:2 }; var bar = foo.bind(obj); var b = bar(3);//2 3 console.log(b);//5
bind(..)会返回一个硬编码的新函数,它会把参数设置累this的上下文并调用原始函数。
apply(..)call(..)
二者的作用完全一样,只是接受参数的方式不太一样。他们都是为了改变某个函数运行时的 context 即上下文而存在的,换句话说,就是为了改变函数体内部 this 的指向。因为 JavaScript 的函数存在「定义时上下文」和「运行时上下文」以及「上下文是可以改变的」这样的概念。
  • Function.apply(obj,args)
    • obj:这个对象将代替Function类里this对象
      args:这个是数组,它将作为参数传给Function(args-->arguments)
  • Function.call(obj,param_1,param_2,...,param_n)
    • 需要把参数按顺序传递进去

new绑定

function foo(a){ this.a = a; } var bar = new foo(2); console.log(bar.a);//2
使用new来调用foo(..)时,会构造一个新对象并把它绑定到foo(..)调用中的this上。

优先级

判断this
函数是否在new中调用(new绑定)?如果是的话this绑定的是新创建的对象。
var bar = new foo();
函数是否通过call apply(显示绑定)或者硬绑定调用?如果是的话,this绑定的是指定的对象。
var bar = foo.call(obj2);
函数是否在某个上下文对象中调用(饮食绑定)?如果是的话,this绑定的是那个上下文对象。
var bar = obj1.foo();
如果都不是的话,使用默认绑定。如果是严格模式,就绑定到undefined,否则就绑定到全局对象。
var bar = foo();

对象

类型

首先来介绍JavaScript中的六种基本类型:
  • string
  • number
  • boolean
  • null
  • undefined
  • object
它们本身并不是对象。但是有一个例外null有时会被当做一种对象类型,这其实只是语言本身的一共bug,即对null执行typeof null时会返回object。但实际上,null本身是基本类型。
内置对象 这些内置对象有些和简单基础类型一样,不过它们的关系更复杂,我们稍后详细介绍。
  • String
  • Number
  • Boolean
  • Object
  • Function
  • Array
  • Date
  • RegExp
  • Error
它们实际上只是一些内置函数。这些内置函数可以当做构造函数来使用,从而可以构造一个对应子类型的新对象。
var strPrimitive = "I am a string" typeof strPrimitive;//"string" strPrimitive instanceof String;//false var strObject = new String("I am a string"); typeof strObject;//"object" strObject instanceof String;//true //检查sub-type对象 Object.prototype.toString.call(strObject);//[object String]
由此可看出strObject是由String构造函数创建的一个对象。
原始值"I am a string"并不是一个对象,它只是一个字面量。如果要在这个字面量上执行一些长度,比如获取长度、访问其中的某个字符等,那需要将其转换成String对象。
在你直接通过字面量去执行那些操作时,语言会自动的把字符串字面量转换成一个String对象,也就是说你不需要显示的创建一个对象。

内容

对一个对象的访问有两种方式:
  • 通过.操作符,通常叫做属性访问
  • 通过[key]访问,通常叫做键访问
实际上他们访问的是同一个位置,这两个术语的意思其实是一致的。 它们的区别在于,[..]语法可以接受任何UTF-8/Unicode字符串作为属性名。例如,如果要引用名称为Super-Fun!的属性那么久必须要使用["Super-Fun!"]来访问。因为实际上[..]是通过字符串来访问属性的。
var myObject = { a:2 }; var idx = "a"; console.log(myObject[idx]);//2
在对象中属性名永远都是字符串。如果你使用string以外的其他值作为属性名,那它首先会被转换成一个字符串。即使是数字也不例外,虽然在数组下标中使用的的确是数字,但是在对象属性名中,数字会被转换成字符串。
var myObject = {}; myObject[true] = "foo"; myObject[3] = "bar"; myObject[myObject] = "baz"; myObject["true"];//"foo" myObject["3"];//"bar" myObject["[object Object]"];//"baz"

可计算属性名

ES6增加了可计算的属性名,可以在文字形式中使用[]包裹一个表达式来作为属性名:
var prefix = "foo"; var myObject = { [prefix + "bar"]:"hello", [prefix + "baz"]:"world" }; myObject["foobar"];//hello myObject["foobaz"];//world

复制对象

首先我们应该分清浅拷贝和深拷贝,对于一般的=运算符来说,对对象的拷贝都是浅拷贝,只是将他们对值的引用拷贝给对方。
如何深拷贝一个对象呢,对于JSON安全(也就是说可以被序列化为一个JSON字符串并且可以根据这个字符串解析出一个结构和值完全一样的对象)的对象来说有一种巧妙的复制方法可以解决这个问题。
var newObj = JSON.parse(JSON.stringify(someObj));
当然,这种方法需要保持对象是JSON安全的,所以只适用于部分情况。

属性描述符

var myObject = { a:2 }; Object.getOwnPropertyDescriptor(myObject,"a"); //{ // value:2, // writable:true, // enumerable:true, // configurable:true //}
这个普通对象属性对应的属性描述符(也被称为“数据描述符”,因为它只保存一个数据值)包含了四个值:value(值)、writable(可写)、enumerable(可枚举)和configurable(可配置)。
  1. Writable
    1. 这个属性决定了是否可以修改属性的值。
      var myObject = {}; Object.defineProperty( myObject, "a", { value: 2, writable: false, // not writable! configurable: true, enumerable: true }); myObject.a = 3; myObject.a; // 2
      我们对属性值的修改静默失败。如果在严格模式下,这种方式会抛出TypeError来表示我们无法修改一个不可写的属性。
  1. Configurable
    1. 只要属性是可配置的(Configurable),就可以用defineProperty(..)方法来修改属性描述符。如果该属性为false那么将无法使用defineProperty(..)方法,而且这个值的设置是单向的。因为一旦修改它为false那么就无法撤销!而且还会禁止删除这个属性,delete语句会对它静默失败。(delete myObject.a
  1. Enumerable
这个描述符控制的是属性是否会出现在对象属性的枚举中,比如for..in循环。如果把enumerable设置为false那么它将不会出现在枚举中,虽然仍然可以正常访问它。
fot (var k in myObject){ console.log(k,myObject[k]); } //输出所有可以被枚举的属性

不变性

禁止扩展
如果你想禁止一个对象添加新属性并保留已有属性,可以使用Object.preventExtensions(..):
var myObject = { a:2 }; Object.preventExtensions(myObject); myObject.b = 3; myObject.b; //undefined
在非严格模式下,创建属性b会静默失败。在严格模式下,将会抛出TypeError错误。
密封Object.seal(..)会创建一个“密封”的对象,这个方法实际上回在一个现有对象上调用Object.preventExtensions(..)并把所有现有属性标记为configurable:false

Getter和Setter

var myObject = { get a(){ return this._a_; }, set a(val){ this._a_ = val*2; } }; Object.defineProperty( myObject, "b", { get:function() { return this.a*2; }, enumerable:true } ); myObject.a= 4; console.log(myObject.a);//8 console.log(myObject.b);//16
通常来说getter和setter是成对出现的(只定义一个的话通常会产生意料之外的行为)

存在性

var myObject = { a:2 }; ("a" in myObject);//true ("b" in myObject);//false myObject.hasOwnProperty("a");//true myObject.hasOwnProperty("b");//false
in操作符会检查属性是否存在对象及其[[Prototype]]原型链中。相比之下,hasOwnProperty(..)只会检查属性是否在myObject对象中,不会检查[[Prototype]]链。
有一个例外,如果对象没有连接到Object.prototype(通过Object.create(null)来创建)。在这种情况下,形如myObject.hasOwnProperty(..)就会失败。这时可以用一种更加强硬的方式来进行判断:Object.prototype.hasOwnProperty.call(myObject,"a")
in操作符是检查某个属性名是否存在。对于数组来说这个区别非常重要,4 in [2,3,4]的结果并不是True,因为[2,3,4]这个数组包含的属性名是0、1、2,没有4。

遍历

数组遍历
var arr = [1,2,3,4,5];
  • forEach(..)
    • 遍历数组中所有的值,并忽略回调函数的返回值。
arr.forEach(function(e) { console.log(e);//1,2,3,4,5 });
  • every(..)
    • 一直运行直到回调函数返回false
arr.every(function(e) { console.log(e);//1,2,3 if (e == 3) { return false; } });
  • some(..)
    • 一直运行直到回调函数返回ture
arr.some(function(e) { console.log(e);//1,2,3 if( e==3){ return true; } });
  • for..in
    • 这种遍历方式无法直接获取属性值,因为它实际上遍历的是对象中所有可枚举的属性。当它用在数组上的时候,会返回数组的下标。 我们先来看对于对象属性的访问:
var foo = { a:1, b:2, c:3, d:4 }; for (var key in foo) { if (foo.hasOwnProperty(key)) { console.log(key);//a,b,c,d } }
接下来我们来看对于数组的访问:
var arr = [1,2,3,4,5]; for (var key in arr) { console.log(key);//0,1,2,3,4 }
  • for..of
这个循环语法是ES6中增加的,它可以返回数组的值而不是下标。
var arr = [1,2,3,4,5]; for(var v of arr){ console.log(v);//1,2,3,4,5 }
for..of循环首先会向被访问对象请求一个迭代器对象,然后通过调用迭代器对象的next()方法来遍历所有返回值。
数组有内置的@@iterator,因此for..of可以直接应用在数组上。我们使用内置的@@iterator来手动遍历数组,看看它是怎么工作的:
var myArray = [1,2,3]; var it = myArray[Symbol.iterator](); it.next();//{value:1,done:false} it.next();//{value:2,done:false} it.next();//{value:3,done:false} it.next();//{value:undefined,done:true}
如果你想遍历一个对象的话,你可以给这个对象定义一个@@iterator
var myObject = { a: 2, b: 3 }; Object.defineProperty( myObject, Symbol.iterator, { enumerable: false, writable: false, configurable: true, value: function() { var o = this; var idx = 0; var ks = Object.keys( o );//['a','b'] return { next: function() { return { value: o[ks[idx++]], done: (idx > ks.length) }; } }; } } ); //console.log(Object.keys(myObject));//['a','b'] // 手动遍历myObject var it = myObject[Symbol.iterator](); it.next(); // { value:2, done:false } it.next(); // { value:3, done:false } it.next(); // { value:undefined, done:true } // 用for..of遍历myObject for (var v of myObject) { console.log( v );//2,3 }
我们也可以在定义对象时进行声明
var myObject = { a: 2, b: 3, [Symbol.iterator]:function() { var o = this; var idx = 0; var ks = Object.keys( o );//['a','b'] return { next: function() { return { value: o[ks[idx++]], done: (idx > ks.length) }; } }; } };

提升

编译器再度来袭

foo(); function foo(){ console.log(a);//undefined var a =2; }
显然这段代码的结果告诉我们,在引擎处理这段代码的时候,只是将声明提升了,但是表达式并没有被提升。 它被引擎理解成了下面这段代码:
function foo(){ var a; console.log(a); a = 2; } foo();
下面再来看一段代码:
foo();//TypeError! bar();//ReferenceError! var foo = function bar(){ //... }
这段代码中的变量标识符foo()被提升并分配给所在作用域,因此foo()不会导致ReferenceError。但是foo此时没有被赋值,它的默认值为undefined,对它进行函数调用而导致非法操作,因此会抛出TypeError异常。 而bar()函数是一个带名字的匿名函数,所以它只能在内部作用域使用,所以在外部作用域会由于导致ReferenceError(引用错误) 这段代码会被引擎理解成以下形式:
var foo; foo(); bar(); foo = function(){ var bar = ..self.. }

函数优先

在多个“重复”声明的代码中,函数会首先被提升,然后才是变量。 考虑以下代码:
foo();//1 var foo; function foo() { console.log('1'); } foo = function() { console.log('2'); } foo();//2
尽管var foo出现在了function foo()...的声明之前,但是它是重复声明,因此它会被忽略,因为函数声明会被提升到普通变量之前。它会被引擎理解成如下形式:
function foo() { console.log(1); } foo();//1 foo = function() { console.log(2); } foo();//2
当你理解了这些以后,我们再来一段相关的代码:
foo();//3 function foo() { consloe.log(1); } var foo = function() { console.log(2); } function foo() { console.log(3); }
虽然这些听起来都是些无用的学院理论,但是它说明了除非你走投无路,不然千万不要在同一个作用域中重复定义,经常会导致各种奇怪的问题。
ok,让我们再来加个餐,看以下代码:
function foo() { a = 1; } foo(); console.log(a);//1
虽然你在foo()内部给a赋值,但是,它的声明却在外部作用域。首先,引擎会在foo内部作用域中查找是否有a这个变量,然后作用域告诉它没有找到,那么它就去foo的上层作用域去找,依然没有找到,这时它会声明一个变量var a在外部作用域,然后,返回这个变量给表达式赋值。所以此时,a其实是属于外部作用域的变量,所以a的值为1。 如果我们把代码改成如下形式,那么便会抛出ReferenceError异常,即引用错误。
function foo() { var a = 1; } foo(); console.log(a);//ReferenceError

小结

我们习惯把var a = 2;看做一个声明,而实际上JavaScript引擎并不这么认为。它将var aa = 2当做两个单独的声明,第一个是编译阶段的任务,而第二个是执行阶段的任务。

作用域闭包

接下来我们将注意力转移到这门语言中一个非常重要但又难以掌握,近乎神话的概念上:闭包

实质问题

你可能会问,闭包到底是什么,先给一段生涩的定义:
当函数可以记住并访问所在词法作用域时,就产生了闭包,即使函数是在当前词法作用域之外执行。
下面用一些代码来解释这个定义。
function foo(){ var a = 2; function bar(){ console.log(a);//2 } return bar; } var baz = foo(); baz();//2 这就是闭包的效果
函数bar()的词法作用域能够访问foo()的内部作用域。然后我们将bar()函数本身当做一个值类型进行传递。在foo()执行后,其返回值(即bar()函数)赋值给变量baz并调用,实际上只是通过不同的标识符引用调用了内部的函数bar()。
foo()执行后,通常会期待foo()的整个内部作用域都会被销毁,因为引擎会自动回收垃圾来释放不再使用的内存空间。但是内部作用域由于bar()在外部还在使用,所以并不会被销毁掉。
bar()依然持有对foo()的作用域的引用,而这个引用就叫做闭包。

循环和闭包

来看一个例子:
for(var i = 1;i<=5;i++) { setTimeout(function timer() { console.log(i); }, i*1000); }
我们对这段代码的预期是分别输出1~5,每秒一次每次一个。但实际上,这段代码在运行时会以每秒一次的频率输出五次6。
这是为什么由于i的循环终止条件是i<=5所以在循环终止的时候i的值为6。然后延迟函数的回调会在循环结束时才执行,所以会输出6。
事实上即使每个迭代中执行setTimeout(..,0),所有的回调依然会在循环结束后才会被执行。这个问题稍后来解释。
由于所有的回调函数都共享一个i的引用,所以在循环结束后输出的i均为6。
如何实现我们的预期呢,答案就是闭包。
for(var i = 1; i<=5; i++) { (function(j) { setTimeout(function timer() { console.log(j); }, j*1000); })(i) }
在迭代内使用IIFE会为每个迭代都生成一个新的作用域,使得延迟函数的回调可以将新的作用域封闭在每个迭代内部,每个迭代中都会有一个具有正确值的变量供我们访问。
IIFE:立即执行函数表达式 (function(){...})()

题外话

我们来解决setTimeout(..,0)的问题。
由于JavaScript是单线程的。而setTimeout()并不会创建一个新的线程去执行,而是被插入了任务队列。我们可以把整个代码看做一个任务。那么就是说代码执行完成就是当前任务完成了。接着就会继续执行下一个任务,这时,由于setTimeout()的回调函数被插入了。所以才会执行回调。
如果不理解的话,我们来看另一个例子:
var isEnd = true; window.setTimeout(function () { isEnd = false;//1s后,改变isEnd的值 }, 1000); //这个while永远的占用了js线程,所以setTimeout里面的函数永远不会执行 while (isEnd); //alert也永远不会弹出 alert('end');
由于while的执行而导致后面setTimeout的回调函数一直无法执行,故不会跳出,alert('end')也就不会弹出。

模块

我们来看一个模块的实现方式:
function CoolModule() { var something = "cool"; var another = [1, 2, 3]; function doSomething() { console.log( something ); } function doAnother() { console.log( another.join( " ! " ) ); } return { doSomething: doSomething, doAnother: doAnother }; } var foo = CoolModule(); foo.doSomething();//cool foo.doAnother();//1!2!3!
这个模式在JavaScript中被称为模块。最常见的模块模式的方法通常被称为模块暴露,这里展示的是其变体。
使用CoolModule()会返回一个用对象字面量语法{key:value,...}来表示的对象。这个返回的对象(模块还可以返回一个函数,因为函数也是一个对象,本身也可以拥有属性,比如JQuery)中含有对内部函数而不是内部数据变量的引用。我们保持内部数据变量是隐藏且私有的状态。可以将这个对象类型的返回值看做本质上是模块的公共API。
模块模式的定义:
必须有外部的封闭函数,该函数必须至少被调用一次(每次调用都会创建一个新的模块实例)封闭函数必须返回至少一个内部函数,这样内部函数才能在私有作用域中形成闭包,并且可以访问或者修改私有的状态。

现代的模块机制

var MyModules = (function Manager() { var modules = {}; function define(name, deps, impl) { for (var i=0; i<deps.length; i++) { deps[i] = modules[deps[i]]; } modules[name] = impl.apply( impl, deps ); } function get(name) { return modules[name]; } return { define: define, get: get }; })();
MyModules.define( "bar", [], function() { function hello(who) { return "Let me introduce: " + who; } return { hello: hello }; } ); MyModules.define( "foo", ["bar"], function(bar) { var hungry = "hippo"; function awesome() { console.log( bar.hello( hungry ).toUpperCase() ); } return { awesome: awesome }; } ); var bar = MyModules.get( "bar" ); var foo = MyModules.get( "foo" ); console.log(bar.hello( "hippo" ));//Let me introduce:hippo foo.awesome();//LET ME INTRODUCE:HIPPO
第一段代码维护了一个模块列表,可以定义模块并引入依赖,也可以从列表中获取模块。
第二段代码是如何使用它来定义切使用模块

未来的模块机制

ES6中为模块增加了一级语法支持。但通过模块系统进行加载时,ES6会将文件当做独立模块来处理。每个模块都可以导入其他模块或特定的API成员,同样也可以导出自己的API成员。
例如:
//bar.js function hello(who){ return "Let me introduce:"+who; } export hello;
//foo.js //仅从"bar"模块中导入hello() import hello from "bar"; var hungry = "hippo"; function awesome(){ console.log( hello(hungry).toUpperCase(); ) } export awesome;
//导入完整的"foo"和"bar"模块 module foo from "foo"; module foo from "bar"; console.log( hello("rhino").toUpperCase(); )//Let me introduce:rhino foo.awesome();//LET ME INTRODUCE:RHINO

小结

当函数可以记住并访问所在的词法作用域,即使函数是在当前词法作用域之外执行,这时就产生了闭包。
模块有两个主要特征
  • 为创建内部作用域而调用了一个包装函数
  • 包装函数的返回值必须至少包括一个对内部函数的引用,这样就会创建涵盖整个包装函数内部作用域的闭包。