0%

JavaScript identifiers

标识符命名规则

JavaScript标识符可以使用如下字符:

  • Unicode字母(包括中文)
  • 字母(A-Z,a-z)
  • 数字(0-9)
  • 下划线(_)- 通常用于变量的前缀或者后缀,用来表示私有变量
  • 美元符号($)- 通常用于一些library,比如jQuery。

注意事项

  • 标识符不能以数字开头
  • 标识符不能包含空格
  • 标识符不能是保留字

JavaScript 保留字

JavaScript 保留字是一些特殊的关键字,它们有特殊的用途,不能用作标识符。下面是一些JavaScript保留字:

1
2
3
4
break case catch class const continue debugger default delete do
else enum export extends false finally for function if import in instanceof
new null return super switch this throw true try typeof var void while with
yield

在strict mode下,还有一些额外的保留字:

1
implements interface let package private protected public static

冷知识

  • undefined/NaN/Infinity等等都属于全局变量,而不是一个关键字,所以你可以使用他们作为变量名,但是不建议这样做。比如下面的代码在Node环境是合法的。

    1
    2
    let undefined = 'Hello';
    console.log(undefined); // Hello
  • 可以使用中文作为变量名,但是不建议这样做。比如下面代码是合法的

    1
    2
    const 你好 = 'Hello';
    console.log(你好); // Hello

Cannot use import statement outside a module

产生这个错误的原因是因为你使用了import或者export语句,但是没有指定模块类型。在JavaScript中,有两种常用的模块类型:CommonJS和ES6 Modules。CommonJS是Node.js的模块化规范,它使用require来引入模块,使用module.exports来暴露接口。ES6 Modules是ES6的模块化规范,它使用import来引入模块,使用export来暴露接口。

解决办法

浏览器端

如果是浏览器端,需要将<script>标签的type属性设置为module,这样浏览器会将这个脚本当作ES6 Modules来处理。此时无论js文件的扩展名是.js还是.mjs都可以正常工作。

1
2
3
<script type="module">
import {add} from './math-utils.js';
</script>
1
2
3
4
// math-utils.js
export function add(a, b) {
return a + b;
}

Node.js端

方法一: 将文件的后缀名改为.mjs,这样Node.js会将这个文件当作ES6 Modules来处理。

1
2
3
4
// math-utils.mjs
export function add(a, b) {
return a + b;
}
1
2
// index.mjs
import {add} from './math-utils.mjs';

方法二: 在package.json中添加"type": "module"字段,这样Node.js会将所有的文件当作ES6 Modules来处理。

1
2
3
4
5
6
7
8
// package.json
{
"name": "json",
"version": "1.0.0",
"type": "module", // 添加这一行,就可以支持ESModule了。
"dependencies": {
}
}

总结

  • js - 常规js文件
  • mjs - ES6 Modules文件, 使用importexport来引入和暴露接口
  • cjs - CommonJS文件,使用requiremodule.exports来引入和暴露接口

Node.js环境可以使用任何上述文件格式,浏览器端其实不在意文件的后缀名,只要<script>标签的type属性设置为module,就可以使用ES6 Modules。

JavaScript Modules

Why use modules?

早期的JavaScript只是做一些简单的交互,所以不需要模块化。但是随着JavaScript的发展,现在的JavaScript已经可以做很多事情了,比如构建大型的应用程序,而Node.js的出现使得JavaScript甚至可以胜任某些Server端的工作,这时候模块化就显得非常重要了。

模块化的好处有很多,比如:

  • 代码复用
  • 代码隔离
  • 代码维护
  • 依赖管理

JavaScript模块化主要经历了以下几个阶段:

  • IIFE
  • CommonJS
  • AMD
  • UMD
  • ES6 Modules

IIFE

IIFE(Immediately Invoked Function Expression)是一种立即执行的函数表达式,它可以用来模拟模块化。IIFE的特点是:

  • 使用闭包来隔离作用域
  • 通过返回一个对象来暴露接口
1
2
3
4
5
6
const module = (function() {
...
return {
// expose some interface
};
})();

