重学JS-技巧篇

20 阅读14分钟

重学JS-技巧篇

对象处理

创建一个绝对空的对象

// 我们可以通过 {} 来创建空对象。 然而,通过方法中创建的对象, proto 、 hasOwnProperty 等对象方法仍然是存在的,这是因为使用 {} 将创建一个继承自 Object 类的对象

let vehical = Object.create(null);
// vehicle.__proto__ === "undefined"

从对象中选择特定数据

var selectObj = (obj, items) => {
  return items.reduce((result, item) => {
    result[item] = obj[item];
    return result;
  }, {});
};
var vehicle = { brand: "BWM", year: 2022, type: "suv" };
var selected = selectObj(vehicle, ["brand", "type"]);
console.log(selected); // { brand: 'BWM', type: 'suv' }

从对象中删除键

var remove = (object, removeList = []) => {
  var result = { ...object };
  removeList.forEach((item) => {
    delete result[item];
  });
  return result;
};
var vehicle = { brand: "BWM", year: 2022, type: "suv" };
var itemRemoved = remove(vehicle, ["year"]);
console.log(itemRemoved); // Result { brand: 'BWM', type: 'suv' }

将对象数据拉入数组

var vehicle = { brand: "BWM", year: 2022, type: "suv" };
console.log(Object.entries(vehicle));
// [ [ 'brand', 'BWM' ], [ 'year', 2022 ], [ 'type', 'suv' ] ]

有条件地向对象添加属性

const type = { type: "suv" };
const vehicle = {
  brand: "BMW",
  year: 2022,
  ...(!type ? {} : type),
};
console.log(vehicle); //{ brand: 'BMW', year: 2022, type: 'suv' }

调试技巧

设置断点

在代码行设置断点

设置代码行断点的步骤:
● 单击切换到 Sources 选项卡;
● 从文件导航部分选中需要调试的源文件;
● 在右侧代码编辑器区域找到需要调试的代码行;
● 单击行号以在行上设置断点。

设置条件断点

设置条件断点的步骤:
● 单击切换到 Sources 选项卡;
● 从文件导航部分选中需要调试的源文件;
● 在右侧代码编辑器区域找到需要调试的代码行;
● 右键单击行号并选择"Add conditional breakpoint"来添加条件断点

在事件监听器上设置断点

在事件监听器上设置断点的步骤:
● 单击切换到 Sources 选项卡;
● 在debugger区域展开Event Listener Breakpoints选项;
● 从事件列表中选择事件监听器来设置断点。我们的程序中有一个按钮单击事件,这里就选择 Mouse事件选项中的click。

在 DOM 节点中设置断点

DevToolsDOM 检查和调试方面同样很强大。当在 DOM 中添加、删除或者修改某些内容时,可以设置断点来暂停代码的执行。
在 DOM 上设置断点的步骤:
● 单击切换到 Elements 选项卡;
● 找到要设置断点的元素;
● 右键单击元素以获得上下文菜单,选择Break on选项,然后选择Subtree modifications、Attribute modifications、Node removal中的一个即可:

这三个选项的含义如下:
● Subtree modifications:当节点内部子节点变化时断点;
● Attribute modifications:当节点属性发生变化时断点;
● Node removal:当节点被移除时断点。

逐步调试

下一步(快捷键:F9)
此选项使我们能够在JavaScript代码执行时逐行执行,如果中途有函数调用,单步执行也会进入函数内部,逐行执行,然后退出。

跳过(快捷键:F10)
此选项允许我们在执行代码时跳过一些代码。有时我们可能已经确定某些功能是正常的,不想花时间去检查它们,就可以使用跳过选项。

进入(快捷键:F11)
使用该选项可以更深入的了解函数。单步执行函数时,当感觉某个函数的行为异常并想检查它时,就可以使用这个选项来进入函数内部并进行调试。

跳出(快捷键:Shift+F11)
在单步执行一个函数时,我们可能不想再继续执行并退出它,就可以使用这些选项退出函数。

