JavaScript 最佳实践

在本文中,我将带您了解编写 JavaScript 时的“良好习惯”。

**1 — 避免使用 new Object()**

在 JavaScript 中,使用 new Object 有点冒险,而出于多种原因,使用原始类型总是更好。让我们深入探讨一下。

一般而言

例如,众所周知,"" 会创建一个字符串原语,而 new String() 则会创建一个字符串对象。由于它们更复杂且具有方法,字符串对象可能会带来意想不到的行为,尤其是在比较和类型强制转换时。

简单

原语的使用更简单、更直接,因为它们的使用避免了不必要的复杂性,并且代码变得易于阅读和维护。

表现

基元在内存和性能方面更高效。而创建对象则需要额外的开销。

可能造成混淆

由于 JavaScript 对对象和基元的处理方式不同,使用 new Object() 可能会导致令人困惑的情况,您会无意中处理对象而不是基元,这可能会导致一系列错误。

在大多数情况下,最好使用原语。

// ❌ Avoid
const str = new String();
const num = new Number();
const bool = new Boolean();
const obj = new Object();
const arr = new Array();
const regEx = new RegExp();
const func = new Function();

// ✅ Use
const str = "JavaScript";
const num = 10;
const bool = true;
const obj = {};
const arr = [];
const regEx = /()/;
const func = function() {};

**2 — 避免将 let 与数组和对象一起使用**

首先,让我们明确一点……使用 let 处理数组和对象本身并没有什么问题。但有些特定的注意事项可能会导致您在某些情况下避免使用它:

重新分配与变异

众所周知,let 允许我们重新分配变量本身,这可能会导致混乱或数据丢失。对象/数组可能会意外地被重新分配一整套新数据(新对象/新数组)。

使用 const 会使其更安全并且清楚,对对象/数组的引用不会改变,但您仍然可以修改其内容。

不变性意图

使用 const,您可以向与您一起工作的其他开发人员发出信号,表示该变量不应重新分配,从而增强代码的可读性和可维护性。

范围

虽然 let 具有块作用域,但它可能会导致循环或条件语句中的意外行为。通过使用 const,变量仍处于作用域内,而不会出现意外重新赋值的风险。

最佳实践

许多编码标准和最佳实践鼓励对不需要重新分配的变量使用 const,从而促进更干净、更可预测的代码。

// ❌ Avoid
let book = { title: "Inferno", author: "Dan Brown" };

// The book object will be overrode with string
book = "Hello world";
// ✅ Use
const book = { title: "Inferno", author: "Dan Brown" };

// The book object cannot be overrode
book = "Hello world";

**3 — 小心自动类型转换**

在 JavaScript 中,类型强制转换也称为类型强制转换,即语言自动将值从一种类型转换为另一种类型。这种情况可能发生在各种情况下,尤其是在涉及不同数据类型的操作期间:

let sum = "5" + 1; // "51" (string concatenation)
// In the code above, typeof sum is a string

let sub = "5" - 1; // 4 (string converted to number)
// In the code obove, typeof sub in a number
Another example can be helpful:

let lang = "JavaScript"; // typeof name is string
lang = 15; // changes typeof x to a number

小心数字,可能会意外转换为字符串或 NaN。因此,请考虑在敏感操作之前进行类型测试,或考虑使用 TypeScript 进行安全输入。

**4 — 避免使用双重相等比较**

== 和 === 是用于比较值的比较运算符,但它们的行为不同。

抽象平等

使用 == 时,JavaScript 会在进行比较之前将值转换为通用类型

console.log(5 == '5'); // true (number is converted to string)
console.log(null == undefined); // true (considered equal)
console.log(0 == false); // true (0 is converted to boolean as it's falsy value)
Strict Equality
With ===, the comparison checks both the value and the type. If types are different, it returns false

console.log(5 === '5'); // false (different types)
console.log(null === undefined); // false (different types)
console.log(0 === false); // false (different types)

何时使用?

当您想要确保值和类型相同时,请使用 ===,这通常是一种避免意外结果的好做法。

