JavaScript 组织代码详解

在本章中,我们将学习如何将 JavaScript 代码组织成可重用、可理解的块。 这种语言本身并不适合这种模块化,但多年来出现了许多组织 JavaScript 代码的方法。 本章将讨论分解代码的必要性,然后讨论创建 JavaScript 模块的方法。

我们将涵盖以下主题:

任何人学习编程的第一件事是无处不在的 Hello World 应用。 这个简单的应用将“hello world”的一些变体打印到屏幕上。 hello world 这个短语可以追溯到 20 世纪 70 年代早期,当时它被用于演示 B 编程语言,甚至可以追溯到 1967 年,当时它出现在 BCL 编程指南中。 在这样一个简单的应用中,不需要担心代码的结构。 事实上,在许多编程语言中,hello world 根本不需要结构。

对于 Ruby,如下所示:

#!/usr/bin/ruby

puts "hello world"

对于 JavaScript(通过 Node.js),如下所示:

#!/usr/local/bin/node

console.log("Hello world")

现代计算机编程最初是用极其简单的技术完成的。 许多第一代计算机都有他们试图解决的问题。 它们不是像我们今天拥有的通用计算机。 相反,它们只是用来解决一个问题,比如解码加密的文本。 存储程序计算机在 20 世纪 40 年代末首次被开发出来。

起初,为这些计算机编写程序的语言非常复杂,通常与二进制非常紧密地联系在一起。 最终创建了越来越高级别的抽象,使编程更容易访问。 随着这些语言在 50 年代和 60 年代开始成形,很明显需要某种方法来分割大块代码。

在某种程度上,这只是为了维护程序员的理智,因为他们在任何时候都无法在头脑中保留一个完整的大型程序。 然而,创建可重用模块也允许在应用内部甚至应用之间共享代码。 最初的解决方案是使用语句,语句将程序的流控制从一个地方跳转到另一个地方。 多年来,人们严重依赖这些 GOTO 声明。 对于一个不断收到关于使用 GOTO 语句的警告的现代程序员来说,这似乎是疯狂的。 然而,直到第一种编程语言出现数年后,结构化编程才逐渐取代了 GOTO 语法。

结构化编程是基于 Böhm-Jacopini 定理的,该定理指出有相当大的一类问题,其答案可以用三个非常简单的结构来计算:

  • 子程序的连续执行
  • 两个子程序的有条件执行
  • 重复执行子程序,直到条件为真

敏锐的读者会将这些结构识别为正常的执行流、分支或if语句和循环。

Fortran 是最早的语言之一,最初是在不支持结构化编程的情况下构建的。 然而,结构化编程很快被采用,因为它有助于避免意大利式代码。

用 Fortran 编写的代码被组织成模块。 模块是过程松散耦合的集合。 对于那些来自现代面向对象语言的人来说,最接近的概念可能是模块就像只包含静态方法的类。

模块有助于将代码划分为逻辑分组。 但是,它没有为实际应用提供任何类型的结构。 面向对象语言的结构,即类和子类,可以追溯到 Ole-Johan Dahl 和 Kristen Nygaard 在 1967 年撰写的一篇论文。 本文将继续形成 Simula-67 的基础,这是第一种支持面向对象编程的语言。

虽然 Simula-67 是第一种拥有类的语言,但与早期面向对象编程相关的最常被讨论的语言是 Smalltalk。 这种语言是在 20 世纪 70 年代在著名的施乐帕洛阿尔托研究中心(PARC)秘密开发的。 它在 1980 年以 Smalltalk-80 的名称发布(似乎所有与历史相关的编程语言都以发布年份作为版本号前缀)。 Smalltalk 带来的是,语言中的所有东西都是一个对象,即使是像 3 这样的数字也可以在其上执行操作。

几乎每一种现代的编程语言都有一些类的概念来组织代码。 这些类通常属于更高级别的结构,通常称为名称空间或模块。 通过使用这些结构,即使是非常大的程序也可以被划分为易于管理和理解的块。

尽管类和模块有着丰富的历史和明显的实用性,但 JavaScript 直到最近才支持它们作为第一类构造。 要理解其中的原因,我们只需回顾一下 JavaScript 的历史,从第 1 章Designing For Fun and Profit开始,我们就会意识到,为了最初的目的,使用这样的结构是多余的。 类是不幸的 ECMAScript 4 标准的一部分,随着 ECMAScript 2015 标准的发布,它们最终成为语言的一部分。