下面的代码使用IIFE封装了一个Person类,其中getName方法是public的,其他方法是private的。因为IIFE是定义后就立即执行的,所以这里我们使用一个person变量来接收IIFE的返回值,这样就可以调用getName方法了。

想要向外暴露任何接口,只需要在返回的对象中添加即可。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
const person = (function person(firstName, lastName) {
const _firstName = firstName;
const _lastName = lastName;

function getFirstName() {
return _firstName;
}

function getLastName() {
return _lastName;
}

function getName() {
return `${getFirstName()} ${getLastName()}`;
}

return {
getName: getName,
};
})('Philip', 'Zhang');

console.log(person.getName()); // 'Philip Zhang'

CommonJS

CommonJS是Node.js的模块化规范,它的特点是:

  • 使用require来引入模块
  • 使用module.exports来暴露接口
1
2
// index.js
const module = require('./module');
1
2
3
4
// module.js
module.exports = {
// expose some interface
};

AMD

AMD(Asynchronous Module Definition)是另一种模块化规范,它的特点是:

  • 使用define来定义模块
  • 使用require来引入模块
1
2
3
4
5
6
define(['module'], function(module) {
...
return {
// expose some interface
};
});

UMD

UMD(Universal Module Definition)是一种通用的模块化规范,它的特点是:

  • 支持CommonJS和AMD
  • 通过判断typeof module来判断当前环境
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
(function (root, factory) {
if (typeof define === 'function' && define.amd) {
// AMD
define(['module'], factory);
} else if (typeof module === 'object' && module.exports) {
// CommonJS
module.exports = factory(require('module'));
} else {
// Browser
root.module = factory(root.module);
}
}(this, function (module) {
...
return {
// expose some interface
};
}));

ES6 Modules

ES6 Modules是JavaScript的模块化规范,它的特点是:

  • 使用import来引入模块
  • 使用export来暴露接口
1
2
// index.js
import module from './module';
1
2
3
4
// module.js
export default {
// expose some interface
};

Node.js中的ES6 Modules

Node.js从v13.2.0开始支持ES6 Modules,但是需要使用.mjs后缀名。

1
2
// index.mjs
import module from './module.mjs';
1
2
3
4
// module.mjs
export default {
// expose some interface
};

总结

JavaScript模块化的发展经历了很多阶段,从IIFE到CommonJS、AMD、UMD,最后到ES6 Modules。ES6 Modules是JavaScript的官方模块化规范,它的特点是简洁、易用,所以在现代的JavaScript开发中,推荐使用ES6 Modules。

References:

如何运行 JavaScript

运行JavaScript代码有很多方式,以下总结四种方式:

  • 使用html运行
  • 使用浏览器控制台运行
  • 使用Node.js REPL(Read-Eval-Print Loop)交互式运行
  • 使用Node.js运行JavaScript文件
  • 使用编辑器运行

使用html运行

在html文件中,使用<script>标签引入JavaScript文件,然后在浏览器中打开html文件即可运行JavaScript代码。

1
2
3
4
5
6
7
8
9
10
11
12
13
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Run JavaScript</title>
</head>
<body>
<script type="text/javascript">
console.log('Hello, world!');
</script>
</body>
</html>

使用浏览器控制台运行(Chrome)

在浏览器中打开网页,然后按F12(Windows)或者Ctrl+Shift+I(Mac)打开控制台,输入JavaScript代码即可运行。

使用Node.js REPL交互式运行

在终端中输入node命令,进入Node.js REPL环境,然后输入JavaScript代码即可运行。

1
2
3
4
$ node
> console.log('Hello, world!');
Hello, world!
undefined

使用Node.js运行JavaScript文件

在终端中输入node命令,后面跟上JavaScript文件路径,即可运行JavaScript文件。

1
2
$ node index.js
Hello, world!

使用编辑器运行

  • VS Code可以使用Code Runner插件运行JavaScript文件,安装插件后,右键点击文件,选择Run Code即可运行。
  • WebStorm可以直接右键点击文件,选择Run即可运行。

今天有幸了解到了另外一个IED,Komodo,主要是针对Python和Perl的,但是也支持JavaScript,有空下载来试试。

跨域问题一则

问题提出