如果您特别需要比较值而不考虑其类型,请使用 ==,但这可能会导致错误,因此通常不建议这样做。

一般来说,考虑使用===进行更可预测和更清晰的比较。

注意:!== 和 != 也一样

5. 使用对象/数组解构

在 JavaScript 中,使用对象和数组的解构技术可以为您带来多种好处。

解构赋值语法是一种 JavaScript 表达式,可以将数组中的值或对象的属性解包到不同的变量中。正如 MDN 网络文档所述。

简明

它允许您在单个语句中提取对象的多个属性或从数组中提取元素,从而减少需要编写的代码量。

const book = { name: 'The Lost Symbol', author: 'Dan Brown' };
const { name, price } = book; // concise extraction

明晰

通过解构,可以清楚地显示您正在使用的属性或元素,从而使您的代码更具可读性。

const colors = ['red', 'green', 'blue'];
const [firstColor, secondColor] = colors; // clear intention

默认值

如果属性或元素不存在,您可以轻松分配默认值。

const { height = 180 } = person; // uses default value if height is undefined

嵌套解构

您可以解构嵌套对象或数组,从而简化访问深度嵌套的数据。

const user = { profile: { name: 'Eren Yeager', age: 20 } };
const { profile: { name } } = user; // easy access to nested properties

函数参数

它对于函数参数很有用,允许您直接解包值。

function display({ name, age }) {
    console.log(`${name} is ${age} years old.`);
}

解构有助于简化你的代码,使其更清晰且更易于维护。

**6 — 默认参数**

默认参数是一种很好的技术,可以使你的代码更清晰、更易读。

默认函数参数允许在未传递任何值或未定义的情况下使用默认值初始化命名参数。正如 MDN 网络文档所述。

单个参数

function greet(name = 'Guest') {
    console.log(`Hello, ${name}!`);
}

greet();         // Output: Hello, Guest!
greet('Chrollo');  // Output: Hello, Chrollo!

多个参数

您可以为多个参数设置默认值。

function multiply(a, b = 1) {
    return a * b;
}

multiply(5);    // Output: 5
multiply(5, 2);  // Output: 10

传递多个参数时,请注意未给出的参数。避免将可能未定义或可能未传递的参数作为第一个参数传递,或在任何其他传递的参数之前传递。

如果您怀疑某个参数值可能未给出或者可能作为未定义传递,请确保将其作为最后一个参数传递,以及对于多个未给定的参数传递。

使用表达式作为默认值

您可以使用表达式来计算默认值。

function add(x, y = x) {
    return x + y;
}

add(5);    // Output: 10 (5 + 5)
add(5, 2);  // Output: 7

具有默认值的剩余参数

您可以将默认参数与其余参数结合起来。

function logMessages(message, ...additionalMessages = ['No additional messages']) {
    console.log(message);
    console.log(additionalMessages);
}

logMessages('Hello!'); // Outputs: Hello! and ['No additional messages']

好处

提高可读性:如果省略参数,则可以清楚地了解使用什么默认值。

更少的样板:减少在函数体内检查和分配默认值的需要。

增强的灵活性:函数可以更优雅地处理更广泛的输入。

默认参数是一个强大的功能,可以增强函数的可用性并使您的代码更简洁!

**7 — 在 Switch 中使用默认设置**

在 JavaScript 中,用默认情况结束 switch 语句是一种很好的做法。当所有指定的情况都不符合输入时,默认情况将充当后备:

switch (expression) {
    case value1:
        // Code to execute if expression === value1
        break;
    case value2:
        // Code to execute if expression === value2
        break;
    default:
        // Code to execute if none of the above cases match
}

全部捕获

它提供了一种处理意外值的方法,确保您的代码不会悄无声息地失败。

const fruit = 'banana';

switch (fruit) {
    case 'apple':
        console.log('This is an apple.');
        break;
    case 'orange':
        console.log('This is an orange.');
        break;
    default:
        console.log('Unknown fruit.'); // Catches all other cases
}

提高可读性

包含默认情况可以让其他开发人员(或您自己)清楚地知道您考虑了所有可能性。

错误处理

当遇到意外值时,它可以用于记录或抛出错误。

