Allen's Notes

Quik notes


  • 首页

  • 归档

使用 pnpm 替代 nvm 进行 Node 版本管理

发表于 2025-04-24

背景

日常工作中,我们经常需要在不同的项目中使用不同的 Node.js 版本。pnpm 提供了一个非常方便的命令 pnpm env use <node-version> 来切换 Node.js 版本。可以帮助我们轻松地在不同的项目中切换 Node.js 版本,这样我们就不需要单独下载 nvm 了。

安装 pnpm

有个注意事项,你通过 npm i -g pnpm 的形式安装是没有 pnpm env use 命令的。需要通过以下几种方式安装才行(如果你已经安装了 nvm,可以参考这个链接删除 nvm,然后再安装 pnpm):

在 Windows 上安装 pnpm

使用 PowerShell:

1
Invoke-WebRequest https://get.pnpm.io/install.ps1 -UseBasicParsing | Invoke-Expression

原官网文档地址

在 POSIX 系统上

1
curl -fsSL https://get.pnpm.io/install.sh | sh -

如果你没有安装 curl,也可以使用 wget:

1
wget -qO- https://get.pnpm.io/install.sh | sh -

原官网文档地址

使用

安装完毕需要重启下命令行工具(或者用 source ~/.bashrc or source ~/.zshrc or source ~/.bash_profile 等命令使配置生效),然后执行 pnpm -v 就可以看到版本号了。

如果你要切换 node 到版本 22 可以执行:

1
2
3
pnpm env use --global 22

node -v # v22.15.0

切换 node 到版本 16 可以执行:

1
2
3
pnpm env use --global 16

node -v # v16.20.2

还有一个小技巧,如果某个项目安装命令需要 pnpm 版本为 8,node 版本为 16,那么可以执行:

1
2
3
pnpm env use --global 16

npx pnpm@8 install

vm.$slots和vm.$scopedSlots 在渲染函数内的使用

发表于 2019-02-28 | Edited on 2025-04-24

以下例子是在 Vue@2.6.2 中运行的。

vm.$slots 与 vm.$scopedSlots 是两个与插槽相关的属性。在使用渲染函数替代 vue 的模板功能时我们可能需要用到这两个属性来实现插槽的功能。

下面利用一个例子来研究 vm.$slots vm.$scopedSlots 的区别。

1
2
3
4
5
6
7
8
9
10
11
12
13
<!-- App.vue -->

<template>
<div id="app">
<HelloWorld>
<h1 slot="head" slot-scope="{author}">this is head slot -- {{author}}</h1>
<div>this is default slot</div>
<div slot="foot">this is foot slot</div>
</HelloWorld>
</div>
</template>

<!-- 后面代码 略 -->
1
2
3
4
5
6
7
8
9
10
11
12
<!-- HelloWorld.vue -->

<template>
<div class="hello">
<slot name="head" author="Allen"></slot>
<slot name="aside"></slot>
<slot></slot>
<slot name="foot"></slot>
</div>
</template>

<!-- 后面代码 略 -->

我们在 HelloWorld.vue 内定义三个具名插槽和一个默认插槽。在 App.vue 内实例化 HelloWorld 组件时,只使用了 head default foot 插槽,其中 head 插槽是一个作用域插槽。

然后我们将 vm.$slots vm.$scopedSlots打印的到控制台上,chrome 浏览器上打印的结果如下:

vm.\$slots

vm.$slots

观察发现

  • 只有被使用到的插槽才会出现在 vm.$slots 内。
  • 默认插槽和具名插槽的值是虚拟 dom 数组(VNode[]),作用域插槽对应的值是一个 get 方法且属性修饰为不可枚举。

vm.\$scopedSlots

vm.$scopedSlots

观察发现

  • 只有被使用到的插槽才会出现在 vm.$scopedSlots 内。
  • 这里面多了 $stable _normalized 属性。具体什么作用,未知。
  • 无论是具名插槽、默认插槽、作用域插槽都在,而且值是一个工厂函数,调用后会返回虚拟 dom 数组(VNode[])。
  • 作用域插槽对应的函数内多个参数 scope,可以通过它将参数传给插槽。
  • 所有值都是可枚举的。

vm.\$slots 在开发中的使用

以下例子来自 Vue 官方文档,仅做了些许的改动。

简单的例子

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<!-- MyHeadline.vue -->

