变量
- 使用有意义的明显的变量名
好的
const yyyymmdstr = moment().format("YYYY/MM/DD");
坏的
const currentDate = moment().format("YYYY/MM/DD");
- 对相同类型的变量使用相同的词汇表
坏的
getUserInfo ();
getClientData ();
getCustomerRecord ();
好的
getUser ();
- 使用可以搜索的名称
- 我们将阅读比编写更多的代码。我们编写的代码具有可读性和可搜索性,这一点很重要。通过不命名最终对理解我们的程序有意义的变量,我们伤害了我们的读者。使您的名字可搜索。
坏的
// 这个86400000 用途是什么?
setTimeout(blastOff, 86400000);
好的
//将它们声明为大写的命名常量。
const MILLISECONDS_IN_A_DAY = 86 _400_000 ;
setTimeout (blastOff , MILLISECONDS_IN_A_DAY );
- 使用解释变量
坏的
const address = "One Infinite Loop, Cupertino 95014";
const cityZipCodeRegex = /^[^,\\]+[,\\\s]+(.+?)\s*(\d{5})?$/;
saveCityZipCode(
address.match(cityZipCodeRegex)[1],
address.match(cityZipCodeRegex)[2]
);
好的
const address = "One Infinite Loop, Cupertino 95014";
const cityZipCodeRegex = /^[^,\\]+[,\\\s]+(.+?)\s*(\d{5})?$/;
const [_, city, zipCode] = address.match(cityZipCodeRegex) || [];
saveCityZipCode(city, zipCode);
- 避免思维导图
- 显式胜于隐式。
坏的
const locations = ["Beijing", "Shanghai", "Guangdong"];
locations.forEach(l => {
doStuff();
doSomeOtherStuff();
// ...
// ...
// ...
// 等待,什么是 ‘l’
dispatch(l);
});
好的
const locations = ["Beijing", "Shanghai", "Guangdong"];
locations.forEach(location => {
doStuff();
doSomeOtherStuff();
// ...
// ...
// ...
dispatch(location);
});
- 不要添加不需要的上下文
- 如果您的类/对象名称告诉您某些内容,请不要在变量名称中重复。
坏的
const Car = {
carMake: "Honda",
carModel: "Accord",
carColor: "Blue"
};
function paintCar(car) {
car.carColor = "Red";
}
好的
const Car = {
make: "Honda",
model: "Accord",
color: "Blue"
};
function paintCar(car) {
car.color = "Red";
}
- 使用默认参数代替短路或条件
- 默认参数通常比短路更干净。请注意,如果使用它们,则函数将仅提供undefined参数的默认值。其他“falsy”的价值观,如’’,"",false,null,0,和NaN,不会被默认值代替。
坏的
function createCode(name) {
const breweryName = name || "Hello World";
// ...
}
好的
function createCode(name = "Hello World") {
// ...
}
函数
- Functions
函数参数(理想情况下为2个或更少)
限制功能参数的数量非常重要,因为它使功能测试变得更加容易。超过三个会导致组合爆炸,您必须使用每个单独的参数测试大量不同的案例。
一个或两个参数是理想的情况,如果可能,应避免三个参数。除此以外的任何事情都应该加以巩固。通常,如果您有两个以上的参数,则您的函数将尝试执行过多操作。在没有这种情况的情况下,大多数情况下,更高级别的对象作为参数就足够了。
由于JavaScript允许您快速创建对象,而无需大量的类模板,因此如果发现自己需要大量参数,则可以使用对象。
为使函数具有明显的属性,您可以使用ES2015 / ES6解构语法。这有一些优点:
当某人查看函数签名时,可以立即清除正在使用的属性。
它可以用来模拟命名参数。
解构还会克隆传递到函数中的参数对象的指定原始值。这可以帮助防止副作用。注意:不会克隆从参数对象解构的对象和数组。
短绒棉可以警告您有关未使用的属性的信息,如果不进行销毁,这是不可能的。
坏的
function createMenu(title, body, buttonText, cancellable) {
// ...
}
createMenu("Foo", "Bar", "Baz", true);
好的
function createMenu({ title, body, buttonText, cancellable }) {
// ...
}
createMenu({
title: "Foo",
body: "Bar",
buttonText: "Baz",
cancellable: true
});
- 函数应该做一件事
- 到目前为止,这是软件工程中最重要的规则。当函数做的事情不止一件事时,它们就很难进行编写,测试和推理。当您可以将一个函数隔离为一个动作时,就可以轻松地对其进行重构,并且代码将变得更加清晰。如果您除了本指南之外没有其他事,您将领先于许多开发人员。
坏的
function emailClients(clients) {
clients.forEach(client => {
const clientRecord = database.lookup(client);
if (clientRecord.isActive()) {
email(client);
}
});
}
好的
function emailActiveClients(clients) {
clients.filter(isActiveClient).forEach(email);
}
function isActiveClient(client) {
const clientRecord = database.lookup(client);
return clientRecord.isActive();
}
- 函数名称应说明其作用
坏的
function addToDate(date, month) {
// ...
}
const date = new Date();
// 很难从函数名称中知道添加了什么
addToDate(date, 1);
好的
function addMonthToDate(month, date) {
// ...
}
const date = new Date();
addMonthToDate(1, date);
- 函数名称应说明其作用
- 当您具有多个抽象级别时,您的函数通常会执行过多操作。拆分功能可提高可重用性并简化测试。
坏的
function parseBetterJSAlternative(code) {
const REGEXES = [
// ...
];
const statements = code.split(" ");
const tokens = [];
REGEXES.forEach(REGEX => {
statements.forEach(statement => {
// ...
});
});
const ast = [];
tokens.forEach(token => {
// lex...
});
ast.forEach(node => {
// parse...
});
}
好的
function parseBetterJSAlternative(code) {
const tokens = tokenize(code);
const syntaxTree = parse(tokens);
syntaxTree.forEach(node => {
// parse...
});
}
function tokenize(code) {
const REGEXES = [
// ...
];
const statements = code.split(" ");
const tokens = [];
REGEXES.forEach(REGEX => {
statements.forEach(statement => {
tokens.push(/* ... */);
});
});
return tokens;
}
function parse(tokens) {
const syntaxTree = [];
tokens.forEach(token => {
syntaxTree.push(/* ... */);
});
return syntaxTree;
}
- 删除重复的代码
- 尽最大努力避免重复的代码。重复的代码是不好的,因为这意味着如果您需要更改某些逻辑,则可以在多个地方进行更改。
想象一下,如果您经营一家餐馆,并且跟踪库存:所有的西红柿,洋葱,大蒜,香料等。如果您保留了多个清单,那么当您将一道菜配以西红柿时,所有这些都必须进行更新在他们之中。如果只有一个列表,则只有一个地方可以更新!
通常,您有重复的代码,因为您有两个或多个稍有不同的事物,它们有很多共同点,但是它们之间的差异迫使您拥有两个或多个独立的函数来完成许多相同的事情。删除重复的代码意味着创建一个仅用一个函数/模块/类就可以处理这组不同事物的抽象。
确保正确的抽象是至关重要的,这就是为什么您应该遵循“类”部分中列出的SOLID原理的原因。错误的抽象可能比重复的代码更糟糕,因此请小心!话虽如此,如果您可以做出一个很好的抽象,那就去做吧!不要重复自己,否则,只要想更改一件事,就会发现自己更新了多个位置。
坏的
function showDeveloperList(developers) {
developers.forEach(developer => {
const expectedSalary = developer.calculateExpectedSalary();
const experience = developer.getExperience();
const githubLink = developer.getGithubLink();
const data = {
expectedSalary,
experience,
githubLink
};
render(data);
});
}
function showManagerList(managers) {
managers.forEach(manager => {
const expectedSalary = manager.calculateExpectedSalary();
const experience = manager.getExperience();
const portfolio = manager.getMBAProjects();
const data = {
expectedSalary,
experience,
portfolio
};
render(data);
});
}
好的
function showEmployeeList(employees) {
employees.forEach(employee => {
const expectedSalary = employee.calculateExpectedSalary();
const experience = employee.getExperience();
const data = {
expectedSalary,
experience
};
switch (employee.type) {
case "manager":
data.portfolio = employee.getMBAProjects();
break;
case "developer":
data.githubLink = employee.getGithubLink();
break;
}
render(data);
});
}
- 使用Object.assign设置默认对象
坏的
const menuConfig = {
title: null,
body: "Bar",
buttonText: null,
cancellable: true
};
function createMenu(config) {
config.title = config.title || "Foo";
config.body = config.body || "Bar";
config.buttonText = config.buttonText || "Baz";
config.cancellable =
config.cancellable !== undefined ? config.cancellable : true;
}
createMenu(menuConfig);
好的
const menuConfig = {
title: "Order",
// 用户未包含 ‘body’ 建
buttonText: "Send",
cancellable: true
};
function createMenu(config) {
config = Object.assign(
{
title: "Foo",
body: "Bar",
buttonText: "Baz",
cancellable: true
},
config
);
// config now equals: {title: "Order", body: "Bar", buttonText: "Send", cancellable: true}
// ...
}
createMenu(menuConfig);
- 不要将标志用作函数参数
- 标志告诉您的用户,此功能可以完成多项任务。函数应该做一件事。如果函数遵循基于布尔的不同代码路径,请拆分它们。
坏的
function createFile(name, temp) {
if (temp) {
fs.create(`./temp/${name}`);
} else {
fs.create(name);
}
}
好的
function createFile(name) {
fs.create(name);
}
function createTempFile(name) {
createFile(`./temp/${name}`);
}
- 避免副作用(一)
- 如果函数除了接受一个值并返回另一个值或多个值以外,不执行任何其他操作,都会产生副作用。副作用可能是写入文件,修改某些全局变量,或者不小心将您的所有资金都汇给了陌生人。
现在,您确实需要偶尔在程序中产生副作用。与前面的示例一样,您可能需要写入文件。您想要做的就是集中执行此操作的位置。没有几个写入特定文件的函数和类。有一项服务可以做到这一点。一个,只有一个。
要点是要避免常见的陷阱,例如在没有任何结构的对象之间共享状态,使用可以被任何东西写入的可变数据类型,而不是集中发生副作用的地方。如果您能做到这一点,您将比绝大多数其他程序员更快乐。
坏的
//以下函数引用的全局变量。
//如果我们还有另一个使用此名称的函数,那么它将是一个数组,并且可能会破坏它。
let name = "Hello World";
function splitIntoFirstAndLastName() {
name = name.split(" ");
}
splitIntoFirstAndLastName();
console.log(name); // ['Hello', 'World'];
好的
function splitIntoFirstAndLastName(name) {
return name.split(" ");
}
const name = "Hello World";
const newName = splitIntoFirstAndLastName(name);
console.log(name); // 'Hello World';
console.log(newName); // ['Hello', 'World'];
- 避免副作用(二)
- 在JavaScript中,基元按值传递,而对象/数组按引用传递。对于对象和数组,如果您的函数在购物车数组中进行了更改(例如,通过添加要购买的商品),则使用该cart数组的任何其他函数都将受到此添加的影响。那可能很好,但是也可能不好。让我们想象一个糟糕的情况:
用户单击“购买”按钮,该按钮将调用一个purchase函数,该函数会生成网络请求并将该cart阵列发送到服务器。由于网络连接不良,该purchase功能必须继续重试请求。现在,如果与此同时,用户在网络请求开始之前意外地在他们实际上不想要的商品上单击了“添加到购物车”按钮,该怎么办?如果发生这种情况,并且网络请求开始,则该购买功能将发送意外添加的项目,因为它具有对购物车阵列的引用,该addItemToCart功能通过添加不需要的项目进行了修改。
最好的解决方案是addItemToCart始终克隆cart,对其进行编辑并返回克隆。这样可以确保任何保存在购物车参考上的其他功能都不会受到任何更改的影响。
关于此方法有两个注意事项:
在某些情况下,您实际上可能想修改输入对象,但是当您采用这种编程习惯时,您会发现这些情况很少见。大多数事情都可以重构为没有副作用!
就性能而言,克隆大对象可能会非常昂贵。幸运的是,在实践中这并不是一个大问题,因为有许多出色的库使这种编程方法能够快速运行,而不像手动克隆对象和数组那样占用大量内存。
坏的
const addItemToCart = (cart, item) => {
cart.push({ item, date: Date.now() });
};
好的
const addItemToCart = (cart, item) => {
return [...cart, { item, date: Date.now() }];
};
- 不要写全局函数
- 污染全局变量在JavaScript中是一种不好的做法,因为您可能会与另一个库发生冲突,并且API的用户将变得更加明智,直到他们在生产中遇到异常为止。让我们考虑一个示例:如果您想扩展JavaScript的本机Array方法以具有一个diff可以显示两个数组之间的差异的方法,该怎么办?您可以将新函数写入Array.prototype,但它可能与另一个尝试执行相同操作的库发生冲突。如果那个其他库只是diff用来查找数组的第一个元素和最后一个元素之间的区别,该怎么办?这就是为什么只使用ES2015 / ES6类并简单地扩展Array全局会更好的原因。
坏的
Array.prototype.diff = function diff(comparisonArray) {
const hash = new Set(comparisonArray);
return this.filter(elem => !hash.has(elem));
};
好的
class SuperArray extends Array {
diff(comparisonArray) {
const hash = new Set(comparisonArray);
return this.filter(elem => !hash.has(elem));
}
}
- 在命令式编程中偏爱函数式编程
- JavaScript不是Haskell那样的功能语言,但是它具有功能性的味道。功能语言可以更简洁,更容易测试。可以的话,请喜欢这种编程风格。
坏的
const programmerOutput = [
{
name: "Uncle Bobby",
linesOfCode: 500
},
{
name: "Suzie Q",
linesOfCode: 1500
},
{
name: "Jimmy Gosling",
linesOfCode: 150
},
{
name: "Gracie Hopper",
linesOfCode: 1000
}
];
let totalOutput = 0;
for (let i = 0; i < programmerOutput.length; i++) {
totalOutput += programmerOutput[i].linesOfCode;
}
好的
const programmerOutput = [
{
name: "Uncle Bobby",
linesOfCode: 500
},
{
name: "Suzie Q",
linesOfCode: 1500
},
{
name: "Jimmy Gosling",
linesOfCode: 150
},
{
name: "Gracie Hopper",
linesOfCode: 1000
}
];
const totalOutput = programmerOutput.reduce(
(totalLines, output) => totalLines + output.linesOfCode,
0
);
- 封装条件
坏的
if (fsm.state === "fetching" && isEmpty(listNode)) {
// ...
}
好的
function shouldShowSpinner(fsm, listNode) {
return fsm.state === "fetching" && isEmpty(listNode);
}
if (shouldShowSpinner(fsmInstance, listNodeInstance)) {
// ...
}
- 避免负面条件
坏的
function isDOMNodeNotPresent(node) {
// ...
}
if (!isDOMNodeNotPresent(node)) {
// ...
}
好的
function isDOMNodePresent(node) {
// ...
}
if (isDOMNodePresent(node)) {
// ...
}
- 避免有条件
- 这似乎是不可能完成的任务。初听到此消息后,大多数人会说:“我应该不做任何if陈述而做任何事情吗?” 答案是,在许多情况下,您可以使用多态来完成相同的任务。第二个问题通常是:“很好,但是我为什么要这么做?” 答案是我们之前学到的干净代码概念:函数只能做一件事。当您具有带有if语句的类和函数时,您就在告诉用户您的函数不仅仅完成一件事。记住,只做一件事。
坏的
class Airplane {
// ...
getCruisingAltitude() {
switch (this.type) {
case "777":
return this.getMaxAltitude() - this.getPassengerCount();
case "Air Force One":
return this.getMaxAltitude();
case "Cessna":
return this.getMaxAltitude() - this.getFuelExpenditure();
}
}
}
好的
class Airplane {
// ...
getCruisingAltitude() {
switch (this.type) {
case "777":
return this.getMaxAltitude() - this.getPassengerCount();
case "Air Force One":
return this.getMaxAltitude();
case "Cessna":
return this.getMaxAltitude() - this.getFuelExpenditure();
}
}
}
对象和数据结构
- 使用getter和setter
- 使用getter和setter访问对象上的数据可能比仅在对象上查找属性要好。“为什么?” 你可能会问。好吧,这是一个无组织的原因列表:
除了获得对象属性之外,您还想做更多的事情时,不必查找和更改代码库中的每个访问器。
使执行时的添加验证变得简单set。
封装内部表示。
获取和设置时易于添加日志记录和错误处理。
您可以延迟加载对象的属性,比方说从服务器获取它。
坏的
function makeBankAccount() {
// ...
return {
balance: 0
// ...
};
}
const account = makeBankAccount();
account.balance = 100;
好的
function makeBankAccount() {
// this one is private
let balance = 0;
// a "getter", made public via the returned object below
function getBalance() {
return balance;
}
// a "setter", made public via the returned object below
function setBalance(amount) {
// ... validate before updating the balance
balance = amount;
}
return {
// ...
getBalance,
setBalance
};
}
const account = makeBankAccount();
account.setBalance(100);
- 使对象具有私人成员
- 这可以通过闭包(对于ES5及以下版本)来实现。
坏的
const Employee = function(name) {
this.name = name;
};
Employee.prototype.getName = function getName() {
return this.name;
};
const employee = new Employee("John Doe");
console.log(`Employee name: ${employee.getName()}`); // Employee name: John Doe
delete employee.name;
console.log(`Employee name: ${employee.getName()}`); // Employee name: undefined
好的
function makeEmployee(name) {
return {
getName() {
return name;
}
};
}
const employee = makeEmployee("John Doe");
console.log(`Employee name: ${employee.getName()}`); // Employee name: John Doe
delete employee.name;
console.log(`Employee name: ${employee.getName()}`); // Employee name: John Doe
类
- 比ES5普通函数更喜欢ES2015 / ES6类
- 对于经典ES5类,很难获得可读的类继承,构造和方法定义。如果需要继承(请注意可能不需要继承),请选择ES2015 / ES6类。但是,在发现自己需要更大,更复杂的对象之前,请优先选择小型函数而不是类。
坏的
const Animal = function(age) {
if (!(this instanceof Animal)) {
throw new Error("Instantiate Animal with `new`");
}
this.age = age;
};
Animal.prototype.move = function move() {};
const Mammal = function(age, furColor) {
if (!(this instanceof Mammal)) {
throw new Error("Instantiate Mammal with `new`");
}
Animal.call(this, age);
this.furColor = furColor;
};
Mammal.prototype = Object.create(Animal.prototype);
Mammal.prototype.constructor = Mammal;
Mammal.prototype.liveBirth = function liveBirth() {};
const Human = function(age, furColor, languageSpoken) {
if (!(this instanceof Human)) {
throw new Error("Instantiate Human with `new`");
}
Mammal.call(this, age, furColor);
this.languageSpoken = languageSpoken;
};
Human.prototype = Object.create(Mammal.prototype);
Human.prototype.constructor = Human;
Human.prototype.speak = function speak() {};
好的
class Animal {
constructor(age) {
this.age = age;
}
move() {
/* ... */
}
}
class Mammal extends Animal {
constructor(age, furColor) {
super(age);
this.furColor = furColor;
}
liveBirth() {
/* ... */
}
}
class Human extends Mammal {
constructor(age, furColor, languageSpoken) {
super(age, furColor);
this.languageSpoken = languageSpoken;
}
speak() {
/* ... */
}
}
- 使用方法链接
- 这种模式在JavaScript中非常有用,您可以在jQuery和Lodash等许多库中看到它。它使您的代码更具表现力,并且不再那么冗长。出于这个原因,我说,使用方法链接并查看代码的干净程度。在您的类函数中,只需this在每个函数的结尾处返回即可,您可以将更多的类方法链接到该函数上。
坏的
class Car {
constructor(make, model, color) {
this.make = make;
this.model = model;
this.color = color;
}
setMake(make) {
this.make = make;
}
setModel(model) {
this.model = model;
}
setColor(color) {
this.color = color;
}
save() {
console.log(this.make, this.model, this.color);
}
}
const car = new Car("Ford", "F-150", "red");
car.setColor("pink");
car.save();
好的
class Car {
constructor(make, model, color) {
this.make = make;
this.model = model;
this.color = color;
}
setMake(make) {
this.make = make;
// NOTE: Returning this for chaining
return this;
}
setModel(model) {
this.model = model;
// NOTE: Returning this for chaining
return this;
}
setColor(color) {
this.color = color;
// NOTE: Returning this for chaining
return this;
}
save() {
console.log(this.make, this.model, this.color);
// NOTE: Returning this for chaining
return this;
}
}
const car = new Car("Ford", "F-150", "red").setColor("pink").save();
- 优先考虑组成而不是继承
- 正如“四人帮”在“设计模式”中著名地指出的那样,您应该尽可能地选择组合而不是继承。有很多使用继承的好理由,也有很多使用合成的好理由。该原则的主要目的是,如果您的思维本能地去继承,请尝试考虑组合是否可以更好地为您的问题建模。在某些情况下可以。
那时您可能想知道,“我什么时候应该使用继承?” 这取决于您手头的问题,但这是当继承比构成更有意义时的一个不错的清单:
您的继承表示“是”关系,而不是“具有”关系(人类->动物与用户->用户详细信息)。
您可以重用基类中的代码(人类可以像所有动物一样移动)。
您想通过更改基类对派生类进行全局更改。(改变所有动物移动时的热量消耗)。
坏的
class Employee {
constructor(name, email) {
this.name = name;
this.email = email;
}
// ...
}
// Bad because Employees "have" tax data. EmployeeTaxData is not a type of Employee
class EmployeeTaxData extends Employee {
constructor(ssn, salary) {
super();
this.ssn = ssn;
this.salary = salary;
}
// ...
}
好的
class EmployeeTaxData {
constructor(ssn, salary) {
this.ssn = ssn;
this.salary = salary;
}
// ...
}
class Employee {
constructor(name, email) {
this.name = name;
this.email = email;
}
setTaxData(ssn, salary) {
this.taxData = new EmployeeTaxData(ssn, salary);
}
// ...
}
坚硬的
- 单一责任原则(SRP)
- 如“清洁代码”中所述,“更改班级的理由永远不只一个”。诱使类具有很多功能的包装很诱人,例如当您只能带一个手提箱旅行时。这样做的问题是,您的课程在概念上不会具有凝聚力,并且会为更改类提供很多理由。尽量减少更改班级的时间很重要。这很重要,因为如果一个类中有太多功能,而您对其进行了修改,则可能很难理解这将如何影响代码库中的其他依赖模块
坏的
class UserSettings {
constructor(user) {
this.user = user;
}
changeSettings(settings) {
if (this.verifyCredentials()) {
// ...
}
}
verifyCredentials() {
// ...
}
}
好的
class UserAuth {
constructor(user) {
this.user = user;
}
verifyCredentials() {
// ...
}
}
class UserSettings {
constructor(user) {
this.user = user;
this.auth = new UserAuth(user);
}
changeSettings(settings) {
if (this.auth.verifyCredentials()) {
// ...
}
}
}
- 开放/封闭原则(OCP)
- 正如Bertrand Meyer所说,“软件实体(类,模块,功能等)应打开以进行扩展,但应关闭以进行修改。” 那是什么意思呢?该原则基本上指出,您应该允许用户添加新功能而不更改现有代码。
坏的
class AjaxAdapter extends Adapter {
constructor() {
super();
this.name = "ajaxAdapter";
}
}
class NodeAdapter extends Adapter {
constructor() {
super();
this.name = "nodeAdapter";
}
}
class HttpRequester {
constructor(adapter) {
this.adapter = adapter;
}
fetch(url) {
if (this.adapter.name === "ajaxAdapter") {
return makeAjaxCall(url).then(response => {
// transform response and return
});
} else if (this.adapter.name === "nodeAdapter") {
return makeHttpCall(url).then(response => {
// transform response and return
});
}
}
}
function makeAjaxCall(url) {
// request and return promise
}
function makeHttpCall(url) {
// request and return promise
}
好的
class AjaxAdapter extends Adapter {
constructor() {
super();
this.name = "ajaxAdapter";
}
request(url) {
// request and return promise
}
}
class NodeAdapter extends Adapter {
constructor() {
super();
this.name = "nodeAdapter";
}
request(url) {
// request and return promise
}
}
class HttpRequester {
constructor(adapter) {
this.adapter = adapter;
}
fetch(url) {
return this.adapter.request(url).then(response => {
// transform response and return
});
}
}
- 里斯科夫替代原理(LSP)
- 这是一个非常简单的概念的可怕术语。正式定义为“如果S是T的子类型,则可以用类型S的对象替换类型T的对象(即,类型S的对象可以替换类型T的对象),而无需更改该程序的任何所需属性(正确性,执行的任务等)。” 这是一个更可怕的定义。
最好的解释是,如果您有一个父类和一个子类,则基类和子类可以互换使用而不会得到错误的结果。这可能仍然令人困惑,所以让我们看一下经典的Square-Rectangle示例。从数学上讲,正方形是矩形,但是如果您通过继承使用“ is-a”关系对其进行建模,则会很快遇到麻烦。
坏的
class Rectangle {
constructor() {
this.width = 0;
this.height = 0;
}
setColor(color) {
// ...
}
render(area) {
// ...
}
setWidth(width) {
this.width = width;
}
setHeight(height) {
this.height = height;
}
getArea() {
return this.width * this.height;
}
}
class Square extends Rectangle {
setWidth(width) {
this.width = width;
this.height = width;
}
setHeight(height) {
this.width = height;
this.height = height;
}
}
function renderLargeRectangles(rectangles) {
rectangles.forEach(rectangle => {
rectangle.setWidth(4);
rectangle.setHeight(5);
const area = rectangle.getArea(); // BAD: Returns 25 for Square. Should be 20.
rectangle.render(area);
});
}
const rectangles = [new Rectangle(), new Rectangle(), new Square()];
renderLargeRectangles(rectangles);
好的
class Shape {
setColor(color) {
// ...
}
render(area) {
// ...
}
}
class Rectangle extends Shape {
constructor(width, height) {
super();
this.width = width;
this.height = height;
}
getArea() {
return this.width * this.height;
}
}
class Square extends Shape {
constructor(length) {
super();
this.length = length;
}
getArea() {
return this.length * this.length;
}
}
function renderLargeShapes(shapes) {
shapes.forEach(shape => {
const area = shape.getArea();
shape.render(area);
});
}
const shapes = [new Rectangle(4, 5), new Rectangle(4, 5), new Square(5)];
renderLargeShapes(shapes);
- 接口隔离原理(ISP)
JavaScript没有接口,因此该原理并不像其他应用严格。但是,即使JavaScript缺乏类型系统,它也很重要且相关。
ISP指出“不应强迫客户端依赖其不使用的接口”。由于鸭子类型,接口在JavaScript中是隐式协定。
一个很好的示例说明了JavaScript中的这一原理,该示例适用于需要大型设置对象的类。不需要客户设置大量选项是有好处的,因为大多数情况下,他们不需要所有设置。将它们设置为可选有助于防止出现“胖界面”。
坏的
class DOMTraverser {
constructor(settings) {
this.settings = settings;
this.setup();
}
setup() {
this.rootNode = this.settings.rootNode;
this.settings.animationModule.setup();
}
traverse() {
// ...
}
}
const $ = new DOMTraverser({
rootNode: document.getElementsByTagName("body"),
animationModule() {} // Most of the time, we won't need to animate when traversing.
// ...
});
好的
class DOMTraverser {
constructor(settings) {
this.settings = settings;
this.options = settings.options;
this.setup();
}
setup() {
this.rootNode = this.settings.rootNode;
this.setupOptions();
}
setupOptions() {
if (this.options.animationModule) {
// ...
}
}
traverse() {
// ...
}
}
const $ = new DOMTraverser({
rootNode: document.getElementsByTagName("body"),
options: {
animationModule() {}
}
});
- 依赖倒置原则(DIP)
- 该原则陈述了两个基本要点:
高级模块不应依赖于低级模块。两者都应依赖抽象。
抽象不应依赖细节。细节应取决于抽象。
起初这很难理解,但是如果您使用过AngularJS,您已经以依赖注入(DI)的形式看到了该原理的实现。尽管它们不是完全相同的概念,但DIP可以使高级模块不了解其低级模块的详细信息并进行设置。它可以通过DI来完成。这样的巨大好处是,它减少了模块之间的耦合。耦合是非常糟糕的开发模式,因为它使您的代码难以重构。
如前所述,JavaScript没有接口,因此依赖的抽象是隐式协定。也就是说,一个对象/类公开给另一个对象/类的方法和属性。在下面的示例中,隐式约定是的任何Request模块InventoryTracker都将具有requestItems方法。
坏的
class InventoryRequester {
constructor() {
this.REQ_METHODS = ["HTTP"];
}
requestItem(item) {
// ...
}
}
class InventoryTracker {
constructor(items) {
this.items = items;
// BAD: We have created a dependency on a specific request implementation.
// We should just have requestItems depend on a request method: `request`
this.requester = new InventoryRequester();
}
requestItems() {
this.items.forEach(item => {
this.requester.requestItem(item);
});
}
}
const inventoryTracker = new InventoryTracker(["apples", "bananas"]);
inventoryTracker.requestItems();
好的
class InventoryTracker {
constructor(items, requester) {
this.items = items;
this.requester = requester;
}
requestItems() {
this.items.forEach(item => {
this.requester.requestItem(item);
});
}
}
class InventoryRequesterV1 {
constructor() {
this.REQ_METHODS = ["HTTP"];
}
requestItem(item) {
// ...
}
}
class InventoryRequesterV2 {
constructor() {
this.REQ_METHODS = ["WS"];
}
requestItem(item) {
// ...
}
}
// By constructing our dependencies externally and injecting them, we can easily
// substitute our request module for a fancy new one that uses WebSockets.
const inventoryTracker = new InventoryTracker(
["apples", "bananas"],
new InventoryRequesterV2()
);
inventoryTracker.requestItems();