function getColorCode(color) {
    switch (color) {
        case 'red':
            return '#FF0000';
        case 'green':
            return '#00FF00';
        case 'blue':
            return '#0000FF';
        default:
            throw new Error('Invalid color'); // Error handling
    }
}

如果有可能收到意外输入,则始终包含默认情况。

使用默认情况来提供有用的反馈或日志记录,尤其是在调试场景中。

如果适用,请考虑使用默认情况来设置后备值。

在 switch 语句中添加默认情况可以增强代码的稳健性和可维护性。

**8 — 避免使用 eval()**

eval() 是一个内置的 JavaScript 函数,它以字符串作为参数并将其作为 JavaScript 代码进行求值。这意味着您可以动态执行在运行时生成的代码。

const x = 10;
const code = 'x + 5';
const result = eval(code); // Evaluates to 15
console.log(result); // Output: 15

由于几个重要的原因,人们普遍建议避免在 JavaScript 中使用 eval()。

安全风险

代码注入:eval() 可以执行任意代码,使您的应用程序容易受到代码注入攻击。如果评估用户输入,攻击者可能会注入恶意代码。

const userInput = 'alert("Hacked!");'; // Malicious input
eval(userInput); // Executes the alert, compromising security

性能问题

执行速度慢:使用 eval() 执行的代码比常规代码运行速度慢,因为它必须在运行时进行解释,从而绕过 JavaScript 引擎所做的某些优化。

调试挑战

调试难度加大:使用 eval() 会使调试变得困难。eval() 内部抛出的错误很难追溯到原始来源。

替代方案

除了使用 eval(),请考虑以下更安全的替代方案:

JSON 解析:如果您处理 JSON 数据,请使用 JSON.parse() 而不是 eval()。

const jsonString = '{"name": "Alice"}';

const obj = JSON.parse(jsonString); // 将 JSON 字符串转换为对象的安全方法

函数构造函数:如果您需要动态创建函数,请考虑使用函数构造函数。

// In this case you can use new Function() -- back to 1st advice
const dynamicFunc = new Function('x', 'return x * x;');
console.log(dynamicFunc(5)); // Output: 25
Using Objects or Maps: For dynamic behavior, use objects or Maps to store key-value pairs instead of evaluating strings.
const operations = {
    add: (x, y) => x + y,
    subtract: (x, y) => x - y
};

const result = operations['add'](5, 3); // Safe and clear

总之,由于安全风险、性能问题和调试困难,请避免使用 eval()。选择更安全的替代方案来实现您的目标,而不会损害代码的完整性和性能。

9. 使用严格模式

在 JavaScript 中,“严格模式”是一种选择使用受限制的语言变体的方式,它有助于捕获常见的编码错误和“不安全”操作。它可以使您的代码更可预测且更易于调试。

启用严格模式

全局:通过将“use strict”; 放置在脚本文件的顶部。

"use strict";
// Your code here
Locally: By placing "use strict"; inside a function. This will enable Strict Mode only for that function.
function myFunction() {
    "use strict";
    // Strict mode is enabled here
}

使用严格模式的好处

防止使用未声明的变量:给未声明的变量赋值会引发错误。

"use strict";
x = 3.14; // Throws ReferenceError: x is not defined

消除这种强制:在严格模式下,在没有明确上下文调用的函数中,this 是未定义的。

"use strict";
function myFunction() {
    console.log(this); // undefined
}
myFunction();
Disallows duplicate property names or parameters
"use strict";
const obj = {
    prop: 1,
    prop: 2 // Throws SyntaxError: Duplicate data property in object literal
};

禁止某些语法:某些被认为有问题或令人困惑的语法是不允许的。

常见陷阱

箭头函数:请注意,箭头函数没有自己的 this,因此严格模式不以相同的方式适用。

eval:在 eval 语句内执行的代码在本地范围内运行,而不是全局范围内运行。

使用严格模式通常被认为是最佳实践,特别是对于较大的应用程序,因为它可以帮助您编写更干净、更安全的代码。

**10 — 保持代码 DRY(不要重复自己)**

