javascript优化

代码是写给人看的
精简的代码让人心情愉悦
优秀的产品离不开程序猿精心雕琢

将查询函数和修改函数分离

将查询动作从修改动作中分离出来的方式

如果遇到一个“既有返回值又有副作用”的函数,此时可以将查询动作从修改动作中分离出来。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
// ==================重构前==================
function alertForMiscreant (people) {
for (const p of people) {
if (p === "Don") {
setOffAlarms();
return "Don";
}
if (p === "John") {
setOffAlarms();
return "John";}
}
return "";
}
// 调用方
const found = alertForMiscreant(people);

// ==================重构后==================
function findMiscreant (people) {
for (const p of people) {
if (p === "Don") {
return "Don";
}
if (p === "John") {
return "John";
}
}
return "";
}
function alertForMiscreant (people) {
if (findMiscreant(people) !== "") setOffAlarms();
}
// 调用方
const found = findMiscreant(people);
alertForMiscreant(people);

以卫语句取代嵌套条件表达式

如果某个条件极其罕见,就应该单独检查该条件,并在该条件为真时立刻从函数中返回。 这样的单独检查常常被称为“卫语句”(guard clauses)。

如果使用if-else结构,你对if分支和else分支的重视是同等的。这样的代码结构传递给阅读者的消息就是:各个分支有同样的重要性。卫语句就不同了,它告诉阅读者: “这种情况不是本函数的核心逻辑所关心的, 如果它真发生了,请做一些必要的整理工作,然后退出。” 为了传递这种信息可以使用卫语句替换嵌套结构。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// ==================重构前==================
function payAmount(employee) {
let result;
if(employee.isSeparated) {
result = {amount: 0, reasonCode:"SEP"};
}
else {
if (employee.isRetired) {
result = {amount: 0, reasonCode: "RET"};
}
else {
result = someFinalComputation();
}
}
return result;
}

// ==================重构后==================
function payAmount(employee) {
if (employee.isSeparated) return {amount: 0, reasonCode: "SEP"};
if (employee.isRetired) return {amount: 0, reasonCode: "RET"};
return someFinalComputation();
}

分解条件表达式

将条件表达式提炼成函数

在带有复杂条件逻辑的函数中,往往可以将原函数中对应的代码改为调用新函数。
对于条件逻辑, 将每个分支条件分解成新函数可以带来的好处:

  • 提高可读性
  • 可以突出条件逻辑, 更清楚地表明每个分支的作用
  • 突出每个分支的原因
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
// ==================重构前==================
// 计算一件商品的总价,该商品在冬季和夏季的单价是不同的
if (!aDate.isBefore(plan.summerStart) && !aDate.isAfter(plan.summerEnd))
charge = quantity * plan.summerRate;
else
charge = quantity * plan.regularRate + plan.regularServiceCharge;

// ==================重构后==================
if (summer())
charge = summerCharge();
else
charge = regularCharge();

function summer() {
return !aDate.isBefore(plan.summerStart) && !aDate.isAfter(plan.summerEnd);
}
function summerCharge() {
return quantity * plan.summerRate;
}
function regularCharge() {
return quantity * plan.regularRate + plan.regularServiceCharge;
}

// 进一步优化(使用三元运算符)
charge = summer() ? summerCharge() : regularCharge();

拆分循环

将一个循环拆分成多个循环

当遇到一个身兼数职的循环时可以将循环拆解,让一个循环只做一件事情, 那就能确保每次修改时你只需要理解要修改的那块代码的行为就可以了。该行为可能会被质疑,因为它会迫使你执行两次甚至多次循环,实际情况是,即使处理的列表数据更多一些,循环本身也很少成为性能瓶颈,更何况拆分出循环来通常还使一些更强大的优化手段变得可能。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
// ==================重构前==================
const people = [
{ age: 20, salary: 10000 },
{ age: 21, salary: 15000 },
{ age: 22, salary: 18000 }
]

let youngest = people[0] ? people[0].age : Infinity;
let totalSalary = 0;
for (const p of people) {
// 查找最年轻的人员
if (p.age < youngest) youngest = p.age;
// 计算总薪水
totalSalary += p.salary;
}
console.log(`youngestAge: ${youngest}, totalSalary: ${totalSalary}`);

// ==================重构后==================
const people = [
{ age: 20, salary: 10000 },
{ age: 21, salary: 15000 },
{ age: 22, salary: 18000 }
]