跳转(快捷键:F8)
有时,我们希望从一个断点跳转到另一个断点,而无需在它们之间进行任何调试,就可以使用这个选项来跳转到下一个断点:

命名约定

// 1 变量的命名约定
// 最推荐的声明 JavaScript 变量的方法是使用驼峰式变量名。我们可以对JavaScript 所有类型的变量使用驼峰式命名约定,这样就不会相同命名的变量。
let dogName = 'Droopy';

// 2 布尔值的命名约定
// 当定义布尔类型的变量时,应该使用is或者has作为变量的前缀。例如,如果需要一个变量来检查狗是否有主人,应该使用 hasOwner 作为变量名
let hasOwner = true;

// 3 函数的命名约定
// JavaScript 中函数的名称也是区分大小写的。因为在声明函数时,推荐使用驼峰式方法来命名函数。
function getName{ }

// 4 常量的命名约定
// JavaScript 中的常量和变量是一样的,都区分大小写,在定义常量时,推荐使用大写,因为它们是不变的变量。
const LEG = 4;

// 5 类的命名约定
// JavaScript 中类的命名约定规则与函数非常相似,推荐使用描述性的名称来描述类的功能。函数名和类名之间的主要区别在于类名要使用大写开头:
class DogName { }

// 6 组件的命名规则
// JavaScript 组件广泛应用于React、Vue等前端框架中。组件的命名建议与类保持一致,使用开头大写的驼峰式命名法:
// 由于组件的命名开头字母是大写,因此在使用时,就很容易和HTML、属性值等区分开来:
<div>
    <DogCartoon roles={{ dogName: 'Scooby-Doo', ownerName: 'Shaggy' }} />
</div>

// 7 方法的命名约定
// 这里说的方法指的是类中方法,在 JavaScript 中,类的方法和函数的结构是非常类似的,因此,命名约定规则也是一样的。
// 推荐需要使用驼峰式方法来声明 JavaScript 方法,并使用动词作为前缀,使方法名称更有意义
class DogCartoon {
    constructor(dogName, ownerName) {
        this.dogName = dogName;
        this.ownerName = ownerName;
    }
    getName() {
        return '${this.dogName} ${this.ownerName}';
    }
}
const cartoon = new DogCartoon('Scooby-Doo', 'Shaggy');
console.log(cartoon.getName()); // "Scooby-Doo Shaggy"

// 8 私有函数的命名约定
// 例如,有一个私有函数名 toonName,则可以通过添加下划线作为前缀(_toonName) 来将其表示为私有函数
class DogCartoon {
    constructor(dogName, ownerName) {
        this.dogName = dogName;
        this.ownerName = ownerName;
        this.name = _toonName(dogName, ownerName);
    }
    _toonName(dogName, ownerName) {
        return `${dogName} ${ownerName}`;
    }
}

// 9 全局变量的命名约定
// 对于 JavaScript 全局变量,没有特定的命名标准。建议对可变全局变量使用驼峰式大小写的方式,对不可变全局对象使用大写。

// 10 文件名的命名约定
// 大多数 Web 服务器(Apache、Unix)在处理文件时都区分大小写。例如,flower.jpg 和 Flower.jpg 是不一样的。
// 尽管它们是支持区分大小写的,建议在所有服务器中还是使用小写来命名文件。

开发技巧

计算代码性能

var startTime = performance.now();
// 某些程序
for (let i = 0; i < 1000; i++) {
  console.log(i);
}
var endTime = performance.now();
var totaltime = endTime - startTime;
console.log(totaltime); // 30.299999952316284

验证 undefined 和 null

if (a === null || a === undefined) {
  doSomething();
}
// 简化
a ?? doSomething();

对象动态声明属性

var dynamic = "color";
var item = {
  brand: "Ford",
  [dynamic]: "Blue",
};
console.log(item);
// { brand: "Ford", color: "Blue" }

缩短 console.log()

