最全面的 JavaScript 设计原则指南
作为 JavaScript 开发人员,我们致力于编写简洁、健康且易于维护的代码。我们力求以优雅的方式解决各种挑战。
虽然这些挑战本身可能各有不同,但我们并不一定需要针对我们面临的每一个新挑战都提出独特的解决方案。
“如果你使用了一种并非独一无二的解决方案来
解决一个本身就是独一无二的挑战,那么你就运用了 JavaScript 设计模式
的力量。”
软件语言已经存在了几十年。如今,人们对软件语言的生命周期已经有了相当清晰的了解。在任何一种语言的生命周期中,该语言社区内的大量开发者都会创建并测试许多可重用的解决方案。凭借开发者经验的积累,被称为设计模式的解决方案得以创建,并被大众所使用。这些解决方案支持创建优化的方法,以更少的设计模式解决多种类型的问题。
“设计模式是针对软件设计中常见问题的可重用解决方案。”
设计模式能给我们带来哪些好处?
- 成熟的解决方案:许多软件开发人员都使用设计模式。由于许多开发人员都成功地使用了设计模式,因此我们在实施特定设计模式时更有把握取得成功。当一种设计模式成为主流时,您可以确信它已经过多次修订和重构。成熟的解决方案通常是最优的,能够考虑到各种极端情况,并适用于各种不同的用例。
- 易于复用:可复用的解决方案可以进行修改,以解决多个特定问题。设计模式记录的是一种可复用的解决方案,它不依赖于任何特定问题,而是针对一系列挑战,设计模式旨在帮助克服这些挑战。
- 表达力强:设计模式通常能够以简洁明了的方式解释大型解决方案。
- 降低代码重构的需求:大多数设计模式都会考虑代码依赖关系,包括现有依赖和未来依赖。例如,开放封闭设计原则可以避免重构已编写的代码。它允许你创建一个新类(或者在其他非 JavaScript 语言中实现一个接口)并添加代码。使用设计模式可以有效减少代码重构的需求。
- 简化沟通:由熟悉设计模式的软件工程师组成的团队,能够更轻松地通过代码库语言进行内部沟通。他们也能就潜在解决方案、值得关注的未来问题以及整体架构设计进行外部沟通。设计模式简化了沟通。
- 减少代码库的网站:由于设计模式优雅、高效且经过深思熟虑,因此通常需要的代码量更少,最终可以简化团队的代码库。
“在深入探讨之前,让我们简要回顾一下 JavaScript 的历史,以便更好地了解我们许多现代设计模式的构建背景。”
JavaScript 简史
在网页开发领域,JavaScript 是当今最流行的编程语言之一。
最初,JavaScript 并非旨在成为如今这种令人印象深刻的、被世界广泛接受的语言,它拥有响应式前端、模块化代码库,以及数百万个可通过 npm 安装的软件包。
完全不是那么回事。最初,JavaScript 更像是一种“粘合剂”,它允许你将各种 HTML 元素粘合在一起。最初它是一种客户端脚本语言,世界上最早的网页浏览器之一——Netscape Navigator——就利用 JavaScript 来显示静态 HTML。
这当然导致了我们现在所知的浏览器大战。
浏览器是当时的新兴事物,炙手可热,发展迅猛——它们是科技行业下一个重大突破。像 Mozilla(前身为 Netscape Communications)、微软 Explorer 以及后来的 Chrome 这样的巨头公司为了争夺浏览器市场的霸主地位而展开激烈竞争。
作为这场浏览器大战中每个浏览器背后的驱动力,各大浏览器厂商都在研究、开发和创造新的、改进的方法来实现他们自己的客户端脚本语言。
-
Netscape: JavaScript(实际上,最初的JS是由Brendan Eich创建的)
-
微软: JScript(还有人知道那是什么吗?)
作为当时的开发者,我能想象到当时的种种挫折。各种浏览器的实现方式差异巨大。开发工作并非面向所有浏览器,而是针对特定浏览器进行开发。
我设想,一群愤怒的软件工程师手持干草叉和火把聚集在一起,人数之多足以媲美世界历史上最残酷战争中的军队。随着我们愤怒的开发者们奋起反抗,我们只有一个诉求——为所有浏览器选择一种该死的编程语言。
(在我的想象中,我们那个时代的开发者先辈更像是渴望战争、为简化和荣耀而战的伟大维京战士,他们甘愿赴死。最近我在Hulu上看了《维京传奇》,这可能让我的想象力有些失控……)
于是,ECMAScript诞生了。
你问ECMAScript是什么?它是自由、包容和非疯狂标准化的呼声。
ECMAScript 是一种标准化的脚本语言规范,所有现代浏览器都力求支持它。它有许多不同的实现方式,如果用传统的人类语言来类比,它们更像是不同的方言。
我喜欢把 ECMAScript 看作所有脚本语言的鼻祖,而把 JavaScript 看作它的英雄儿子——英雄中的英雄,战胜一切困难的斗士,以及赢得所有美女芳心的英雄,因为它太棒了(说真的,JavaScript 是你们这些女工程师最常用的软件语言之一)。
JavaScript 是从 ECMAScript 衍生出的最流行的方言。
自 ECMAScript 诞生以来,它为软件工程界做出了诸多重要贡献。它规范了许多重要的东西,这些内容在维基百科上均有列出。
浏览器对 ECMAScript 版本 6 (ES6) 及更高版本的支持仍然不完整,必须将其转译为 ES5 才能得到完全支持。
JavaScript 除了“很棒”之外,还是什么?
让我们先来介绍一些非常重要的 JavaScript 语言特性。在深入学习本文讨论的 JS 设计模式之前,您需要对这些 JavaScript 特性有一定的了解和认识。
嘿,就是你!正在阅读这篇文章的你。我有个问题想问你……
什么是 JavaScript?
一种可能的答案是:
“JavaScript 是一种轻量级、解释型、面向对象的编程语言,具有一流函数,最常用于网页脚本编写。”
嗯……
啊?!
基本上,这段由比我聪明得多的人写的极其复杂的引言,其含义大致是这样的:
- JavaScript 的内存占用量很低。
- JS 易于实现
- JS很容易学
- JS 的语法与其他流行语言(例如 C++ 和 Java)类似。
- JS是一种脚本语言
- 这意味着它的代码是解释执行的,而不是编译执行的。
- JS 具有过程支持
- JS 支持面向对象编程
- JS 支持函数式编程风格
- JS 对开发者来说很灵活!(直到出现问题,那就很痛苦了)
这些是 JavaScript 的属性或特性,在许多其他流行的软件语言中也能找到——但是,正如我们许多人都非常清楚的那样,JavaScript 很独特,它有自己独特的节奏。
JavaScript 支持一等函数
一等函数功能非常强大,但起初也比较难以理解。所谓编程语言拥有一等函数,仅仅意味着该语言中的函数与其他变量一样受到同等对待。
- 一等函数可以:作为参数传递给其他函数
/* We pass a function as the argument */
function action (name, payload, callback_function) {
let context = { name, payload };
callback_function(context);
};
action('log-alert', 'hello world', function (context) {
console.log(
"The action context: ", context,
"The action name: ", context.name,
"The action payload: ", context.payload
);
})
- 一等函数可以:由另一个函数返回 ```js
function sayHello() {
return function() {
console.log("Hello!");
}
}
- **First Class Functions Can Be:** Assigned as a value to a variable
```js
const foo = function() {
console.log("foobar");
}
// Invoke it using the variable
foo();
基于 JavaScript 原型
JavaScript 是面向对象的语言——因此它支持对象。现在,大多数不熟悉 JavaScript 的人可能会首先想到对象,然后是类,最后才想到继承。
JavaScript 的方法略有不同……
JavaScript 本身并不支持类……它使用的是基于原型或基于实例的 继承机制。
ES6 正式引入了Class关键字。截至本文撰写之时,所有浏览器都已完全支持 ES6,因此我们可以使用Class关键字——但它在像 JavaScript 这样的原型语言中仍然有不同的表现形式。
基于原型的编程
-
面向对象编程风格
行为重用(也称为继承)是通过委托重用现有对象来实现的,这些委托充当原型。
-
本文后面的设计模式部分会更深入地探讨这个问题。理解原型对于 JavaScript 来说至关重要,但我们暂且不深入探讨细节。
JavaScript 事件循环
你听说过回调函数吗?如果你经常使用JavaScript,我相信你肯定听说过。
回调函数是指作为参数传递给另一个函数的函数(由于函数是一等公民,这种做法是可以接受的)。当某个事件触发后,该回调函数就会被调用。通常,回调函数用于事件订阅。
例如:鼠标右键事件会触发一个函数被调用——这就是回调函数。
- 事件附带一个监听器。
- 每次该事件触发时(否则事件将丢失)
- 一条消息被发送到消息队列。
- 该消息队列(先进先出)同步处理。
这个过程就是我们所说的 JavaScript事件循环。
每个队列消息
- 具有相关功能
一旦队列消息被出队
- 运行时会在处理任何其他消息之前完全执行该函数。
如果一个函数包含其他函数调用
- 这些操作都在处理队列中的新消息之前执行。
这叫做运行完成
while (queue.waitForMessage())
{
queue.processNextMessage();
}
queue.waitForMessage()
- 同步等待新消息。
- 正在处理的每条消息都有自己的堆栈。
- 并一直处理直到堆栈为空。
- 处理完成后
- 从队列中处理一条新消息(如果有的话)。
您是 Clean Code Studio 内容的粉丝吗?
“订阅我们的电子报,获取更多类似内容!”
你听说过“非阻塞”或“异步”这些术语用于描述 JavaScript 吗?
当执行异步操作时
- 它不会暂停或停止运行时。
- 该程序还能处理其他事情。
- 例如:接收用户输入
- 等待异步操作完成
异步操作不会阻塞主执行线程。
这是 JavaScript 内部以及外部用于特定 JavaScript 用例的一项极其有用的功能。异步与同步是 JavaScript 中一个庞大的话题,但如果我们深入探讨,就无法触及
设计模式——而这正是本文的重点。
什么是设计模式
设计模式是针对软件设计中常见问题的可复用解决方案。让我们来看看设计模式的一些类别。
原型模式
如何创建设计模式?你是否注意到任何常见的重复性问题?你是否已经找到了一种独特的解决方案来解决这个问题?假设你的这个解决方案尚未被全球认可或记录在案。
每当遇到这个问题时,你都使用这个解决方案。你创建的这个解决方案是可复用的,整个开发者社区都会从这种模式中受益。
但这并不意味着它就一定是设计模式。程序员可能写出了不错的代码,却误把看起来像模式的东西当成了真正的设计模式——而实际上,它并非真正的设计模式。
什么才能称之为真正的设计模式?
答案:开发者普遍认同。
如果你能从大量开发者那里获得意见,那就说明你走对了路。通过了解模式的创建过程,并熟悉现有的模式,你就开始学习这个过程了。任何设计模式都必须经历这个阶段才能成为成熟的模式。这被称为原型模式。
如果一个原型模式符合一系列标准,那么它就可以成为一种成熟的模式。这些标准是通过一系列由不同数量的开发者进行的测试而制定的。原型模式必须在多种挑战环境下进行测试,在多种场景下进行分析,并最终通过大量的测试和社区的普遍共识,证明其是一种实用且可复用的设计模式。
为了展示如何使一个成熟的模式被特定软件语言的开发者社区认可,人们做了大量的工作和文档工作。
反模式
值得注意的是,就像软件领域的许多事物一样,任何概念都有其逆概念。那么,设计模式的逆概念是什么呢?
反模式
反模式代表了一种不良实践。例如,修改类Object原型就是一种反模式。
在 JavaScript 中,几乎所有东西都继承自某个类Object。JavaScript 使用基于原型的继承,因此在任何情况下,你都修改了某个类。你创建了一个变体,这可能会改变 JavaScript 中所有其他的设计模式、概念或技术。这很不好,因此是一种反设计模式。
设计模式分类
设计模式的分类方法有很多种,但以下是一种常见的分类方法。
- 创作型设计模式
- 结构设计模式
- 行为设计模式
- 并发设计模式
- 建筑设计模式
创造型设计模式
创建型设计模式是用于创建对象的模式。这些设计模式优化了创建单个或一组对象的机制。
- 构建器设计模式
- 工厂设计模式
- 单例设计模式
- 原型设计模式
- 抽象工厂设计模式
这些都是创造型设计模式的例子吗?
结构设计模式
结构化设计模式与对象关系相关。这类设计模式确保当系统的一部分发生变化时,整个系统无需随之改变。
- 代理设计模式
- 桥梁设计模式
- 立面设计模式
- 适配器设计模式
- 装饰设计图案
- 轻量级设计模式
- 复合设计模式
这些都是结构化设计模式的例子。
行为设计模式
行为设计模式用于识别、实现和改进系统中不同对象之间的通信。它们用于确保给定软件系统中不同部分的数据同步。
- 状态设计模式
- 访客设计模式
- 命令设计模式
- 纪念品设计图案
- 迭代器设计模式
- 中介者设计模式
- 观察者设计模式
- 策略设计模式
- 责任链设计模式
这些都是行为设计模式的例子。
并发设计模式
并发设计模式用于实现多线程编程范式的解决方案。
- 调度器设计模式
- 主动对象设计模式
- 核反应设计模式
这些都是并发设计模式的例子吗?
建筑设计模式
建筑设计模式用于实现建筑最佳实践。
- MVP 设计模式(模型-视图-表示器)
- MVC设计模式(模型-视图-控制器)
- MVVM 设计模式(模型-视图-视图模型)
这些都是架构设计模式的例子。
设计模式示例
每一种设计模式都代表着针对特定问题的一种特定解决方案。最佳设计模式并非万能的。为了成为优秀的软件工程师,我们需要学习何时应该使用特定的设计模式。我们需要从具体情境的角度来判断哪种设计模式才是最佳选择。
针对特定问题采用不合适的设计模式,不仅无济于事,反而可能损害我们和我们的应用程序目标。
构造器模式
在经典的面向对象编程语言中,构造函数是我们最早学习的特殊函数之一。它用于使用一组默认属性值初始化对象。
在 JavaScript 中如何创建对象?最常见的方法有哪些?
let obj = {}
let obj = Object.create(Object.prototype)
let obj = new Object();
一旦我们创建了对象,从 ES3 开始,就有四种方法可以向新创建的 js 对象添加属性。
点记法
obj.key = 'value'
括号表示法
obj['key'] = 'value'
Object.definePropeties 表示法
Object.defineProperties(obj, {
'keyOne': { value: 'one', writable: true },
'keyTwo': { value: 'two', writable: false },
})
在 JavaScript 中,花括号是创建对象最常用的方法。点号(或方括号)是定义属性和设置属性值最常用的方法。
正如我们之前讨论的,JavaScript 实际上并不支持传统的面向对象类。但是,newJavaScript 中有 `constructor` 关键字。我们可以通过 `constructor`new关键字在 JavaScript 中实现构造函数。
我们可以使用函数作为构造函数,最终使用属性初始化对象,并传入该对象的初始属性值new。
function Person(name, email, admin) {
this.name = name
this.email = email
this.admin = admin
this.isAdmin = () => this.admin === true
this.isNotAdmin = () => this.admin === false
}
let tim = new Person('Tim', 'tim@gmail.com', false)
let sarah = new Person('Sarah', 'sarah@gmail.com', true)
tim.isAdmin() // false
tim.isNotAdmin() // true
sarah.isAdmin() // true
sarah.isNotAdmin() // false
我们能否改进这种语法?我们真的需要在对象的构造函数中定义其功能吗?我们还可以访问对象本身,prototype为其添加方法。请查看以下语法。
function Person(name, email, admin) {
this.name = name
this.email = email
this.admin = admin
}
Person.prototype.isAdmin = function () {
return this.admin === true
}
Person.prototype.isNotAdmin = function () {
return this.admin === false
}
let tim = new Person('Tim', 'tim@gmail.com', false)
let sarah = new Person('Sarah', 'sarah@gmail.com', true)
tim.isAdmin() // false
tim.isNotAdmin() // true
sarah.isAdmin() // true
sarah.isNotAdmin() // false
模块设计模式
JavaScript 总能带来令人惊叹的奇妙体验,它能做到很多意想不到的事情。没错,有时候这些特性确实令人费解——但这同时也赋予了它实现一些非常强大的功能的能力。
与其他语言相比,JavaScript 的其中一个奇特之处在于它能够支持对修饰符的访问。
在深入探讨模块模式之前,我们先来了解一下 JavaScript 中的闭包。理解闭包对于真正理解 JavaScript 中最强大的几种模式至关重要。
JavaScript闭包
闭包是指即使父函数关闭后,仍然能够访问父函数作用域的函数。闭包帮助我们通过作用域来模拟访问修饰符的行为。
让我们通过一个例子来学习。
let Countable = (function () {
let count = 0
return function () {
return count++
}
})()
console.log(Countable()) // 1
console.log(Countable()) // 2
console.log(Countable()) // 3
在这个例子中,我们使用了IIFE,即立即调用函数表达式。
每次调用 `countable` 函数时,它所关联的函数都会立即执行。这得益于 JavaScript 中函数作为一等公民的强大特性。
调用此函数时,实际上返回的是另一个嵌套函数。由于我们无法count从 Countable 对象外部访问该变量,因此我们利用设计模式,将其设为privateCountable 对象的成员。
count属于私人信息。
借助闭包,我们可以创建包含私有部分和公共部分的对象。这些对象被称为模块,当我们需要隐藏对象某些子部分的行为时,它们非常有用。我们可以修改哪些行为是公开的,哪些部分是私有的,不公开。
再举一个例子:
const Collection = (function() {
// items is a private property
let items = [];
// everything returned engulfed public properties and methods
return {
add: function (item) {
items.push(item)
},
remove: function (item) {
let index = items.indexOf(item)
if (index >= 0) items.splice(index, 1)
},
all: function () {
return JSON.parse(JSON.stringify(items))
}
}
})()
Collection.add('Tim')
Collection.add('Sarah')
Collection.add('Raphael')
console.log(Collection.all()) // ['Tim', 'Sarah', 'Raphael']
Collection.remove('Sarah')
console.log(Collection.all()) // ['Tim', 'Raphael']
这种模式允许我们在对象的私有部分和公共部分之间建立清晰的界限。对于具有经典面向对象编程经验的开发者来说,这个概念并不陌生。
话虽如此,但这并不能让一切都像我们希望的那样完美无缺。
如果想更改某个成员的可见性该怎么办?
由于我们需要进行一些特殊或不同的设置来实现模块设计模式,因此您需要修改所有使用该成员的代码。
将私有部分更改为公共部分或反之亦然,需要更改代码中的几个内部依赖点。
揭示模块设计模式
让我们改进上面提到的模块设计模式。主要区别在于,我们将把所有对象逻辑都写在模块的私有作用域内,然后通过返回匿名对象来公开需要公开的部分。
在将私有成员映射到其对应的公共成员时,我们也可以更改私有成员的名称。
const Collection = (function () {
/* Private Members */
let items = []
function all () {
return JSON.parse(JSON.stringify(items))
}
function add (item) {
items.push(item)
}
function remove (item) {
let index = items.indexOf(item)
if (index >= 0) items.splice(index, 1)
}
/* Public Members */
return {
addItem: add,
allItems: all,
removeItem: remove,
}
})()
Collection.addItem('Tim')
Collection.addItem('Sam')
Collection.addItem('Ben')
console.log(Collection.allItems()) // ['Tim', 'Sam', 'Ben']
Collection.remove('Sam')
console.log(Collection.allItems()) // ['Tim', 'Ben']
上面这个例子就是所谓的揭示式模块模式。它是实现模块模式的至少三种不同方式之一。
揭示式模块模式与其他所有模块设计模式的变体之间有什么区别?
主要区别在于公共成员的引用方式。因此,揭示模块设计模式更易于使用和修改。
也就是说,这种设计模式在某些情况下可能比较脆弱(记住,没有一种设计模式是普遍适用的最佳选择)。
在考虑是否应该使用揭示模块模式时,以下是一些需要考虑的问题情况。
-
- 一个私有函数引用了一个公共函数。在这种情况下,我们不能使用这种设计模式来重写公共函数。如果我们尝试重写它,就会因为私有函数继续引用该函数的私有实现而导致软件出现错误。
-
- 如果一个公共成员指向一个私有变量,然后我们又试图从模块外部覆盖该公共成员,那么就不应该使用这种设计模式。在这种情况下,其他函数仍然会引用该变量的私有值,从而导致软件出现错误。
单例设计模式
单例设计模式适用于只需要一个类实例的情况。单例设计模式属于创建型设计模式。
例如,假设我们需要一个对象,其中包含应用程序运行时启动之前定义的一些配置设置。在这种情况下,每次需要此配置对象时,都无需创建一个全新的对象。用户定义的配置设置只需加载到对象中一次,以便我们的 JavaScript 运行时可以访问这些配置设置,但我们无需每次尝试访问配置设置时都重新创建该对象。
const Singleton = (function () {
// Private config
let config;
function initializedConfigurationSettings (values) {
this.random = Mathod.random()
values = values || {}
this.number = values.number || 5
this.size = values.size || 10
}
return {
getConfig: function (values) {
// we initialize the singleton value only once
if (config === undefined) {
config = new initializedConfigurationSettings(values)
}
return config
}
}
}();
const ConfigurationSettings = singleton.getConfig({ app: 'HelloWorld', environment: 'local' })
console.log(ConfigurationSettings) // { app: 'HelloWorld', environment: 'local' }
ConfigurationSettings.getConfig({ "number": 8 })
// same randomDecimalValue as in the first config - aka we've proven it's the same object
在这个例子中,你可以看到我们生成了一个随机数。如果你使用这段代码,那么在第一次调用之后,生成的随机数将保持不变singleton.getConfig。这是我们证明单例对象每次都返回同一个对象的方法。我们只创建对象一次,之后每次都返回同一个配置对象。
观察者设计模式
在我看来,观察者设计模式是最强大的设计模式之一——尤其是在 JavaScript 中。
观察者设计模式是一种行为型设计模式。我们可以使用这种设计模式来改善软件应用程序中不同部分之间的沟通。
这种设计模式在实现上确实有几种变体,但其最基本的形式包含两个主要部分。
- 第一部分:主题
- 第二部分:观察员
某个主题负责处理与该主题相关的所有操作。观察员订阅该主题。
观察者可以订阅和取消订阅某个话题。
假设我们有两种类型的物体:
一位顾客。
一家商店。
顾客对某个特定品牌的产品(例如:iPhone)感兴趣,该产品应该很快就会在商店里有售。
让顾客每天到店查询商品库存会耗费大量资源。相比之下,顾客可以订阅商店提供的 iPhone 主题帖。
解决方案:
具有某种特殊状态的对象就是主体。由于它还会将自身状态的变化通知其他对象,所以我们也称它为发布者。
所有其他对象,在本例中即为客户,都将是订阅者。
let publisher = {}
(function (container) {
// represents a unique subscription id to a topic
let id = 0
container.subscribe = function (topic, callback) {
if (!(topic in container)) container[topic] = []
container[topic].push({ id: id++, callback: callback })
return id
}
container.unsubscribe = function (topic, id) {
let subscribers = []
for (let subscriber of container[topic])
if (subscriber.id !== id)
subscribers.push(subscriber)
container[topic] = subscribers
}
container.publish = function (topic, data) {
for (let subscriber of container[topic])
subscriber.callback(data)
}
})(publisher)
let subscription_1 = publisher.subscribe('mouseClicked', function (data) {
console.log(
"Sam's callback for mouse click: ",
"Event Data: ",
JSON.stringify(data)
)
})
let subscription_2 = publisher.subscribe('mouseHovered', function (data) {
console.log(
"Sam's callback for mouse hovered: ",
"Event Data: ",
JSON.stringify(data)
)
})
let subscription_3 = publisher.subscribe('mouseClicked', function (data) {
console.log(
"Sarah's callback function for mouse click: ",
"Event Data: ",
JSON.stringify(data)
)
})
publisher.publish('mouseClicked', { data: 'data1' })
publisher.publish('mouseHovered', { data: 'data2' })
// unsubsribe from an event
publisher.unsubcribe('mouseClicked', subscription_3)
publisher.publish('mouseClicked', { data: 'data1' })
publisher.publish('mouseHovered', { data: 'data2' })
观察者设计模式在我们需要根据触发的单个事件执行多个操作的情况下非常有用。
例子:
想象一下这样的场景:我们需要向 API 发出多个 AJAX 请求,然后我们需要根据最初一组请求返回的数据结果,进一步执行其他 AJAX 请求。
你需要将 AJAX 调用层层嵌套,这可能会导致所谓的“回调地狱”。使用发布/订阅模式则是一种更优雅的解决方案。
观察者设计模式的缺点
- 对我们系统的各个部分进行测试非常困难。
原型设计模式
正如本文通篇所述,JS不支持经典面向对象编程原生理解中的类。
正因如此,对象间的继承是通过基于原型的编程来实现的。这使得我们能够创建可以作为其他 JavaScript 对象原型的对象。原型对象充当构造函数创建的每个对象的蓝图。
让我们用 JS 展示一下这种原型模式的简单实现。
let PersonPrototype = {
hi: function () { console.log(`Hello, my name is ${this.name}, and I'm ${this.age}.`) },
bye: function () { console.log(`I'm ${this.name} and I'm saying bye now!`) }
}
function Person (name, age) {
age = age || 26
name = name || "John Doe"
function constructorFunction(name, age) {
this.age = age
this.name = name
}
constructorFunction.prototype = PersonPrototype
let obj = new constructorFunction(name, age)
return obj
}
let person1 = Person()
let person2 = Person("Tim", 38)
person1.hi() // "hello, my name is John Doe and I'm 26
person2.hi() // "hello, my name is Tim and I'm 38
原型是 JavaScript 中继承的工作原理,这只是一个简单的实现示例。
命令设计模式
命令设计模式适用于我们想要将执行命令的对象与发出我们要执行的命令的对象解耦的情况。
例子:
设想这样一种情况:我们的应用程序需要调用大量的 API 来获取特定的应用程序服务。而这些 API 服务会发生变化。
要理解这种奇怪的现象,并为解决该问题提供可靠的编码方案,我们可以使用命令设计模式。
我们希望实现一个抽象层。这个抽象层会将调用 API 服务的对象与指示它们何时调用 API 服务的对象分离。这样一来,我们就无需在所有需要调用服务的地方修改代码。我们只需要修改发出调用的对象本身——也就是说,我们只需要在一个地方进行修改,而不是在多个地方。
设计模式的一个重要方面是,我们必须习惯于理解在决定使用任何给定的设计模式时所做的权衡取舍。
我们是在添加不必要的抽象层,还是在解决依赖侵蚀问题,而这个问题需要通过抽象层来妥善解决?
let invoker = {
add: function (x, y) { return x + y },
subtract: (x, y) { return x - y },
}
let manager = {
execute: function (name, args) {
if (name in invoker) {
return invoker[name].apply(invoker, [].slice.call(arguments, 1))
}
return false
}
}
console.log(manager.execute("add", 3, 5)) // 8
console.log(manager.execute("subtract", 5, 3)) // 2
立面设计模式
什么是外观模式?使用这种设计模式,我们可以在公开显示的内容和幕后实现的内容之间创建一个抽象层。这种设计模式能够有效地提高代码的可读性。
这种模式的一个很好的例子是 DOM 操作库(例如 jQuery、Dojo 或 D3)中的选择器。您可能已经注意到,这些库具有非常强大的选择器功能;您可以编写复杂的查询,例如:
jQuery(".parent .child div.span")
在这个例子中,选择功能的语法得到了很大的简化。虽然表面上看起来很简单,但其背后的逻辑却复杂得多。底层实现了很多功能,但面向外部 API 的最终用户(在本例中是开发人员)却被简化了。
我们喜欢化繁为简 :)
后续步骤
设计模式是软件工程师工具箱中最强大的工具之一——如果你想成为一名高级 JavaScript 开发人员,那么你应该了解大量的设计模式。
了解设计模式的实现方式、时间、地点以及权衡取舍,都是领导团队的 JavaScript 工程师应该非常了解的特性。
Refactoring Guru是理解这些设计模式的绝佳资源。
我们可以在本文中展示上百个设计原则示例,但事实是,一篇文章不可能涵盖所有可供您使用的设计原则。
我强烈建议你设定目标,每周学习两条新的设计原则。一年后,你将掌握104条设计原则。作为一名软件工程师,你对任何团队和组织的价值都会成倍提升。
对于那些有兴趣深入了解的人,我推荐《四人帮》这本书。
设计模式:可重用面向对象软件的要素。
感谢阅读,欢迎关注 Clean Code Studio 获取更多资讯!
Clean Code Studio
JavaScript 设计模式
Clean Code
JavaScript 算法示例
你知道我有电子报吗?📬
如果您想在我发布新博客文章或
发布重大项目公告时收到通知,
文章来源:https://dev.to/cleancodestudio/the-most-compressive-javascript-design-principles-guide-7i3