let totalSalary = 0;
for (const p of people) {
// 只计算总薪资
totalSalary += p.salary;
}
let youngest = people[0] ? people[0].age : Infinity;
for (const p of people) {
// 只查找最年轻的人员
if (p.age < youngest) youngest = p.age;
}
console.log(`youngestAge: ${youngest}, totalSalary: ${totalSalary}`);

// ==================提炼函数==================
const people = [
{ age: 20, salary: 10000 },
{ age: 21, salary: 15000 },
{ age: 22, salary: 18000 }
]

console.log(`youngestAge: ${youngestAge()}, totalSalary: ${totalSalary()}`);

function totalSalary() {
let totalSalary = 0;
for (const p of people) {
totalSalary += p.salary;
}
return totalSalary;
}
function youngestAge() {
let youngest = people[0] ? people[0].age : Infinity;
for (const p of people) {
if (p.age < youngest) youngest = p.age;
}
return youngest;
}

// ==================使用工具类进一步优化==================
const people = [
{ age: 20, salary: 10000 },
{ age: 21, salary: 15000 },
{ age: 22, salary: 18000 }
]

console.log(`youngestAge: ${youngestAge()}, totalSalary: ${totalSalary()}`);

function totalSalary() {
return people.reduce((total,p) => total + p.salary, 0);
}
function youngestAge() {
return Math.min(...people.map(p => p.age));
}

提炼函数、函数参数化、使用策略模式替换 if-else、switch-case

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
function getPrice(tag, originPrice) {
if(tag === 'newUser') {
return originPrice > 50.1 ? originPrice - 50 : originPrice
}
if(tag === 'back') {
return originPrice > 200 ? originPrice - 50 : originPrice
}
if(tag === 'activity') {
return originPrice > 300 ? originPrice - 100 : originPrice
}
}
// ->
const priceHandler = {
newUser(originPrice){
return originPrice > 50.1 ? originPrice - 50 : originPrice
},
back(originPrice){
return originPrice > 200 ? originPrice - 50 : originPrice
},
activity(originPrice){
return originPrice > 300 ? originPrice - 100 : originPrice
}
}

function getPrice(tag, originPrice){
return priceHandler[tag](originPrice)
}

封装变量

将变量封装起来,只允许通过函数访问

对于所有可变的数据, 只要它的作用域超出单个函数,就可以采用封装变量的方法。数据被使用得越广, 就越是值得花精力给它一个体面的封装。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// ==================重构前==================
let defaultOwner = {firstName: "Martin", lastName: "Fowler"};
// 访问
spaceship.owner = defaultOwner;
// 赋值
defaultOwner = {firstName: "Rebecca", lastName: "Parsons"};

// ==================重构后==================
function getDefaultOwner() {return defaultOwner;}
function setDefaultOwner(arg) {defaultOwner = arg;}
// 访问
spaceship.owner = getDefaultOwner();
// 赋值
setDefaultOwner({firstName: "Rebecca", lastName: "Parsons"});

多个条件判断

数组includes方法用来判断是否包含某个元素,如果是返回 true,否则false。

1
2
3
4
5
6
7
8
9
// longhand
if (student === "Tom" || student === "Jack" || student === "Shanguagua") {
  // business
}

// shorthand
if (['Tom''Jack''Shanguagua'].includes(student)) {
  // business
}

if … else 缩写

使用三元表达式替代

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// longhand
let win;
let score = 70;