DRY(不要重复自己)原则是软件开发中的一个关键概念,旨在减少代码中的重复。通过确保每条知识或逻辑都集中在一个地方,您可以让代码更易于维护、理解和重构。

功能

将重复的逻辑封装在函数中。这样,您可以重复使用相同的代码,而无需重复。

function calculateArea(width, height) {
    return width * height;
}

// Use the function instead of repeating the code
const area1 = calculateArea(5, 10);
const area2 = calculateArea(7, 3);

模块

使用模块来组织代码。这有助于将相关函数和变量放在一起,使它们可以在应用程序的不同部分重复使用。

// math.js
export function add(a, b) {
    return a + b;
}

// main.js
import { add } from './math.js';
const sum = add(5, 10);

类和对象

利用类或对象对相关数据和行为进行分组。这种封装有助于避免在使用类似数据结构时重复。

class Rectangle {
    constructor(width, height) {
        this.width = width;
        this.height = height;
    }

    area() {
        return this.width * this.height;
    }
}

const rect1 = new Rectangle(5, 10);
const rect2 = new Rectangle(7, 3);

注意:如果您在日常编码中采用“函数式编程”范式,请考虑使用除此“类和对象”之外的任何其他技巧。

模板和组件

在 Web 开发中,使用模板或组件(在 React、Vue 等框架中)来封装可重用的 UI 逻辑和样式。

// React component
function Button({ label, onClick }) {
    return ;
}

// Usage

  

数据结构

使用数组或对象来存储相关数据,而不是为每条数据创建单独的变量。

const members = [
    { name: 'Feitan Portor', nen: "Transmutation" },
    { name: 'Nobonaga Hazama', nen: "Enhancement"},
];

// Iterate over users without repeating code
members.forEach(member => console.log(member.name));
Configuration Objects
Use configuration objects to pass parameters to functions or classes instead of having multiple parameters.

function addMember({ name, nen }) {
    return { name, nen };
}

const member = addMember({ name: 'Illumi Zoldyck', nen: "Manipulation" });

遵循 DRY 原则可使代码更简洁、更易于维护。它有助于最大限度地降低错误风险,因为只需在一个地方进行更改,并且通过减少混乱来提高可读性。请记住,虽然避免重复很重要,但需要取得平衡;过度抽象会导致复杂性,因此在应用这些原则时要谨慎判断。

**11 — 使用有意义的变量和函数名称**

使用有意义的变量和函数名称对于编写清晰、可维护和可理解的代码至关重要。

描述性

选择能够清楚描述变量或函数的用途或值的名称。

// ❌ Bad
let x = 10; // What does x represent?

// ✅ Good

let itemCount = 10; // Clearly indicates it's a count of items.

使用动作词来表达功能

用描述正在执行的操作的动词启动函数。

// ❌ Bad
function process() {
    // Function details
}

// ✅ Good
function calculateTotalPrice() {
    // Function details
}

避免使用缩写

虽然简短的名称可能看起来很方便,但可能会导致混淆。除非缩写被广泛理解,否则请避免使用缩写。

// ❌ Bad
let amt = 50; // What does amt mean?

// ✅ Good
let amount = 50; // Clear and understandable.

使用一致的命名约定

在整个代码库中坚持一致的命名约定,例如变量和函数使用 camelCase,类使用 PascalCase。

function fetchData() {} // Function
const userList = []; // Variable
class UserProfile {} // Class

在名称中指明数据类型或用途

如果变量包含特定类型的数据或用于特定用途,请将其包含在名称中。

// ❌ Bad
let data = []; // What kind of data?

// ✅ Good
let userProfiles = []; // Indicates it's an array of user profiles.

使用上下文信息

考虑变量或函数的使用上下文,以使名称更有意义。

// ❌ Bad
let list = []; // Vague

// ✅ Good
let todoList = []; // Clearly indicates it's for to-do items.

保持简洁但清晰

虽然名称应该具有描述性,但不应过长。力求在清晰和简洁之间取得平衡。

// ❌ Bad
let numberOfUsersInTheSystem = 100; // Too long

