0%

javascript-avoid-comments-with-refactoring

下面这段代码是今天在Youtube上看到的一个视频中的代码, 代码的功能是从一个字符串数组中找出所有最长的字符串:比如给定字符串数组:[“aba”, “aa”, “ad”, “vcd”, “aba”],则要求返回[“aba”, “vcd”, “aba”]。

可以看到这段代码中有很多注释,我们的目的是优化这段代码,让代码可以self explanation

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// gets all longest strings
function longestString(inputArray) {
// initialize the longest string to first value
let { length } = inputArray[0];
for (let i = 1; i < inputArray.length; i++) {
// checks if current string is longer than current longest
if (length < inputArray[i].length) {
length = inputArray[i].length;
}
}
// filters out any values not equal to the longest string
const strs = inputArray.filter((word) => word.length === length);

// return the longest strings
return strs;
}

命名优化

可以从以下几个方面入手:

  1. 函数名字不够清晰,longestString是单数形式,且没有动词,一般函数名字都应该加动词,可以改为getLongestStrings - 这样我们就知道,这个函数返回的是多个字符串。而且是从输入参数中get而来的。
  2. 函数的参数名字不够清晰,inputArray是一个数组,但是我们不知道这个数组的内容是什么,而且input这个单词有点多余,参数当然是input的,没必要再加上input这个单词。可以改为stringArray

此时,我们的代码变成了这样:

1
2
3
function getLongestStrings(stringArray) {

}
  1. 原来的代码中有一个length变量,这个变量的名字不够清晰,并没有说明是什么长度,也无法表明是最大长度,可以改为longestStringLength
  2. 原代码中使用的是经典的for循环,在Modern JavaScript中,我们更推荐函数式编程,所以可以改为for...of循环或者使用Array.prototype.forEach,这样代码更加简洁。

此时代码变成下面的样子。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
function getLongestStrings(stringArray) {
let longestStringLength = 0;

stringArray.forEach((str) => {
if (longestStringLength < str.length) {
longestStringLength = str.length;
}
});

const longestStrings = stringArray.filter(
(word) => word.length === longestStringLength
);

return longestStrings;
}

性能优化

此时代码中有一个forEach,有一个filter,这两个函数都是遍历数组的,所以实际上我们遍历了两次数组,不够高效,将其改为一次遍历。此时代码变成下面的样子。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
function getLongestStrings(stringArray) {
let longestStringLength = 0;
let longestStrings = [];

stringArray.forEach((str) => {
if (str.length > longestStringLength) {
longestStringLength = str.length;
longestStrings = [str];
} else if (str.length === longestStringLength) {
longestStrings.push(str);
}
});

return longestStrings;
}

使用reduce优化

多数数组的遍历方法都可以使用reduce来实现,所以我们可以将上面的代码改为使用reduce来实现。

1
2
3
4
5
6
7
8
9
10
function getLongestStrings(stringArray) {
return stringArray.reduce((acc, str) => {
if (str.length > acc.maxLength) {
return { maxLength: str.length, longestStrings: [str] };
} else if (str.length === acc.maxLength) {
acc.longestStrings.push(str);
}
return acc;
}, { maxLength: 0, longestStrings: [] }).longestStrings;
}

坦率的说,使用reduce这一版的可读性不如上一版,大家酌情使用。

reduce通常用来处理比较简单的逻辑,比如累加一个数组中所有的数字。在这种情况下,reduce通常只接受一个参数,就是待处理的数组。

1
2
const sum = [1, 2, 3, 4].reduce((acc, cur) => acc + cur, 0);
console.log(sum); // 10

如果逻辑比较复杂,那么reduce就要使用多个参数,比如上面的例子,我们实际上传入了如下两个参数,只不过我们把这两个参数封装到了一个对象中。

  • longesStringLength - 用来记录当前最长的字符串的长度
  • longestStrings - 用来记录当前最长的字符串

处理的时候数组longestStrings是不断变化的,如果当前字符串的长度等于longestStringLength,那么就把当前字符串加入到longestStrings中。(此过程数组元素不断增多,原有的元素还在),如果当前字符串的长度大于longestStringLength,那么就把longestStrings清空,然后把当前字符串加入到longestStrings中。(此过程原有数组元素被清空,只有当前元素)。

此外,我们还注意到,第一个if分支有return,而第二个没有,有点奇怪,其实原本可以这样写。

1
2
3
4
5
6
7
if (str.length > acc.maxLength) {
acc.maxLength = str.length;
acc.longestStrings = [str];
} else if (str.length === acc.maxLength) {
acc.longestStrings.push(str);
}
return acc;