<script>
export default {
name: "MyHeadline",
props: {
level: {
type: Number,
required: true
}
},
render(createElement) {
return createElement("h" + this.level, this.$slots.default);
}
};
</script>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
<!-- App.vue -->

<template>
<div id="app">
<MyHeadline :level="1">这是level 1</MyHeadline>
<MyHeadline :level="2">这是level 2</MyHeadline>
<MyHeadline :level="3">这是level 3</MyHeadline>
<MyHeadline :level="4">这是level 4</MyHeadline>
<MyHeadline :level="5">这是level 5</MyHeadline>
<MyHeadline :level="6">这是level 6</MyHeadline>
</div>
</template>

<!-- 后面代码 略 -->

页面渲染的结果:

渲染结果

页面的 html 结构:

html结构

MyHeadline 组件的目的很明显,就是根据传入的 level 属性生成对应级别的 h 标签。使用 vue 的模板是很难实现这样的功能的,所以这里用到了 vue 的渲染函数(其实 vue 模板最终也是编译成渲染函数)。

进阶例子

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
<!-- AnchoredHeading.vue -->

<script>
var getChildrenTextContent = function(children) {
return children
.map(function(node) {
return node.children
? getChildrenTextContent(node.children)
: node.text;
})
.join("");
};

export default {
name: "AnchoredHeading",
props: {
level: {
type: Number,
required: true
}
},
render(createElement) {
// 创建 kebab-case 风格的ID
var headingId = getChildrenTextContent(this.$slots.default)
.toLowerCase()
.replace(/\W+/g, "-")
.replace(/(^\-|\-$)/g, "");

return createElement("h" + this.level, [
createElement(
"a",
{
attrs: {
name: headingId,
href: "#" + headingId
}
},
this.$slots.default
)
]);
}
};
</script>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
<!-- App.vue -->

<template>
<div id="app">
<AnchoredHeading :level="1">chapter 1 love</AnchoredHeading>
<AnchoredHeading :level="2">chapter 2 marry</AnchoredHeading>
<AnchoredHeading :level="3">chapter 3 rival</AnchoredHeading>
<AnchoredHeading :level="4">chapter 4 hate</AnchoredHeading>
<AnchoredHeading :level="5">chapter 5 divorce</AnchoredHeading>
<AnchoredHeading :level="6">chapter 6 真香</AnchoredHeading>
</div>
</template>

<!-- 后面代码 略 -->

页面渲染结果:

渲染结果2

页面 html 结构:

html结构2

vm.\$scopedSlots 在开发中的使用

简单例子

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<!-- Menu.vue -->

<script>
export default {
name: "Menu",
render(createElement) {
return createElement(
"div",
{ class: "menuBox" },
this.$scopedSlots.default({ title: "🌎 菜单栏目 🌎" })
);
}
};
</script>
1
2
3
4
5
6
7
8
9
10
<!-- MenuItem.vue -->

<script>
export default {
name: "MenuItem",
render(createElement) {
return createElement("div", { class: "MenuItem" }, this.$slots.default);
}
};
</script>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<!-- App.vue -->

<template>
<div id="app">
<menu>
<template slot-scope="{title}">
<h4>{{title}}</h4>
<menuitem>首页</menuitem>
<menuitem>产品</menuitem>
<menuitem>公司简介</menuitem>
<menuitem>联系我们</menuitem>
</template>
</menu>
</div>
</template>

<!-- 后面代码 略 -->

在Menu.vue内我们通过this.$scopedSlots.default({ title: "🌎 菜单栏目 🌎" })向作用域插槽传了参数({ title: "🌎 菜单栏目 🌎" })。然后我们在 App.vue 内使用 <template slot-scope="{title}"> 获得传过来的参数(这里用了解构赋值)。

上面只是为了展示vm.$scopedSlots在渲染函数内的使用,这个例子内的行为有点多此一举。

页面渲染结果:

scopedslots渲染结果.png

页面 html 结构:

scopedslots_html结构.png

在渲染函数内插入作用域插槽

如果我们把上面那个例子里App.vue的模板写法改成渲染函数的话,我们该如何插入作用域插槽呢?

在模板写法内插入插槽很简单,只要在组件标签内插入即可。在渲染函数内需要用到createElement方法内的scopedSlots选项。

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
<!-- App.vue -->

<script>
import Menu from "./components/Menu.vue";
import MenuItem from "./components/MenuItem.vue";