今天在编写一个简单的html时,遇到了如下问题,html中引入了一个js文件,且html和js位于同一个目录下。

index.html

1
2
3
4
5
6
7
8
<html>
<body>
<script type="module">
import add from "./index.js";
console.log(add(1, 2));
</script>
</body>
</html>

index.js

1
2
3
export default function add(a, b) {
return a + b;
}

当我使用浏览器打开html文件时,Chrome浏览器控制台报错:

1
Access to script at 'file:///Users/philip/Projects/test.js' from origin 'null' has been blocked by CORS policy: Cross origin requests are only supported for protocol schemes: http, data, chrome, chrome-extension, https.

查看network tab,发现如下信息,在status一栏,发现跨域错误:CORS error

Alt text

首先复习一下什么是同源策略
同源策略是浏览器的一个安全功能,它要求一个网页只能与其本身的服务器进行交互,而不能与其他域的服务器进行交互。这样可以防止恶意的网站窃取数据。

从具体的实现来分析,协议,域名,端口号都相同的两个url就是同源的。以https://www.example.com:8080为例,它的组成部分如下:

如果省略端口,那么http默认是80,https默认是443。

那么问题来了,为什么我引入的js文件会报错呢?因为浏览器认为这是一个跨域请求,而不是同源请求。这是因为我们的html文件是通过http://协议打开的(双击html文件时,会使用系统默认的浏览器打开,这时观察浏览器中的地址,可知是http协议。),而html中引用的js文件是通过file://协议引入的,协议不同,所以浏览器认为这是一个跨域请求。

解决办法

  1. WebStorm - WebStorm打开网页时没有这个问题,因为WebStorm内置了一个服务器,所以它的协议是http://,所以不会报错。
  2. VSCode - 安装插件Live Server,使用Live Server打开网页,也不会报错。
    1. npm install -g live-server
    2. Open terminal, cd 到html文件所在目录
    3. run live-server .

当然也可以使用http-server

TypeScript Array Literal Type

问题提出

考虑下面的类型定义:

1
2
3
4
5
6
7
8
interface response {
students: [
{
name: string;
age: number;
}
];
}

注意这里的students是一个数组字面量,而不是普通的数组,数组中的元素是一个对象,对象中有nameage两个属性。我们在使用这个类型的时候,可能会遇到这样的情况:

1
2
3
4
5
6
7
8
9
10
11
12
const data: response = {
students: [
{
name: 'Philip',
age: 18,
},
{
name: 'Tom',
age: 20,
},
],
};

这样的情况下,data是一个符合response类型的对象,但是students数组中的元素个数是不对的,因为students的类型定义是一个元素的数组字面量。所以我们不能传入两个元素(这相当于把students当作数组类型来使用,而不是当作数组字面量来使用)。所以TS会给出如下提示:

1
2
is not assignable to type [{ name: string; age: number; }]
Source has 2 element(s) but target allows only 1

解决办法

这时候我们可以使用Array类型来定义students

1
2
3
4
5
6
interface response {
students: Array<{
name: string;
age: number;
}>;
}

这样的定义,students是一个数组,数组中的元素是一个对象,对象中有nameage两个属性。这样的定义,students数组中的元素个数是不确定的,可以是0个或者多个。

也可以使用[]来定义students

1
2
3
4
5
6
interface response {
students: {
name: string;
age: number;
}[];
}

这样的定义和上面的是等价的。这时,我们再使用上面的data

1
2
3
4
5
6
7
8
9
10
11
12
const data: response = {
students: [
{
name: 'Philip',
age: 18,
},
{
name: 'Tom',
age: 20,
},
],
};

就不会报错了。

但是有一种特殊情况,就是类型定义有时是无法修改的,比如定义在第三方库中,这时候我们可以使用push完成同样的效果。

1
2
3
4
5
6
7
8
9
10
const data: response = {
students: [
{
name: 'Philip',
age: 18,
},
],
};

data.students.push({ name: 'John', age: 20 })

github port 22 not working

今天在使用github的时候,发现git push的时候报错:

1
2
3
4
5
ssh: connect to host github.com port 22: Connection timed out
fatal: Could not read from remote repository.

Please make sure you have the correct access rights
and the repository exists.