var c = console.log.bind(document);
c(996);
c("hello world");

数字取整

// 可以使用~~运算符来消除数字的小数部分,它相对于数字的那些方法会快很多。
~~3.1415926; // 3

// 这个运算符的作用有很多,通常是用来将变量转化为数字类型的,不同类型的转化结果不一样:
// 如果是数字类型的字符串,就会转化为纯数字;
// 如果字符串包含数字之外的值,就会转化为0;
// 如果是布尔类型,true会返回1,false会返回0;
23.9 | 0; // 23
-23.9 | 0; // -23

使用 switch case 替换 if/else

if (1 == month) {
  days = 31;
} else if (2 == month) {
  days = IsLeapYear(year) ? 29 : 28;
} else if (3 == month) {
  days = 31;
} else if (4 == month) {
  days = 30;
} else if (5 == month) {
  days = 31;
} else if (6 == month) {
  days = 30;
} else if (7 == month) {
  days = 31;
} else if (8 == month) {
  days = 31;
} else if (9 == month) {
  days = 30;
} else if (10 == month) {
  days = 31;
} else if (11 == month) {
  days = 30;
} else if (12 == month) {
  days = 31;
}
// 使用switch...case来改写:
switch (month) {
  case 1:
    days = 31;
    break;
  case 2:
    days = IsLeapYear(year) ? 29 : 28;
    break;
  case 3:
    days = 31;
    break;
  case 4:
    days = 30;
    break;
  case 5:
    days = 31;
    break;
  case 6:
    days = 30;
    break;
  case 7:
    days = 31;
    break;
  case 8:
    days = 31;
    break;
  case 9:
    days = 30;
    break;
  case 10:
    days = 31;
    break;
  case 11:
    days = 30;
    break;
  case 12:
    days = 31;
    break;
  default:
    break;
}

获取数组中的最后一项

var arr = [1, 2, 3, 4, 5];
arr[arr.length - 1]; // 5
arr.slice(-1)[0]; // 5
// 当我们将slice方法的参数设置为负值时,就会从数组后面开始截取数组值,如果我们想截取后两个值,参数传入-2即可。
arr.slice(-2)[(4, 5)];

空值合并运算符:??

// 比如 x ?? y 的结果是:
// 如果 x 不是 null 或 undefined ,则返回 x。
// 如果 x 是 null 或 undefined ,则返回 y。

let grade = 0;
console.log(grade || 100); // 输出结果: 100
console.log(grade ?? 100); // 输出结果: 0

内存使用

var data = [
  { name: "Frogi", type: Type.Frog },
  { name: "Mark", type: Type.Human },
  { name: "John", type: Type.Human },
  { name: "Rexi", type: Type.Dog },
];

// 我们想要为每个实体添加一些属性,具体取决于它的类型:
var mappedArr = data.map((entity) => {
  return {
    ...entity,
    walkingOnTwoLegs: entity.type === Type.Human,
  };
});
// ...
var tooManyTimesMappedArr = mappedArr.map((entity) => {
  return {
    ...entity,
    greeting: entity.type === Type.Human ? "hello" : "none",
  };
});
console.log(tooManyTimesMappedArr);
// 输出结果:
// [
// { name: 'Frogi', type: 'frog', walkingOnTwoLegs: false, greeting: 'none' },
// { name: 'Mark', type: 'human', walkingOnTwoLegs: true, greeting: 'hello' },
// { name: 'John', type: 'human', walkingOnTwoLegs: true, greeting: 'hello' },
// { name: 'Rexi', type: 'dog', walkingOnTwoLegs: false, greeting: 'none' }
// ]
// 可以看到,通过使用 map,可以进行简单的转换并多次使用它。对于一个小数组来说,内存消耗是微不足道的,但对于较大的数组来说,肯定会发现内存的显著影响。
// 1. 链式使用 map 来避免多次克隆:
var mappedArr = data
  .map((entity) => {
    return {
      ...entity,
      walkingOnTwoLegs: entity.type === Type.Human,
    };
  })
  .map((entity) => {
    return {
      ...entity,
      greeting: entity.type === Type.Human ? "hello" : "none",
    };
  });