export default {
name: "app",
components: {
Menu,
MenuItem
},
render(createElement) {
return createElement("div", { attrs: { id: "app" } }, [
createElement("Menu", {
scopedSlots: {
default: ({ title }) => {
return [
createElement("h4", title),
createElement("MenuItem", "首页"),
createElement("MenuItem", "产品"),
createElement("MenuItem", "公司简介"),
createElement("MenuItem", "联系我们")
];
}
}
})
]);
}
};
</script>

这个写法和上面的模板写法的效果是等同的。

Object.defineProperty

发表于 2019-02-28 | Edited on 2025-04-24

Object.defineProperty 可以给对象定义属性,以及属性的描述。

基本使用方法

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
var person = { name: "Allen" };

// 定义属性 'gender'
// 可配置(configurable)
// 可枚举(enumerable)
// 可写(writable)
Object.defineProperty(person, "gender", {
configurable: true,
enumerable: true,
writable: true,
value: "man"
});

// 定义'_birthday'
// 我们希望生日是不可改写的,
// 也不希望枚举对象的时候出现它
Object.defineProperty(person, "_birthday", {
configurable: false,
enumerable: false,
writable: false,
value: new Date("2000-03-03")
});

// 定义'age'
// 年龄是通过生日获得的,并且不可改,
// 所以只需要设置get方法
Object.defineProperty(person, "age", {
configurable: false,
enumerable: true,
get() {
return new Date().getFullYear() - this._birthday.getFullYear();
}
});
  • configurable 设置属性描述是否可修改。如果为 false,将无法对 configurable enumerable writable 的值进行修改。有一点例外: 当 writable 为 true 时,可以将其改为 false。
  • enumerable 设置属性是否会在枚举的过程中出现。for..in Object.keys Object.values Object.entries 只会枚举属性描述 enumerable 为 true 的属性。其中 for..in 会将原型链上所有可枚举属性都遍历一遍。
  • writable 设置属性是否可写。以上面的 person 对象为例,其中的 _birthday 属性是不可写的,假如你对它赋值(person._birthday = new Date('2019-03-03'))会没有效果,如果在严格模式下还会抛出异常。
  • value 设置属性的值。
  • get 读取属性的值。以上面 person 对象为例,当使用者要获取 age 的值时(person.age),获取的值就是 get 方法返回的值。
  • set 写入属性的值。如果只有 get 没有 set 那么这个属性是不可写的。

注意 writable value是一对,get方法 set方法是一对。有 value 就没有 get set。

获取一个属性的描述

如果我们想知道一个属性描述可以使用 Object.getOwnPropertyDescriptor。

1
Object.getOwnPropertyDescriptor(person, "age");

yield-delegation

发表于 2019-02-28 | Edited on 2025-04-24

在 generator 函数内部,通过yield*语句,可以将yield委托给其他任何实现iterable的对象。

委托给 generator 函数生成的iterable对象

调用 generator 函数会返回一个实现iterable的对象(该对象同时也是一个iterator)。

通过yield* otherGenerator()可以将 generator 内的yield委托给其他 generator 生成的iterable对象。

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
function* foo() {
console.log("*foo() starting");
yield "foo 1";
yield "foo 2";
console.log("*foo() finished");
}

function* bar() {
yield "bar 1";
yield "bar 2";
yield* foo(); // `yield`-delegation!
yield "bar 3";
}

var it = bar();

it.next().value;
// "bar 1"
it.next().value;
// "bar 2"
it.next().value;
// *foo() starting
// "foo 1"
it.next().value;
// "foo 2"
it.next().value;
// *foo() finished
// "bar 3"

可以看到上面的代码中,在调用第 3 个next方法时返回的是foo里面yield的"foo 1";在调用第 5 个next时,并没有返回foo generator 隐式return的undefined,而是返回了"bar 3"。

如果foo内有显式的return语句,那么在进行 yield-delegation 时是否会返回foo return的值吗?

下面的代码在foo中添加了一个return语句。在bar内将yield* foo()表达式的值赋值给tmp变量并打印。

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
function* foo() {
console.log("*foo() starting");
yield "foo 1";
yield "foo 2";
console.log("*foo() finished");
return "foo 3";
}

function* bar() {
yield "bar 1";
yield "bar 2";
var tmp = yield* foo(); // `yield`-delegation!
console.log("在bar内打印", tmp);
yield "bar 3";
}

var it = bar();