但是,当str.length > acc.maxLength时,直接返回一个新对象也可以,这个对象会作为reduce的下一次迭代的第一个参数。

其实再进一步,我们可以在if-else中也返回一个新对象。

1
2
3
4
5
6
7
8
9
10
11
12
13
if (str.length > acc.maxLength) {
return { maxLength: str.length, longestStrings: [str] };
}

if (str.length === acc.maxLength) {
return {
maxLength: acc.maxLength,
longestStrings: [...acc.longestStrings, str],
};
}

/// str.length < acc.maxLength
return acc;

1. 什么是cookie

In Node.js, you can use the response.setHeader method to set cookies from the server side. Here’s an example using the Express.js framework:

1
2
3
4
5
6
7
8
9
10
11
const express = require('express');
const app = express();

app.get('/', (req, res) => {
res.cookie('username', 'John Doe', { maxAge: 900000, httpOnly: true });
res.end('Cookie has been set');
});

app.listen(3000, () => {
console.log('Server is running on port 3000');
});

In this example, res.cookie('username', 'John Doe', { maxAge: 900000, httpOnly: true }); sets a cookie named “username” with the value “John Doe”. The maxAge option sets the expiry time for the cookie in milliseconds. The httpOnly option is a security enhancement that restricts the cookie from being accessed by client-side scripts.

In Node.js, you can use the request.headers.cookie property to get cookies from the client side. Here’s an example using the Express.js framework:

1
2
3
4
5
6
7
8
const express = require('express');
const app = express();

app.get('/', (req, res) => {
const cookie = req.headers.cookie;
console.log(cookie);
res.end('Cookie has been set');
});

In this example, const cookie = req.headers.cookie; gets the cookie from the client side and logs it to the console.

You can also use cookie-parser middleware to parse the cookie from the request header. Here’s an example using the cookie-parser middleware:

1
2
3
4
5
6
7
8
9
10
const express = require('express');
const cookieParser = require('cookie-parser');

const app = express();
app.use(cookieParser());

app.get('/', (req, res) => {
const { cookies } = req;
console.log(cookies);
});

使用JavaScript操作cookie

Note that the cookie value cannot contain semicolons, commas, or whitespace. For a solution, use the encodeURIComponent() function to encode the value.

1
document.cookie = "username=John Doe; expires=Thu, 18 Dec 2023 12:00:00 UTC; path=/";

document.cookie will return all cookies in one string, each cookie is separated by a semicolon and a space.

1
let allCookies = document.cookie;

To delete a cookie, just set the expires parameter to a passed date.

document.cookie = "username=; expires=Thu, 01 Jan 1970 00:00:00 UTC; path=/;";