if (score > 100) {
  win = true;
else {
  win = false;
}

// shorthand
let win = (score > 100) ? true : false;
// 数值的比较会返回一个Boolean值
let win = score > 100;

多个三元表达式建议拆开写

声明变量

如果需要声明两个具有共同值或共同类型的变量时,试一下这种写法。

1
2
3
4
5
6
//Longhand 
let Tom = 1;
let Jack = 1;

//Shorthand
let TomJack = 1;

多个变量赋值

当我们需要给不同的变量赋不同值的时候,这个方法就有点给力了,代码之美体现的淋漓尽致。

1
2
3
4
5
6
7
8
// Longhand 
var TomJackShanguagaua;
Tom = 'A';
Jack = 'B';
Shanguagaua = 'C';

// Shorthand 
let [TomJackShanguagau] = ['A''B''C'];

多个条件的AND(&&)运算符

如果要在true的情况下再执行其它操作,if是一种方法,但是AND运算符逼格稍高。

1
2
3
4
5
6
7
// Longhand 
if (ready) {
    goToEat(); 


// Shorthand 
ready && goToEat();

箭头函数

箭头函数是ES6的特性,简化了函数的写法,还有其他特性。

1
2
3
4
5
6
7
//Longhand 
function GoToEat(a, b) { 
return a + b; 


//Shorthand 
const GoToEat = (a, b) => a + b;

短函数调用

根据条件判断两个函数的调用。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// Longhand
function A() {
  console.log("A");
}
function B() {
  console.log("B");
}

var c = 5;
if (c == 10) {
  A();
else {
  B();
}

// Shorthand
(c === 1 ? A : B)();

Switch速记

根据条件判断多个函数的调用。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// Longhand
switch (key) {
    case 1:
      A();
    break;
    case 2:
      B();
    break;
    case 3:
      C();
    break;
    // And so on...
  }
  
  // Shorthand
  var data = {
    1: A,
    2: B,
    3: C
  };
  
  data[something] && data[something]();

因为某个值如果不存在对象中会得到假值,反之真值,再结合AND运算符就可以了。

默认参数值

利用函数的默认值特性,可以避免校验假值操作。

1
2
3
4
5
6
7
8
9
10
//Longhand
function GoToEat(A, B) {
  if (A === undefined) A = 1;
  if (B === undefined) B = 2;
  return A + B;
}

//shorthand
GoToEat = (A = 1, B = 2) => A + B;
GoToEat(); // output: 3

点运算符

数组的拼接可以使用点运算符来完成。

1
2
3
4
5
6
7
//Longhand
const A = [123];
const B = [456].concat(A);

// Shorthand
const A = [123];
const B = [456, ...A]; // [ 4, 5, 6, 1, 2, 3]

当然拷贝一个数组也可以用点运算符,注意是深拷贝。

1
2
3
4
5
6
7
// Longhand
var A = [123];
var B = A.slice();

//shorthand
var A = [123];
var B = [...A];

Array.find的简写

当我们有一个对象数组并且我们想要根据对象属性查找特定对象时,find方法确实很有用,这是数据过滤时很常用的操作。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
// Longhand
const data = [
  {
    type"student",
    name"Tom",
  },
  {
    type"teacher",
    name"Mick",
  },
  {
    type"student",
    name"Shanguagua",
  },
];
function findStudent(name) {
  for (let i = 0; i < data.length; ++i) {
    if (data[i].type === "student" && data[i].name === name) {
      return data[i];
    }
  }
}

//Shorthand
filterdData = data.find(
  (data) => data.type === "student" && data.name === "Shanguagua"
); // { type: 'student', name: 'Shanguagua' }

重复一个字符串多次

要一次又一次地重复相同的字符,我们可以使用for循环并将它们添加到同一循环中。重复的操作总会有简洁的写法。

1
2
3
4
5
6
7
8
9
// Longhand 
let Tom = ''
for(let i = 0; i < 5; i ++) { 
  Tom += 'Tom '

console.log(str); // Tom Tom Tom Tom Tom 

// Shorthand 
'Tom '.repeat(5); // Tom Tom Tom Tom Tom 

在数组中查找最大值和最小值

咋一看,额,用for循环吧,哦,不是的。

1
2
3
const arr = [123]; 
Math.max(…arr); // 3
Math.min(…arr); // 1

指数幂简化

1
2
3
4
//longhand
Math.pow(2, 3); // 8
//shorthand
2**3 // 8

双重按位操作

1
2
3
4
5
// Longhand
Math.floor(1.9) === 1 // true
// Shorthand
// ~1.9 === -2 // true
~~1.9 === 1 // true

将字符串转成数字

1
2
3
4
5
6
7
8
9
10
11
//Longhand 
let test1 = parseInt('123');
let test2 = parseFloat('12.3');
//Shorthand
let test1 = +'123';
let test2 = +'12.3';

// 其他用法
new Date().getTime()
Date.now()
+new Date() // 1666079777767

参考资料

https://juejin.cn/post/7131211363493347335
https://juejin.cn/post/6956844713479520286
https://juejin.cn/post/7078575191244144670


javascript优化
http://example.com/20221018-javascript优化/
作者
csorz
发布于
2022年10月18日
许可协议