it.next().value;
// "bar 1"
it.next().value;
// "bar 2"
it.next().value;
// *foo() starting
// "foo 1"
it.next().value;
// "foo 2"
it.next().value;
// *foo() finished
// 在bar内打印 foo 3
// "bar 3"

在第 5 次调用next方法时,可以发现foo return的"foo 3"变成了yield* foo()表达式的值,并被打印为"在bar内打印 foo 3";"bar 3"成为next方法的返回值。

委托给其他任何实现iterable的对象

generator 内部的yield*语句也能将yield委托给其他非 generator 生成的iterable对象。例如 数组就是一个实现了iterable的对象。

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
function* bar() {
console.log("inside `*bar()`:", yield "A");

// `yield`-delegation to a non-generator!
console.log("inside `*bar()`:", yield* ["B", "C", "D"]);

console.log("inside `*bar()`:", yield "E");

return "F";
}

var it = bar();

console.log("outside:", it.next().value);
// outside: A

console.log("outside:", it.next(1).value);
// inside `*bar()`: 1
// outside: B

console.log("outside:", it.next(2).value);
// outside: C

console.log("outside:", it.next(3).value);
// outside: D

console.log("outside:", it.next(4).value);
// inside `*bar()`: undefined
// outside: E

console.log("outside:", it.next(5).value);
// inside `*bar()`: 5
// outside: F

也可以委托给自己手写的iterable对象。由于 javascript 不是强类型语言,如果对象上含有Symbol.iterator方法,那么就可以将该对象当做一个iterable对象;如果对象上含有next方法,那就可以将该对象当做一个iterator对象。下面的myIterable对象即实现了Symbol.iterator方法也实现了next方法,所以它即是一个iterable又是一个iterator。

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
var myIterable = {
[Symbol.iterator]: function() {
return this;
},
num: 98,
next: function() {
var self = this;
if (self.num < 101) {
return { value: String.fromCharCode(self.num++), done: false };
} else {
return { value: String.fromCharCode(101), done: true };
}
}
};

function* bar() {
console.log("inside `*bar()`:", yield "A");

// `yield`-delegation to a non-generator!
console.log("inside `*bar()`:", yield* myIterable);

console.log("inside `*bar()`:", yield "E");

return "F";
}

var it = bar();

console.log("outside:", it.next().value);
// outside: A
console.log("outside:", it.next(1).value);
// inside `*bar()`: 1
// outside: b
console.log("outside:", it.next(2).value);
// outside: c
console.log("outside:", it.next(3).value);
// outside: d
console.log("outside:", it.next(4).value);
// inside `*bar()`: e
// outside: E
console.log("outside:", it.next(5).value);
// inside `*bar()`: 5
// outside: F

异常委托

被委托的iterator内部执行过程发生异常,如果异常被捕获,那么捕获处理完异常后还按原来的逻辑运行;如果异常未被捕获,那么异常会被抛出,异常会被抛到yield*语句那。下面是《YOU DON’T KNOW 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
54
55
function* foo() {
try {
yield "B";
} catch (err) {
console.log("error caught inside `*foo()`:", err);
}

yield "C";

throw "D";
}

function* bar() {
yield "A";

try {
yield* foo();
} catch (err) {
console.log("error caught inside `*bar()`:", err);
}

yield "E";

yield* baz();

// note: can't get here!
yield "G";
}

function* baz() {
throw "F";
}

var it = bar();

console.log("outside:", it.next().value);
// outside: A

console.log("outside:", it.next(1).value);
// outside: B

console.log("outside:", it.throw(2).value);
// error caught inside `*foo()`: 2
// outside: C

console.log("outside:", it.next(3).value);
// error caught inside `*bar()`: D
// outside: E

try {
console.log("outside:", it.next(4).value);
} catch (err) {
console.log("error caught outside:", err);
}
// error caught outside: F

递归委托

下面是《YOU DON’T KNOW JS》里的例子。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
function* foo(val) {
if (val > 1) {
// generator recursion
val = yield* foo(val - 1);
}

return yield request("http://some.url/?v=" + val);
}

function* bar() {
var r1 = yield* foo(3);
console.log(r1);
}

run(bar);

实现一个async函数

发表于 2019-02-28 | Edited on 2025-04-24

async 函数能将 promise 链式调用的写法改成同步的写法。让代码变得更简洁易懂。es6 中的 async 是语法糖,我们自己可以实现类似 async 函数的功能。