// ✅ Good
let userCount = 100; // Clear and concise.

使用领域特定语言

如果您在特定领域工作(如金融、医疗保健等),请使用该领域熟悉的术语。

让 interestRate = 5.5; // 在财务背景下清晰。

必要时进行重构

如果您发现随着代码的发展某个名称不再合适,请毫不犹豫地重构它以获得更好的清晰度。

let temp = 30; // After some time, this name may become unclear.
let roomTemperature = 30; // After refactor... More descriptive.

有意义的变量和函数名称可显著提高代码的可读性和可维护性。它们可以帮助其他人(和您自己)一目了然地了解代码的用途和功能,从而使协作和调试变得更加容易。始终努力使命名约定清晰易懂。

**12 — 避免使用全局变量**

避免使用全局变量是 JavaScript(以及一般编程)中保持代码整洁、模块化和可维护的关键做法。全局变量可能会导致意外行为、命名冲突和调试困难。

使用函数作用域

在函数内声明变量以限制其范围并防止它们被全局访问。

function calculateTotal(items) {
    let total = 0; // Local scope
    items.forEach(item => total += item.price);
    return total;
}

通过 let 和 const 使用块级作用域

利用 let 和 const 在块(如循环或条件)内声明变量,确保它们在该块之外不可访问。

for (let i = 0; i < 10; i++) {
    let square = i * i; // `square` is block-scoped
    console.log(square);
}
// console.log(square); // ReferenceError: square is not defined

模块化你的代码

将代码组织成模块。使用 ES6 模块或 IIFE(立即调用函数表达式)来封装变量。

Using ES6 Modules:
// mathUtils.js
export function add(a, b) {
    return a + b;
}

// main.js
import { add } from './mathUtils.js';
const result = add(5, 10);
Using IIFE:
(function() {
    let privateVar = "I am private";
    function privateFunction() {
        console.log(privateVar);
    }
    privateFunction();
})();

封装在对象中:

将相关变量和函数分组到一个对象内,以避免污染全局范围。

const calculator = {
    total: 0,
    add(num) {
        this.total += num;
    },
    reset() {
        this.total = 0;
    }
};

明智地使用本地存储

如果需要保存数据,请考虑使用本地存储、会话存储或 indexedDB,而不是全局变量。

localStorage.setItem('userSettings', JSON.stringify(settings));
const settings = JSON.parse(localStorage.getItem('userSettings'));

限制全局变量的使用

如果必须使用全局变量,请将其使用范围限制为配置常量或应用程序范围的设置。明确命名以表明其全局性。

const APP_VERSION = "1.0.0"; // A constant that might be needed globally

避免副作用

设计函数时,避免修改全局变量。这样可以使函数可预测且更易于测试。

let counter = 0;

function incrementCounter() {
    return ++counter; // Avoid this! Instead, return a new value.
}

明智地使用“this”

在面向对象编程中,使用它来管理实例内的状态,而不是依赖全局变量。

class Counter {
    constructor() {
        this.count = 0;
    }

    increment() {
        this.count++;
    }
}

通过避免使用全局变量,您可以增强代码的模块化和可维护性。它有助于防止命名冲突和意外副作用,使您的代码更可预测且更易于使用。遵循这些最佳实践将使代码库更干净、更易于管理。

**13 - 使用 Promises 和 Async/Await 编写异步代码**

在 JavaScript 中使用 Promises 和 async/await 有助于更有效地管理异步操作,使您的代码更清晰、更易于阅读。

理解 Promise

Promise 是一个表示异步操作最终完成(或失败)及其结果值的对象。

你可以使用 Promise 构造函数创建一个 Promise:

const myPromise = new Promise((resolve, reject) => {
    // Simulate an asynchronous operation
    setTimeout(() => {
        const success = true; // Change to false to simulate failure
        if (success) {
            resolve('Operation successful!');
        } else {
            reject('Operation failed!');
        }
    }, 1000);
});

消费 Promise

您可以使用 .then() 来处理 Promise 的成功结果,使用 .catch() 来处理错误结果。

