在本文中,我们将研究几种快速简洁的单行解决方案,以解决 JavaScript 中经常出现的各种问题。
什么是单行代码?
在我们开始之前,让我们确保我们了解是什么单行代码。
单行代码是问题的代码解决方案,使用特定编程语言中的单个语句实现,无需任何第三方实用程序。
该定义包含许多其他定义中没有的重要区别特征:
1). “……单句……”
并非每一段只占用一行的代码都是单行代码。例如,看看这个将两个平方和相加并返回结果的方法。
const sum = (a, b) => { const s1 = a * a; const s2 = b * b; return s1 + s2; }
你会称之为单行代码吗?在大多数情况下,这只会作为格式错误的代码通过。Prettier 之类的工具可以轻松地将这三个语句自动拆分为多行。
获得两个平方和的真正单行方法是这样的:
const sum = (a, b) => a * a + b * b;
一个简短、简洁的陈述可以同样清晰地完成同样的工作。
另一方面,此方法跨越多行代码以提高可读性,但它仍然可以作为一行代码通过:
const capitalizeWithoutSpaces = (str) =>
str
.split('')
.filter((char) => char.trim())
.map((char) => char.toUpperCase())
.join('');
因此,尽管名称如此,“单行”并不一定意味着是一行代码。
2). “……特定的编程语言……”
这与每个高级语言程序在执行前都必须翻译成低级语言这一事实有关。一个非常简单的程序最终可能会占用数十或数百行汇编代码和机器代码。
例如,这里是另一个也添加两个平方和的单行代码,这次是在 C++ 中:
int sum(int a, int b) {
return a * a + b * b;
}
让我们看看编译成汇编语言后的样子:
sum(int, int):
push rbp
mov rbp, rsp
mov DWORD PTR [rbp-4], edi
mov DWORD PTR [rbp-8], esi
mov eax, DWORD PTR [rbp-4]
imul eax, eax
mov edx, eax
mov eax, DWORD PTR [rbp-8]
imul eax, eax
add eax, edx
pop rbp
ret
这个汇编程序显然不止一行或一行代码。 想象一下等效的机器语言程序会有多少。 所以这个函数可以说是仅在 C++ 上下文中的单行函数。
3). “……没有任何第三方实用程序”
对于单行代码,它不应该引用编程语言本身不可用的任何方法或函数,记住我们之前看过的单行代码:
const capitalizeWithoutSpaces = (str) =>
str
.split('')
.filter((char) => char.trim())
.map((char) => char.toUpperCase())
.join('');
这里使用的所有方法都是内置的 JavaScript 方法。 它不包括来自 NPM 或其他地方的第三方代码。
但是,如果我们决定实现自己的 filter() 方法来替换 Array filter(),则该方法将不再符合单行方法的条件。
// Not a one-liner
const capitalizeWithoutSpaces = (str) =>
filter(str.split(''), (char) => char.trim())
.map((char) => char.toUpperCase())
.join('');
function filter(arr, callback) {
// Look at all these lines
const result = [];
for (const item of arr) {
if (callback(item)) {
result.push(item);
}
}
return result;
}
抛开定义,现在让我们看一些聪明的 JavaScript 单行代码以及它们解决方案。
1. 获取数组的最小元素
要获得数组中的最小项,我们可以采用这种使用 for 循环和 if 语句的命令式方法。
const getSmallest = (arr) => {
let smallest = Number.POSITIVE_INFINITY;
for (const num of arr) {
if (num < smallest) {
smallest = num;
}
}
return smallest;
};
const arr = [13, 7, 11, 3, 9, 15, 17];
console.log(getSmallest(arr)); // 3
这没关系,但有一个简洁且声明性的单行替代方案同样有效:
const getSmallest = (arr) =>
arr.reduce((smallest, num) => Math.min(smallest, num));
const arr = [13, 7, 11, 3, 9, 15, 17];
console.log(getSmallest(arr)); // 3
2. 获取数组的最大元素
这是获取数组中最大元素的可接受方法。
const getLargest = (arr) => {
let largest = Number.NEGATIVE_INFINITY;
for (const num of arr) {
if (num > largest) {
largest = num;
}
}
return largest;
};
const arr = [13, 7, 11, 3, 9, 15, 17];
console.log(getLargest(arr)); // 17
但就像我们看到的获取最小数组元素一样,有一种更短、更简洁的方法。
const getLargest = (arr) =>
arr.reduce((largest, num) => Math.max(largest, num));
const arr = [13, 7, 11, 3, 9, 15, 17];
console.log(getLargest(arr)); // 17
您可以看到,此函数与单行 getSmallest() 函数之间的唯一区别是 Math.min() 已替换为 Math.max()。
3. 打乱数组
数组/列表洗牌的一个常见用途是在纸牌游戏中,其中牌组中的牌必须随机排序。
Fisher-Yates 洗牌是一种著名的洗牌算法。 查看它在 JavaScript 中的可能实现:
const shuffleArray = (arr) => {
for (let i = arr.length - 1; i > 0; i--) {
const j = Math.floor(Math.random() * (i + 1));
let temp = arr[i];
arr[i] = arr[j];
arr[j] = temp;
}
return arr;
};
const arr = [1, 2, 3, 4, 5];
shuffleArray(arr);
// [ 2, 3, 5, 1, 4 ] (varies)
console.log(arr);
用一些函数式编程魔法重构它,我们有:
const shuffleArray = (arr) =>
[...Array(arr.length)]
.map((_, i) => Math.floor(Math.random() * (i + 1)))
.reduce(
(shuffled, r, i) =>
shuffled.map((num, j) =>
j === i ? shuffled[r] : j === r ? shuffled[i] : num
),
arr
);
// [ 2, 4, 1, 3, 5 ] (varies)
console.log(shuffleArray([1, 2, 3, 4, 5]));
这以 O(n2) 时间复杂度(二次)运行,并且可能会导致大型数组出现性能问题,但它是一种优雅的解决方案。 此外,与第一种方法不同,它不会改变原始数组。
另一种函数式方法利用 Array sort() 方法的实现方式来随机排列数组。
const shuffleArray = (arr) => arr.sort(() => Math.random() - 0.5);
const arr = [1, 2, 3, 4, 5];
// [ 5, 2, 4, 1, 3 ] (varies)
console.log(shuffleArray(arr));
由于它使用了 sort(),因此,它的运行时间复杂度为 O(n log n),并且比前面的方法具有更好的性能。
4. 按对象属性对数组进行分组
有时我们需要使用它们都具有的特定属性对一组对象进行分组,例如,按国家/地区对用户进行分组,按出版年份对书籍进行分组,按颜色对汽车进行分组等。
在下面的示例中,我们根据姓名的长度将人物对象分组到一个数组中。
const groupBy = (arr, groupFn) => {
const grouped = {};
for (const obj of arr) {
const groupName = groupFn(obj);
if (!grouped[groupName]) {
grouped[groupName] = [];
}
grouped[groupName].push(obj);
}
return grouped;
};
const people = [
{ name: 'Matt' },
{ name: 'Sam' },
{ name: 'John' },
{ name: 'Mac' },
];
const groupedByNameLength = groupBy(people, (person) => person.name.length);
/**
{
'3': [ { name: 'Sam' }, { name: 'Mac' } ],
'4': [ { name: 'Matt' }, { name: 'John' } ]
}
*/
console.log(groupedByNameLength);
这是单行代码的解决方案:
const groupBy = (arr, groupFn) =>
arr.reduce(
(grouped, obj) => ({
...grouped,
[groupFn(obj)]: [...(grouped[groupFn(obj)] || []), obj],
}),
{}
);
const people = [
{ name: 'Matt' },
{ name: 'Sam' },
{ name: 'John' },
{ name: 'Mac' },
];
const groupedByNameLength = groupBy(people, (person) => person.name.length);
/**
{
'3': [ { name: 'Sam' }, { name: 'Mac' } ],
'4': [ { name: 'Matt' }, { name: 'John' } ]
}
*/
console.log(groupedByNameLength);
5.反转字符串
我们可以在 JavaScript 中使用反向 for 循环来反转字符串,如下所示:
const reverseString = (str) => {
let reversed = '';
for (let i = str.length - 1; i >= 0; i--) {
const ch = str[i];
reversed += ch;
}
return reversed;
};
const reverse = reverseString('javascript');
console.log(reverse); // tpircsavaj
但是再一次,我们可以利用强大的内置数组方法,如 reverse() 和 join() 来创建一个做同样事情的单行代码。
const reverseString = (str) => str.split('').reverse().join('');
const reverse = reverseString('javascript');
console.log(reverse); // tpircsavaj
6. 生成随机的十六进制颜色
十六进制颜色代码是指定 RGB 颜色的一种方式。 它们具有#RRGGBB 格式,其中 RR 代表红色,GG 代表绿色,BB 代表蓝色。 每种颜色的值范围从 0 到 255,并以十六进制格式表示 - 0 到 FF。
这个单行生成一个随机的十六进制颜色并返回结果。
const randomHexColor = () =>
`#${Math.random().toString(16).slice(2, 8).padEnd(6, '0')}`;
console.log(randomHexColor()); // #7a10ba (varies)
console.log(randomHexColor()); // #65abdc (varies)
7. 获取数组的平均值
这是众多问题中的另一个问题,其中涉及循环的解决方案可以使用一种或多种 Array 方法来缩短。
因此,虽然我们可以像这样获得数组中数字的平均值:
const getAverage = (arr) => {
let sum = 0;
for (const num of arr) {
sum += num;
}
return sum / arr.length;
};
const arr = [5, 13, 9, 11, 10, 15, 7];
const average = getAverage(arr);
console.log(average); // 10
Array reduce() 方法让我们创建了这个紧凑的单行替代方案:
const getAverage = (arr) => arr.reduce((sum, num) => sum + num, 0) / arr.length;
const arr = [5, 13, 9, 11, 10, 15, 7];
const average = getAverage(arr);
console.log(average); // 10
8. 检查两个数组是否包含相同的值
这是一个确保两个数组包含相同元素(以任何顺序)并且这些元素在两个数组中出现相同次数的问题。
使用 for 循环,我们可以实现以下解决方案:
const areEqual = (arr1, arr2) => {
if (arr1.length === arr2.length) {
for (const num of arr1) {
if (!arr2.includes(num)) {
return false;
}
}
return true;
}
return false;
};
const arr1 = [1, 2, 3, 4];
const arr2 = [3, 4, 1, 2];
const arr3 = [1, 2, 3];
console.log(areEqual(arr1, arr2)); // true
console.log(areEqual(arr1, arr3)); // false
使用 Array sort() 和 join() 方法,我们可以创建这个单行替代方案:
const areEqual = (arr1, arr2) =>
arr1.sort().join(',') === arr2.sort().join(',');
const arr1 = [1, 2, 3, 4];
const arr2 = [3, 4, 1, 2];
const arr3 = [1, 2, 3];
console.log(areEqual(arr1, arr2)); // true
console.log(areEqual(arr1, arr3)); // false
9. 从数组中删除重复项
我们可以像这样从数组中删除重复项:
const removeDuplicates = (arr) => {
const result = [];
for (const num of arr) {
if (!result.includes(num)) {
result.push(num);
}
}
return result;
};
const arr = [1, 2, 3, 4, 5, 3, 1, 2, 5];
const distinct = removeDuplicates(arr);
console.log(distinct); // [1, 2, 3, 4, 5]
但是我们可以利用 Set() 构造函数在短短一行中删除重复项:
const removeDuplicates = (arr) => [...new Set(arr)];
const arr = [1, 2, 3, 4, 5, 3, 1, 2, 5];
const distinct = removeDuplicates(arr);
console.log(distinct); // [1, 2, 3, 4, 5]
10. 将Map转换为 JSON
这个简短的函数让我们可以快速将 Map 对象转换为 JSON 字符串而不会丢失任何信息:
const mapToJson = (map) => JSON.stringify(Object.fromEntries(map));
const map = new Map([
['user1', 'John'],
['user2', 'Kate'],
['user3', 'Peter'],
]);
const json = mapToJson(map);
// {"user1":"John","user2":"Kate","user3":"Peter"}
console.log(json);
11. 将 JSON 转换为Map
另一个一行可以反转上面的转换。 以下函数会将 JSON 字符串转换为 Map 对象。
const jsonToMap = (json) => new Map(Object.entries(JSON.parse(json)));
const json = '{"user1":"John","user2":"Kate","user3":"Peter"}';
const map = jsonToMap(json);
// Kate
console.log(map.get('user2'));
// Map(3) { 'user1' => 'John', 'user2' => 'Kate', 'user3' => 'Peter' }
console.log(map);
12. 将蛇形字符串转换为驼峰大小写
在蛇形字符串中,每个单词由下划线 (_) 分隔并以小写字母开头,例如:variable_name、bread_and_eggs 等。
但是,对于驼峰式字符串,第一个单词以小写字母开头,后面的单词均以大写字母开头。 单词之间没有空格或标点符号。 驼峰式字符串的示例有:variableName、breadAndEggs 等。
使用这个简洁的函数,我们可以将任何蛇形大小写的字符串转换为驼峰大小写。
const snakeToCamelCase = (s) =>
s.toLowerCase().replace(/(_\w)/g, (w) => w.toUpperCase().substr(1));
const str1 = 'learn_javascript';
const str2 = 'coding_beauty';
console.log(snakeToCamelCase(str1)); // learnJavaScript
console.log(snakeToCamelCase(str2)); // codingBeauty
13.生成随机UUID
“UUID”是大学唯一标识符的首字母缩写词。 UUID 是一个 128 位的值,可唯一标识 Internet 上的对象或实体。
这个单行生成一个随机 UUID:
const generateRandomUUID = (a) =>
a
? (a ^ ((Math.random() * 16) >> (a / 4))).toString(16)
: ([1e7] + -1e3 + -4e3 + -8e3 + -1e11).replace(
/[018]/g,
generateRandomUUID
);
console.log(generateRandomUUID()); // f138f635-acbd-4f78-9be5-ca3198c4cf34
console.log(generateRandomUUID()); // 8935bb0d-6503-441f-bb25-7bc685b5b5bc
14.条件流控制
我们可以使用嵌套的三元运算符将 if...else 或 switch 语句转换为单行语句。 考虑一个返回特定范围内数字的英文单词形式的函数。
使用 if...else 语句,这样的函数可以这样实现:
const getNumWord = (num) => {
if (num === 1) {
return 'one';
} else if (num === 2) {
return 'two';
} else if (num === 3) {
return 'three';
} else if (num === 4) {
return 'four';
} else return 'unknown';
};
console.log(getNumWord(1)); // one
console.log(getNumWord(3)); // three
console.log(getNumWord(7)); // unknown
使用 switch...case 语句:
const getNumWord = (num) => {
switch (num) {
case 1:
return 'one';
case 2:
return 'two';
case 3:
return 'three';
case 4:
return 'four';
default:
return 'unknown';
}
};
console.log(getNumWord(1)); // one
console.log(getNumWord(3)); // three
console.log(getNumWord(7)); // unknown
现在使用嵌套的三元组来创建单行代码:
const getNumWord = (num) =>
num === 1
? 'one'
: num === 2
? 'two'
: num === 3
? 'three'
: num === 4
? 'four'
: 'unknown';
console.log(getNumWord(1)); // one
console.log(getNumWord(3)); // three
console.log(getNumWord(7)); // unknown
结论
我们已经了解了针对常见JavaScript编程问题的简明解决方案。 我们看到许多实例,其中包含多个语句的命令式解决方案被转换为使用各种内置方法和语言结构的声明式单行代码。
这些紧凑的解决方案有时性能和可读性较低,但使用它们可以证明您的编程能力和对语言的掌握程度。使用任何一种方法,我们都是需要根据具体的情况来使用。