下面是myAsync的代码:

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
/**
*
* @param {Function} gen 一个generator函数
*/
function myAsync(gen) {
var args = [].slice.call(arguments, 1),
it;
it = gen.apply(this, args);

function fn(nextVal) {
if (!nextVal.done) {
return Promise.resolve(nextVal.value).then(
function(v) {
return fn(it.next(v));
},
function(err) {
return fn(it.throw(err));
}
);
} else {
return Promise.resolve(nextVal.value);
}
}

return fn(it.next());
}

下面是测试用例:

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
/**
* 下面这个函数模拟异步请求返回一个promise
* @param {String} resource 资源的路径地址
* @param {Boolean} success true表示能请求成功,false表示请求失败
* @returns {Promise}
*/
function request(resource, success) {
var randomTime = Math.ceil(Math.random() * 10) * 100;
console.log(resource + " spend:", randomTime);
return new Promise(function(resolve, reject) {
setTimeout(function() {
if (success) {
resolve(resource);
} else {
reject(resource);
}
}, randomTime);
});
}

/**
* 下面的generator函数写平常的业务逻辑。
* 如果下面的console.time得出来的时间与三条请求的时间之和
* 大约相等,说明main函数内的代码以类似同步的方式执行。
*/
function* main(returnValue) {
console.time("count");
var rq1 = yield request("/request1", true);
var rq2 = yield request("/request2", true);

try {
var rq3 = yield request("/request3", false);
} catch (e) {
console.log("this is an error", e);
}

var rq4 = yield request("/request4", true);

console.timeEnd("count");
console.log(returnValue);
return returnValue;
}

myAsync(main, "hello my async");

可以看出main函数内部写法和 es6 的 async 函数很相似,相当于将await替换成yield。

下面是 generator 中出现yield-delegation 中的测试。

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
//这里依赖上面的 myAsync 和 request 方法

function* subTask() {
var rq2 = yield request("/request2", true);
var rq3 = yield request("/request3", true);
var rq4 = yield request("/request4", false); // promise rejected
var rq5 = yield request("/request5", true);
}

function* main(returnValue) {
console.time("count");
var rq1 = yield request("/request1", true);

try {
yield* subTask();
} catch (err) {
console.log("catch error from subTask.", err);
}

var rq5 = yield request("/request6", true);

console.timeEnd("count");
return returnValue;
}

myAsync(main, "hello my async");

上面的 main 方法内yield*语句将yield委托给了 subTask 方法,并且subTask内 req4 的 promise 被 rejected。此时的 myAsync 还能正常运行。

下面是 generator 中出现递归委托的测试。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
//这里依赖上面的 myAsync 和 request 方法

function* subTask(num) {
if (num < 3) {
yield* subTask(num + 1);
}

yield request("/request" + num, true);
}

function* main(returnValue) {
console.time("count");
var rq1 = yield request("/request1", true);

yield* subTask(2);

var rq4 = yield request("/request4", true);

console.timeEnd("count");
return returnValue;
}

myAsync(main, "hello my async");

generator内部抛出异常时的执行机制

发表于 2019-02-28 | Edited on 2025-04-24

执行 generator 函数会返回一个 iterator,iterator 调用next方法会返回一个值。如果next执行过程中出现异常是否还会返回值呢?如果返回值的话,会返回那个值呢?

情况一:捕获的异常

main generator 内出现了一个被捕获的异常。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
function* main() {
yield 1;
try {
undefinedValue.sayHello(); // 这里会出现ReferenceError
yield 2;
} catch (e) {
console.log("捕获了异常");
}
yield 3;
return 4;
}
var it = main();
it.next();
// {value: 1, done: false}
it.next();
// 捕获了异常
// {value: 3, done: false}
it.next();
// {value: 4, done: true}

第二个next方法被调用时,打印了“捕获了异常”的字样,并返回了 {value: 3, done: false}。说明yield 2被跳过,并执行yield 3。

情况二:未捕获的异常

main generator 内出现了一个未被捕获的异常。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
function *main(){
yield 1;

undefinedValue.sayHello();// 这里会出现ReferenceError
yield 2;

yield 3;
return 4;
}
var it=main();
it.next();
// {value: 1, done: false}
it.next()
console.log('测试是否会打印')
// Uncaught ReferenceError: undefinedValue is not defined
// at main (<anonymous>:4:5)
// at main.next (<anonymous>)
// at <anonymous>:1:4
it
// main {<closed>}