解决办法,使用443端口,首先测试是否可用。

1
ssh -T -p 443 git@ssh.github.com

如果可以正常连接,那么就可以修改~/.ssh/config文件,添加如下内容:

1
2
3
Host github.com
Hostname ssh.github.com
Port 443

然后再次测试:

1
ssh -T git@github.com

输出如下信息:

1
Hi Philip! You've successfully authenticated, but GitHub does not provide shell access.

接下来就可以愉快的使用git了。

Reference

Groovy JSON operation

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
import groovy.json.JsonOutput
import groovy.json.JsonSlurper

// Convert a groovy map to json object
Map user = [name: 'zdd', age: 18, info: [address: 'beijing', phone: '123456789']];
println JsonOutput.toJson(user)

// Convert a json object to groovy map
def jsonString = '{"name": "John", "age": 30, "city": "New York"}'
def jsonSlurper = new JsonSlurper()
def jsonObject = jsonSlurper.parseText(JsonOutput.toJson(user))
println jsonObject

// Given a string "feat(configuration): id-xxxx add configuration for user page", please extract the jira id: id-xxx
// can you do it with regex? the target string start with ": " and end with " ", return null if not found
def str = "feat(configuration): id-xxxx add configuration for user page"
def jiraId2 = str =~ /: (.*?) /
println jiraId2[0][1]

async & defer, what’s the differences?

Html引入外部脚本

Html中引入外部脚本的方式有很多种,最常见的就是使用<script>标签,我们可以在<head>或者<body>中引入外部脚本。

1
2
3
4
5
<head>
<script src="./my_script.js"></script>
<script async src="./my_script1.js"></script>
<script defer src="./my_script2.js"></script>
</head>
1
2
3
4
5
<body>
<script src="./my_script.js"></script>
<script async src="./my_script1.js"></script>
<script defer src="./my_script2.js"></script>
</body>

那么从<head><body>中引入外部脚本有什么区别呢?

  • <head>中引入外部脚本,会阻塞Dom的解析,直到脚本下载完毕并执行完毕。
  • <body>末尾中引入外部脚本,不会阻塞Dom的解析,脚本下载和Dom解析并行进行。

你一定听说过上面的说法,但是上面的说法有个前提,那就是同步引入脚本,也就是不加async或者defer.

如果使用async或者defer关键字,那么在<head>中引入外部脚本和在<body>末尾引入外部没有太大区别。(只是<head>中的脚本会先于<body>中的脚本下载)

async & defer

在Html引入外部脚本时,可以使用async或者defer,那么两者有和区别呢?我们通过一个表格来分析一下:

关键字 下载时机 执行时机 是否阻断Dom解析 多个脚本的执行顺序
遇到对应的<script>标签,立即下载 下载后立即执行 是,下载阶段和执行阶段都阻断DOM解析。 按顺序执行
async 遇到对应的<script>标签,立即下载 下载后立即执行,只能保证在window.load事件之前执行,但是可能在window.DomContentLoade之前或之后。 没有固定顺序,取决于哪个脚本先下载完成。
defer 遇到对应的<script>标签,立即下载 在页面解析完之后,且在DomContentLoaded事件触发之前执行。 <script>标签出现的顺序执行

注意事项:

  1. async脚本的执行无固定顺序,谁先下载完,谁先执行。
  2. defer脚本按出现的先后顺序执行。

以下代码中,我们使用defer加载两个脚本,其中short.js非常小,很快就下载完了,而long.js非常大,下载时间很长。因为defer脚本是按照书写顺序进行执行的,所以即使short.js先下载完了,也要等到long.js下载完毕才能开始执行。

1
2
<script defer src='https://xxx/yyy/long.js'>
<script defer src='https://xxx/yyy/short.js'>

用一张图来总结一下,图里中颜色含义如下:
绿色线条 - 表示dom解析
蓝色线条 - 表示脚本的下载
红色线条 - 表示脚本的执行。
script分为普通脚本和模块化脚本。

https://html.spec.whatwg.org/images/asyncdefer.svg