// 2. 更好的方法是减少 map 和克隆操作的数量:
var mappedArr = data.map((entity) =>
  entity.type === Type.Human
    ? {
        ...entity,
        walkingOnTwoLegs: true,
        greeting: "hello",
      }
    : {
        ...entity,
        walkingOnTwoLegs: false,
        greeting: "none",
      }
);

// 使用 Map 或 Object 代替 switch-case
function findCities(country) {
  switch (country) {
    case "Russia":
      return ["Moscow", "Saint Petersburg"];
    case "Mexico":
      return ["Cancun", "Mexico City"];
    case "Germany":
      return ["Munich", "Berlin"];
    default:
      return [];
  }
}

// 可以使用对象字面量以更清晰的语法来实现相同的结果
var citiesCountry = {
  Russia: ["Moscow", "Saint Petersburg"],
  Mexico: ["Cancun", "Mexico City"],
  Germany: ["Munich", "Berlin"],
};
function findCities(country) {
  return citiesCountry[country] ?? [];
}
console.log(findCities(null)); // 输出结果: []
console.log(findCities("Germany")); // 输出结果: ['Munich', 'Berlin']

// Map 是 ES6 中引入的一种对象类型,它允许存储键值对,也可以使用 Map 来实现相同的结果:
var citiesCountry = new Map()
  .set("Russia", ["Moscow", "Saint Petersburg"])
  .set("Mexico", ["Cancun", "Mexico City"])
  .set("Germany", ["Munich", "Berlin"]);
function findCities(country) {
  return citiesCountry.get(country) ?? [];
}
console.log(findCities(null)); // 输出结果: []
console.log(findCities("Germany")); // 输出结果: ['Munich', 'Berlin']

// Map 和对象字面量之间的主要区别如下:
// 键: 在 Map 中,键可以是任何数据类型(包括对象和原始值)。而在对象字面量中,键必须是字符串或符号。
// 迭代: 在 Map 中,可以使用 for...of 循环或 forEach() 方法迭代。在对象字面量中,需要使用 Object.keys() 、 Object.values() 或 Object.entries() 来迭代。
// 性能: 一般来说,在处理大型数据集或频繁添加/删除时,Map 的性能优于对象字面量。对于小型数据集或不经常操作的情况下,性能差异可以忽略不计。 选择使用哪种数据结构取决于具体的用例。

单行代码

日期处理

// 1. 检察日期是否有效
var isDateValid = (...val) => !Number.isNaN(new Date(...val).valueOf());
isDateValid("December 17, 1995 03:24:00"); // true
isDateValid("December 77, 1995 03:24:00"); // false

// 2. 计算两个日期之间的间隔
var dayDif = (date1, date2) => Math.ceil(Math.abs(date1.getTime() - date2.getTime()) / (1000 * 60 * 60 * 24));
dayDif(new Date("2021-11-3"), new Date("2022-2-1")) // 90

// 3. 查找日期位于一年中的第几天
var dayOfYear = (date) => Math.floor((date - new Date(date.getFullYear(), 0, 0)) / 1000 / 60 / 60 / 24)
dayOfYear(new Date()); // 20231204 -> 338

// 4. 时间格式化
var timeFromDate = date => date.toTimeString().slice(0, 8);
timeFromDate(new Date(2021, 11, 2, 12, 30, 0)); // 12:30:00
timeFromDate(new Date()); // 返回当前时间 11:29:30

字符串处理

// 1. 字符串首字母大写
var capitalize = str => str.charAt(0).toUpperCase() + str.slice(1)
capitalize("hello world") // Hello world

// 2. 翻转字符串
var reverse = str => str.split('').reverse().join('');
reverse('hello world'); // 'dlrow olleh'

// 3. 随机字符串
var randomString = () => Math.random().toString(36).slice(2);
randomString();