第二个next方法被调用时直接抛出了异常,并且后面的console.log('测试是否会打印')没被执行。再次查看it时,发现它的状态变成了closed。

情况三:通过 iterator 的throw方法产生的异常

我们在第二个next方法之后调用 iterator 的throw方法产生一个异常,该异常刚好被try..catch捕获。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
function* main() {
yield 1;
try {
yield 2;
yield 3;
} catch (e) {
console.log(e);
}
yield 4;
return 5;
}
var it = main();
it.next();
// {value: 1, done: false}
it.next();
// {value: 2, done: false}
it.throw("异常");
// 异常
// {value: 4, done: false}
it.next();
// {value: 5, done: true}

我们在第二个next方法后面调用 iterator 的throw方法产生一个异常,此时的异常出现在yield 2的位置,并被try..catch捕获处理,所以程序跳过了yield 3执行了yield 4。

总结

如果 generator’s iterator 调用next的过程中遇到被捕获的异常,它处理完异常后会继续运行,直到碰到yield return或函数的结尾。如果遇到的是未被捕获的异常,它将抛出异常,并且 iterator 的状态变为closed。通过 iterator 的throw方法产生的异常和普通的异常是一样的执行逻辑。

javascript中内建类型的判断

发表于 2019-02-28 | Edited on 2025-04-24

javascript 中内建类型(built-in types)有七个,包括 null undefined boolean number string symbol object。其中除了 object 外,另外的 6 个类型都是原始(primitive)类型。javascript 内的函数和数组都是 object 类型。

在平时工作中,我们需要判断数据的类型。

用 typeof 来区分内建类型

1
2
3
4
5
6
7
8
9
10
11
typeof null; // "object"
typeof undefined; // "undefined"
typeof true; // "boolean"
typeof 10; // "number"
typeof "hello"; // "string"
typeof Symbol(); // "symbol"
typeof { name: "Allen" }; // "object"

typeof [1, 2, 3]; // "object"
typeof function hi() {}; // "function"
typeof NaN; // "number"

从上面的结果中可以看出 typeof 返回的结果基本是正确的,除了 typeof null 和 typeof function。

typeof nulll 返回 object 是早期语言设计上的错误。由于太多代码利用了这个错误特性,所以浏览器基本上没可能去修正这个 bug 了,不然会导致更多的 bug。

typeof function hi(){} 返回 function 看似很合理,其实 function 在 JS 内并不是顶级的内建类型,它是 object 的子类型,所以它返回 object 会更合理些。

javascript中的稀疏数组与empty slot

发表于 2019-02-28 | Edited on 2025-04-24
1
2
3
4
5
6
7
8
9
var a = [];

a[0] = 1;
// no `a[1]` slot set here
a[2] = 3;

a[1]; // undefined;

a.lenght; // 3

这里产生了一个稀疏数组,a[1]这里是一个 empty slot。在谷歌浏览器控制台内显示样子如下:

稀疏数组.png

delete 操作符也能产生一个 empty,以上面的数组a为例子。

1
2
3
delete a[2];

a; // [1, empty x 2]

在谷歌浏览器控制台内显示样子如下:

delete一个属性.png

用new Array(3)方法也会生成 empty slot。

empty和刻意赋值为undefined有差别吗?

从第一个例子看到,a[1]返回的值是undefined。那么这和我们直接赋值a[1]为undefined有差别吗?

有差别。看下面的例子:

1
2
3
4
5
var a = [1, , 3]; // [1,empty,3]
var b = [1, undefined, 3]; // [1,undefined,3]

a.map(v => v * 2); // [2,empty,6]
b.map(v => v * 2); // [2, NaN, 6]

我们发现,map方法会跳过a数组的 empty slot,而不会跳过被显式设为undefined的b[1]项。

不仅仅是map方法,还有很多与遍历数组相关的操作都为跳过 empty slot。

我测试了所有我能想到与数组相关的操作,列出了以下两个列表。

会跳过 empty slot 的操作

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
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
var a = [1, , 3];
// [1,empty,3]
var b = [1, undefined, 3];
// [1,undefined,3]

// map方法
a.map(v => v * 2);
// [2,empty,6]
b.map(v => v * 2);
// [2, NaN, 6]

// forEach方法
a.forEach((v, k) => console.log(v, k));
// 1 0
// 3 2
b.forEach((v, k) => console.log(v, k));
// 1 0
// undefined 1
// 3 2