解释一下上面这张图:

  1. 第一行,普通脚本,没有指定async或者defer,下载和执行阶段会阻断dom解析。
  2. 第二行,普通脚本,有defer关键字,下载和执行阶段都不阻断dom解析。(下载和dom解析并行进行,执行在dom解析完成后开始)
  3. 第三行,普通脚本,有async关键字,下载阶段不阻断dom解析,但执行阶段有可能阻断dom解析(如果脚本已经下载完毕,但是dom解析尚未完成的情况下)
  4. 第四行,模块化脚本,默认包含defer属性,该脚本及其依赖的其他脚本的下载与dom解析平行进行,待dom解析完毕开始执行脚本。
  5. 第五行,模块化脚本,有async关键字,该脚本及其依赖的其他脚本的下载与dom解析平行进行,下载完毕后立即执行脚本。

async & defer

  • 标记为async或defer的script,下载阶段都不会阻断Dom的解析,但是async是下载后立即执行,而defer是下载后且等待dom解析完毕才执行,所以两者唯一的区别就是:async脚本执行阶段可能会阻断dom解析(前提是脚本已经下载完毕,但dom解析尚未完成)。
  • module script默认包含defer属性
  • 多个标记为async的脚本,无法保证执行顺序。
  • 多个defer脚本按照script标签出现的顺序执行。
  • 没有标记async或defer的脚本会阻断Dom的解析。

window.load和window.DomContentLoaded

这是两个重要的事件,与async及defer的执行时机息息相关。

  • window.load - 标志整个页面全部加载完成,包括images,styles和JavaScript等所有外部资源。
  • window.DomContentLoaded - Html文件解析和加载完成(parsed and loaded),且所有标记为defer的js脚本全部下载并执行完成后触发,注意,该事件不会等待其他资源,比如images,subframes,或者标记为async的script下载完成。另外,该事件不会等待stylesheet完成,但是:因为defer脚本会等待stylesheet加载完才执行,而该事件又在defer脚本执行完才触发,所以如果有defer脚本存在的话,那么该事件一定会等待stylesheet加载完才触发。

问题来了

看起来async和defer没有太大的区别,那么两者分别在什么场合使用呢?

  • async一般用在与当前页面无关联的外部脚本,比如Google统计,计数脚本等。
  • defer一般用于需要操作当前页面的脚本,所以它需要等Dom解析完之后才执行。

一道小题

下面代码的输出结果是什么?

index.html

1
2
3
4
5
6
7
8
9
10
11
12
13
<body>
<div id="container"></div>
<script>
window.addEventListener("DOMContentLoaded", () => {
console.log("DOMContentLoaded event triggered");
});
window.addEventListener("load", () => {
console.log("load event triggered");
});
</script>
<script async src="./async_script.js"></script>
<script defer src="./defer_script.js"></script>
</body>

async_script.js

1
console.log('async script loaded');

defer_script.js

1
console.log('defer script loaded');

答案:

1
2
3
4
async script loaded
defer script loaded
DOMContentLoaded event triggered
load event triggered

多执行几次,你会发现,有时候输出还会是下面这样的。这充分印证了上面的结论,defer script一定在DOMContentLoaded之前执行,但是async script可能在DOMContentLoaded之前,也可能在其之后。

1
2
3
4
defer script loaded
DOMContentLoaded event triggered
async script loaded
load event triggered

References:

https://javascript.info/script-async-defer

https://html.spec.whatwg.org/multipage/scripting.html#attr-script-defer V8引擎

https://developer.mozilla.org/en-US/docs/Web/HTML/Element/script

https://developer.mozilla.org/en-US/docs/Web/API/Document/DOMContentLoaded_event

https://developer.mozilla.org/en-US/docs/Web/API/Window/load_event

https://javascript.info/onload-ondomcontentloaded

Famous NPM Packages

cross-env

cross-env is a cross platform solution to setting and using environment variables. It’s available as a command line utility as well as a Node.js module.

cross-env 是一个用于处理跨平台环境变量的 Node.js 包。在 Node.js 应用程序中,设置环境变量通常使用 process.env。然而,不同的操作系统(如 Windows、macOS 和 Linux)有不同的方式来设置和获取环境变量。cross-env 的目标是提供一种简单的方法来在跨平台上设置和获取环境变量。