// 4. 截断字符串
var truncateString = (string, length) => string.length < length ? string : `${string.slice(0, length - 3)}...`
truncateString('aafgadgsdgsdfghf', 10); // aafgadg...

// 5. 去除字符串中的HTML
var stripHtml = html => (new DOMParser().parseFromString(html, 'text/html')).body.textContent || ''
stripHtml('aaaa<div>dada</div>') // aaaadada

数组处理

// 1. 从数组中移除重复项
var removeDuplicates = (arr) => [...new Set(arr)];
console.log(removeDuplicates([1, 2, 2, 3, 3, 4, 4, 5, 5, 6]));

// 2. 判断数组是否为空
var isNotEmpty = arr => Array.isArray(arr) && arr.length > 0;
isNotEmpty([1, 2, 3]); // true

// 3. 合并两个数组
var merge = (a, b) => a.concat(b);
var merge = (a, b) => [...a, ...b];

数字操作


// 1. 判断一个数是奇数还是偶数
var isEven = num => num % 2 === 0;
isEven(996); // true

// 2. 获得一组数的平均值
var average = (...args) => args.reduce((a, b) => a + b) / args.length
average(1, 2, 3, 4, 5); // 3

// 3. 获取两个整数之间的随机整数
var random = (min, max) => Math.floor(Math.random() * (max - min + 1) + min)
random(1, 50); // 20

// 4. 指定位数四舍五入
var round = (n, d) => Number(Math.round(n + "e" + d) + "e-" + d)
round(1.005, 2) //1.01
round(1.555, 2) //1.56

颜色操作

// 1. 将RGB转化为十六机制
var rgbToHex = (r, g, b) => "#" + ((1 << 24) + (r << 16) + (g << 8) + b).toString(16).slice(1);
rgbToHex(255, 255, 255); // '#ffffff'

// 2. 获取随机十六进制颜色
var randomHex = () => `#${Math.floor(Math.random() * 0xffffff).toString(16).padEnd(6, '0')}`;
randomHex();

浏览器操作

// 1. 复制内容到剪切板
// 该方法使用 navigator.clipboard.writeText 来实现将文本复制到剪贴板
var copyToClipboard = (text) => navigator.clipboard.writeText(text);
copyToClipboard("Hello World");

// 2. 清除所有cookie
const clearCookies = document.cookie.split(';').forEach(cookie => { document.cookie = cookie.replace(/^ +/, '').replace(/=.*/, `=;expires=${new Date().toUTCString()};path=/`); });

// 3. 获取选中的文本
// 4. 检测是否是黑暗模式
// 5. 滚动到页面顶部
// 6. 判断当前标签页是否激活
// 7. 判断当前是否是苹果设备
// 8. 是否滚动到页面底部
// 9. 重定向到一个URL
// 10. 打开浏览器打印框

其他操作

// 1. 随机布尔值
// 2. 变量交换
// 3. 获取变量的类型
// 4. 华氏度和摄氏度之间的转化
// 5. 检测对象是否为空

ESLint 异步

异步代码的ESLint规则

// no-async-promise-executor
// 此规则不允许将async函数传递给new Promise构造函数。
new Promise(async (resolve, reject) => { });// ❌
new Promise((resolve, reject) => { });// ✅


// no-await-in-loop
// 此规则不允许在循环内使用 await
// 当对可迭代的每个元素执行操作并等待异步任务时,通常表明程序没有充分利用JavaScript的事件驱动架构。通过并行执行这些任务,可以大大提高代码的效率。
// ❌
for (var url of urls) {
  var response = await fetch(url);
}
// ✅
var responses = [];
for (var url of urls) {
  var response = fetch(url);
  responses.push(response);
}
await Promise.all(responses);



// no-promise-executor-return
// 此规则不允许在 Promise 构造函数中返回值
// ❌
new Promise((resolve, reject) => {
  return result;
});
// ✅
new Promise((resolve, reject) => {
  resolve(result);
});