# References
- [MDN web docs](https://developer.mozilla.org/en-US/docs/Web/HTTP/Cookies)

Working Efficiently

Useful VSCode Plugins

  1. Code Runner - Run code snippet or code file for multiple languages.
  2. Mithril Emmet - Quickly write Html/CSS/JSX tags.

HTTP Content-Type

x-www-form-urlencoded

先看一个例子:

1
2
3
4
5
6
7
8
9
<form
action="/urlencoded?firstname=philip&lastname=zhang"
enctype="application/x-www-form-urlencoded"
method="POST"
>
<input name="username" type="text" value="user1" />
<input name="password" type="text" value="pwd1" />
<input type="submit" value="Submit" />
</form>

这里的 enctype 属性指定了表单数据的编码类型,application/x-www-form-urlencoded 是默认的编码类型,也是最常用的编码类型。当表单数据被编码为 application/x-www-form-urlencoded 时:

  • 表单数据会被编码为键值对,键值对之间用 & 连接。
  • 键和值之间用 = 连接。

上面的例子中,表单数据被编码为 username=user1&password=pwd1。(实际上和url中的参数格式一样,所以才叫urlencoded

用浏览器打开这个页面,然后点击 Submit 按钮,按F12打开开发者工具,切换到 Network 选项卡,在HeadersTab可以看到请求的 Content-Typeapplication/x-www-form-urlencoded
alt text

PayloadTab可以看到请求的详细信息:

  • Query Stringfirstname=philip&lastname=zhang - 对应form action中的url参数
  • Form Datausername=user1&password=pwd1- 对应form体中的两个input

alt text

但是,上面这个例子并没有展示出url-encoded的精髓,这里的encoded到底encoded了什么呢?我们来看一个更加直观的例子:

1
2
3
4
5
6
7
8
9
<form
action="/urlencoded?firstname=philip&lastname=zhang"
enctype="application/x-www-form-urlencoded"
method="POST"
>
<input name="username" type="text" value="foo&bar" />
<input name="password" type="text" value="1=2?" />
<input type="submit" value="Submit" />
</form>

这里的 usernamepassword 的值中包含了 &=,这些字符在url中有特殊含义,如果直接放在url中,会导致url解析错误。但是,当表单数据被编码为 application/x-www-form-urlencoded 时,这些特殊字符会被转义,所以编码后的表单数据如下图所示。

  • username: foo%26bar, 其中 %26& 的转义
  • password: 1%3D2%3F, 其中 %3D= 的转义,%3F? 的转义
    如果不转义的话那么最终的结果就是username=foo&bar&password=1=2?,此时浏览器就懵逼了。导致数据解析出错。
    alt text

那么到底哪些字符需要编码呢?参考这里:http://www.blooberry.com/indexdot/html/topics/urlencoding.htm

multipart/form-data

application/json

text/plain

references

Web API

  • REST - Representational State Transfer
  • SOAP - Simple Object Access Protocol
  • GraphQL - A query language for your API

difference between REST and SOAP

Feature REST SOAP
Protocol Can use any protocol but typically uses HTTP/HTTPS Uses HTTP, SMTP, TCP, and more
Standards No official standard, uses HTTP methods (GET, POST, PUT, DELETE) Official standard by W3C
Performance Lightweight, less data overhead Heavyweight, more data overhead due to XML usage
Message format Can use multiple formats (JSON, XML, etc.) Uses XML
Security Uses web security standards Uses WS-Security which is more robust
Transaction support No official support Official support
State management Stateless Stateful

说明:

  1. SOAP by itself is a protocol, but it can use any protocol to transport its messages, REST is not a protocol, it typically uses HTTP/HTTPS.
  2. REST is stateless which means each request from client to server must contain all of the information necessary to understand the request, and cannot take advantage of any stored context on the server. Session state is therefore kept entirely on the client.

JavaScript File Collecting

今天收到老婆的一个需求,她有一个文件夹,里面包含若干文件和子文件夹,子文件夹又包含文件及子文件夹,她需要将这些文件全部收集到一个文件夹中。收集后的文件要从1开始命名,依次递增。我想到了 Node.js,可以使用 Node.js 来实现这个需求。

代码实现 - 百度文心一言版

这个版本一开始错误百出,经过我修改后才能使用。

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
const fs = require('fs').promises;
const path = require('path');

let fileIndex = 3001;

async function copyAndRenameFiles(srcDir, destDir) {
try {
// 确保目标目录存在
await fs.access(destDir);
} catch {
// 如果不存在,则创建目标目录
await fs.mkdir(destDir, { recursive: true });
}

// 读取srcDir中的所有文件和文件夹
const entries = await fs.readdir(srcDir, { withFileTypes: true });

for (const entry of entries) {
const srcPath = path.join(srcDir, entry.name);
console.log(srcPath);

if (entry.isDirectory()) {
// 如果是文件夹,则递归调用
await copyAndRenameFiles(srcPath, destDir);
} else if (entry.isFile()) {
const destBaseName = `${fileIndex}`; // 新文件的基本名称
const destExtname = path.extname(entry.name); // 获取原文件的扩展名
const destPath = path.join(destDir, `${destBaseName}${destExtname}`); // 目标文件完整路径
// 如果是文件,则复制到目标文件夹并重命名
await fs.copyFile(srcPath, destPath);
fileIndex++; // 更新文件索引
}
}

return fileIndex; // 返回下一个可用的文件索引
}

// 使用示例
const sourceFolder = 'xxx'; // 顶层文件夹路径
const destinationFolder = 'xxx'; // 目标文件夹路径

// 从1开始编号
copyAndRenameFiles(sourceFolder, destinationFolder)
.then((finalFileIndex) => {
console.log(
`所有文件已成功复制到目标文件夹,并从1开始重新编号到${
finalFileIndex - 1
}。`
);
})
.catch((error) => {
console.error('在复制过程中发生错误:', error);
});

代码实现-ChatGPT4

这个版本一次通过,一行未改。

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
const fs = require('fs').promises;
const path = require('path');

// 这里使用参数arrayOfFiles递归收集所有文件,思路很巧妙。
async function getAllFiles(dirPath, arrayOfFiles) {
const files = await fs.readdir(dirPath);

arrayOfFiles = arrayOfFiles || [];

for (const file of files) {
const filepath = path.join(dirPath, file);
const stat = await fs.stat(filepath);

if (stat.isDirectory()) {
arrayOfFiles = await getAllFiles(filepath, arrayOfFiles);
} else {
arrayOfFiles.push(filepath);
}
}

return arrayOfFiles;
}

async function copyFilesWithNumberedNames(sourceDir, targetDir) {
try {
// Create target directory if it does not exist
await fs.mkdir(targetDir, { recursive: true });

// Get all files from the source directory
const filesToCopy = await getAllFiles(sourceDir);

// Copy each file with a new numbered name
let fileNumber = 1;
for (const file of filesToCopy) {
const newFilename = `${fileNumber}${path.extname(file)}`;
const targetPath = path.join(targetDir, newFilename);
await fs.copyFile(file, targetPath);
console.log(`Copied ${file} to ${targetPath}`);
fileNumber++;
}

console.log('All files have been copied successfully.');
} catch (err) {
console.error('An error occurred:', err);
}
}

const sourceDirectory = 'xxx'; // Replace with your source directory path
const targetDirectory = 'xxx'; // Replace with your target directory path

copyFilesWithNumberedNames(sourceDirectory, targetDirectory);

JavaScript Return

今天一起了解一下 JavaScript 中的 return 语句。

语法

return 语句用于指定函数的返回值。当 JavaScript 遇到 return 语句时,函数将立即返回,并且不再执行任何后续的语句。

1
return [expression]; // expression 是要返回的值
1
2
3
4
5
function add(a, b) {
return a + b;
}

console.log(add(1, 2)); // 3

如果省略 expression,或者 return 语句没有在函数中,则返回值为 undefined

1
return; // undefined

return后面省略表达式,返回值为undefined

1
2
3
4
5
6
7
function foo() {
const a = 1;
const b = 2;
return;
}

console.log(foo()); // undefined

没有return语句,也返回undefined

1
2
3
4
5
6
function bar() {
const a = 1;
const b = 2;
}

console.log(bar()); // undefined

返回对象

return 语句返回复杂值时,return不能独占一行,否则会报错。

下面这样写会导致语法错误。

1
2
3
4
5
6
7
function getPerson() {
return // return 不能独占一行
{
name: 'zdd',
age: 18
};
}

正确的写法是:

1
2
3
4
5
6
function getPerson() {
return { // '{' 与 'return' 在同一行
name: 'zdd',
age: 18
};
}

try…catch…finally

如果在 try-catch-finally 语句块中使用了 return 语句,则执行顺序如下:

  1. 先计算return后面的表达式的值。
  2. 然后执行 finally 语句块中的代码。
  3. 最后return步骤1中计算出的值。
1
2
3
4
5
6
7
8
9
function foo() {
try {
console.log(1);
return 2;
} finally {
console.log('3');
}
}
console.log(foo()); // 1 3 2

再看一个复杂的例子

1
2
3
4
5
6
7
8
9
10
11
function foo() {
let a = 1;
try {
console.log(a);
return a++;
} finally {
console.log(a);
}
}

console.log(foo()); // 1 2 1

为啥结果是1 2 1呢?

  • try中`console.log(a)输出1,这个很好理解。
  • return a++,这里的a++是先返回a的值,然后再执行a++,所以return返回的是1
  • finallyconsole.log(a)输出的是2,因为a++执行了一次。

所以执行的顺序是:

  1. 先计算return后面的表达式,a = 2.
  2. 然后执行finally中的代码,输出a = 2
  3. 最后return返回1.

JavaScript Regex Groups

分组匹配是正则表达式中非常重要的一部分,它允许我们将多个字符组合在一起,并对它们进行操作。在JavaScript正则表达式中,我们可以使用括号 () 来创建一个分组。在这篇文章中,我们将学习如何使用分组匹配。

匿名分组

匿名分组的格式如下,其中 xxx 是要匹配的内容。匿名分组返回的匹配没有名字,只能通过数组下标的形式访问。

1
(xxx)

给定字符串Personal info: name: zdd, age: 18, gender: male,如果要从中提取出姓名,年龄和性别,则可以使用匿名分组来实现。

1
2
3
4
5
6
7
const str = `Personal info: name: zdd, age: 18, gender: male`;
const regex = /name: (\w+), age: (\d+), gender: (\w+)/;
const match = str.match(regex);
console.log(match); // 返回整个匹配 name: zdd, age: 18, gender: male
console.log(match[1]); // 第一个分组:zdd
console.log(match[2]); // 第二个分组:18
console.log(match[3]); // 第三个分组:male

在上面的例子中,正则表达式/name: (\w+), age: (\d+), gender: (\w+)/中有三个(),所以一共包含三个分组:

  • 第一个分组: \(w+) - 匹配zdd
  • 第二个分组:(\d+) - 匹配18
  • 第三个分组:(\w+) - 匹配male

使用匿名分组时,返回值是一个数组,数组第一个元素是整个匹配,我们要的分组匹配结果从数组第二个元素(下标1)开始,返回值如下:

1
2
3
4
5
6
7
8
9
[
'name: zdd, age: 18, gender: male',
'zdd', // 第一组
'18', // 第二组
'male', // 第三组
index: 0,
input: 'name: zdd, age: 18, gender: male',
groups: undefined
]

注意,使用匿名分组时,返回值中groups属性值为undefined,这个值只有使用命名匹配时才有值。

命名分组

命名分组的格式如下,其中 name 是分组的名称,xxx 是要匹配的内容。命名匹配返回的分组有名字,除了通过数组下标访问外,还可以通过对象属性的方式访问。

1
(?<name>xxx)

还是以上面的字符串为例,我们使用命名分组来提取姓名,年龄和性别。

1
2
3
4
5
6
7
const str = `Personal info: name: zdd, age: 18, gender: male`;
const regex = /name: (?<name>\w+), age: (?<age>\d+), gender: (?<gender>\w+)/;
const match = str.match(regex);
console.log(match.groups); // [Object: null prototype] { name: 'zdd', age: '18', gender: 'male' }
console.log(match.groups.name); // zdd
console.log(match.groups.age); // 18
console.log(match.groups.gender); // male

match返回值如下:与匿名匹配相比,命名匹配的返回值中groups属性不再是undefined,而是一个对象,对象的属性名就是分组的名称,属性值就是匹配的结果。

1
2
3
4
5
6
7
8
9
[
'name: zdd, age: 18, gender: male',
'zdd',
'18',
'male',
index: 0,
input: 'name: zdd, age: 18, gender: male',
groups: [Object: null prototype] { name: 'zdd', age: '18', gender: 'male' }
]

命名匹配的好处是,除了可以使用数组下标来访问分组匹配外,还可以使用groups字段来获取分组匹配。比如上面的match.groups.name就表示name分组对应的值。

再强调一次:groups字段只有在命名匹配时才有值,匿名匹配时用不到这个字段。

javascript regex summary

js中正则表达式的几种应用场景:

验证

  • RegExp.prototype.test

提取

  • String.prototype.match
  • String.prototype.matchAll
  • RegExp.prototype.exec
  1. match/exec方法,如果没有分组匹配,则通常取matches[0],如果有分组匹配,则取matches[1]、matches[2]...

替换

  • String.prototype.replace
  • String.prototype.replaceAll

拆分

  • String.prototype.split

javascript regex exec

exec是正则表达式的方法,用来返回匹配的字符串。

exec的返回值

exec的返回值是一个数组,数组的第一个元素是匹配的字符串,后面的元素是分组匹配的值。看一个列子:

1
2
3
4
const str = 'There are 3 dogs, 5 cats, 2 birds and 1 cow';
const regex = /(\d+) (cat|dog|bird|cow)/g;
const match = regex.exec(str);
console.log(match);

返回值如下:

1
2
3
4
5
6
7
8
[
'3 dog', // 匹配的整个字符串
'3', // 第一个分组匹配的值
'dog', // 第二个分组匹配的值
index: 10, // 匹配值对应的下标
input: 'There are 3 dogs, 5 cats, 2 birds and 1 cow', // 原始字符串
groups: undefined // 分组匹配的值
]

循环处理

exec通常配合while循环使用,一边遍历,一边处理结果。注意,如果使用while循环处理,需要正则表达式中添加/g标志。

1
2
3
4
5
6
7
8
9
const str = 'There are 3 dogs, 5 cats, 2 birds and 1 cow';
const regex = /(\d+) (cat|dog|bird|cow)/g; // 添加/g标志
let match;
const result = {};
while ((match = regex.exec(str)) !== null) {
result[match[2]] = match[1];
}

console.log(result);

输出结果:

1
2
3
4
5
6
{
dog: '3',
cat: '5',
bird: '2',
cow: '1'
}

/g标志

如果想要使用while循环来遍历所有匹配的字符串,需要使用/g标志。否则会导致while死循环。

match/matchAll vs exec

  1. match/matchAll - 一次性返回所有匹配的字符串。
  2. exec - 逐个返回匹配的字符串。

match/matchAll使用/g时,需要对结果进行遍历。
exec是一边遍历,一边处理结果。