// filter方法
a.filter(v => typeof (v + "a") === "string");
// [1,3]
b.filter(v => typeof (v + "a") === "string");
// [1, undefined, 3]

// some方法
a.some(v => typeof v === "undefined");
// false
a.some(v => typeof v === "undefined");
// true

// every方法
a.every(v => typeof v === "number");
// true
b.every(v => typeof v === "number");
// false

// reduce方法
a.reduce((ac, v) => ac + v, "");
// "13"
b.reduce((ac, v) => ac + v, "");
// "1undefined3"

// reduceRight方法
a.reduceRight((ac, v) => ac + v, "");
// "31"
b.reduceRight((ac, v) => ac + v, "");
// "3undefined1"

// indexOf方法
a.indexOf(undefined); // false
b.indexOf(undefined); // true

// for..in 循环
for (let i in a) {
console.log(i);
}
// 0
// 2
for (let i in b) {
console.log(i);
}
// 0
// 1
// 2

// in 操作符
1 in a;
// false
1 in b;
// true

// Object.keys方法
Object.keys(a);
// ["0", "2"]
Object.keys(b);
// ["0", "1", "2"]

// Object.values方法
Object.values(a);
// [1, 3]
Object.values(b);
// [1, undefined, 3]

// Object.entries方法
Object.entries(a);
// [["0",1],["2",3]]
Object.entries(b);
// [["0",1],["1", undefined],["2",3]]

不会跳过 empty slot 的操作

以下列出我们可能误以为会跳过 empty slot 的操作,实际上下面的操作不会跳过 empty slot。尤其是下面的for..of操作需要特别注意。

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
var a = [1,,3]; // [1,empty,3]
var b = [1,undefined,3]; // [1,undefined,3]

// find方法,该方法返回找到的第一个符合条件的值
// find回调方法内理应返回一个boolean值,下面的写法是没什么用的,仅仅只是为了测试
a.find(function(v){console.log(v)});
// 1
// undefined
// 3
b.find(function(v){console.log(v)});
// 1
// undefined
// 3

// findIndex方法,同上。
// 不同的是findIndex返回的是索引值
a.findIndex(function(v){console.log(v)});
// 1
// undefined
// 3
b.findIndex(function(v){console.log(v)});
// 1
// undefined
// 3

// for..of 循环
for(let v of a){console.log(v)}
// 1
// undefined
// 3
for(let v of b){console.log(v)}
// 1
// undefined
// 3

// 下面的三个方法看起来与Object构造函数上的keys values entries方法很像,
// 但是他们不会跳过empty slot,而且返回的值是Iterator而不是数组。
// 我们可以用扩展运算符将Iterator内的值取出,并放到数组内
// Array.prototype.keys方法
[...a.keys()]
// [0, 1, 2]
[...b.keys()]
// [0, 1, 2]

// Array.prototype.values方法
[...a.values()]
// [1, undefined, 3]
[...b.values()]
// [1, undefined, 3]

// Array.prototype.entries方法
[...a.entries()]
// [[0,1],[1,undefined], [2,3]]
[...b.entries()]
// [[0,1],[1,undefined], [2,3]]

总结

不给数组内的某个 slot 赋值,或使用delete操作数组,或用new Array生成数组都会生成 empty slot。大部分与遍历相关的方法和操作会跳过 empty slot;但有几个操作例外,尤其要注意for..of循环。

javascript中的undefined

发表于 2019-02-28 | Edited on 2025-04-24

javascript 中的undefined有很多的迷惑性,非常让人难以理解。

undefined 等同于 “未声明” 吗?

undefined中文意思为“未定义”,我们可能就会认为undefined就是变量“未声明”。

但其实并不是这样,看下面的例子:

1
2
3
4
var a;

a; // undefined
b; // ReferenceError: b is not defined

我们只声明了变量a,但获取a时,返回的结果是undefined,获取b时抛出了一个异常。

这说明undefined不是“未声明”的意思,在引用“未声明”的变量时会抛出ReferenceError的异常。

安全守卫(safety guard)

上面的例子说明了“未声明”的变量在引用时会抛出异常。但是我们的代码有时需要依赖全局的某个变量,而且我们不知道该变量是否已被声明。我们可能会像下面那样写一个if语句来判断该变量是否存在。

1
2
3
if (DEBUG) {
console.log("Debugging is starting");
}

在上面例子中,如果DEBUG并未被声明的话,会抛出ReferenceError并打断了代码的运行。我们需要的是当DEBUG为 true 时,执行内部的逻辑,DEBUG为 falsy 或“未声明”时,跳过里面的代码,而不是抛出异常。