// require-atomic-updates
// 此规则不允许由于 await 或 yield 的使用而可能导致出现竞态条件的赋值。
// ❌
let totalPosts = 0;
async function getPosts(userId) {
  var users = [{ id: 1, posts: 5 }, { id: 2, posts: 3 }];
  // await sleep(Math.random() * 1000);
  return users.find((user) => user.id === userId).posts;
}
async function addPosts(userId) {
  totalPosts += await getPosts(userId);
}
await Promise.all([addPosts(1), addPosts(2)]);
console.log('Post count:', totalPosts);



// max-nested-callbacks
// 此规则会强制回调的最大嵌套深度。 换句话说,这条规则可以防止回调地狱。



// no-return-await
// 此规则不允许返回不必要的 await。
// ❌
async () => {
  return await getUser(userId);
}
// ✅
async () => {
  return getUser(userId);
}
// 等待一个 Promise 并立即返回它是没有必要的,因为从异步函数返回的所有值都包含在一个 Promise中。 因此,可以直接返回 Promise。

// 此规则的一个例外情况就是当有 try...catch 语句时, 删除 await 关键字将导致不会捕获 Promise 拒绝的原因。 在这种情况下,建议将结果分配给变量以明确意图。
// 👎
async () => {
  try {
    return await getUser(userId);
  } catch (error) {
    // 处理错误
  }
}
// 👍
async () => {
  try {
    var user = await getUser(userId);
    return user;
  } catch (error) {
    // 处理错误
  }
}



// prefer-promise-reject-errors
// 此规则在拒绝 Promise 时强制使用 Error 对象。
// ❌
Promise.reject('An error occurred');
// ✅
Promise.reject(new Error('An error occurred'));

Node.js 特定规则

// 1. node/handle-callback-err
// ❌
function callback(err, data) {
  console.log(data);
}
// ✅
function callback(err, data) {
  if (err) {
    console.log(err);
    return;
  }
  console.log(data);
}



// 2. node/no-callback-literal
// 此规则强制使用 Error 对象作为第一个参数调用回调函数。 如果没有 Error,会接受 null 或 undefined 。
// ❌
cb('An error!');
callback(result);
// ✅
cb(new Error('An error!'));
callback(null, result);


// 3. node/no-sync
// 此规则不允许在存在异步替代方案的 Node.js 核心 API 中使用同步方法。
// ❌
var file = fs.readFileSync(path);
// ✅
var file = await fs.readFile(path);

TypeScript 特定规则

// 1. typescript-eslint/await-thenable
// 此规则不允许 await 不是 Promise 的函数或值。
// ❌
function getValue() {
  return someValue;
}
await getValue();
// ✅
async function getValue() {
  return someValue;
}
await getValue();



// 2. typescript-eslint/no-floating-promises
// 此规则强制 Promises 添加错误处理程序。
// ❌
myPromise()
  .then(() => { });
// ✅
myPromise()
  .then(() => { })
  .catch(() => { });



// 3. typescript-eslint/no-misused-promises
// 此规则不允许将 Promise 传递到并非旨在处理它们的地方,例如 if 条件:
// ❌
if (getUserFromDB()) { }
// ✅ 👎
if (await getUserFromDB()) { }
// ✅ 👍
const user = await getUserFromDB();
if (user) { }
// 此规则可防止忘记添加 await 异步函数。虽然该规则确实允许在 if 条件中 await,但建议将结果分配给一个变量并在条件中使用该变量以提高可读性。


// 4. typescript-eslint/promise-function-async
// 此规则强制返回 Promise 的是 async 函数。
// ❌
function doSomething() {
  return somePromise;
}
// ✅
async function doSomething() {
  return somePromise;
}
// 返回 Promise 的非异步函数可能会抛出 Error 对象并返回被拒绝的 Promise。 通常不会编写代码来处理这两种情况。 此规则确保函数返回被拒绝的 Promise 或抛出 Error,但绝不会两者都有。