在这一章中,我们将探索一些在 JavaScript 中重新创建其他现代编程语言的老旧类结构的方法。

在基于浏览器的 JavaScript 中,你创建的每个对象都被分配到全局作用域。 对于浏览器,这个对象被简单地称为window。 通过在您喜欢的浏览器中打开开发控制台,可以很容易地看到这种行为。

提示

打开开发控制台

现代浏览器内置了一些非常高级的调试和审计工具。 访问它们有一个菜单项,坐落在【T5 工具 】 | 在 Chrome【T7 开发工具 】 | 【T9 的工具 】 | 在 Firefox Web 开发人员,并直接在菜单【病人】F12 开发工具在 Internet Explorer。 键盘快捷键也用于访问工具。 Windows 和 Linux 下,标准配置为F12;OSX 下,标准配置为Option+Command+I。****

在开发人员工具中有一个控制台窗口,可以直接访问当前页面的 JavaScript。 这是测试小段代码或访问页面 JavaScript 的一个非常方便的地方。

一旦你打开了控制台,输入以下代码:

> var words = "hello world"
> console.log(window.words);

结果将被打印到控制台hello world。 通过全局声明单词,它会自动附加到顶层容器:window。

在 Node.js 中,情况有些不同。 以这种方式赋值变量实际上会将其附加到当前模块。 不包括var对象将把变量附加到global对象。

多年来,您可能听说过使用全局变量是一件坏事。 这是因为全局变量很容易被其他代码污染。

考虑一个非常常见的命名变量,例如index。 在任何规模可观的应用中,这个变量名很可能会在多个地方使用。 当任何一段代码使用该变量时,它将在另一段代码中导致意想不到的结果。 当然,重用变量是可能的,甚至在内存非常有限的系统(如嵌入式系统)中也是有用的,但在大多数应用中,在单个范围内重用变量来表示不同的东西是很难理解的,也是错误的来源。

使用全局作用域变量的应用也容易受到其他代码的故意攻击。 从其他代码更改全局变量的状态是很简单的,这可能会将登录信息等秘密暴露给攻击者。

最后,全局变量给应用增加了很大的复杂性。 将变量的范围缩小到一小段代码可以让开发人员更容易地理解变量的使用方式。 如果作用域是全局的,那么对该变量的更改可能会产生远远超出一段代码的影响。 对变量的简单更改可以级联到整个应用中。

一般来说,应该避免使用全局变量。

JavaScript 是一种面向对象的语言,但大多数人不会使用它的面向对象特性,除非顺便使用。 JavaScript 使用混合对象模型,因为它有一些原语和对象。 JavaScript 有五种基本类型:

  • 未定义的
  • 布尔
  • 字符串
  • 数量

在这五个中,只有两个是我们认为是物体的。 其他三种类型,boolean、string 和 number 都有包装过的版本,它们是对象:boolean、string 和 number。 它们是以大写字母开始区分的。 这与 Java 使用的模型类型相同,是对象和原语的混合。

JavaScript 还将根据需要对原语进行装箱和拆箱。

在这段代码中,你可以看到 JavaScript 原语的装箱和未装箱版本:

var numberOne = new Number(1);
var numberTwo = 2;
typeof numberOne; //returns 'object'
typeof numberTwo; //returns 'number'
var numberThree = numberOne + numberTwo;
typeof numberThree; //returns 'number'

用 JavaScript 创建对象很简单。 这可以在 JavaScript 中创建对象的代码中看到:

var objectOne = {};
typeof objectOne; //returns 'object'
var objectTwo = new Object();
typeof objectTwo; //returns 'object'

因为 JavaScript 是一种动态语言,所以向对象添加属性也很容易。 即使在创建对象之后也可以这样做。 下面的代码创建了这个对象:

var objectOne = { value: 7 };
var objectTwo = {};
objectTwo.value = 7;

对象包含数据和功能。 到目前为止,我们只看到了数据部分。 幸运的是,在 JavaScript 中,函数是第一类对象。 函数可以传递,函数可以赋值给变量。 让我们尝试添加一些函数到我们在这段代码中创建的对象:

var functionObject = {};
functionObject.doThings = function() {
  console.log("hello world");
}
functionObject.doThings(); //writes "hello world" to the console

这种语法有点的痛苦,每次建立对象都是一个赋值。 让我们看看是否可以改进创建对象的语法:

var functionObject = {
  doThings: function() {
    console.log("hello world");
  }
}
functionObject.doThings();//writes "hello world" to the console

至少在我看来,这种语法是一种更干净、更传统的构建对象的方法。 当然,在对象中混合数据和功能也是可能的:

var functionObject = {
  greeting: "hello world",
  doThings: function() {
    console.log(this.greeting);
  }
}
functionObject.doThings();//prints hello world

在这段代码中有两件事需要注意。 首先,宾语中的不同项用逗号而不是分号分隔。 那些来自其他语言(如 c#或 Java)的人很可能会犯这个错误。 下一个需要注意的是,我们需要使用this限定符来从doThings函数中寻址greeting变量。 如果我们在对象中有如下所示的一些函数,这也将是正确的:

var functionObject = {
  greeting: "hello world",
  doThings: function() {
    console.log(this.greeting);
    this.doOtherThings();
  },
  doOtherThings: function() {
    console.log(this.greeting.split("").reverse().join(""));
  }
}
functionObject.doThings();//prints hello world then dlrow olleh

this关键字在 JavaScript 中的行为与你可能预期来自其他 c 语法语言的行为不同。 this被绑定到其所在函数的所有者。 然而,函数的所有者有时并不是您所期望的。 在上面的例子中,this被绑定到functionObject对象,但是如果函数在一个对象之外声明,那么它将引用全局对象。 在某些情况下(通常是事件处理程序),这将被反弹到触发事件的对象。

让我们看看下面的代码:

var target = document.getElementById("someId");
target.addEventListener("click", function() {
  console.log(this);
}, false);

this具有目标价值。 习惯this的值,也许是 JavaScript 中最棘手的事情之一。

ECMAScript-2015 引入了let关键字,可以替换var关键字来声明变量。 let使用块级作用域,这是你可能在大多数语言中使用的作用域。 让我们来看看它们的区别:

for(var varScoped =0; varScoped <10; varScoped++)
{
  console.log(varScoped);
}
console.log(varScoped +10);
for(let letScoped =0; letScoped<10; letScoped++)
{
  console.log(letScoped);
}
console.log(letScoped+10);

使用 var 作用域版本,可以看到变量位于块的外部。 这是因为在幕后,varScoped的声明被吊到代码块的开头。 对于代码的let作用域版本,letScoped的作用域仅在for循环内,因此,一旦我们离开循环,letScoped是未定义的。 当被给予使用letvar的选择时,我们倾向于总是使用let。 在某些情况下,你确实想使用 var 作用域,但它们很少,而且相差很远。

关于如何在 JavaScript 中构建对象,我们已经建立了一个相当完整的模型。 然而,对象和类不是一回事。 对象是类的实例。 如果我们想要创建functionObject对象的多个实例,我们就没有运气了。 尝试这样做将导致一个错误。 在 Node.js 的情况下,错误如下:

let obj = new functionObject();
TypeError: object is not a function
  at repl:1:11
  at REPLServer.self.eval (repl.js:110:21)
  at repl.js:249:20
  at REPLServer.self.eval (repl.js:122:7)
  at Interface.<anonymous> (repl.js:239:12)
  at Interface.EventEmitter.emit (events.js:95:17)
  at Interface._onLine (readline.js:202:10)
  at Interface._line (readline.js:531:8)
  at Interface._ttyWrite (readline.js:760:14)
  at ReadStream.onkeypress (readline.js:99:10)

这里的堆栈跟踪显示了一个名为repl的模块中的错误。 这是在启动 Node.js 时默认加载的 read-execute-print 循环。

每次需要一个新实例时,都必须重新构建对象。 为了解决这个问题,我们可以使用一个函数来定义对象,如下所示:

let ThingDoer = function(){
  this.greeting = "hello world";
  this.doThings = function() {
    console.log(this.greeting);
    this.doOtherThings();
  };
  this.doOtherThings = function() {
    console.log(this.greeting.split("").reverse().join(""));
  };
}
let instance = new ThingDoer();
instance.doThings(); //prints hello world then dlrow olleh

该语法允许定义构造函数,并允许从该函数创建新对象。 没有返回值的构造函数是在创建对象时调用的函数。 在 JavaScript 中,构造函数实际上返回创建的对象。 你甚至可以使用构造函数赋值内部属性,让它们成为初始函数的一部分,如下所示:

let ThingDoer = function(greeting){
  this.greeting = greeting;
  this.doThings = function() {
    console.log(this.greeting);
  };
}
let instance = new ThingDoer("hello universe");
instance.doThings();

正如前面提到的,直到最近,都不支持在 JavaScript 中创建真正的类。 虽然 ECMAScript-2015 给类带来了一些语法上的甜头,但底层对象系统仍然和过去一样,因此了解如果没有这种甜头我们将如何创建对象仍然具有指导意义。 使用上一节中的结构创建的对象有一个相当大的缺点:创建多个对象不仅耗时,而且占用大量内存。 每个对象都与以相同方式创建的其他对象完全不同。 这意味着用于保存函数定义的内存不是在所有实例之间共享的。 更有趣的是,您可以在不更改所有实例的情况下重新定义类的单个实例。 下面的代码演示了这一点:

let Castle = function(name){
  this.name = name;
  this.build = function() {
    console.log(this.name);
  };
}
let instance1 = new Castle("Winterfell");
let instance2 = new Castle("Harrenhall");
instance1.build = function(){ console.log("Moat Cailin");}
instance1.build(); //prints "Moat Cailin"
instance2.build(); //prints "Harrenhall" to the console

以这种方式改变单个实例或任何已经定义的对象的功能被称为monkey**patch**。 对于这是否是一种好的做法,存在一些分歧。 在处理库代码时,它当然是有用的,但它增加了很大的混乱。 通常认为扩展现有类是更好的实践。

当然,如果没有适当的类系统,JavaScript 就没有继承的概念。 然而,它确实有一个原型。 在最基本的层次上,JavaScript 中的对象是键和值的关联数组。 对象上的每个属性或函数都被简单地定义为该数组的一部分。 你甚至可以通过使用数组语法访问对象的成员来看到这一点,如下所示:

let thing = { a: 7};
console.log(thing["a"]);

提示

使用数组语法访问对象的成员是避免使用 eval 函数的一种非常方便的方法。 例如,如果我有函数的名字我想叫一个字符串称为funcName我想称之为物体上,obj1,那么我可以通过obj1[funcName](),而不是使用一个有潜在危险的调用 eval。 Eval 允许执行任意代码。 在页面上允许这样做意味着攻击者可能会在其他人的浏览器上输入恶意脚本。

当一个对象被创建时,它的定义继承自一个原型。 奇怪的是,每个原型也是一个对象,所以即使是原型也有原型。 除了这个对象,它是顶级原型。 将函数附加到原型的优点是只创建函数的一个副本; 节省内存。 原型有一些复杂性,但你可以在不了解它们的情况下生存下来。 为了使用原型,你需要简单地分配函数给它,如下所示:

let Castle = function(name){
  this.name = name;
}
Castle.prototype.build = function(){ console.log(this.name);}
let instance1 = new Castle("Winterfell");
instance1.build();

需要注意的是,只有功能被分配给原型。 实例变量如name仍然被赋值给实例。 因为这些对每个实例都是唯一的,所以对内存使用没有实际影响。

在许多方面,原型语言比基于类的继承模型更强大。

如果你在以后对一个对象的原型做了更改,那么所有共享该原型的对象都将用新函数更新。 这消除了一些关于 monkey typing 的担忧。 这个行为的一个例子如下:

let Castle = function(name){
  this.name = name;
}
Castle.prototype.build = function(){
  console.log(this.name);
}
let instance1 = new Castle("Winterfell");
Castle.prototype.build = function(){
  console.log(this.name.replace("Winterfell", "Moat Cailin"));
}
instance1.build();//prints "Moat Cailin" to the console

当构建对象时,你应该确保尽可能地利用原型对象。

现在我们知道了在 JavaScript 中构建对象的另一种方法,那就是使用Object.create函数。 这是 ECMAScript 5 中引入的新语法。 语法如下:

Object.create(prototype [, propertiesObject ] )

create 语法将基于给定的原型构建一个新对象。 您也可以传入一个propertiesObject对象来描述创建的对象上的附加字段。 这些描述符由一些可选字段组成:

  • writable:指定字段是否应该是可写的
  • configurable:这规定了文件是否应该从对象中删除或在创建后支持进一步配置
  • enumerable:指定在枚举对象属性时是否可以列出该属性
  • value:指定字段的默认值

也可以在描述符内指定getset函数,它们作为其他内部属性的 getter 和 setter 方法。

使用object.create为我们的城堡,我们可以使用Object.create创建一个实例,如下所示:

let instance3 = Object.create(Castle.prototype, {name: { value: "Winterfell", writable: false}});
instance3.build();
instance3.name="Highgarden";
instance3.build();

您会注意到,我们显式地定义了name字段。 Object.create绕过构造函数,因此前面代码中描述的初始赋值不会被调用。 您可能还注意到,writeable 被设置为false。 这样做的结果是,将name重新分配到Highgarden没有影响。 输出如下:

Winterfell
Winterfell

对象的优点之一是可以在其基础上创建越来越复杂的对象。 这是一个常见的模式,可以用于很多事情。 JavaScript 中没有继承,因为它具有原型性质。 但是,您可以将一个原型中的函数组合到另一个原型中。

假设我们有一个名为Castle的基类,我们想将它定制为一个名为Winterfell的更具体的类。 我们可以先将所有属性从Castle原型复制到Winterfell原型。 可以这样做:

let Castle = function(){};
Castle.prototype.build = function(){console.log("Castle built");}

let Winterfell = function(){};
Winterfell.prototype.build = Castle.prototype.build;
Winterfell.prototype.addGodsWood = function(){}
let winterfell = new Winterfell();
winterfell.build(); //prints "Castle built" to the console

当然,这是一种非常痛苦的构建对象的方式。 您必须确切地知道基类必须复制哪些函数。 它可以用类似 naïve 的方式进行抽象:

function clone(source, destination) {
  for(var attr in source.prototype){ destination.prototype[attr] = source.prototype[attr];}
}

如果你喜欢物体图,这张图展示了临冬城如何延伸城堡:

Inheritance

这可以很简单地使用如下:

let Castle = function(){};
Castle.prototype.build = function(){console.log("Castle built");}

let Winterfell = function(){};
clone(Castle, Winterfell);
let winterfell = new Winterfell();
winterfell.build();

我们说这是 naïve,因为它没有考虑到许多潜在的失败条件。 一个成熟的实现是相当广泛的。 jQuery 库提供了一个名为extend的函数,它以一种健壮的方式实现了原型继承。 它大约有 50 行长,处理深度拷贝和空值。 在 jQuery 内部,该函数被广泛使用,但在您自己的代码中,它可能是一个非常有用的函数。 我们提到原型继承比传统的继承方法更强大。 这是因为可以混合和匹配来自许多基类的位来创建一个新类。 大多数现代语言只支持单继承:一个类只能有一个直接父类。 然而,有一些语言具有多重继承,当试图决定在运行时调用哪个版本的方法时,这种做法增加了很大的复杂性。 通过强制在组装时选择方法,原型继承避免了许多这样的问题。

以这种方式组合对象允许从两个或更多不同的基础获取属性。 很多时候这是有用的。 例如,一个代表狼的类可能从描述狗的类和描述四足动物的类中获取一些属性。

通过使用以这种方式构建的类,我们可以满足构建包括继承在内的类系统的几乎所有需求。 然而,继承是一种非常强的耦合形式。 在几乎所有情况下,最好避免继承,而采用更松散的耦合形式。 这将允许类被替换或更改,而对系统其余部分的影响最小。

既然我们已经有了一个完整的类系统,那么最好解决前面讨论的全局名称空间。 同样,这里没有对名称空间的一级支持,但我们可以很容易地将功能隔离到名称空间的等级物。 在 JavaScript 中创建模块有许多不同的方法。 我们将从最简单的开始,并在此过程中添加一些功能。

首先,我们只需要将一个对象附加到全局名称空间。 这个对象将包含根名称空间。 我们将命名空间为Westeros; 代码看起来像这样:

Westeros = {}

默认情况下,这个对象是附加到顶层对象的,所以我们不需要做更多的事情。 典型的用法是首先检查对象是否已经存在,然后使用该版本,而不是重新分配变量。 这允许您将定义分散到多个文件中。 理论上,您可以在每个文件中定义一个类,然后在将它们交付给客户端或在应用中使用它们之前,将它们作为构建过程的一部分放在一起。 这个的简写形式是:

Westeros = Westeros || {}

一旦我们有了对象,就只需要简单地将我们的类赋值为该对象的属性。 如果我们继续使用Castle对象,那么它看起来像:

let Westeros = Westeros || {};
Westeros.Castle = function(name){this.name = name}; //constructor
Westeros.Castle.prototype.Build = function(){console.log("Castle built: " +  this.name)};

如果我们想要构建一个层次结构的名称空间,这也是很容易做到的,如下面的代码所示:

let Westeros = Westeros || {};
Westeros.Structures = Westeros.Structures || {};
Westeros.Structures.Castle = function(name){ this.name = name}; //constructor
Westeros.Structures.Castle.prototype.Build = function(){console.log("Castle built: " +  this.name)};

这个类可以以类似于前面示例的方式实例化和使用:

let winterfell = new Westeros.Structures.Castle("Winterfell");
winterfell.Build();

当然,使用 JavaScript,构建相同代码结构的方法不止一种。 构造上述代码的一个简单方法是利用创建并立即执行函数的能力:

let Castle = (function () {
  function Castle(name) {
    this.name = name;
  }
  Castle.prototype.Build = function () {
    console.log("Castle built: " + this.name);
  };
  return Castle;
})();
Westros.Structures.Castle = Castle;

这段代码似乎比前面的代码示例长一些,但我发现它更容易理解,因为它具有层次性。 我们可以用它们在前面代码中所示的相同结构中创建一个新的城堡:

let winterfell = new Westeros.Structures.Castle("Winterfell");
winterfell.Build();

使用此结构进行继承也相对容易。 如果我们要定义一个BaseStructure类,它将在所有结构的祖先中,然后使用它将像这样:

let BaseStructure = (function () {
  function BaseStructure() {
  }
  return BaseStructure;
})();
Structures.BaseStructure = BaseStructure;
let Castle = (function (_super) {

__extends(Castle, _super);

  function Castle(name) {
    this.name = name;
    _super.call(this);
  }
  Castle.prototype.Build = function () {
    console.log("Castle built: " + this.name);
  };
  return Castle;
})(BaseStructure);

您会注意到,在计算闭包时,基本结构被传递到Castle对象中。 突出显示的代码行使用了一个名为__extends的助手方法。 该方法负责将函数从基原型复制到派生类。 这段代码是由 TypeScript 编译器生成的,它还生成了一个extends方法,类似于:

let __extends = this.__extends || function (d, b) {
  for (var p in b) if (b.hasOwnProperty(p)) d[p] = b[p];
  function __() { this.constructor = d; }
  __.prototype = b.prototype;
  d.prototype = new __();
};

我们可以继续使用非常漂亮的闭包语法来实现整个模块。 如图所示:

var Westeros;
(function (Westeros) {
  (function (Structures) {
    let Castle = (function () {
      function Castle(name) {
        this.name = name;
      }
       Castle.prototype.Build = function () {
         console.log("Castle built " + this.name);
       };
       return Castle;
     })();
     Structures.Castle = Castle;
  })(Westeros.Structures || (Westeros.Structures = {}));
  var Structures = Westeros.Structures;
})(Westeros || (Westeros = {}));

在这个结构中,您可以看到与前面讨论的创建模块相同的代码。 在单个模块中定义多个类也相对容易。 这可以在下面的代码中看到:

var Westeros;
(function (Westeros) {
  (function (Structures) {
    let Castle = (function () {
      function Castle(name) {
        this.name = name;
      }
      Castle.prototype.Build = function () {
        console.log("Castle built: " + this.name);
        var w = new Wall();
      };
      return Castle;
    })();
    Structures.Castle = Castle;

 var Wall = (function () {

 function Wall() {

 console.log("Wall constructed");

 }

 return Wall;

 })();

 Structures.Wall = Wall;

  })(Westeros.Structures || (Westeros.Structures = {}));
  var Structures = Westeros.Structures;
})(Westeros || (Westeros = {}));

高亮显示的代码在模块中创建了第二个类。 在每个文件中定义一个类也是完全允许的。 因为在盲目地重新赋值之前,代码会检查获取Westeros的当前值,所以我们可以安全地将模块定义分割到多个文件中。

突出显示部分的最后一行显示了在闭包之外公开类。 如果我们想要创建只在模块内可用的私有类,那么我们只需要排除这一行。 这实际上被称为揭示模块模式。 我们只揭示需要全局可用的类。 将尽可能多的功能放在全局名称空间之外是一种良好的实践。

到目前为止,我们已经看到,在预 ECMAScript -2015 JavaScript 中,完全有可能构建类,甚至模块。 显然,它的语法比 c#或 Java 等语言要复杂一些。 幸运的是,ECMAScript-2015 支持一些创建类的语法糖:

class Castle extends Westeros.Structures.BaseStructure {
  constructor(name, allegience) {
    super(name);
    ...
  }
  Build() {
    ...
    super.Build();
  }
}

ECMAScript-2015 也为 JavaScript 带来了一个深思熟虑的模块系统。 还有创建模块的语法糖,看起来像这样:

module 'Westeros' {
  export function Rule(rulerName, house) {
    ...
    return "Long live " + rulerName + " of house " + house;
  }
}

模块可以包含函数,当然也可以包含类。 ECMAScript-2015 还定义了一个模块导入语法,并支持从远程位置检索模块。 导入模块如下所示:

import westeros from 'Westeros';
module JSON from 'http://json.org/modules/json2.js';
westeros.Rule("Rob Stark", "Stark");

在任何完全支持 ECMAScript-2015 的环境中,都可以使用一些这种语法糖。 在撰写本文时,所有主要的浏览器供应商都对 ECMAScript-2015 的类部分有很好的支持,所以如果你不需要支持老式浏览器,几乎没有理由不使用它。

在一个理想的世界,每个人都可以在绿地项目上工作,在那里他们可以从一开始就建立标准。 然而,事实并非如此。 您可能经常会发现自己处于这样一种情况:遗留系统中有一堆非模块化的 JavaScript 代码。

在这些情况下,简单地忽略非模块化代码,直到实际需要对其进行升级,可能是有利的。 尽管 JavaScript 很流行,但许多 JavaScript 工具仍不成熟,因此很难依靠编译器来查找 JavaScript 重构所引入的错误。 JavaScript 的动态特性也使自动重构工具变得复杂。 然而,对于新代码,适当使用模块化 JavaScript 可以非常有助于避免名称空间冲突和提高可测试性。

如何安排 JavaScript 是一个有趣的问题。 从 web 的角度来看,我采取的方法是将我的 JavaScript 与 web 页面保持一致。 因此,每个页面都有一个相关的 JavaScript 文件,负责该页面的功能。 此外,页面之间常见的组件(比如网格控件)被放置到一个单独的文件中。 在编译时,所有文件被组合成一个 JavaScript 文件。 这有助于在使用小代码文件和减少浏览器对服务器的请求数量之间取得平衡。

有人说,在计算机科学中只有两件事是真正困难的。 这些问题是什么取决于发言的人是谁。 通常是缓存失效和命名的一些变化。 如何组织代码是命名问题的很大一部分。

作为一个团队,我们似乎已经相当坚定地确定了名称空间和类的概念。 正如我们所看到的,JavaScript 中并没有直接支持这两个概念。 然而,有许多方法可以解决这个问题,其中一些方法实际上提供了比传统名称空间/类系统更强大的功能。

JavaScript 的主要关注点是避免使用大量类似名称、未连接的对象污染全局名称空间。 将 JavaScript 封装到模块中是编写可维护和可重用代码的关键一步。

随着时间的推移,我们将会看到在 JavaScript 的土地上,许多非常复杂的接口安排模式变得简单得多。 基于原型的继承在一开始似乎很困难,但它是帮助简化设计模式的巨大工具。

教程来源于Github,感谢apachecn大佬的无私奉献,致敬!

技术教程推荐

Linux实战技能100讲 -〔尹会生〕

ZooKeeper实战与源码剖析 -〔么敬国〕

体验设计案例课 -〔炒炒〕

etcd实战课 -〔唐聪〕

操作系统实战45讲 -〔彭东〕

手把手带你写一门编程语言 -〔宫文学〕

中间件核心技术与实战 -〔丁威〕

Web 3.0入局攻略 -〔郭大治〕

手把手教你落地DDD -〔钟敬〕