以下有多种方式实现一个安全守卫,防止 javascript 抛出异常。

方法 1:typeof
1
2
3
4
var a;

typeof a; // "undefined"
typeof b; // "undefined"

从上面的例子中可以发现,无论是已声明的还是未声明的变量,typeof 返回的结果都是undefined,而且typeof b也不会抛出异常。

可以利用这个特性,作为一个 safety guard。代码如下:

1
2
3
if (typeof DEBUG !== "undefined") {
console.log("Debugging is starting");
}
方法 2:对象属性
1
2
3
4
5
var person = {
name: "Allen"
};

person.gender; // undefined

上面的例子中可以得出,访问一个未定义的对象属性不会抛出异常。

我们知道,javascript 中全局的变量,其实就是window对象上的属性。结合上面得出的结论,我们可以利用该特性,实现 safety guard。

1
2
3
if (window.DEBUG) {
console.log("Debugging is starting");
}

但是,这个方法,不利与跨平台,应为在 nodejs 中没有window对象。

javascript API JSON

发表于 2019-02-28 | Edited on 2025-04-24

所有 javascript 内所有 JSON-safe 的值都可以被 JSON.stringify。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
JSON.stringify(42);
// "42"

JSON.stringify("42");
// ""42"" (a string with a quoted string value in it)

JSON.stringify(null);
// "null"

JSON.stringify(true);
// "true"

JSON.stringify({ name: "Allen", hobby: "painting" });
// "{"name":"Allen","hobby":"painting"}"

JSON-safe 的值

什么是 JSON-safe 的值呢?我的理解是不方便跨语言的值都不是 JSON-safe 的。例如 undefined function symbol 还有循环引用的 object。

1
2
3
4
5
6
7
8
9
10
11
12
13
// 非JSON-safe的值会被略掉
JSON.stringify(undefined);

// 数组内非JSON-safe的值会被替换为null
JSON.stringify([1, undefined, "a"]); // "[1,null,"a"]"

// 对象内非JSON-safe的值会被排除掉
JSON.stringify({ name: "Allen", age: undefined }); // "{"name":"Allen"}"

// 循环引用的对象会报错
var a = { name: "outer" };
a.b = { name: "inner", outer: a };
JSON.stringify(a);

toJSON 方法

当对一个对象直接 stringify 的结果不是我们想要的,或者那个对象含循环引用时,我们可以在该对象上定义一个toJSON方法来自定义它需要返回的结果。返回的结果得是 JSON-safe 的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
var o = {};

var a = {
b: 42,
c: o,
d: function() {}
};

// create a circular reference inside `a`
o.e = a;

// would throw an error on the circular reference
// JSON.stringify( a );

// define a custom JSON value serialization
a.toJSON = function() {
// only include the `b` property for serialization
return { b: this.b };
};

JSON.stringify(a); // "{"b":42}"
JSON.stringify(o); // "{"e":{"b":42}}"

stringify 的第二个参数 replacer

1
2
3
4
5
6
7
8
9
10
11
12
var a = {
b: 42,
c: "42",
d: [1, 2, 3]
};

JSON.stringify(a, ["b", "c"]); // "{"b":42,"c":"42"}"

JSON.stringify(a, function(k, v) {
if (k !== "c") return v;
});
// "{"b":42,"d":[1,2,3]}"

如上所示,如果在第二个参数传入了一个字符串数组,那么 stringify 的过程中就会只保留对应的键值对。

如果在第二个参数传入一个函数,那么可以自定义需要保留的键值对。

stringify 的第三个参数 space

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
var a = {
b: 42,
c: "42",
d: [1, 2, 3]
};

JSON.stringify(a, null, 3);
// "{
// "b": 42,
// "c": "42",
// "d": [
// 1,
// 2,
// 3
// ]
// }"

JSON.stringify(a, null, "-----");
// "{
// -----"b": 42,
// -----"c": "42",
// -----"d": [
// ----------1,
// ----------2,
// ----------3
// -----]
// }"

如上所示,第三个参数可以是数字,表示缩进的空格个数;也可以是字符串,表示占位的符号。

12
博主

博主

记录学习的过程

18 日志
3 标签
© 2025 Allen Bai
由 Hexo 强力驱动 v3.8.0
|
主题 – NexT.Pisces v7.0.0