myPromise
  .then(result => {
      console.log(result); // 'Operation successful!'
  })
  .catch(error => {
      console.error(error); // Handle error
});

链接 Promises

您可以使用 Promises 链接多个异步操作。

fetch('https://jsonplaceholder.typicode.com/users')
  .then(response => response.json())
  .then(users => {
      console.log(users);
  })
  .catch(error => {
      console.error('Error:', error);
});

使用 Async/Await

async/await 提供了一种更同步的方式来编写异步代码,使其更易于阅读和维护。

声明异步函数:

在函数前使用 async 关键字将其定义为异步函数。

async function fetchUsers() {
    try {
        const response = await fetch('https://jsonplaceholder.typicode.com/users');
        const users = await response.json();
        console.log(users);
    } catch (error) {
        console.error('Error:', error);
    }
}

调用异步函数

你可以像调用常规函数一样调用异步函数。但请注意,它始终会返回 Promise。

fetchUsers().then(() => {
    console.log('Data fetched successfully!');
});

处理多个异步操作

您可以使用 Promise.all 并行运行多个承诺并等待它们全部解决。

async function fetchUsersPosts() {
    try {
        const [users, posts] = await Promise.all([
            fetch('https://jsonplaceholder.typicode.com/users').then(res => res.json()),
            fetch('https://jsonplaceholder.typicode.com/posts').then(res => res.json()),
        ]);
        console.log(users, posts);
    } catch (error) {
        console.error('Error:', error);
    }
}

错误处理

Promises 和 async/await 都提供了优雅处理错误的方法。

将 .catch() 与 Promises 结合使用:

myPromise
  .then(result => {
      console.log(result);
  })
  .catch(error => {
      console.error('Caught error:', error);
});

将 try/catch 与 Async/Await 结合使用:

async function example() {
    try {
        const result = await myPromise;
        console.log(result);
    } catch (error) {
        console.error('Caught error:', error);
    }
}

使用 Promises 和 async/await 使 JavaScript 中的异步操作处理变得更加易于管理。它们有助于避免回调地狱并提高代码的可读性。采用这些模式将使代码更简洁、更易于维护且不易出错。

**14 — 记录你的代码**

记录代码对于保持清晰度、帮助协作和确保长期可维护性至关重要。

使用清晰的评论

解释“为什么”,而不是“什么”:重点解释为什么做某事,而不是代码做了什么。代码本身应该足够易读,能够传达它做了什么。

// ❌ Bad: This doesn't provide much context
let x = 10; // Assigns 10 to x

// ✅ Good: Explains the purpose
let maxRetries = 10; // Maximum number of attempts for the API call

注释复杂逻辑:对于复杂或不明显的代码部分,提供详细的解释。

// Check if user has the necessary permissions to access the resource
if (user.role === 'admin' || user.permissions.includes('access_resource')) {
    // Proceed with the operation
}

使用文档字符串样式注释

在 JavaScript 中,尤其是使用 JSDoc 时,您可以使用结构化注释记录函数、类和方法。

/**
 * Calculates the total price including tax.
 * @param {number} price - The original price of the item.
 * @param {number} tax - The tax rate as a decimal.
 * @returns {number} The total price after tax.
 */
function calculateTotal(price, tax) {
    return price + (price * tax);
}
Document Public APIs
For libraries or modules, provide clear documentation for public APIs, including parameters, return values, and usage examples.

/**
 * Fetches user data from the server.
 * @async
 * @param {string} userId - The ID of the user to fetch.
 * @returns {Promise} A promise that resolves to the user data.
 * @throws {Error} Throws an error if the fetch fails.
 */
async function fetchUserData(userId) {
    const response = await fetch(`/api/users/${userId}`);
    if (!response.ok) {
        throw new Error('Failed to fetch user data');
    }
    return response.json();
}

维护 README 文件

对于项目,维护一个 README.md 文件,提供概述、安装说明、使用示例和贡献指南。

有效的文档可让您的代码更易于理解和维护,从而帮助当前和未来的开发人员(包括您自己)高效工作。通过将这些做法纳入您的开发工作流程,您将促进更好的协作并降低与您的代码交互的任何人学习的难度。