diff --git a/404.html b/404.html new file mode 100644 index 0000000..4605c30 --- /dev/null +++ b/404.html @@ -0,0 +1,33 @@ + + + + + + + + + 瓢儿白施肥记 + + + + +

404

That's a Four-Oh-Four.
返回首页
+ + + diff --git a/assets/01.html-4acb4a17.js b/assets/01.html-4acb4a17.js new file mode 100644 index 0000000..626608e --- /dev/null +++ b/assets/01.html-4acb4a17.js @@ -0,0 +1 @@ +const e=JSON.parse('{"key":"v-415e2210","path":"/frontend/js/red-book/01.html","title":"红宝书","lang":"zh-CN","frontmatter":{"title":"红宝书","prev":{"text":"目录","link":"./README.md"},"next":{"text":"HTML 中的 JavaScript","link":"./02.md"}},"headers":[{"level":2,"title":"什么是 JavaScript","slug":"什么是-javascript","link":"#什么是-javascript","children":[{"level":3,"title":"历史","slug":"历史","link":"#历史","children":[]},{"level":3,"title":"JavaScript 实现","slug":"javascript-实现","link":"#javascript-实现","children":[{"level":4,"title":"ECMAScript","slug":"ecmascript","link":"#ecmascript","children":[]},{"level":4,"title":"DOM","slug":"dom","link":"#dom","children":[]},{"level":4,"title":"BOM","slug":"bom","link":"#bom","children":[]}]}]}],"git":{"updatedTime":1687690350000},"filePathRelative":"frontend/js/red-book/01.md"}');export{e as data}; diff --git a/assets/01.html-9c89b4ec.js b/assets/01.html-9c89b4ec.js new file mode 100644 index 0000000..7a2bb28 --- /dev/null +++ b/assets/01.html-9c89b4ec.js @@ -0,0 +1 @@ +import{_ as i,o as a,c as t,e as r}from"./app-b4fb6edd.js";const c={},e=r('

什么是 JavaScript

小结

1. JavaScript 由哪三部分组成?

2. JavaScript 和 ECMAScript 有什么关系?

ECMAScript 是 JavaScript 的标准化规范,JavaScript 是 ECMAScript 的一个实现

历史

JavaScript 实现

JavaScript 不限于 ECMA-262 所定义的那样,它包含以下几个部分:

ECMAScript

ECMA-262 定义了什么?

ECMAScript 是实现 ECMA-262 这个规范描述的所有方面的一门语言,JavaScript 和 Adobe ActionScript 都实现了 ECMAScript

Web 浏览器是 ECMAScript 的一种宿主环境,宿主环境提供 ECMAScript 的基准实现和与环境自身交互必需的扩展,扩展(如 DOM)使用 ECMAScript 的核心类型和语法提供特定于环境的额外功能

DOM

文档对象模型(Document Object Model)是一个应用编程接口(API),DOM 通过创建表示文档的树,让开发者可以控制网页的结构和内容,使用 DOM API 可以轻松删除、添加、替换、修改节点

BOM

浏览器对象模型 BOM,用于支持访问和操作浏览器的窗口

',16),p=[e];function l(d,o){return a(),t("div",null,p)}const n=i(c,[["render",l],["__file","01.html.vue"]]);export{n as default}; diff --git a/assets/02.html-9d224a5a.js b/assets/02.html-9d224a5a.js new file mode 100644 index 0000000..deb8610 --- /dev/null +++ b/assets/02.html-9d224a5a.js @@ -0,0 +1 @@ +const l=JSON.parse('{"key":"v-4312faaf","path":"/frontend/js/red-book/02.html","title":"红宝书","lang":"zh-CN","frontmatter":{"title":"红宝书","prev":{"text":"什么是 JavaScript","link":"./01.md"},"next":{"text":"语言基础","link":"./03.md"}},"headers":[{"level":2,"title":"HTML 中的 JavaScript","slug":"html-中的-javascript","link":"#html-中的-javascript","children":[{"level":3,"title":"script","slug":"script","link":"#script","children":[{"level":4,"title":"标签位置","slug":"标签位置","link":"#标签位置","children":[]},{"level":4,"title":"推迟执行脚本","slug":"推迟执行脚本","link":"#推迟执行脚本","children":[]},{"level":4,"title":"异步执行脚本","slug":"异步执行脚本","link":"#异步执行脚本","children":[]},{"level":4,"title":"动态加载脚本","slug":"动态加载脚本","link":"#动态加载脚本","children":[]}]},{"level":3,"title":"noscript","slug":"noscript","link":"#noscript","children":[]}]}],"git":{"updatedTime":1688124671000},"filePathRelative":"frontend/js/red-book/02.md"}');export{l as data}; diff --git a/assets/02.html-aee8f56b.js b/assets/02.html-aee8f56b.js new file mode 100644 index 0000000..b0db4f3 --- /dev/null +++ b/assets/02.html-aee8f56b.js @@ -0,0 +1,15 @@ +import{_ as s,o as n,c as a,e as t}from"./app-b4fb6edd.js";const e={},c=t(`

HTML 中的 JavaScript

小结

1. script 元素有哪些属性?

async、defer、type、src、crossorigin、integrity

2. noscript

当浏览器不支持脚本或禁用脚本时,noscript 元素会显示出来

script

<script> 元素有以下属性:

使用 <script> 的方式有两种:

  1. 直接在页面中嵌入行内 JavaScript 代码
<script>
+	function sayScript() {
+    // 出现字符串 </script> 时,需要转义
+		console.log('<\\/script>')
+	}
+</script>
+



 


  1. 通过 src 属性包含外部 JavaScript 文件

注:使用了 src 属性的 <script> 元素不应该在其 <script></script> 标签之间再包含额外的 JavaScript 代码,否则会忽略这些额外的代码

标签位置

  1. 放在页面的 <head> 元素中
  2. 放在页面的 <body> 元素中的页面内容后面

推迟执行脚本

<script> 元素中设置 defer 属性(只适用于外部脚本),告诉浏览器立即下载,但延迟执行(解析到 </html> 后)

HTML5 规范要求脚本按照它们出现的先后顺序执行,因此第一个延迟脚本会先于第二个延迟脚本执行,而这两个脚本会先于 DOMContentLoaded 事件执行。但是延迟脚本不一定会按照顺序执行,也不一定会在 DOMContentLoaded 事件触发前执行,因此最好只包含一个延迟脚本

异步执行脚本

<script> 元素中设置 async 属性(只适用于外部脚本),告诉浏览器立即下载,与 defer 的区别是,异步脚本不保证按照它们的先后顺序执行

给脚本添加 async 属性的目的是告诉浏览器,不必等待其他脚本,也不必阻塞文档呈现,立即下载并执行脚本。从这个意义上讲,标记为 async 的脚本不应该在加载期间修改 DOM。异步脚本一定会在页面的 load 事件前执行,但可能会在 DOMContentLoaded 事件触发之前或之后执行

动态加载脚本

通过向 DOM 中动态添加 <script> 元素加载指定的脚本

const script = document.createElement('script')
+script.src = 'gibberish.js'
+document.head.appendChild(script)
+

默认情况下,动态添加的脚本是异步执行的(相当于 async 为 true)

因为所有浏览器都支持 createElement() 方法,但不是所有浏览器都支持 async 属性,因此如果要统一动态脚本的加载行为,可以明确将其设置为同步加载:

const script = document.createElement('script')
+script.src = 'gibberish.js'
+script.async = false
+document.head.appendChild(script)
+

以这种方式获取的资源对浏览器预加载器是不可见的,严重影响它们在资源获取队列中的优先级,可能会影响性能

要想让预加载器知道这些动态请求文件的存在,可以在文档头部显示生声明它们:

<link rel="preload" href="gibberish.js" />
+

noscript

<noscript> 元素可以包含任何可以出现在 <body> 元素中的 HTML 元素,它的作用是提供替代内容,只有以下情况下才会显示:

`,30),p=[c];function i(o,l){return n(),a("div",null,p)}const d=s(e,[["render",i],["__file","02.html.vue"]]);export{d as default}; diff --git a/assets/03.html-2aeb7c67.js b/assets/03.html-2aeb7c67.js new file mode 100644 index 0000000..7c34625 --- /dev/null +++ b/assets/03.html-2aeb7c67.js @@ -0,0 +1,118 @@ +import{_ as t,r as p,o as l,c as o,a as s,d as n,b as i,e as a}from"./app-b4fb6edd.js";const c={},u=a(`

语言基础

小结

1. 语法特点?

2. let、var 和 const

3. 数据类型

ECMAScript 标准定义了 8 种数据类型:

4. null 和 undefined

5. 转布尔值为 false 的值''0NaNnullundefined

6. 转数值

以下三个函数最终得到的都是十进制数或者 NaN

Number()

parseInt() 区别与 Number()

parseFloat() 区别与 parseInt()

7. 转字符串

ECMA-262 以一个名为 ECMAScript 的伪语言(pseudo language)的形式,定义了 JavaScript 的所有这些方面

数据类型

Number 类型

Number 类型使用 IEEE754 格式来表示整数和浮点值

超出 Number.MAX_VALUE 的值会被转换为 Infinity

isFinite() 函数可以用来判断一个数值是否有限

const result = Number.MAX_VALUE + Number.MAX_VALUE
+console.log(isFinite(result)) // false
+

NaN 是一个特殊的数值,表示一个本来要返回数值的操作失败了

console.log(0 / 0) // NaN
+console.log(Infinity / Infinity) // NaN
+console.log(5 / 0) // Infinity
+
+console.log(NaN == NaN) // false
+
+// isNaN() 函数可以用来判断一个数值是否是 NaN
+console.log(isNaN(NaN)) // true
+

String 类型

String 类型表示零或多个 16 位 Unicode 字符序列

模板字面量标签函数:

const a = 6
+const b = 9
+
+const zipTag = (strings, ...expressions) => {
+	console.log(strings)
+	console.log(expressions)
+	return expressions.reduce((prev, cur, i) => {
+		return prev + cur + strings[i + 1]
+	}, strings[0])
+}
+const taggedResult = zipTag\`\${a} + \${b} = \${a + b}\`
+
+console.log(taggedResult)
+

String.raw() 函数:获取原始字符串

console.log(String.raw\`Hi\\n\${2 + 3}!\`) // Hi\\n5!
+

Symbol 类型

Symbol 的用途是确保对象属性使用唯一标识符,没有字面量语法

Object 类型

每个 Object 实例都有如下属性和方法:

操作符

一元操作符

  1. 递增 ++ 、递减 -- 操作符

  2. 一元加和减

位操作符

ECMAScript 中的所有数值都以 IEEE754 64 位格式存储,但是位操作符先把数值转换为 32 位整数,再进行操作,最后再将结果转换回 64 位

`,28),r=s("strong",null,"符号位",-1),d=a(`

正值以真正的二进制格式存储,负值则以二进制补码形式存储

位操作应用到非数值,首先会使用 Number() 函数将该值转换为数值,然后再应用位操作

ECMAScript 中的所有整数都表示为有符号数。特殊值 NaN 和 Infinity 在位操作中都会被当成 0

  1. 按位非(~)

  2. 按位与(&)

  3. 按位或(|)

  4. 按位异或(^)

  5. 左移(<<)

  6. 有符号的右移(>>)

  7. 无符号右移(>>>)

布尔操作符

  1. 逻辑非(!)

  2. 逻辑与(&&)

  3. 逻辑或(||)

乘性操作符

如果操作数不是数值,会先使用 Number() 函数将其转换为数值,再进行操作

  1. 乘法(*)

  2. 除法(/)

  3. 求模(%)

指数操作符

  1. 指数操作符(**)

加性操作符

  1. 加法操作符(+)

  2. 减法操作符(-)

关系操作符

<><=>=

相等操作符

  1. 相等和不相等(== 和 !=)

  2. 全等和不全等(=== 和 !==)

条件操作符

variable = boolean_expression ? true_value : false_value

赋值操作符

简单赋值用 = 表示

每个数学操作符以及其他一些操作符都有对应的复合赋值操作符,如 +=-=*=/=%=**=<<=>>=>>>=

逗号操作符

逗号操作符可以用来在一条语句中执行多个操作,如

let num1 = 1,
+	num2 = 2,
+	num3 = 3
+

在赋值语句中,逗号操作符会返回表达式中的最后一项,如

let num = (5, 1, 4, 8, 0) // num 的值为 0
+

语句

if 语句

if (expression) statement1 else statement2
+

do-while 语句

do {
+	statement
+} while (expression)
+

while 语句

while (expression) statement
+

for 语句

for (initialization; expression; post - loop - expression) statement
+

for-in 语句

for (property in expression) statement
+

ECMAScript 中对象的属性是无序的,因此通过 for-in 循环输出的属性名的顺序是不可预测的。换句话说,所有可枚举的属性都会返回一次,但返回的顺序可能会因浏览器而异

for-of 语句

for (variable of object) statement
+

for-of 循环会按照可迭代对象的 next() 方法产生值的顺序迭代元素。如果尝试迭代的变量不支持迭代,则会抛出错误

标签语句

用于给语句加标签

label: statement
+

在下面的例子中, start 是一个标签,可以在后面通过 break 或 continue 语句引用它

start: for (let i = 0; i < count; i++) {
+	console.log(i)
+}
+

break 和 continue 语句

break 和 continue 都可以与标签语句一起使用,返回代码中特定的位置。通常是在嵌套循环中,如:

let num = 0
+outermost: for (let i = 0; i < 10; i++) {
+	for (let j = 0; j < 10; j++) {
+		if (i === 5 && j === 5) {
+			break outermost
+		}
+		num++
+	}
+}
+console.log(num) // 55
+

with 语句

严格模式下不允许使用 with 语句

with 语句影响性能且难于调试其中的代码,因此不建议使用

with 语句的作用是将代码的作用域设置到一个特定的对象中

with (expression) statement
+

使用 with 语句的主要场景是针对一个对象反复操作,如:

let qs = location.search.substring(1)
+let hostName = location.hostname
+let url = location.href
+

上面的每一行都包含 location 对象,如果使用 with 语句,可以简化为:

with (location) {
+	let qs = search.substring(1)
+	let hostName = hostname
+	let url = href
+}
+

switch 语句

switch (expression) {
+	case value1:
+		statement
+		break
+	case value2:
+		statement
+		break
+	case value3:
+		statement
+		break
+	default:
+		statement
+}
+

函数

最佳实践是函数要么返回值,要么不返回值。只在某个条件下返回值的函数会带来麻烦,尤其是调试时

严格模式对函数有一些限制:

`,65);function k(v,m){const e=p("Badge");return l(),o("div",null,[u,s("p",null,[n("有符号整数使用 32 位的前 31 位表示整数值,第 32 位(第一位 表示 2"),i(e,{type:"tip",text:"0",vertical:"top"}),n(")表示符号,0 表示正数,1 表示负数。这一位称为 "),r,n(",它的值决定了数值其余部分的格式")]),d])}const h=t(c,[["render",k],["__file","03.html.vue"]]);export{h as default}; diff --git a/assets/03.html-6e4a25f7.js b/assets/03.html-6e4a25f7.js new file mode 100644 index 0000000..d17776e --- /dev/null +++ b/assets/03.html-6e4a25f7.js @@ -0,0 +1 @@ +const l=JSON.parse('{"key":"v-44c7d34e","path":"/frontend/js/red-book/03.html","title":"红宝书","lang":"zh-CN","frontmatter":{"title":"红宝书","prev":{"text":"HTML 中的 JavaScript","link":"./02.md"},"next":{"text":"变量、作用域与内存","link":"./04.md"}},"headers":[{"level":2,"title":"语言基础","slug":"语言基础","link":"#语言基础","children":[{"level":3,"title":"数据类型","slug":"数据类型","link":"#数据类型","children":[{"level":4,"title":"Number 类型","slug":"number-类型","link":"#number-类型","children":[]},{"level":4,"title":"String 类型","slug":"string-类型","link":"#string-类型","children":[]},{"level":4,"title":"Symbol 类型","slug":"symbol-类型","link":"#symbol-类型","children":[]},{"level":4,"title":"Object 类型","slug":"object-类型","link":"#object-类型","children":[]}]},{"level":3,"title":"操作符","slug":"操作符","link":"#操作符","children":[{"level":4,"title":"一元操作符","slug":"一元操作符","link":"#一元操作符","children":[]},{"level":4,"title":"位操作符","slug":"位操作符","link":"#位操作符","children":[]},{"level":4,"title":"布尔操作符","slug":"布尔操作符","link":"#布尔操作符","children":[]},{"level":4,"title":"乘性操作符","slug":"乘性操作符","link":"#乘性操作符","children":[]},{"level":4,"title":"指数操作符","slug":"指数操作符","link":"#指数操作符","children":[]},{"level":4,"title":"加性操作符","slug":"加性操作符","link":"#加性操作符","children":[]},{"level":4,"title":"关系操作符","slug":"关系操作符","link":"#关系操作符","children":[]},{"level":4,"title":"相等操作符","slug":"相等操作符","link":"#相等操作符","children":[]},{"level":4,"title":"条件操作符","slug":"条件操作符","link":"#条件操作符","children":[]},{"level":4,"title":"赋值操作符","slug":"赋值操作符","link":"#赋值操作符","children":[]},{"level":4,"title":"逗号操作符","slug":"逗号操作符","link":"#逗号操作符","children":[]}]},{"level":3,"title":"语句","slug":"语句","link":"#语句","children":[{"level":4,"title":"if 语句","slug":"if-语句","link":"#if-语句","children":[]},{"level":4,"title":"do-while 语句","slug":"do-while-语句","link":"#do-while-语句","children":[]},{"level":4,"title":"while 语句","slug":"while-语句","link":"#while-语句","children":[]},{"level":4,"title":"for 语句","slug":"for-语句","link":"#for-语句","children":[]},{"level":4,"title":"for-in 语句","slug":"for-in-语句","link":"#for-in-语句","children":[]},{"level":4,"title":"for-of 语句","slug":"for-of-语句","link":"#for-of-语句","children":[]},{"level":4,"title":"标签语句","slug":"标签语句","link":"#标签语句","children":[]},{"level":4,"title":"break 和 continue 语句","slug":"break-和-continue-语句","link":"#break-和-continue-语句","children":[]},{"level":4,"title":"with 语句","slug":"with-语句","link":"#with-语句","children":[]},{"level":4,"title":"switch 语句","slug":"switch-语句","link":"#switch-语句","children":[]}]},{"level":3,"title":"函数","slug":"函数","link":"#函数","children":[]}]}],"git":{"updatedTime":1688357509000},"filePathRelative":"frontend/js/red-book/03.md"}');export{l as data}; diff --git a/assets/04.html-cf0be750.js b/assets/04.html-cf0be750.js new file mode 100644 index 0000000..4200dcd --- /dev/null +++ b/assets/04.html-cf0be750.js @@ -0,0 +1 @@ +const l=JSON.parse('{"key":"v-467cabed","path":"/frontend/js/red-book/04.html","title":"红宝书","lang":"zh-CN","frontmatter":{"title":"红宝书","prev":{"text":"语言基础","link":"./03.md"},"next":{"text":"基本引用类型","link":"./05.md"}},"headers":[{"level":2,"title":"变量、作用域与内存","slug":"变量、作用域与内存","link":"#变量、作用域与内存","children":[{"level":3,"title":"原始值与引用值","slug":"原始值与引用值","link":"#原始值与引用值","children":[]},{"level":3,"title":"执行上下文与作用域","slug":"执行上下文与作用域","link":"#执行上下文与作用域","children":[{"level":4,"title":"作用域链增强","slug":"作用域链增强","link":"#作用域链增强","children":[]},{"level":4,"title":"变量声明","slug":"变量声明","link":"#变量声明","children":[]}]},{"level":3,"title":"垃圾回收","slug":"垃圾回收","link":"#垃圾回收","children":[{"level":4,"title":"标记清理","slug":"标记清理","link":"#标记清理","children":[]},{"level":4,"title":"引用计数","slug":"引用计数","link":"#引用计数","children":[]},{"level":4,"title":"内存管理","slug":"内存管理","link":"#内存管理","children":[]}]}]}],"git":{"updatedTime":1688469859000},"filePathRelative":"frontend/js/red-book/04.md"}');export{l as data}; diff --git a/assets/04.html-e5c01e6e.js b/assets/04.html-e5c01e6e.js new file mode 100644 index 0000000..c08129c --- /dev/null +++ b/assets/04.html-e5c01e6e.js @@ -0,0 +1,28 @@ +import{_ as c,r as o,o as p,c as i,a as n,d as a,b as s,w as e,e as r}from"./app-b4fb6edd.js";const u={},d=r(`

变量、作用域与内存

原始值与引用值

执行上下文与作用域

执行上下文(execution context,EC):JavaScript 代码被解析和执行时所在环境的抽象概念

全局上下文是最外层的上下文,根据 ECMAScript 实现的宿主环境,表示全局上下文的对象可能不一样,浏览器中是 window 对象,Node.js 中是 global 对象

上下文在其所有代码都执行完毕后会被销毁,包括定义在上下文中的所有变量和函数(全局上下文在应用程序退出前才会被销毁,比如关闭网页或退出浏览器)

上下文中的代码在执行的时候,会创建变量对象的一个作用域链,这个作用域链决定了各级上下文中的代码在访问变量和函数时的顺序

代码正在执行的上下文变量对象始终位于作用域链最前端,全局上下文的变量对象始终位于作用域链的最末端

如果是函数上下文,其活动对象(activation object,AO)用作变量对象

函数参数被认为是当前上下文中的变量

作用域链增强

虽然执行上下文主要有全局上下文和函数上下文两种(eval() 调用内部存在第三种上下文),但有其他方式来增强作用域链

某些语句会导致在作用域链前端临时添加一个变量对象,该变量对象会在代码执行后被移除

变量声明

严格来讲, let 在 JavaScript 运行时中也会被提升,但由于暂时性死区(temporal dead zone,TDZ)的存在,直到执行到 let 语句时,变量才会被添加到执行上下文中

由于 const 声明暗示变量的值是单一类型且不可修改,JavaScript 运行时编译器可以将其所有实例都替换成实际的值,而不会通过查询表进行变量查找。谷歌的 V8 引擎就执行这种优化

垃圾回收

JavaScript 是使用垃圾回收的语言,执行环境负责在代码执行时管理内存,这个过程是周期性的,垃圾回收程序每隔一定时间(或者说在代码执行过程中某个预定的收集时间)就会自动运行

JavaScript 通过自动内存管理实现内存分配和闲置资源回收

标记清理

JavaScript 最常用的垃圾回收策略是标记清理。当变量进入上下文,这个变量就会被加上存在于上下文中的标记,当变量离开上下文时,就会被加上离开上下文的标记

垃圾回收程序运行的时候,会标记内存中存储的所有变量。然后,它会将所有在上下文中的变量,以及被在上下文中的变量引用的变量的标记去掉。在此之后再被加上标记就是待删除的了。随后垃圾回收程序做一次内存清理,销毁带标记的所有值并回收它们所占用的内存空间

引用计数

引用计数的思路是对每个值都记录它被引用的次数。当声明了一个变量并将一个引用类型赋值给该变量时,则这个值的引用次数就是 1。如果同一个值又被赋给另一个变量,则该值的引用次数加 1。相反,如果包含对这个值引用的变量又取得了另外一个值,则这个值的引用次数减 1。当这个值的引用次数变成 0 时,则说明没有办法再访问这个值了,因而就可以将其占用的内存空间回收回来。这样,当垃圾回收程序下次再运行时,它就会释放那些引用次数为 0 的值所占用的内存

引用计数的问题在于循环引用,两个对象相互引用,导致它们的引用次数都不为 0,所以垃圾回收程序不会回收它们占用的内存,比如:

function problem() {
+	var objectA = new Object()
+	var objectB = new Object()
+	objectA.someOtherObject = objectB
+	objectB.anotherObject = objectA
+}
+

内存管理

如果数据不再必要,最好通过将其值设置为 null 来释放其引用,这个做法叫解除引用。这个建议最适合全局变量和全局对象的属性,局部变量在超出作用域后会被自动解除引用,比如

function createPerson(name) {
+	var localPerson = new Object()
+	localPerson.name = name
+	return localPerson
+}
+var globalPerson = createPerson('Nicholas')
+// 手动解除引用
+globalPerson = null
+
`,29),k=n("li",null,[n("p",null,"通过 const 和 let 声明提升性能"),n("p",null,"const 和 let 都以块(而非函数)为作用域,所以相比于使用 var 声明,使用这个两个关键字可能会更早地让垃圾回收程序介入,尽早回收应该回收的内存")],-1),h=n("p",null,"隐藏类和删除操作",-1),v=n("p",null,"V8 会将创建的对象与隐藏类关联起来,以跟踪他们的属性特征,能够共享相同隐藏类的对象性能会更好:",-1),m=n("div",{class:"language-javascript line-numbers-mode","data-ext":"js"},[n("pre",{class:"language-javascript"},[n("code",null,[n("span",{class:"token keyword"},"function"),a(),n("span",{class:"token function"},"Article"),n("span",{class:"token punctuation"},"("),n("span",{class:"token punctuation"},")"),a(),n("span",{class:"token punctuation"},"{"),a(` + `),n("span",{class:"token keyword"},"this"),n("span",{class:"token punctuation"},"."),a("title "),n("span",{class:"token operator"},"="),a(),n("span",{class:"token string"},"'Inauguration Ceremony Features Kazoo Band'"),a(` +`),n("span",{class:"token punctuation"},"}"),a(` +`),n("span",{class:"token keyword"},"const"),a(" a1 "),n("span",{class:"token operator"},"="),a(),n("span",{class:"token keyword"},"new"),a(),n("span",{class:"token class-name"},"Article"),n("span",{class:"token punctuation"},"("),n("span",{class:"token punctuation"},")"),a(` +`),n("span",{class:"token keyword"},"const"),a(" a2 "),n("span",{class:"token operator"},"="),a(),n("span",{class:"token keyword"},"new"),a(),n("span",{class:"token class-name"},"Article"),n("span",{class:"token punctuation"},"("),n("span",{class:"token punctuation"},")"),a(` +`),n("span",{class:"token comment"},"// 导致两个 Article 实例的对应两个不同的隐藏类"),a(` +a2`),n("span",{class:"token punctuation"},"."),a("author "),n("span",{class:"token operator"},"="),a(),n("span",{class:"token string"},"'Jake'"),a(` +`)])]),n("div",{class:"line-numbers","aria-hidden":"true"},[n("div",{class:"line-number"}),n("div",{class:"line-number"}),n("div",{class:"line-number"}),n("div",{class:"line-number"}),n("div",{class:"line-number"}),n("div",{class:"line-number"}),n("div",{class:"line-number"})])],-1),b=n("div",{class:"language-javascript line-numbers-mode","data-ext":"js"},[n("pre",{class:"language-javascript"},[n("code",null,[n("span",{class:"token keyword"},"function"),a(),n("span",{class:"token function"},"Article"),n("span",{class:"token punctuation"},"("),n("span",{class:"token parameter"},"opt_author"),n("span",{class:"token punctuation"},")"),a(),n("span",{class:"token punctuation"},"{"),a(` + `),n("span",{class:"token keyword"},"this"),n("span",{class:"token punctuation"},"."),a("title "),n("span",{class:"token operator"},"="),a(),n("span",{class:"token string"},"'Inauguration Ceremony Features Kazoo Band'"),a(` + `),n("span",{class:"token keyword"},"this"),n("span",{class:"token punctuation"},"."),a("author "),n("span",{class:"token operator"},"="),a(` opt_author +`),n("span",{class:"token punctuation"},"}"),a(` +`),n("span",{class:"token keyword"},"const"),a(" a1 "),n("span",{class:"token operator"},"="),a(),n("span",{class:"token keyword"},"new"),a(),n("span",{class:"token class-name"},"Article"),n("span",{class:"token punctuation"},"("),n("span",{class:"token punctuation"},")"),a(` +`),n("span",{class:"token keyword"},"const"),a(" a2 "),n("span",{class:"token operator"},"="),a(),n("span",{class:"token keyword"},"new"),a(),n("span",{class:"token class-name"},"Article"),n("span",{class:"token punctuation"},"("),n("span",{class:"token string"},"'Jake'"),n("span",{class:"token punctuation"},")"),a(` +`)])]),n("div",{class:"line-numbers","aria-hidden":"true"},[n("div",{class:"line-number"}),n("div",{class:"line-number"}),n("div",{class:"line-number"}),n("div",{class:"line-number"}),n("div",{class:"line-number"}),n("div",{class:"line-number"})])],-1),_=n("li",null,"使用 delete 关键字会导致生成新的隐藏类,最佳实践是把不想要的属性设置为 null",-1),w=n("li",null,[n("p",null,"内存泄露")],-1),f=n("ul",null,[n("li",null,"意外的全局变量"),n("li",null,"闭包")],-1),g=n("ol",{start:"4"},[n("li",null,[n("p",null,"静态分配与对象池"),n("p",null,"为了提升 JavaScript 性能,一个关键的问题就是如何减少浏览器执行垃圾回收的次数"),n("p",null,"浏览器决定何时运行垃圾回收程序的一个标准就是对象更替的速度,越快越频繁"),n("p",null,[a("为了提升性能,V8 引入了"),n("strong",null,"对象池"),a(",它会对一些常见的对象结构进行缓存,当需要创建这些对象时,就会从对象池中取出,而不是重新创建")])])],-1);function y(j,x){const t=o("CodeGroupItem"),l=o("CodeGroup");return p(),i("div",null,[d,n("ol",null,[k,n("li",null,[h,v,n("ul",null,[n("li",null,[a("避免 JavaScript 的“先创建再补充(ready-fire-aim)”式的动态属性赋值,并在构造函数中一次性声明所有属性"),s(l,null,{default:e(()=>[s(t,{title:"before"},{default:e(()=>[m]),_:1}),s(t,{title:"after"},{default:e(()=>[b]),_:1})]),_:1})]),_])]),w]),f,g])}const C=c(u,[["render",y],["__file","04.html.vue"]]);export{C as default}; diff --git a/assets/05.html-4cfb85ff.js b/assets/05.html-4cfb85ff.js new file mode 100644 index 0000000..8bfaed4 --- /dev/null +++ b/assets/05.html-4cfb85ff.js @@ -0,0 +1,76 @@ +import{_ as p,r as o,o as c,c as l,a as n,d as a,b as t,e}from"./app-b4fb6edd.js";const i={},u=e(`

基本引用类型

引用值(或者对象)是某个特定引用类型的实例

Date

Date 对象基于 Unix Time Stamp,即自 1970 年 1 月 1 日(UTC)起经过的毫秒数

new Date(); // 实例化时刻的日期和时间
+new Date(value); // value 表示 1970 年 1 月 1 日(UTC)起经过的毫秒数
+new Date(dateString); // dateString 表示日期字符串,该字符串应该能被 Date.parse() 正确方法识别
+new Date(year, monthIndex [, day [, hours [, minutes [, seconds [, milliseconds]]]]]);
+

继承的方法

日期格式化方法

Date 类型有几个专门用于格式化日期的方法,它们都会返回字符串

这些方法的输出与 toLocaleString() 和 toString() 一样,会因浏览器而异,因此不能用于在用户界面上一致的显示日期

`,12),r={href:"https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/Date#%E5%AE%9E%E4%BE%8B%E6%96%B9%E6%B3%95",target:"_blank",rel:"noopener noreferrer"},k=e(`

RegExp

let expression = /pattern/flags;
+
`,2),d={href:"https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/RegExp#%E5%AE%9E%E4%BE%8B%E5%B1%9E%E6%80%A7",target:"_blank",rel:"noopener noreferrer"},m={href:"https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/RegExp#%E5%AE%9E%E4%BE%8B%E6%96%B9%E6%B3%95",target:"_blank",rel:"noopener noreferrer"},v={href:"https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/RegExp#%E9%9D%99%E6%80%81%E5%B1%9E%E6%80%A7",target:"_blank",rel:"noopener noreferrer"},g=e(`

原始值包装类型

每当用到某个原始值的方法或属性时,后台都会创建一个相应原始包装类型的对象,从而暴露出操作原始值的各种方法

引用类型与原始包装类型的主要区别在于对象的生命周期。在通过 new 实例化引用类型后,得到的实例会在离开作用域时被销毁,而自动创建的原始值包装对象则只存在于访问它的那行代码执行期间。这意味着不能在运行时给原始值添加属性和方法,比如:

let name = 'Nicholas'
+name.age = 27
+console.log(name.age) // undefined
+

可以显示地使用 Boolean、Number 和 String 创建原始值包装对象,实例上调用 typeof 会返回 object

原始值和包装对象之间的区别:

Boolean

要创建一个 Boolean 对象,就使用 Boolean 构造函数并传入 true 或 false

let booleanObject = new Boolean(true)
+

Number

要创建一个 Number 对象,就使用 Number 构造函数并传入数值

let numberObject = new Number(10)
+

继承的方法:

let num = 10
+console.log(num.toString()) // '10'
+console.log(num.toString(2)) // '1010'
+console.log(num.toString(8)) // '12'
+console.log(num.toString(10)) // '10'
+console.log(num.toString(16)) // 'a'
+

格式化数值:

isInteger() 方法与安全整数:

String

要创建一个 String 对象,就使用 String 构造函数并传入字符串

let stringObject = new String('hello world')
+

继承的方法:

1. JavaScript 字符串

JavaScript 字符串由 16 位码元(code unit)组成。对于大多数字符,每个码元对应一个字符

对于 U+0000~U+FFFF 范围内的字符,上面的属性和方法返回的结果都跟预期是一样的

16 位只能唯一表示 2^16 个字符,这对于大多数语言字符集是足够了,在 Unicode 中称为基本多语言平面(BMP)

为了表示更多的字符,Unicode 采用了一个策略,即每个字符使用另外 16 位去选择一个增补平面。这种每个字符使用两个 16 位码元的策略称为代理对(surrogate pair)

在涉及增补平面的字符时,前面讨论的字符串方法和属性就会出问题

// "smiling face with smiling eyes" 表情符号的码点是 U+1F60A
+// 0x1F60A === 128522
+let message = 'ab😊de'
+console.log(message.length) // 6
+console.log(message.charAt(1)) // 'b'
+console.log(message.charAt(2)) // '�'
+console.log(message.charAt(3)) // '�'
+console.log(message.charAt(4)) // 'd'
+
+console.log(message.charCodeAt(1)) // 98
+console.log(message.charCodeAt(2)) // 55357
+console.log(message.charCodeAt(3)) // 56842
+console.log(message.charCodeAt(4)) // 100
+
+console.log(String.fromCharCode(0x1f60a)) // '�'
+console.log(String.fromCodePoint(0x1f60a)) // '😊'
+console.log(String.fromCharCode(97, 98, 55357, 56842, 100, 101)) // 'ab😊de'
+

码点是 Unicode 中一个字符的完整标识

迭代字符串可以智能地识别代理对的码点:

console.log([...'ab😊de']) // ['a', 'b', '😊', 'd', 'e']
+

2. normalize() 方法

某些 Unicode 字符可以有多种编码方式,多种形式间使用 === 的结果为 false。为解决这个问题,ES6 提供了 normalize() 方法,用于将字符的不同表示方法统一为同样的形式,使用时需要传入表示哪种形式的字符串:"NFC"、"NFD"、"NFKC"、"NFKD"

3. 字符串操作方法

4. 字符串位置方法

5. 字符串包含方法

6. trim() 方法

去除字符串两端的空格,返回新字符串

trimLeft() / trimRight(),去除字符串左边 / 右边的空格,返回新字符串

7. repeat() 方法

接收一个整数参数,表示将原字符串重复多少次,返回新字符串

8. padStart() / padEnd() 方法

两个方法会复制字符串,如果小于指定长度,则在相应一边填充字符,直至满足长度条件

第一个参数是长度,第二个参数是可选的填充字符,默认为空格

9. 字符串迭代与解构

字符串的原型上暴露了一个 @@iterator 方法,表示可迭代字符串中的每个字符

手动使用迭代器:

let message = 'ab😊de'
+let iterator = message[Symbol.iterator]()
+console.log(iterator.next()) // { value: 'a', done: false }
+console.log(iterator.next()) // { value: 'b', done: false }
+console.log(iterator.next()) // { value: '😊', done: false }
+console.log(iterator.next()) // { value: 'd', done: false }
+console.log(iterator.next()) // { value: 'e', done: false }
+console.log(iterator.next()) // { value: undefined, done: true }
+

在 for-of 循环中可以通过这个迭代器按序访问每个字符:

let message = 'ab😊de'
+for (let c of message) {
+	console.log(c)
+}
+// 'a'
+// 'b'
+// '😊'
+// 'd'
+// 'e'
+

字符串也可以通过解构赋值的方式进行迭代:

let message = 'ab😊de'
+let [a, b, c, d, e] = message
+console.log(a, b, c, d, e) // 'a' 'b' '😊' 'd' 'e'
+

10. 字符串大小写转换

如果不知道代码涉及什么语言,则最好使用地区特定的转换方法

11. 字符串模式匹配方法

String 类型专门为在字符串中实现模式匹配设计了几个方法

12. localeCompare() 方法

因为返回的具体值可能因具体实现而异,所以最好像这样使用:

function determineOrder(value) {
+	let result = str1.localeCompare(value)
+	if (result < 0) {
+		console.log('str1 comes before ' + value)
+	} else if (result > 0) {
+		console.log('str1 comes after ' + value)
+	} else {
+		console.log('str1 is equal to ' + value)
+	}
+}
+

localeCompare() 的独特之处在于,实现所在的地区(国家和语言)决定了这个方法如何比较字符串

单例内置对象

ECMA-262 对内置对象的定义是“任何由 ECMAScript 实现提供、与宿主环境无关,并在 ECMAScript 程序开始执行时就存在的对象”,如前面接触的 Object、Array、String、Function、Date、RegExp、Error、Boolean、Number,包括接下来介绍的两个单例内置对象 Math、Global

Global

ECMA-262 规定 Global 对象为一种兜底对象,它所针对的是不属于任何对象的属性和方法。在全局作用域中定义的变量和函数都会变成 Global 对象的属性。isNaN()、isFinite()、parseInt() 和 parseFloat() 都是 Global 对象的方法,除了这些 ECMAScript 还为 Global 对象定义了其他方法

1. URL 编码方法

有效的 URI 不能包含某些字符,比如空格。使用 URI 编码方法可以让浏览器理解它们,同时又以特殊的 UTF-8 编码替换所有无效字符,比如空格会被替换成 %20

encodeURI()、encodeURIComponent()、decodeURI()、decodeURIComponent(),用于对 统一资源标识符(URI)进行编码和解码

2. eval() 方法

这个方法就是一个完整的 ECMAScript 解释器,它接收一个参数,即要执行的 ECMAScript(或 JavaScript)字符串

eval('console.log("hi")') // 'hi'
+

3. Global 对象的属性

4. window 对象

虽然 ECMA-262 没有规定直接访问 Global 对象的方式,但浏览器将 window 对象实现为 Global 对象的代理

另一种获取 Global 对象的方式:

var global = (function () {
+	return this
+})()
+

Math

ECMAScript 提供了 Math 对象作为保存数学公式、信息和计算的地方。Math 对象提供了一些辅助计算的属性和方法

1. Math 对象属性

2. min() 和 max() 方法

接收任意多个参数

3. 舍入方法

4. random 方法

返回大于等于 0 小于 1 的一个随机数

可以基于如下公式使用 Math.random() 从一组整数中随机选择一个整数:

Math.floor(Math.random() * 可能值的总数 + 第一个可能的值)
+

5. 其他方法

`,99),b={href:"https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/Math#%E6%96%B9%E6%B3%95",target:"_blank",rel:"noopener noreferrer"};function h(f,j){const s=o("ExternalLinkIcon");return c(),l("div",null,[u,n("p",null,[n("a",r,[a("日期/时间组件方法"),t(s)])]),k,n("p",null,[n("a",d,[a("实例属性"),t(s)])]),n("p",null,[n("a",m,[a("实例方法"),t(s)])]),n("p",null,[n("a",v,[a("静态属性"),t(s)])]),g,n("p",null,[n("a",b,[a("Math 的方法"),t(s)])])])}const E=p(i,[["render",h],["__file","05.html.vue"]]);export{E as default}; diff --git a/assets/05.html-ddabefdf.js b/assets/05.html-ddabefdf.js new file mode 100644 index 0000000..ef13e59 --- /dev/null +++ b/assets/05.html-ddabefdf.js @@ -0,0 +1 @@ +const l=JSON.parse('{"key":"v-4831848c","path":"/frontend/js/red-book/05.html","title":"红宝书","lang":"zh-CN","frontmatter":{"title":"红宝书","prev":{"text":"变量、作用域与内存","link":"./04.md"},"next":{"text":"集合引用类型","link":"./06.md"}},"headers":[{"level":2,"title":"基本引用类型","slug":"基本引用类型","link":"#基本引用类型","children":[{"level":3,"title":"Date","slug":"date","link":"#date","children":[{"level":4,"title":"继承的方法","slug":"继承的方法","link":"#继承的方法","children":[]},{"level":4,"title":"日期格式化方法","slug":"日期格式化方法","link":"#日期格式化方法","children":[]}]},{"level":3,"title":"RegExp","slug":"regexp","link":"#regexp","children":[]},{"level":3,"title":"原始值包装类型","slug":"原始值包装类型","link":"#原始值包装类型","children":[{"level":4,"title":"Boolean","slug":"boolean","link":"#boolean","children":[]},{"level":4,"title":"Number","slug":"number","link":"#number","children":[]},{"level":4,"title":"String","slug":"string","link":"#string","children":[]}]},{"level":3,"title":"单例内置对象","slug":"单例内置对象","link":"#单例内置对象","children":[{"level":4,"title":"Global","slug":"global","link":"#global","children":[]},{"level":4,"title":"Math","slug":"math","link":"#math","children":[]}]}]}],"git":{"updatedTime":1690188978000},"filePathRelative":"frontend/js/red-book/05.md"}');export{l as data}; diff --git a/assets/06.html-4c3bc427.js b/assets/06.html-4c3bc427.js new file mode 100644 index 0000000..014c0f8 --- /dev/null +++ b/assets/06.html-4c3bc427.js @@ -0,0 +1,314 @@ +import{_ as n,o as s,c as a,e as t}from"./app-b4fb6edd.js";const e={},p=t(`

集合引用类型

Object

显示的创建 Object 实例有两种方式:

  1. new 操作符后跟 Object 构造函数
let person = new Object()
+person.name = 'Nicholas'
+person.age = 29
+
  1. 使用对象字面量
let person = {
+	name: 'Nicholas',
+	age: 29,
+	5: true, // 数值属性会自动转换成字符串
+}
+

在这个例子中,左大括号({)表示对象字面量开始,因为它出现在一个**表达式上下文(expression context)**中。在 ECMAScript 中,表达式上下文是指期待返回值的上下文。

同样是左大括号({),如果出现在**语句上下文(statement context)**中,比如 if 语句的条件后面,则表示一个语句块的开始

注意:在使用字面量表示法定义对象时,并不会实际调用 Object 构造函数

存取属性的两种方式:

  1. 点语法

  2. 中括号

从功能上讲,这两种存取属性的方式没有区别

使用中括号的主要优势:

Array

创建数组

有两种基本的方式可以创建数组:

  1. 使用 Array 构造函数

创建数组时给构造函数传入一个值。如果是数值,则会创建一个长度为指定数值的数组;如果是其他类型,则会创建包含那个值的数组

在使用 Array 构造函数时,也可以省略 new 操作符,结果是一样的

  1. 使用数组字面量表示法

与对象一样,在使用数组字面量表示法创建数组时,不会调用 Array 构造函数

Array 构造函数还有两个 ES6 新增的用于创建数组的静态方法:

  1. from(),用于将类数组结构转换为数组实例

  2. of(),用于将一组参数转换为数组实例

数组空位

使用数组字面量初始化数组时,可以使用一串逗号来创建空位(hole)

const options = [, , , , ,]
+console.log(options.length) // 5
+console.log(options, options[0]) // [empty × 5] undefined
+

注:ES6 新增的方法普遍将这些空位当成存在的元素;而 ES6 之前的方法则会忽略这个空位,但具体的行为也会因方法而异。因此实践中要避免使用数组空位,可以显式地使用 undefined 值代替

数组索引

let colors = ['red', 'blue', 'green']
+console.log(colors[0]) // red
+colors[2] = 'black'
+colors[3] = 'brown'
+

在中括号提供索引,表示要访问的值。如果把一个值设置给超过数组最大索引的索引,则数组长度会自动扩展到该索引值 + 1

数组中元素的数量保存在 length(>= 0) 中,它不是只读的。可以通过修改 length 属性从数组末尾删除或者添加元素

let colors = ['red', 'blue', 'green']
+colors.length = 2
+console.log(colors[2]) // undefined
+
+colors.length = 4 // 新添加的值都将以 undefined 填充
+console.log(colors[3]) // undefined
+

使用 length 属性可以方便地向数组末尾添加元素

let colors = ['red', 'blue', 'green']
+colors[colors.length] = 'black'
+colors[colors.length] = 'brown'
+

检测数组

如果判断一个对象是不是数组?

在只有一个网页(因为只有一个全局作用域)的情况下,使用 instanceof 操作符足矣:

if (value instanceof Array) {
+	// do something
+}
+

如果网页里有多个框架,则可能涉及两个不同的全局上下文,因此会有两个不同版本的 Array 构造函数。如果把数组从一个框架传给另一个框架,则这个数组的构造函数将有别于在另一个框架内本地创建的数组

为解决这个问题,ECMAScript 提供了 Array.isArray() 方法

if (Array.isArray(value)) {
+	// do something
+}
+

迭代器方法

ES6 中,Array 的原型上暴露了 3 个用于检索数组内容的方法:keys()、values() 和 entries()

const colors = ['red', 'blue', 'green']
+const keys = colors.keys()
+const values = colors.values()
+const entries = colors.entries()
+console.log(Array.isArray(keys)) // false
+
+const keysArr = Array.from(keys)
+const valuesArr = Array.from(values)
+const entriesArr = Array.from(entries)
+
+console.log(keysArr, typeof keysArr[0] === 'number') // [0, 1, 2] true
+console.log(valuesArr) // ['red', 'blue', 'green']
+console.log(entriesArr) // [[0, 'red'], [1, 'blue'], [2, 'green']]
+

复制和填充方法

ES6 新增了两个方法:批量复制方法 copyWithin() 和填充数组方法 fill()

转换方法

栈方法

队列方法

排序方法

操作方法

搜素和位置方法

ECMAScript 提供两类搜索数组的方法:按严格相等搜索和按断言函数搜索

1. 严格相等(===)

以下三个方法接收两个参数:要查找的项和(可选的)表示查找起点位置的索引

2. 断言函数

这两个方法也都接受第二个可选参数,用于指定断言函数内部 this 的值

迭代方法

ECMAScript 为数组定义了 5 个迭代方法,每个方法都接收两个参数:要在每一项上运行的函数和(可选的)运行该函数的作用域对象——影响 this 的值

归并方法

ECMAScript 为数组提供了两个归并方法:reduce() 和 reduceRight()

定型数组

定型数组(typed array)是 ECMAScript 新增的结构,目的是提升向原生库传输数据的效率,它所指的其实是一种特殊的包含数值类型的数组

ArrayBuffer

Float32Array 实际上是一种“视图”,可以允许 JavaScript 运行时访问一块名为 ArrayBuffer 的预分配内存。ArrayBuffer 是所有定型数组及视图引用的基本单位

ArrayBuffer() 是一个普通的 JavaScript 构造函数,可用于在内存中分配特定数量的字节空间:

const buffer = new ArrayBuffer(16) // 在内存中分配 16 字节
+console.log(buffer.byteLength) // 16
+

ArrayBuffer 一经创建就不能再调整大小。不过,可以使用 slice() 复制其全部或者部分到一个新的 ArrayBuffer 实例中:

const buf1 = new ArrayBuffer(16)
+const buf2 = buf1.slice(4, 12)
+console.log(buf2.byteLength) // 8
+

不能仅通过对 ArrayBuffer 的引用就读取或者写入内容,而是需要通过视图来实现。视图有不同的类型,但引用的都是 ArrayBuffer 中存储的二进制数据

ArrayBuffer 在分配失败时会抛出错误

ArrayBuffer 分配的内存不能超过 Number.MAX_SAFE_INTEGER(2^53 - 1)个字节

声明 ArrayBuffer 会将所有二进制初始化为 0

通过声明 ArrayBuffer 分配的堆内存可以被当成垃圾回收,不用手动释放

DataView

DataView 是允许读写 ArrayBuffer 的一种视图,专为文件 I/O 和网络 I/O 设计,其 API 支持对缓冲数据的高度控制,但相比于其他类型的视图性能差一些。DataView 对缓冲内容没有任何预设,也不能迭代

必须在对已有的 ArrayBuffer 读取或者写入时才能创建 DataView 实例:

const buf = new ArrayBuffer(16)
+
+// DataView 默认使用整个 ArrayBuffer
+const fullDataView = new DataView(buf)
+console.log(fullDataView.byteOffset) // 0
+console.log(fullDataView.byteLength) // 16
+console.log(fullDataView.buffer === buf) // true
+
+// 构造函数接收一个可选的字节偏移量和一个可选的字节长度
+//   byteOffset=0 表示视图从缓冲起点开始
+//   byteLength=8 表示限制视图为前 8 个字节
+const firstHalfDataView = new DataView(buf, 0, 8)
+console.log(firstHalfDataView.byteOffset) // 0
+console.log(firstHalfDataView.byteLength) // 8
+console.log(firstHalfDataView.buffer === buf) // true
+
+// 如果不指定,则 DataView 会使用剩余的缓存
+//   byteOffset=8 表示视图从缓冲的第 8 个字节开始
+//   byteLength 未指定,默认为剩余缓冲
+const secondHalfDataView = new DataView(buf, 8)
+console.log(secondHalfDataView.byteOffset) // 8
+console.log(secondHalfDataView.byteLength) // 8
+console.log(secondHalfDataView.buffer === buf) // true
+

要通过 DataView 读取缓冲,还需要几个组件:

1. ElementType

DateView 对存储在缓冲内的数据类型没有预设。它暴露的 API 强制开发者在读、写时指定一个 ElementType,然后 DataView 会忠实地为读、写完成相应的转换

ECMAScript6 支持 8 种不同的 ElementType(见下表):

ElementType字节说明等价的 C 类型值的范围
Int818 位有符号整数signed char-128 ~ 127
Uint818 位无符号整数unsigned char0 ~ 255
Int16216 位有符号整数short-32768 ~ 32767
Uint16216 位无符号整数unsigned short0 ~ 65535
Int32432 位有符号整数int-2147483648 ~ 2147483647
Uint32432 位无符号整数unsigned int0 ~ 4294967295
Float32432 位浮点数float1.2e-38 ~ 3.4e38
Float64864 位浮点数double5.0e-324 ~ 1.8e308

DataView 为上表中的每种类型都暴露了 get 和 set 方法,这些方法使用 byteOffset 定位要读取或者写入值的位置。类型时可以互换使用的,如下例所示:

// 在内存中分配两个字节并声明一个 DataView
+const buf = new ArrayBuffer(2)
+const view = new DataView(buf)
+
+// 说明整个缓冲确实所有二进制位都是 0
+// 检查第一个和第二个字符
+console.log(view.getInt8(0)) // 0
+console.log(view.getInt8(1)) // 0
+// 检查整个缓冲
+console.log(view.getInt16(0)) // 0
+
+// 将整个缓冲都设置为 1
+// 255 的二进制表示为 11111111 (2^8 - 1)
+view.setUint8(0, 255)
+
+// DataView 会自动将数据转换为特定的 ElementType
+// 255 的十六进制表示是 0xFF
+view.setUint8(1, 0xff)
+
+// 现在缓冲里都是 1 了
+// 如果把它当作二补数的有符号整数,则应该是 -1
+console.log(view.getInt16(0)) // -1
+

2. 字节序

“字节序”指的是计算系统维护的一种字节顺序的约定,DataView 只支持两种约定:大端字节序和小端字节序

JavaScript 运行时所在系统的原生字节序决定了如何读取或写入数据,但 DataView 并不遵守这个约定。对一段内存而言,DataView 是一个中立接口,它遵守指定的字节序。DataView 的所有 API 方法都以大端字节序作为默认值,但接收一个可选的布尔值参数,设置为 true 即可启用小端字节序

// 在内存中分配两个字节并声明一个 DataView
+const buf = new ArrayBuffer(2)
+const view = new DataView(buf)
+
+// 填充缓冲,让第一位和最后一位都是 1
+view.setUint8(0, 0x80) // 设置最左边的位等于 1(1000 0000)
+view.setUint8(1, 0x01) // 设置最右边的位等于 1 (0000 0001)
+// 则缓冲的内容为 1000 0000 0000 0001
+
+// 按大端字节序读取 Unit16
+// 0x80 是高字节,0x01 是低字节
+// 0x8001 = 2^15 + 2^0 = 32769
+console.log(view.getUint16(0)) // 32769
+
+// 按小端字节序读取 Unit16
+// 0x80 是低字节,0x01 是高字节
+// 0x0180 = 2^7 + 2^8 = 384
+console.log(view.getUint16(0, true)) // 384
+
+// 按大端字节序写入 Uint16
+view.setUint16(0, 0x0004)
+// 缓冲内容:
+// 0000 0000 0000 0100
+console.log(view.getUint8(0)) // 0
+console.log(view.getUint8(1)) // 4
+
+// 按小端字节序写入 Uint16
+view.setUint16(0, 0x0002, true)
+// 缓冲内容:
+// 0000 0010 0000 0000
+console.log(view.getUint8(0)) // 2
+console.log(view.getUint8(1)) // 0
+

3. 边界情形

DataView 完成读、写操作的前提是必须有充足的缓冲区,否则会抛出 RangeError 异常

const buf = new ArrayBuffer(6)
+const view = new DataView(buf)
+
+// 尝试读取部分超出缓冲范围的值
+view.getInt32(4) // RangeError
+
+// 尝试读取超出缓冲范围的值
+view.getInt32(8) // RangeError
+view.getInt32(-1) // RangeError
+
+// 尝试写入超出缓冲范围的值
+view.setInt32(4, 123) // RangeError
+

DataView 在写入缓冲里会尽最大努力把一个值转换为适当的类型,后备为 0。如果无法转换,则抛出 TypeError 异常

const buf = new ArrayBuffer(1)
+const view = new DataView(buf)
+
+view.setInt8(0, 1.5)
+console.log(view.getInt8(0)) // 1
+
+view.setInt8(0, [4])
+console.log(view.getInt8(0)) // 4
+
+view.setInt8(0, 'f')
+console.log(view.getInt8(0)) // 0
+
+view.setInt8(0, Symbol()) // TypeError
+

定型数组

定型数组是另一种形式的 ArrayBuffer 视图。虽然概念上与 DataView 接近,但定型数组的区别在于,它特定于一种 ElementType 且遵循系统原生的字节序。相应地,定型数组提供了适用面更广的 API 和更高的性能

设计定型数组的目的是提高于 WebGL 等原生库交换二进制数据的效率

创建定型数组的方式包括:读取已有的缓冲、使用自由缓冲、填充可迭代结构,以及填充基于任意类型的定型数组。另外通过 <ElementType>.from()<ElementType>.of() 也可以创建定型数组

// 创建一个 12 字节的缓冲
+const buf = new ArrayBuffer(12)
+// 创建一个引用该缓冲的 Int32Array
+const ints = new Int32Array(buf)
+// 这个定型数组知道自己每个元素需要 4 字节
+// 因此长度为 3
+console.log(ints.length) // 3
+
+// 创建一个长度为 6 的 Int32Array
+const ints2 = new Int32Array(6)
+// 每个数值使用 4 个字节,因此 ArrayBuffer 需要 24 字节
+console.log(ints2.length) // 6
+// 类似 DataView,定型数组也有一个指向关联缓冲的引用
+console.log(ints2.buffer.byteLength) // 24
+
+// 创建一个包含 [2, 4, 6, 8] 的 Int32Array
+const ints3 = new Int32Array([2, 4, 6, 8])
+console.log(ints3.length) // 4
+console.log(ints3.buffer.byteLength) // 16
+console.log(ints3[2]) // 6
+
+// 通过复制 ints3 创建一个 Int16Array
+const ints4 = new Int16Array(ints3)
+// 这个新类型数组会分配自己的缓冲
+// 对应索引的值会相应地转换为新格式
+console.log(ints4.length) // 4
+console.log(ints4.buffer.byteLength) // 8
+console.log(ints4[2]) // 6
+
+// 基于普通数组来创建一个 Int16Array
+const ints5 = Int16Array.from([3, 5, 7, 9])
+console.log(ints5.length) // 4
+console.log(ints5.buffer.byteLength) // 8
+console.log(ints5[2]) // 7
+
+// 基于传入的参数创建一个 Float32Array
+const floats = Float32Array.of(3.14, 2.718, 1.618)
+console.log(floats.length) // 3
+console.log(floats.buffer.byteLength) // 12
+console.log(floats[2]) // 1.6180000305175781
+

定型数组的构造函数和实例都有一个 BYTES_PER_ELEMENT 属性,返回该类型数组中每个元素的大小:

console.log(Int16Array.BYTES_PER_ELEMENT) // 2
+console.log(Int32Array.BYTES_PER_ELEMENT) // 4
+
+const ints = new Int32Array(1),
+	floats = new Float64Array(1)
+
+console.log(ints.BYTES_PER_ELEMENT) // 4
+console.log(floats.BYTES_PER_ELEMENT) // 8
+

如果定型数组没有用任何值初始化,则其关联的缓冲会以 0 填充:

const ints = new Int32Array(4)
+console.log(ints[0]) // 0
+console.log(ints[1]) // 0
+console.log(ints[2]) // 0
+console.log(ints[3]) // 0
+

1. 定型数组的行为

定型数组的行为与普通数组类似,但也有一些不同之处

不能在定型数组中使用的方法:

2. 上溢和下溢

// 长度为 2 有符号整数数组
+// 每个索引保存一个二补数形式的有符号整数
+// 范围是 -128 到 127
+const ints = new Int8Array(2)
+
+// 长度为 2 的无符号整数数组
+// 每个索引保存一个无符号整数
+// 范围是 0 到 255
+const unsignedInts = new Uint8Array(2)
+
+// 上溢的位不会影响相邻索引
+// 索引只取最低有效位上的 8 位
+unsignedInts[1] = 256
+console.log(unsignedInts[1]) // [0, 0]
+unsignedInts[1] = 511
+console.log(unsignedInts[1]) // [0, 255]
+
+// 下溢的位会被转换为无符号的等价值
+// 0xFF 是以二补数形式表示的 -1(截取到 8 位)
+// 但 255 是一个无符号整数
+unsignedInts[1] = -1
+console.log(unsignedInts[1]) // [0, 255]
+
+// 上溢自动变成二补数形式
+// 0x80 是无符号整数的 128,是二补数形式的 -128
+ints[1] = 128
+console.log(ints[1]) // [0, -128]
+
+// 下溢自动变为二补数形式
+// 0xFF 是无符号整数的 255,是二补数形式的 -1
+ints[1] = 255
+console.log(ints[1]) // [0, -1]
+

除了 8 种元素类型,还有一种“夹板”数组类型:Uint8ClampedArray,不允许任何方向溢出(除非真的做跟 canvas 相关的开发,否则不要使用它)。超出最大值 255 的值会被向下舍入为 255,低于最小值 0 的值会被向上舍入为 0

const clampedInts = new Uint8ClampedArray([-1, 0, 255, 256])
+console.log(clampedInts[0]) // [0, 0, 255, 255]
+

Map

作为 ECMAScript6 的新增特性,Map 是一种新的集合类型,为这门语言带来了真正的键/值存储机制。Map 的大多数特性都可以通过 Object 来实现,但二者之间还是存在一些细微的差异:

基本 API

创建和初始化:

// 使用嵌套数组初始化映射
+const m1 = new Map([
+	['key1', 'val1'],
+	['key2', 'val2'],
+	['key3', 'val3'],
+])
+console.log(m1.size) // 3
+
+// 使用自定义迭代器初始化映射
+const m2 = new Map({
+	[Symbol.iterator]: function* () {
+		yield ['key1', 'val1']
+		yield ['key2', 'val2']
+		yield ['key3', 'val3']
+	},
+})
+console.log(m2.size) // 3
+console.log(m2.has(undefined)) // false
+console.log(m2.get(undefined)) // undefined
+

顺序与迭代

映射实例可以提供一个迭代器(Iterator),能以插入顺序生成 [key, value] 形式的数组。可以通过 entries() 方法(或者 Symbol.iterator 属性,它引用 entires())获取这个迭代器

因为 entires() 是默认迭代器,因此可以直接对映射实例使用扩展操作符,把映射转换为数组

WeakMap

WeakMap 是 Map 的“兄弟”类型,其 API 也是 Map 的子集,但有一些重要的区别:

基本 API

创建和初始化:

const key1 = { id: 1 },
+	key2 = { id: 2 },
+	key3 = { id: 3 }
+
+// 使用嵌套数组初始化弱映射
+const wm1 = new WeakMap([
+	[key1, 'val1'],
+	[key2, 'val2'],
+	[key3, 'val3'],
+])
+

弱键

WeakMap 的键是弱键,这意味着如果键不再被引用,它所对应的值也会被回收

不可迭代键

WeakMap 的键不可迭代,因此没有 entries()、keys() 和 values() 方法,也没有 forEach() 方法,同时也没有 clear() 方法

WeakMap 实例之所以限制只能用对象作为键,是为了保证只有通过键对象的引用才能取得值

Set

Set 是 ECMAScript6 新增的集合类型,在很多方面都像是加强的 Map,因为它们的大多数 API 和行为都是共有的

基本 API

创建和初始化:

// 使用数组初始化集合
+const s1 = new Set(['val1', 'val2', 'val3'])
+console.log(s1.size)
+
+// 使用自定义迭代器初始化集合
+const s2 = new Set({
+	[Symbol.iterator]: function* () {
+		yield 'val1'
+		yield 'val2'
+		yield 'val3'
+	},
+})
+console.log(s2.size)
+

顺序与迭代

Set 会维护插入时的顺序,因此支持顺序迭代

集合实例可以提供一个迭代器,能以插入顺序生成集合内容的数组。可以通过 values() 方法及其别名方法 keys()(或者 Symbol.iterator 属性,它引用 values())获取这个迭代器

因为 values() 是默认迭代器,随意可以直接对集合实例使用扩展操作,把集合转换为数组

集合的 entires() 方法返回一个迭代器,可以按照插入顺序产生包含两个元素的数组,这两个元素是集合种每个值的重复出现

WeakSet

WeakSet 是 Set 的“兄弟”类型,其 API 也是 Set 的子集,但有一些重要的区别:

基本 API

创建和初始化:

const val1 = { id: 1 },
+	val2 = { id: 2 },
+	val3 = { id: 3 }
+
+// 使用数组初始化弱集合
+const ws1 = new WeakSet([val1, val2, val3])
+

弱值

WeakSet 的值是弱值,这意味着如果值不再被引用,它就会被回收

const ws = new WeakSet()
+ws.add({}) // 因为没有指向这个对象的其他引用,所以当这行代码执行完成后,这个对象值就会被当作垃圾回收
+

不可迭代值

WeakSet 的值不可迭代,因此没有 entries()、keys() 和 values() 方法,也没有 forEach() 方法,同时也没有 clear() 方法

迭代与扩展操作

ECMAScript6 新增的迭代器和扩展操作符对集合引用类型特别有用,有 4 种原生集合类型定义了默认迭代器:

意味着上述所有类型都支持顺序迭代,都可以传入 for-of 循环,兼容扩展操作符...

`,168),o=[p];function c(l,i){return s(),a("div",null,o)}const r=n(e,[["render",c],["__file","06.html.vue"]]);export{r as default}; diff --git a/assets/06.html-907b0bed.js b/assets/06.html-907b0bed.js new file mode 100644 index 0000000..344dd34 --- /dev/null +++ b/assets/06.html-907b0bed.js @@ -0,0 +1 @@ +const l=JSON.parse('{"key":"v-49e65d2b","path":"/frontend/js/red-book/06.html","title":"红宝书","lang":"zh-CN","frontmatter":{"title":"红宝书","prev":{"text":"基本引用类型","link":"./05.md"},"next":{"text":"迭代器与生成器","link":"./07.md"}},"headers":[{"level":2,"title":"集合引用类型","slug":"集合引用类型","link":"#集合引用类型","children":[{"level":3,"title":"Object","slug":"object","link":"#object","children":[]},{"level":3,"title":"Array","slug":"array","link":"#array","children":[{"level":4,"title":"创建数组","slug":"创建数组","link":"#创建数组","children":[]},{"level":4,"title":"数组空位","slug":"数组空位","link":"#数组空位","children":[]},{"level":4,"title":"数组索引","slug":"数组索引","link":"#数组索引","children":[]},{"level":4,"title":"检测数组","slug":"检测数组","link":"#检测数组","children":[]},{"level":4,"title":"迭代器方法","slug":"迭代器方法","link":"#迭代器方法","children":[]},{"level":4,"title":"复制和填充方法","slug":"复制和填充方法","link":"#复制和填充方法","children":[]},{"level":4,"title":"转换方法","slug":"转换方法","link":"#转换方法","children":[]},{"level":4,"title":"栈方法","slug":"栈方法","link":"#栈方法","children":[]},{"level":4,"title":"队列方法","slug":"队列方法","link":"#队列方法","children":[]},{"level":4,"title":"排序方法","slug":"排序方法","link":"#排序方法","children":[]},{"level":4,"title":"操作方法","slug":"操作方法","link":"#操作方法","children":[]},{"level":4,"title":"搜素和位置方法","slug":"搜素和位置方法","link":"#搜素和位置方法","children":[]},{"level":4,"title":"迭代方法","slug":"迭代方法","link":"#迭代方法","children":[]},{"level":4,"title":"归并方法","slug":"归并方法","link":"#归并方法","children":[]}]},{"level":3,"title":"定型数组","slug":"定型数组","link":"#定型数组","children":[{"level":4,"title":"ArrayBuffer","slug":"arraybuffer","link":"#arraybuffer","children":[]},{"level":4,"title":"DataView","slug":"dataview","link":"#dataview","children":[]},{"level":4,"title":"定型数组","slug":"定型数组-1","link":"#定型数组-1","children":[]}]},{"level":3,"title":"Map","slug":"map","link":"#map","children":[{"level":4,"title":"基本 API","slug":"基本-api","link":"#基本-api","children":[]},{"level":4,"title":"顺序与迭代","slug":"顺序与迭代","link":"#顺序与迭代","children":[]}]},{"level":3,"title":"WeakMap","slug":"weakmap","link":"#weakmap","children":[{"level":4,"title":"基本 API","slug":"基本-api-1","link":"#基本-api-1","children":[]},{"level":4,"title":"弱键","slug":"弱键","link":"#弱键","children":[]},{"level":4,"title":"不可迭代键","slug":"不可迭代键","link":"#不可迭代键","children":[]}]},{"level":3,"title":"Set","slug":"set","link":"#set","children":[{"level":4,"title":"基本 API","slug":"基本-api-2","link":"#基本-api-2","children":[]},{"level":4,"title":"顺序与迭代","slug":"顺序与迭代-1","link":"#顺序与迭代-1","children":[]}]},{"level":3,"title":"WeakSet","slug":"weakset","link":"#weakset","children":[{"level":4,"title":"基本 API","slug":"基本-api-3","link":"#基本-api-3","children":[]},{"level":4,"title":"弱值","slug":"弱值","link":"#弱值","children":[]},{"level":4,"title":"不可迭代值","slug":"不可迭代值","link":"#不可迭代值","children":[]}]},{"level":3,"title":"迭代与扩展操作","slug":"迭代与扩展操作","link":"#迭代与扩展操作","children":[]}]}],"git":{"updatedTime":1690280044000},"filePathRelative":"frontend/js/red-book/06.md"}');export{l as data}; diff --git a/assets/07.html-04baef94.js b/assets/07.html-04baef94.js new file mode 100644 index 0000000..6778490 --- /dev/null +++ b/assets/07.html-04baef94.js @@ -0,0 +1 @@ +const l=JSON.parse('{"key":"v-4b9b35ca","path":"/frontend/js/red-book/07.html","title":"红宝书","lang":"zh-CN","frontmatter":{"title":"红宝书","prev":{"text":"集合引用类型","link":"./06.md"},"next":{"text":"对象、类与面向对象编程","link":"./08.md"}},"headers":[{"level":2,"title":"迭代器与生成器","slug":"迭代器与生成器","link":"#迭代器与生成器","children":[{"level":3,"title":"理解迭代","slug":"理解迭代","link":"#理解迭代","children":[]},{"level":3,"title":"迭代器模式","slug":"迭代器模式","link":"#迭代器模式","children":[{"level":4,"title":"可迭代协议","slug":"可迭代协议","link":"#可迭代协议","children":[]},{"level":4,"title":"迭代器协议","slug":"迭代器协议","link":"#迭代器协议","children":[]},{"level":4,"title":"自定义迭代器","slug":"自定义迭代器","link":"#自定义迭代器","children":[]},{"level":4,"title":"提前终止迭代器","slug":"提前终止迭代器","link":"#提前终止迭代器","children":[]}]},{"level":3,"title":"生成器","slug":"生成器","link":"#生成器","children":[{"level":4,"title":"通过 yield 中断执行","slug":"通过-yield-中断执行","link":"#通过-yield-中断执行","children":[]},{"level":4,"title":"生成器作为默认迭代器","slug":"生成器作为默认迭代器","link":"#生成器作为默认迭代器","children":[]},{"level":4,"title":"提前终止生成器","slug":"提前终止生成器","link":"#提前终止生成器","children":[]}]}]}],"git":{"updatedTime":1690363481000},"filePathRelative":"frontend/js/red-book/07.md"}');export{l as data}; diff --git a/assets/07.html-66107e55.js b/assets/07.html-66107e55.js new file mode 100644 index 0000000..ecc8325 --- /dev/null +++ b/assets/07.html-66107e55.js @@ -0,0 +1,246 @@ +import{_ as n,o as s,c as a,e as t}from"./app-b4fb6edd.js";const p={},e=t(`

迭代器与生成器

小结

迭代器是一个可以由任意对象实现的接口,支持连续获取对象产出的每一个值。任何实现 Iterable 接口的对象有一个 Symbol.iterator 属性,这个属性引用默认迭代器。默认迭代器就像一个迭代器工厂,也就是一个函数,调用之后会产生一个实现 Iterator 接口的对象

生成器是一种特殊的函数,调用之后会返回一个生成器对象。生成器对象实现了 Iterable 接口,因此可用在任何消费可迭代对象的地方。生成器的独特之处在于支持 yield 关键字,这个关键字能够暂停执行生成器函数。使用 yield 关键字还可以通过 next() 方法接收输入和产出输出。在加上 * 之后,yield 可以将跟在它后面的可迭代对象序列化为一连串值

理解迭代

在 ECMAScript 较早的版本中,执行迭代必须使用循环或其他辅助结构。随着代码里增加,代码会变得越发混乱。很多语言都通过原生语言结构解决了这个问题,开发者无须事先知道如何迭代就能实现迭代操作。这个解决方案就是迭代器模式

迭代器模式

迭代器是按需创建的一次性对象。每个迭代器都会关联一个可迭代对象,而迭代器会暴露迭代其关联可迭代对象的 API。迭代器无须了解与其关联的可迭代对象的结构,只需要知道如何取得连续的值

可迭代协议

很多内置对象都实现了 Iterable 接口(可迭代协议):

检查是否存在默认迭代器属性可以暴露这个工厂函数(调用工厂函数会生成一个迭代器):

function isIterable(object) {
+	return typeof object[Symbol.iterator] === 'function'
+}
+

实际写代码过程中,不需要显示调用这个工厂函数来生成迭代器。实现可迭代协议的所有类型都会自动兼容接收可迭代对象的任何语言特性

接收可迭代对象的语言特性包括:

迭代器协议

迭代器是一种一次性使用的对象,用于迭代与其关联的可迭代对象。迭代器 API 使用 next() 方法在可迭代对象中遍历数据。每次调用 next() 方法都会返回一 IteratorResult 对象,这个结果对象包含两个属性:done 和 value

每个迭代器都表示对可迭代对象的一次性有序遍历,不同迭代器实例之间没有联系,只会独立地遍历可迭代对象

迭代器并不与可迭代对象某个时刻的快照绑定,而仅仅是使用游标来记录遍历可迭代对象的历程。如果可迭代对象在迭代期间被修改,迭代器会反映出这些变化:

const arr = ['foo', 'bar']
+const iter = arr[Symbol.iterator]()
+console.log(iter.next()) // { value: 'foo', done: false }
+arr.splice(1, 0, 'baz')
+console.log(iter.next()) // { value: 'baz', done: false }
+console.log(iter.next()) // { value: 'bar', done: false }
+console.log(iter.next()) // { value: undefined, done: true }
+

注意

迭代器维护一个指向可迭代对象的引用,因此迭代器会阻止垃圾回收程序回收可迭代对象

自定义迭代器

与 Iterator 接口类似,任何实现 Iterator 接口的对象都可以作为迭代器使用

class Counter {
+	constructor(limit) {
+		this.limit = limit
+	}
+
+	[Symbol.iterator]() {
+		let count = 1
+		const limit = this.limit
+		return {
+			next() {
+				if (count <= limit) {
+					return { done: false, value: count++ }
+				} else {
+					return { done: true, value: undefined }
+				}
+			},
+		}
+	}
+}
+const counter = new Counter(3)
+
+for (const i of counter) {
+	console.log(i)
+}
+

每个以这种方式创建的迭代器也实现了 Iterable 接口,并且 Symbol.iterator 属性引用的工厂函数会返回相同的迭代器

const arr = ['foo', 'bar', 'baz']
+const iter1 = arr[Symbol.iterator]()
+console.log(iter1[Symbol.iterator]) // ƒ [Symbol.iterator]() { [native code] }
+const iter2 = iter1[Symbol.iterator]()
+console.log(iter1 === iter2)
+



 

提前终止迭代器

可选的(意味着并不是所有的迭代器都是可关闭的,可以测试这个迭代器实例的 return 属性是不是函数来判断该迭代器是否可关闭) return() 方法用于指定在迭代器提前关闭时执行的逻辑。执行迭代的结构在想让迭代器知道它不想遍历到可迭代对象耗尽时,就可以“关闭”迭代器。可能的情况包括:

class Counter {
+	constructor(limit) {
+		this.limit = limit
+	}
+
+	[Symbol.iterator]() {
+		let count = 1
+		const limit = this.limit
+		return {
+			next() {
+				if (count <= limit) {
+					return { done: false, value: count++ }
+				} else {
+					return { done: true, value: undefined }
+				}
+			},
+			return() {
+				console.log('Exiting early')
+				return { done: true }
+			},
+		}
+	}
+}
+const counter = new Counter(5)
+
+for (const i of counter) {
+	if (i > 2) {
+		break
+	}
+
+	console.log(i)
+}
+
+const [a, b] = counter
+
















 
 
 
 














如果迭代器没有关闭,则还可以继续从上次离开的地方继续迭代。比如,数组的迭代器就是不能关闭的:

const a = [1, 2, 3, 4, 5]
+const iter = a[Symbol.iterator]()
+
+for (const i of iter) {
+	console.log(i)
+	if (i > 2) {
+		break
+	}
+}
+// 1
+// 2
+// 3
+
+for (const i of iter) {
+	console.log(i)
+}
+// 4
+// 5
+

注意,仅仅给一个不可关闭的迭代器增加 return() 方法并不能让它变成可关闭的,这个因为调用 return() 并不会强制迭代器进入关闭状态。即便如此,return() 方法还是会被调用:

const a = [1, 2, 3, 4, 5]
+const iter = a[Symbol.iterator]()
+
+iter.return = function () {
+	console.log('Exiting early')
+	return { done: true }
+}
+
+for (const i of iter) {
+	console.log(i)
+	if (i > 2) {
+		break
+	}
+}
+// 1
+// 2
+// 3
+// Exiting early
+
+for (const i of iter) {
+	console.log(i)
+}
+// 4
+// 5
+



 
 
 
 

















生成器

生成器的形式是一个函数,函数名称前面加一个 * 表示它是一个生成器,标识生成器的星号不受两侧空格的影响。只要是可以定义函数的地方就可以定义生成器

注意

箭头函数不能用来定义生成器函数

调用生成器会产生一个生成器对象。生成器对象一开始处于暂停执行(suspended)的状态。与迭代器相似,生成器也实现了 Iterator 接口,因此具有 next() 方法,调用这个方法会让生成器开始或恢复执行

function* generatorFn() {}
+
+const g = generatorFn()
+console.log(g) // generatorFn {<suspended>}
+console.log(g.next()) // {value: undefined, done: true}
+

next() 方法的返回值类似于迭代器,由一个 done 属性和一个 value 属性

value 属性是生成器函数的返回值,默认值为 undefined,可以通过生成器函数的返回值指定:

function* generatorFn() {
+	return 'foo'
+}
+
+const generatorObject = generatorFn()
+console.log(generatorObject) // generatorFn {<suspended>}
+console.log(generatorObject.next()) // {value: 'foo', done: true}
+

生成器对象实现了 Iterator 接口,它们默认的迭代器是自引用的

通过 yield 中断执行

yield 关键字可以让生成器停止和开始执行。生成器函数在遇到 yield 关键字之前会正常执行,遇到这个关键字之后,执行会停止,函数作用域的状态会被保留。停止执行的生成器函数只能通过在生成器对象上调用 next() 方法来恢复执行

yield 关键字生成的值会出现在 next() 方法返回的对象里。通过 yield 退出的生成器函数会处在 done:false 状态;通过 return 退出的生成器函数会处于 done:true 状态

生成器函数内部的执行流程会针对每个生成器对象区分作用域

yield 只能在生成器函数内部使用,用在其他地方会抛出错误

1. 生成器对象作为可迭代对象

在生成器对象上显示调用 next() 方法的用处并不大。如果把生成器当作可迭代对象:

function* generatorFn() {
+	yield 1
+	yield 2
+	yield 3
+}
+
+for (const i of generatorFn()) {
+	console.log(i)
+}
+// 1
+// 2
+// 3
+

2. 使用 yield 实现输入和输出

除了作为函数的中间返回语句使用,yield 还可以作为函数的中间参数使用。yield 语句的值可以通过 next() 方法的参数传入生成器函数

function* generatorFn(initial) {
+	console.log(initial)
+	console.log(yield)
+	console.log(yield)
+}
+
+const generatorObject = generatorFn('foo')
+generatorObject.next('bar') // foo
+generatorObject.next('baz') // baz
+generatorObject.next('qux') // qux
+

yield 可以同时用于输入和输出

function* generatorFn() {
+	return yield 'foo'
+}
+
+const generatorObject = generatorFn()
+console.log(generatorObject.next()) // {value: 'foo', done: false}
+console.log(generatorObject.next('bar')) // {value: 'bar', done: true}
+

使用生成器填充数组

function* zeros(n) {
+	for (let i = 0; i < n; i++) {
+		yield 0
+	}
+}
+console.log(Array.from(zeros(3)))
+

3. 产生可迭代对象 可以使用星号(两侧的空格不影响)增强 yield 的行为,让它那能够迭代一个可迭代对象,从而一次产出一个值:

function* generatorFn() {
+	yield* [1, 2, 3]
+}
+
+for (const i of generatorFn()) {
+	console.log(i)
+}
+// 1
+// 2
+// 3
+

yield* 的值是关联迭代器返回 done: true 时的 value 属性

4. 使用 yield* 实现递归算法

yield* 最有用的地方时实现递归操作,此时生成器可以产生自身:

function* nTimes(n) {
+	if (n > 0) {
+		yield* nTimes(n - 1)
+		yield n - 1
+	}
+}
+
+for (const i of nTimes(3)) {
+	console.log(i)
+}
+// 0
+// 1
+// 2
+

生成器作为默认迭代器

因为生成器对象实现了 Iterable 接口,而且生成器函数和默认迭代器被调用之后都产生迭代器,所以生成器格外适合作为默认迭代器

class Foo {
+	constructor() {
+		this.values = [1, 2, 3]
+	}
+
+	*[Symbol.iterator]() {
+		yield* this.values
+	}
+}
+

提前终止生成器

1. return()

return() 方法会强制生成器进入关闭状态。提供给 return() 方法的值,就是终止迭代器对象的值

所有的生成器都有 return(),只要通过它进入关闭状态,就无法恢复了

function* generatorFn() {
+	yield* [1, 2, 3]
+}
+
+const g = generatorFn()
+console.log(g) // generatorFn {<suspended>}
+console.log(g.return(4)) // {value: 4, done: true}
+console.log(g) // generatorFn {<closed>}
+

for-of 循环等内置语言结构会忽略状态为 done: true 的 IteratorObject 内部返回的值

function* generatorFn() {
+	yield* [1, 2, 3]
+}
+
+const g = generatorFn()
+
+for (const i of g) {
+	if (i > 1) {
+		g.return(4)
+	}
+
+	console.log(i)
+}
+// 1
+// 2
+

2. throw()

throw() 方法会在暂停的时候将一个提供的错误注入到生成器对象中。如果错误未被处理,生成器就会关闭:

function* generatorFn() {
+	yield* [1, 2, 3]
+}
+
+const g = generatorFn()
+
+console.log(g) // generatorFn {<suspended>}
+try {
+	g.throw(new Error('foo'))
+} catch (e) {
+	console.log(e) // Error: foo
+}
+console.log(g) // generatorFn {<closed>}
+

假如生成器内部处理了这个错误,那么生成器就不会关闭,而且还可以恢复执行。错误处理会跳过对应的 yield:

function* generatorFn() {
+	for (const x of [1, 2, 3]) {
+		try {
+			yield x
+		} catch (e) {
+			console.log(e)
+		}
+	}
+}
+
+const g = generatorFn()
+
+console.log(g.next()) // {value: 1, done: false}
+g.throw('foo')
+console.log(g.next()) // {value: 3, done: false}
+
`,78),o=[e];function c(l,i){return s(),a("div",null,o)}const r=n(p,[["render",c],["__file","07.html.vue"]]);export{r as default}; diff --git a/assets/08.html-8eebe63e.js b/assets/08.html-8eebe63e.js new file mode 100644 index 0000000..dc3e7ec --- /dev/null +++ b/assets/08.html-8eebe63e.js @@ -0,0 +1,317 @@ +import{_ as n,o as s,c as a,e as t}from"./app-b4fb6edd.js";const p={},e=t(`

对象、类与面向对象编程

ECMA-262 将对象定义为一组属性的无序集合

理解对象

属性的类型

ECMA-262 使用一些内部特性来描述属性的特征,属性分为两种:数据属性和访问器属性

1. 数据属性

数据属性包含一个保存数据值的位置,在这个位置可以读取和写入值。数据属性有 4 个特性描述它的行为:

Object.defineProperty() 修改属性的默认特性,如果不指定 configurable / enumerable / writable,则默认为 false:

let person = {}
+Object.defineProperty(person, 'name', {
+	writable: false,
+	value: 'Nicholas',
+})
+

2. 访问器属性

访问器属性不包含数据值。相反,它们包含一个获取(getter)函数和一个设置(setter)函数,不过这两个函数不是必需的。访问器属性有 4 个特性描述它的行为:

访问器属性是不能直接定义的,必须使用 Object.definedProperty():

let book = {
+	year_: 2004, // 下划线表示该属性并不希望在对象外部被访问
+	edition: 1,
+}
+
+Object.defineProperty(book, 'year', {
+	get() {
+		return this.year_
+	},
+	set(newValue) {
+		if (newValue > 2004) {
+			this.year_ = newValue
+			this.edition += newValue - 2004
+		}
+	},
+})
+
+book.year = 2005
+console.log(book.edition) // 2
+

定义多个属性

Object.defineProperties() 可以通过多个描述符一次定义多个属性,如果数据属性不指定 configurable / enumerable / writable,则默认为 false

读取属性的特性

Object.getOwnPropertyDescriptor() 可以取得给定属性的描述符

ECMAScript 2017 新增了 Object.getOwnPropertyDescriptors() 静态方法,可以取得给定对象所有自有属性的描述符

合并对象

Object.assign() 可以把任意多个源对象可枚举的自有属性浅拷贝到目标对象,然后返回目标对象。对每个符合条件的属性,这个方法会使用源对象上的 [[Get]] 取得属性值,然后使用目标对象上的 [[Set]] 设置属性值

const dest = {
+	set a(val) {
+		console.log(\`Invoked dest setter with param \${val}\`)
+	},
+}
+
+const src = {
+	get a() {
+		console.log('Invoked src getter')
+		return 'foo'
+	},
+}
+
+Object.assign(dest, src)
+// Invoked src getter
+// Invoked dest setter with param foo
+console.log(dest)
+

对象标识及相等判定

Object.is() 用于比较两个值是否相等,与 === 的行为基本一致,但是它对 NaN 和 +0/-0 作了特殊处理

要检查超过两个值,递归地利用相等性传递即可:

function recursivelyCheckEqual(x, ...rest) {
+	return Object.is(x, rest[0]) && (rest.length < 2 || recursivelyCheckEqual(...rest))
+}
+

增强的对象语法

ECMAScript 6 为定义和操作对象新增了很多及其有用的语法糖特性:

对象解构

对象解构就是使用与对象匹配的结构来实现对象属性赋值

如果是给事先声明的变量赋值,则赋值表达式必须包含在一对括号中,否则会被当成一个代码块:

let personName, personAge
+let person = {
+	name: 'Nicholas',
+	age: 29,
+}
+;({ name: personName, age: personAge } = person)
+

创建对象

使用 Object 构造函数和对象字面量可以方便的创建对象,但是这种方式有个缺点:创建具有同样接口的多个对象需要重复编写很多代码

工厂模式

工厂模式用于抽象创建特定对象的过程

下面的例子展示了一种按照特定接口创建对象的方式:

function createPerson(name, age, job) {
+	let o = new Object()
+	o.name = name
+	o.age = age
+	o.job = job
+	o.sayName = function () {
+		console.log(this.name)
+	}
+	return o
+}
+
+let person1 = createPerson('Nicholas', 29, 'Software Engineer')
+let person2 = createPerson('Greg', 27, 'Doctor')
+

这种工厂模式虽然可以解决创建多个类似对象的问题,但没有解决对象标识问题(即新创建的对象是什么类型)

构造函数模式

ECMAScript 中的构造函数就是用于创建特定类型对象的

构造函数不一定要写成函数声明的形式,赋值给变量的函数表达式也可以

使用 new 操作符调用构造函数会执行如下操作:

instanceof 操作符用于检测构造函数的 prototype 属性是否出现在某个实例对象的原型链上

构造函数的问题在于其定义的方法会在每个实例上都创建一遍

原型模式

每个函数都会创建一个 prototype 属性,这个属性是一个对象,包含应该由特定引用类型的实例共享的属性和方法

function Person() {}
+
+Person.prototype.name = 'Nicholas'
+Person.prototype.age = 29
+Person.prototype.job = 'Software Engineer'
+Person.prototype.sayName = function () {
+	console.log(this.name)
+}
+
+let person1 = new Person()
+person1.sayName() // Nicholas
+
+let person2 = new Person()
+person2.sayName() // Nicholas
+
+console.log(person1.sayName === person2.sayName) // true
+

与构造函数模式不同,使用这种原型模式定义的属性和方法是由所有实例共享的

1. 理解原型

构造函数.prototype 指向原型对象,原型对象.constructor 指向构造函数,实例对象.__proto__ 指向原型对象

isPrototypeOf() 方法用于检测当前对象是否是传入对象的原型

Object.getPrototypeOf() 方法返回传入对象的原型

Object.setPrototypeOf() 方法用于设置传入对象的原型,但其可能会严重影响性能,因为其涉及所有访问了那些修改过 [[Prototype]] 的对象的代码

Object.create() 方法创建一个新对象,将参数作为新创建的对象的__proto__

2. 原型层级

hasOwnProperty() 方法用于确定某个属性实在实例上还是在原型对象上,继承自 Object

Object.getOwnPropertyDescriptor() 方法也只对实例有效

3. 原型和 in 操作符

有两张方式使用 in 操作符:

单独使用 in 结合 'hasOwnProperty()' 方法可以确定属性是否存在于原型上:

function hasPrototypeProperty(object, name) {
+	return !object.hasOwnProperty(name) && name in object
+}
+

在 for-in 循环中使用 in 操作符时,可以通过对象访问且可以被枚举的属性都会返回

Object.keys() 方法返回对象上所有可枚举的实例属性

Object.getOwnPropertyNames() 方法返回对象上所有实例属性,无论是否可枚举

Object.getOwnPropertySymbols() 方法同上,但是只针对符号

4. 属性枚举顺序

const k1 = Symbol('k1')
+const k2 = Symbol('k2')
+
+let o = {
+	1: 1,
+	first: 'first',
+	[k2]: 'k2',
+	second: 'second',
+	0: 0,
+}
+
+o[k1] = 'k1'
+o[3] = 3
+o.third = 'third'
+o[2] = 2
+
+console.log(Object.getOwnPropertyNames(o)) // ['0', '1', '2', '3', 'first', 'second', 'third']
+console.log(Object.getOwnPropertySymbols(o)) // [Symbol(k2), Symbol(k1)]
+

对象迭代

Object.values() / Object.entires:非字符串属性会被转换为字符串输出,且执行对象的浅复制,符号属性会被忽略

1. 其他原型语法

function Person() {}
+
+Person.prototype = {
+	name: 'Nicholas',
+	age: 29,
+	job: 'Software Engineer',
+	sayName() {
+		console.log(this.name)
+	},
+}
+
+// 恢复 constructor 属性
+Object.defineProperty(Person.prototype, 'constructor', {
+	enumerable: false,
+	value: Person,
+})
+

2. 原生对象原型

不推荐在产品环境中修改原生对象原型,而是创建一个自定义类继承原生类型

3. 原型的问题

最主要的问题源自它的共享特性,针对包含引用值的属性,会导致实例间相互影响,故通常不单独使用原型模式

继承

实现继承是 ECMAScript 唯一支持的继承方式,且主要通过原型链实现

原型链

ECMA-262 把原型链定义为 ECMAScript 的主要继承方式,其基本思想是通过原型继承多个引用类型的属性和方法

1. 默认原型

任何函数的默认原型都是一个 Object 的实例

2. 原型与继承关系

确定原型与实例的关系:

3. 原型链的问题

这些问题导致原型链基本不会单独使用

盗用构造函数

为了解决原型包含引用值导致继承问题,一种叫作“盗用构造函数(constructor stealing)”的技术在开发社区流行起来,有时也称作“对象伪装”或“经典继承”

基本思路:在子类构造函数中调用父类构造函数

function SuperType() {
+	this.colors = ['red', 'blue', 'green']
+}
+
+function SubType() {
+	// 继承了 SuperType
+	SuperType.call(this)
+}
+
+let instance1 = new SubType()
+

1. 传递参数

相比于使用原型链,盗用构造函数的一个优点就是可以在子类构造函数中向父类构造函数传参

2. 盗用构造函数的问题

主要缺点:必须在构造函数中定义方法,导致函数不能重用。此外子类也不能访问父类原型上定义的方法,导致所有类型只能使用构造函数模式

故“盗用构造函数”基本上也不能单独使用

组合继承

组合继承(伪经典继承)综合了原型链和盗用构造函数,将两者的优点集中了起来

基本思路:通过原型链继承原型上的属性和方法,通过盗用构造函数继承实例属性

这样既可以把方法定义在原型上以实现重用,又可以让每个实例都有自己的属性

function SuperType(name) {
+	this.name = name
+	this.colors = ['red', 'blue', 'green']
+}
+
+SuperType.prototype.sayName = function () {
+	console.log(this.name)
+}
+
+function SubType(name, age) {
+	// 继承属性
+	SuperType.call(this, name)
+
+	this.age = age
+}
+
+// 继承方法
+SubType.prototype = new SuperType()
+
+SubType.prototype.sayAge = function () {
+	console.log(this.age)
+}
+

原型式继承

原型式继承的思路:即使不自定义类型也可以通过原型实现对象之间的信息共享

原型式继承非常适合不需要单独创建构造函数,但仍然需要在对象间共享信息的场合

ECMAScript 5 通过 Object.create() 方法将原型式继承的概念规范化了

const person = {
+	name: 'Nicholas',
+	friends: ['Shelby', 'Court', 'Van'],
+}
+
+const anotherPerson = Object.create(person, {
+	name: {
+		value: 'Greg',
+	},
+})
+

寄生式继承

寄生式继承的思路:创建一个实现继承的函数,以某种方式增强对象,然后返回这个对象

function createAnother(original) {
+	const clone = Object.create(original) // 通过调用函数创建一个新对象(任何返回新对象的函数都可以在这里使用)
+	clone.sayHi = function () {
+		// 以某种方式增强这个对象
+		console.log('hi')
+	}
+	return clone // 返回这个对象
+}
+

通过寄生式继承给对象添加函数会导致函数难以重用,与构造函数模式类似

寄生式组合继承

组合继承存在效率问题:父类构造函数始终会被调用两次(一次是创建子类原型时调用,另一次是在子类构造函数中调用),且子类的原型上会存在多余的属性(构造函数中的)

寄生式组合继承的基本思路:通过盗用构造函数来继承属性,但使用混合式原型链继承方法。基本思路是不通过调用父类构造函数给子类原型赋值,而是取得父类原型的一个副本

function inheritPrototype(subType, superType) {
+	const prototype = Object.create(superType.prototype) // 创建对象
+	prototype.constructor = subType // 增强对象
+	subType.prototype = prototype // 指定对象
+}
+
+function SuperType(name) {
+	this.name = name
+	this.colors = ['red', 'blue', 'green']
+}
+
+SuperType.prototype.sayName = function () {
+	console.log(this.name)
+}
+
+function SubType(name, age) {
+	SuperType.call(this, name)
+
+	this.age = age
+}
+
+inheritPrototype(SubType, SuperType)
+
+SubType.prototype.sayAge = function () {
+	console.log(this.age)
+}
+
















 




 




ECMAScript 6 类表面上看起来可以支持正式第面向对象编程,但实际上背后使用的仍然是原型和构造函数的概念

类定义

类定义主要有两种方式:类声明和类表达式

// 类声明
+class Person {}
+
+// 类表达式
+const Animal = class {}
+

默认情况下,类定义中的代码都在严格模式下执行

类构造函数与构造函数的主要区别:类构造函数必须使用 new 操作符, 否则会抛出错误;而普通构造函数如果不使用 new 调用,那么就会以全局的 this 作为内部对象

从各方面看,ECMAScript 类就是一种特殊函数

类是 JavaScript 的一等公民,因此可以像其他对象或函数引用一样把类作为参数传递

实例、原型和类成员

1. 实例成员

通过 new 调用类标识符,都会执行构造函数。在这个函数的内部,可以为新创建的实例添加“自有”属性

2. 原型方法与访问器

为了在实例间共享方法,类定义语法把在类块中定义的方法作为原型方法

类方法等同于对象属性,因此可以使用字符串、符号或计算的值作为键

不能在类块中给原型添加原始值或者对象作为成员数据

类定义也支持获取和设置访问器,语法与行为跟普通对象一样:

class Person {
+	get name() {
+		return this.name_
+	}
+	set name(newName) {
+		this.name_ = newName
+	}
+}
+

3. 静态类方法

静态类成员在类定义中使用 static 关键字作为前缀。在静态成员中,this 引用类自身

静态类方法非常适合作为实例工厂:

class Person {
+	constructor(age) {
+		this.age_ = age
+	}
+
+	sayAge() {
+		console.log(this.age_)
+	}
+
+	static create() {
+		return new Person(Math.floor(Math.random() * 100))
+	}
+}
+

4. 迭代器与生成器方法

类定义语法支持在原型和类本身上定义生成器方法,所以可以通过一个默认的迭代器,把类实例变成可迭代对象

class Person {
+	constructor() {
+		this.nickNames = ['di', 'diqiu', 'didi']
+	}
+
+	// *[Symbol.iterator]() {
+	// 	yield* this.nickNames
+	// }
+
+	// 也可以只返回迭代器实例
+	[Symbol.iterator]() {
+		return this.nickNames.values()
+	}
+}
+
+const p = new Person()
+
+for (const nickName of p) {
+	console.log(nickName)
+}
+

继承

ECMAScript 6 新增特性中最出色的一个就是原生支持了类继承机制。虽然类继承使用的是新语法,但背后依旧使用的是原型链

1. 继承基础

ES6 类支持单继承,使用 extends 关键字,就可以继承任何拥有 [[Construct]] 和原型的对象(类和普通的构造函数)

派生类都会通过原型链访问到类和原型上定义的方法

2. 构造函数、HomeObject 和 super()

派生类的方法可以通过 super 关键字引用它们的原型

super 关键字只能在派生类中使用,而且仅限于类构造函数、实例方法和静态方法内部

在类构造函数中使用 super 可以调用父类构造函数:

class Vehicle {
+	constructor() {
+		this.hasEngine = true
+	}
+}
+
+class Bus extends Vehicle {
+	constructor() {
+		/* 不要在 super 之前引用 this,否则会抛出 ReferenceError */
+		super() // 相当于 super.constructor()
+		console.log(this instanceof Vehicle) // true
+		console.log(this) // Bus {hasEngine: true}
+	}
+}
+
+new Bus()
+

在静态方法中可以通过 super 调用继承的类上的静态方法:

class Vehicle {
+	static identify() {
+		console.log('vehicle')
+	}
+}
+
+class Bus extends Vehicle {
+	static identify() {
+		super.identify()
+	}
+}
+
+Bus.identify() // vehicle
+

ES6 给类构造函数和静态方法内部添加了内部属性 [[HomeObject]],这个特性是一个指针,指向定义该方法的对象。这个指针是自动赋值的,而且只能在 JavaScript 引擎内部访问。super 始终会定义为 [[HomeObject]] 的原型

在使用 super 时需要注意几个问题:

3. 抽象基类

有时候可能需要定义这样一个类,它可供其他类继承,但本身不会被实例化

虽然 ECMAScript 没有专门支持这种类的语法,但通过 new.target(保存通过 new 关键字调用的类或函数) 也很容易实现

通过在实例化时检测 new.target 是不是抽象基类,可以阻止对抽象基类实例化:

// 抽象基类
+class Vehicle {
+	constructor() {
+		console.log(new.target)
+		if (new.target === Vehicle) {
+			throw new Error('Vehicle cannot be directly instantiated')
+		}
+	}
+}
+
+// 派生类
+class Bus extends Vehicle {}
+
+new Bus() // Bus {}
+
+new Vehicle() // Uncaught Error: Vehicle cannot be directly instantiated
+

另外,通过在抽象基类构造函数中进行检查,可以要求派生类必须定义某个方法(原型方法在嗲用类构造函数之前就已经存在了):

class Vehicle {
+	constructor() {
+		if (new.target === Vehicle) {
+			throw new Error('Vehicle cannot be directly instantiated')
+		}
+		if (!this.foo) {
+			throw new Error('Inheriting class must define foo()')
+		}
+		console.log('success')
+	}
+}
+
+// 派生类
+class Bus extends Vehicle {
+	foo() {}
+}
+
+// 派生类
+class Van extends Vehicle {}
+
+// new Bus() // success
+new Van() // Uncaught Error: Inheriting class must define foo()
+

4. 继承内置类型

ES6 类为继承内置引用类型提供了顺畅的机制,开发者可以方便地扩展内置类型:

有些内置类型的方法会返回新实例。默认情况下,返回实例的类型与原始实例的类型时一致的。如果想覆盖这个默认行为,则可以覆盖 Symbol.species 访问器,这个访问器决定在创建返回的实例时使用的类:

class SuperArray extends Array {
+	static get [Symbol.species]() {
+		return Array
+	}
+}
+
+const a1 = new SuperArray(1, 2, 3, 4)
+const a2 = a1.map(x => x * x)
+
+console.log(a1 instanceof SuperArray) // true
+console.log(a2 instanceof SuperArray) // false
+

5. 类混入

把不同类的行为集中到一个类是一种常见的 JavaScript 模式。虽然 ES6 没有显示支持多类继承,但通过现有特性可以轻松地模拟这种行为

注意

Object.assign() 方法是为了混入对象行为而设计的。只有在需要混入类的行为时才有必要自己实现混入表达式。如果只是需要混入多个对象的属性,那么使用 Object.assign() 就足够了

很多 JavaScript 框架(特别是 React)已经抛弃混入模式,转向了组合模式(把方法提取到独立的类和辅助对象中,然后把它们组合起来,但不是继承)。这反映了那个众所周知的软件设计原则:“组合胜过继承”

`,177),o=[e];function c(i,l){return s(),a("div",null,o)}const r=n(p,[["render",c],["__file","08.html.vue"]]);export{r as default}; diff --git a/assets/08.html-ce11059a.js b/assets/08.html-ce11059a.js new file mode 100644 index 0000000..2d4f0d7 --- /dev/null +++ b/assets/08.html-ce11059a.js @@ -0,0 +1 @@ +const l=JSON.parse('{"key":"v-4d500e69","path":"/frontend/js/red-book/08.html","title":"红宝书","lang":"zh-CN","frontmatter":{"title":"红宝书","prev":{"text":"迭代器与生成器","link":"./07.md"},"next":{"text":"对象、类与面向对象编程","link":"./08.md"}},"headers":[{"level":2,"title":"对象、类与面向对象编程","slug":"对象、类与面向对象编程","link":"#对象、类与面向对象编程","children":[{"level":3,"title":"理解对象","slug":"理解对象","link":"#理解对象","children":[{"level":4,"title":"属性的类型","slug":"属性的类型","link":"#属性的类型","children":[]},{"level":4,"title":"定义多个属性","slug":"定义多个属性","link":"#定义多个属性","children":[]},{"level":4,"title":"读取属性的特性","slug":"读取属性的特性","link":"#读取属性的特性","children":[]},{"level":4,"title":"合并对象","slug":"合并对象","link":"#合并对象","children":[]},{"level":4,"title":"对象标识及相等判定","slug":"对象标识及相等判定","link":"#对象标识及相等判定","children":[]},{"level":4,"title":"增强的对象语法","slug":"增强的对象语法","link":"#增强的对象语法","children":[]},{"level":4,"title":"对象解构","slug":"对象解构","link":"#对象解构","children":[]}]},{"level":3,"title":"创建对象","slug":"创建对象","link":"#创建对象","children":[{"level":4,"title":"工厂模式","slug":"工厂模式","link":"#工厂模式","children":[]},{"level":4,"title":"构造函数模式","slug":"构造函数模式","link":"#构造函数模式","children":[]},{"level":4,"title":"原型模式","slug":"原型模式","link":"#原型模式","children":[]},{"level":4,"title":"对象迭代","slug":"对象迭代","link":"#对象迭代","children":[]}]},{"level":3,"title":"继承","slug":"继承","link":"#继承","children":[{"level":4,"title":"原型链","slug":"原型链","link":"#原型链","children":[]},{"level":4,"title":"盗用构造函数","slug":"盗用构造函数","link":"#盗用构造函数","children":[]},{"level":4,"title":"组合继承","slug":"组合继承","link":"#组合继承","children":[]},{"level":4,"title":"原型式继承","slug":"原型式继承","link":"#原型式继承","children":[]},{"level":4,"title":"寄生式继承","slug":"寄生式继承","link":"#寄生式继承","children":[]},{"level":4,"title":"寄生式组合继承","slug":"寄生式组合继承","link":"#寄生式组合继承","children":[]}]},{"level":3,"title":"类","slug":"类","link":"#类","children":[{"level":4,"title":"类定义","slug":"类定义","link":"#类定义","children":[]},{"level":4,"title":"实例、原型和类成员","slug":"实例、原型和类成员","link":"#实例、原型和类成员","children":[]},{"level":4,"title":"继承","slug":"继承-1","link":"#继承-1","children":[]}]}]}],"git":{"updatedTime":1690683083000},"filePathRelative":"frontend/js/red-book/08.md"}');export{l as data}; diff --git a/assets/09.html-4f635d79.js b/assets/09.html-4f635d79.js new file mode 100644 index 0000000..c6dc5d0 --- /dev/null +++ b/assets/09.html-4f635d79.js @@ -0,0 +1,81 @@ +import{_ as n,o as s,c as a,e}from"./app-b4fb6edd.js";const t={},p=e(`

代理与反射

ECMAScript 6 新增的代理和反射为开发者提供了拦截并向基本操作嵌入额外行为的能力

代理基础

代理是目标对象的抽象

创建空代理

最简单的代理是空代理,即除了作为一个抽象的目标对象,什么也不做

代理使用 Proxy 构造函数创建,接收两个必填参数:目标对象和处理程序对象

const target = {
+	id: 'target',
+}
+
+const handler = {}
+
+const proxy = new Proxy(target, handler)
+
+console.log(target === proxy) // false
+

针对上述代码中 targetproxy 两个对象,注意:

定义捕获器

使用代理的主要目的是可以定义捕获器(trap)

例如,定义一个 get() 捕获器,在 ECMAScript 操作以某种形式调用 get() 时触发:

const target = {
+	foo: 'bar',
+}
+
+const handler = {
+	get() {
+		return 'handler override'
+	},
+}
+
+const proxy = new Proxy(target, handler)
+

注意:

捕获器参数和反射 API

所有捕获器都可以访问相应的参数,基于这些参数可以重建被捕获方法的原始行为

比如,get() 捕获器接收以下参数:

通过这些参数可以重建被捕获方法的原始行为:

const target = {
+	foo: 'bar',
+}
+
+const handler = {
+	get(trapTarget, property, receiver) {
+		return trapTarget[property]
+	},
+}
+
+const proxy = new Proxy(target, handler)
+

但并非所有捕获器行为都像 get() 这么简单,因此 ECMAScript 6 为所有捕获器定义了一组默认行为,这些行为可以在 Reflect 对象上找到

处理程序对象所有可以捕获的方法都有对应的反射(Reflect)API 方法,使用反射 API 定义空代理对象:

const target = {
+	foo: 'bar',
+}
+
+const proxy = new Proxy(target, Reflect)
+

在反射 API 的基础上可以用最少的代码修改捕获的方法。比如,在某个属性被访问时,对返回的值进行一番修饰:

const target = {
+	foo: 'bar',
+}
+
+const handler = {
+	get(trapTarget, property, receiver) {
+		const decoration = property === 'foo' ? '!!!' : ''
+		return Reflect.get(...arguments) + decoration
+	},
+}
+
+const proxy = new Proxy(target, handler)
+
+console.log(proxy.foo) // bar!!!
+

捕获器不变式

捕获器处理程序的行为必须遵守“捕获器不变式(trap invariant)”

比如,如果目标对象有一个不可配置且不可写的数据属性,那么在捕获器返回一个与该属性不同的值时,会抛出 TypeError

可撤销代理

new Proxy() 创建的代理对象与目标对象之间的联系会在代理对象的生命周期内一直存在

Proxy.revocable() 方法创建一个可撤销的代理,返回一个对象,包含两个属性:

注意:

实用反射 API

某些情况下应该优先使用反射 API:

1. 反射 API 与 对象 API

2. 状态标记

很多反射方法返回称作“状态标记”的布尔值,表示操作是否成功。

以下方法都会提供状态标记:

3. 用一等函数替代操作符

4. 安全地应用函数

在通过 apply 方法调用函数,被调用的函数可能也定义了自己的 apply 属性,此时:

Function.prototype.apply.call(myFunc, thisVal, argumentList)
+
+// 可替换为
+Reflect.apply(myFunc, thisVal, argumentList)
+

代理另外一个代理

代理可以拦截反射 API 的操作,而这意味着完全可以创建一个代理,通过它去代理另外一个代理。这样就可以在一个目标对象上建立多层拦截网:

const target = {
+	foo: 'bar',
+}
+
+const firstProxy = new Proxy(target, {
+	get() {
+		console.log('first proxy')
+		return Reflect.get(...arguments)
+	},
+})
+
+const secondProxy = new Proxy(firstProxy, {
+	get() {
+		console.log('second proxy')
+		return Reflect.get(...arguments)
+	},
+})
+
+console.log(secondProxy.foo)
+// second proxy
+// first proxy
+// bar
+

代理的问题与不足

1. 代理中的 this

如果目标对象依赖于对象标识,那就可能遇到意料之外的问题

2. 代理与内部插槽 有些 ECMAScript 内置类型可能会依赖代理无法控制的机制,结果导致在代理上调用某些方法会出错

比如,Date 类型方法的执行依赖 this 值上的内部槽位 [[NumberDate]],代理对象上不存在这个内部槽位,而且这个内部槽位的值也不能通过普通的 get() 和 set() 访问到,于是代理拦截后本应转发给目标对象的方法会抛出 TypeError:

const target = new Date()
+const proxy = new Proxy(target, {})
+console.log(proxy instanceof Date) // true
+proxy.getDate() // TypeError
+

代理捕获器与反射方法

代理可以捕获 13 种不同的基本操作,这些操作有不同的反射 API 方法、参数、关联 ECMAScript 操作和不变式

代理模式

使用代理可以在代码中实现一些有用的编程模式

`,61),o=[p];function c(l,i){return s(),a("div",null,o)}const u=n(t,[["render",c],["__file","09.html.vue"]]);export{u as default}; diff --git a/assets/09.html-e58cbdae.js b/assets/09.html-e58cbdae.js new file mode 100644 index 0000000..ac0cf6d --- /dev/null +++ b/assets/09.html-e58cbdae.js @@ -0,0 +1 @@ +const l=JSON.parse('{"key":"v-4f04e708","path":"/frontend/js/red-book/09.html","title":"红宝书","lang":"zh-CN","frontmatter":{"title":"红宝书","prev":{"text":"对象、类与面向对象编程","link":"./08.md"},"next":{"text":"函数","link":"./10.md"}},"headers":[{"level":2,"title":"代理与反射","slug":"代理与反射","link":"#代理与反射","children":[{"level":3,"title":"代理基础","slug":"代理基础","link":"#代理基础","children":[{"level":4,"title":"创建空代理","slug":"创建空代理","link":"#创建空代理","children":[]},{"level":4,"title":"定义捕获器","slug":"定义捕获器","link":"#定义捕获器","children":[]},{"level":4,"title":"捕获器参数和反射 API","slug":"捕获器参数和反射-api","link":"#捕获器参数和反射-api","children":[]},{"level":4,"title":"捕获器不变式","slug":"捕获器不变式","link":"#捕获器不变式","children":[]},{"level":4,"title":"可撤销代理","slug":"可撤销代理","link":"#可撤销代理","children":[]},{"level":4,"title":"实用反射 API","slug":"实用反射-api","link":"#实用反射-api","children":[]},{"level":4,"title":"代理另外一个代理","slug":"代理另外一个代理","link":"#代理另外一个代理","children":[]},{"level":4,"title":"代理的问题与不足","slug":"代理的问题与不足","link":"#代理的问题与不足","children":[]}]},{"level":3,"title":"代理捕获器与反射方法","slug":"代理捕获器与反射方法","link":"#代理捕获器与反射方法","children":[]},{"level":3,"title":"代理模式","slug":"代理模式","link":"#代理模式","children":[]}]}],"git":{"updatedTime":1691537084000},"filePathRelative":"frontend/js/red-book/09.md"}');export{l as data}; diff --git a/assets/10.html-530a222f.js b/assets/10.html-530a222f.js new file mode 100644 index 0000000..be7e199 --- /dev/null +++ b/assets/10.html-530a222f.js @@ -0,0 +1 @@ +import{_ as t,o as c,c as o,a as e,d as n}from"./app-b4fb6edd.js";const a={},s=e("h2",{id:"函数",tabindex:"-1"},[e("a",{class:"header-anchor",href:"#函数","aria-hidden":"true"},"#"),n(" 函数")],-1),r=e("p",null,"函数实际上是对象,每个函数都是 Function 类型的实例",-1),d=e("p",null,[n("不推荐使用 Function 构造函数来定义函数 "),e("code",null,'let sum = new Function("num1", "num2", "return num1 + num2")'),n(",因为这种方式会导致解析两次代码(第一次是解析常规 ECMAScript 代码,第二次是解析传入构造函数的字符串)")],-1),_=e("h3",{id:"箭头函数",tabindex:"-1"},[e("a",{class:"header-anchor",href:"#箭头函数","aria-hidden":"true"},"#"),n(" 箭头函数")],-1),i=[s,r,d,_];function u(h,l){return c(),o("div",null,i)}const f=t(a,[["render",u],["__file","10.html.vue"]]);export{f as default}; diff --git a/assets/10.html-92f88bd8.js b/assets/10.html-92f88bd8.js new file mode 100644 index 0000000..1f262be --- /dev/null +++ b/assets/10.html-92f88bd8.js @@ -0,0 +1 @@ +const e=JSON.parse('{"key":"v-748f84b2","path":"/frontend/js/red-book/10.html","title":"红宝书","lang":"zh-CN","frontmatter":{"title":"红宝书","prev":{"text":"代理与反射","link":"./09.md"},"next":{"text":"函数","link":"./10.md"}},"headers":[{"level":2,"title":"函数","slug":"函数","link":"#函数","children":[{"level":3,"title":"箭头函数","slug":"箭头函数","link":"#箭头函数","children":[]}]}],"git":{"updatedTime":1691537084000},"filePathRelative":"frontend/js/red-book/10.md"}');export{e as data}; diff --git a/assets/404.html-b2dd1c29.js b/assets/404.html-b2dd1c29.js new file mode 100644 index 0000000..0df26da --- /dev/null +++ b/assets/404.html-b2dd1c29.js @@ -0,0 +1 @@ +import{_ as e,o as c,c as t}from"./app-b4fb6edd.js";const _={};function o(r,n){return c(),t("div")}const a=e(_,[["render",o],["__file","404.html.vue"]]);export{a as default}; diff --git a/assets/404.html-f9875e7b.js b/assets/404.html-f9875e7b.js new file mode 100644 index 0000000..5a0b8de --- /dev/null +++ b/assets/404.html-f9875e7b.js @@ -0,0 +1 @@ +const t=JSON.parse('{"key":"v-3706649a","path":"/404.html","title":"","lang":"zh-CN","frontmatter":{"layout":"NotFound"},"headers":[],"git":{},"filePathRelative":null}');export{t as data}; diff --git a/assets/ali-iconfont.html-7f7a09e2.js b/assets/ali-iconfont.html-7f7a09e2.js new file mode 100644 index 0000000..383d2e9 --- /dev/null +++ b/assets/ali-iconfont.html-7f7a09e2.js @@ -0,0 +1 @@ +const t=JSON.parse('{"key":"v-4525d36b","path":"/frontend/other/tools/ali-iconfont.html","title":"小工具","lang":"zh-CN","frontmatter":{"title":"小工具"},"headers":[{"level":2,"title":"阿里图标库","slug":"阿里图标库","link":"#阿里图标库","children":[{"level":3,"title":"Font class 用法","slug":"font-class-用法","link":"#font-class-用法","children":[]}]}],"git":{"updatedTime":1694268022000},"filePathRelative":"frontend/other/tools/ali-iconfont.md"}');export{t as data}; diff --git a/assets/ali-iconfont.html-e6785a18.js b/assets/ali-iconfont.html-e6785a18.js new file mode 100644 index 0000000..b2a7fea --- /dev/null +++ b/assets/ali-iconfont.html-e6785a18.js @@ -0,0 +1,78 @@ +import{_ as n,o as s,c as a,e as t}from"./app-b4fb6edd.js";const p={},e=t(`

阿里图标库

Font class 用法

只需要引入 css 和字体文件,把字体编码为 base64 格式,那只需要引入一个 css 文件即可

import fetch from 'node-fetch'
+import fs from 'fs'
+import prompts from 'prompts'
+
+const main = async () => {
+const validateUrl = url => (/\\/(font.*)\\.css/.test(url) ? true : '地址不对哦')
+const questions = [
+  {
+    type: 'text',
+    name: 'url',
+    message: '输入下iconfont的fontClass地址?',
+    validate: validateUrl
+  }
+]
+const { url } = await prompts(questions)
+const cssUrl = \`https://\${url}\`
+// 指定保存字体文件的目录
+const fontDirectory = './fonts'
+const cssDirectory = '.s'
+// 创建字体文件保存目录
+if (!fs.existsSync(fontDirectory)) {
+  fs.mkdirSync(fontDirectory)
+}
+// 使用node-fetch获取CSS文件内容
+fetch(cssUrl)
+    .then(response => response.text())
+    .then(cssContent => {
+      // 使用正则表达式提取字体文件的URL
+      const fontUrls = cssContent.match(/url\\('\\/\\/([^']+)'\\)/g)
+
+      if (fontUrls) {
+        // 使用Promise.all()等待所有字体文件的下载完成
+        Promise.all(
+          fontUrls.map(fontUrl => {
+            // 提取URL中的字体文件链接
+            const urlMatch = fontUrl.match(/url\\('\\/\\/([^']+)'\\)/)
+            if (urlMatch && urlMatch[1]) {
+              const fontFileUrl = \`https://\${urlMatch[1]}\` // 添加协议
+              // 下载字体文件并转换为Base64编码
+              return fetchAndEncodeToBase64(fontFileUrl)
+            }
+            return Promise.resolve('')
+          })
+        ).then(encodedFonts => {
+          // 将所有字体的Base64编码插入CSS
+          encodedFonts.forEach((encodedFont, index) => {
+            cssContent = cssContent.replace(fontUrls[index], encodedFont)
+          })
+
+          // 生成包含所有字体的Base64编码的CSS文件
+          fs.writeFileSync('src/styles/iconfont.scss', cssContent)
+          console.log('包含所有字体的Base64编码的CSS文件已生成')
+        })
+      } else {
+        console.error('未找到字体文件URL')
+      }
+    })
+    .catch(error => {
+      console.error('下载CSS文件时出错:', error)
+    })
+}
+
+// 下载字体文件并转换为Base64编码
+async function fetchAndEncodeToBase64(fontFileUrl) {
+  try {
+    const response = await fetch(fontFileUrl)
+    const buffer = await response.arrayBuffer()
+    const base64Font = Buffer.from(buffer).toString('base64')
+    const ext = fontFileUrl.substring(fontFileUrl.lastIndexOf('.') + 1)
+    return \`url('data:application/font-\${ext};charset=utf-8;base64,\${base64Font}')\`
+  } catch (error) {
+    console.error(\`下载字体文件并转换为Base64编码时出错:\${fontFileUrl}\`, error)
+    return '' // 返回一个空字符串以避免破坏CSS
+  }
+}
+
+main()
+
`,4),o=[e];function c(l,i){return s(),a("div",null,o)}const r=n(p,[["render",c],["__file","ali-iconfont.html.vue"]]);export{r as default}; diff --git a/assets/app-b4fb6edd.js b/assets/app-b4fb6edd.js new file mode 100644 index 0000000..69444de --- /dev/null +++ b/assets/app-b4fb6edd.js @@ -0,0 +1,10 @@ +const jl="modulepreload",Bl=function(e){return"/study-notes/"+e},Ko={},Q=function(t,n,r){if(!n||n.length===0)return t();const o=document.getElementsByTagName("link");return Promise.all(n.map(s=>{if(s=Bl(s),s in Ko)return;Ko[s]=!0;const i=s.endsWith(".css"),l=i?'[rel="stylesheet"]':"";if(!!r)for(let u=o.length-1;u>=0;u--){const f=o[u];if(f.href===s&&(!i||f.rel==="stylesheet"))return}else if(document.querySelector(`link[href="${s}"]${l}`))return;const c=document.createElement("link");if(c.rel=i?"stylesheet":jl,i||(c.as="script",c.crossOrigin=""),c.href=s,document.head.appendChild(c),i)return new Promise((u,f)=>{c.addEventListener("load",u),c.addEventListener("error",()=>f(new Error(`Unable to preload CSS for ${s}`)))})})).then(()=>t())};function ho(e,t){const n=Object.create(null),r=e.split(",");for(let o=0;o!!n[o.toLowerCase()]:o=>!!n[o]}const we={},nn=[],ot=()=>{},zl=()=>!1,Vl=/^on[^a-z]/,Hn=e=>Vl.test(e),po=e=>e.startsWith("onUpdate:"),Pe=Object.assign,mo=(e,t)=>{const n=e.indexOf(t);n>-1&&e.splice(n,1)},Ul=Object.prototype.hasOwnProperty,ue=(e,t)=>Ul.call(e,t),G=Array.isArray,rn=e=>br(e)==="[object Map]",oi=e=>br(e)==="[object Set]",se=e=>typeof e=="function",pe=e=>typeof e=="string",vo=e=>typeof e=="symbol",xe=e=>e!==null&&typeof e=="object",si=e=>xe(e)&&se(e.then)&&se(e.catch),ii=Object.prototype.toString,br=e=>ii.call(e),Wl=e=>br(e).slice(8,-1),li=e=>br(e)==="[object Object]",go=e=>pe(e)&&e!=="NaN"&&e[0]!=="-"&&""+parseInt(e,10)===e,Ln=ho(",key,ref,ref_for,ref_key,onVnodeBeforeMount,onVnodeMounted,onVnodeBeforeUpdate,onVnodeUpdated,onVnodeBeforeUnmount,onVnodeUnmounted"),yr=e=>{const t=Object.create(null);return n=>t[n]||(t[n]=e(n))},ql=/-(\w)/g,ft=yr(e=>e.replace(ql,(t,n)=>n?n.toUpperCase():"")),Kl=/\B([A-Z])/g,Yt=yr(e=>e.replace(Kl,"-$1").toLowerCase()),Er=yr(e=>e.charAt(0).toUpperCase()+e.slice(1)),Mr=yr(e=>e?`on${Er(e)}`:""),Rn=(e,t)=>!Object.is(e,t),Nr=(e,t)=>{for(let n=0;n{Object.defineProperty(e,t,{configurable:!0,enumerable:!1,value:n})},Yl=e=>{const t=parseFloat(e);return isNaN(t)?e:t},Jl=e=>{const t=pe(e)?Number(e):NaN;return isNaN(t)?e:t};let Yo;const Gr=()=>Yo||(Yo=typeof globalThis<"u"?globalThis:typeof self<"u"?self:typeof window<"u"?window:typeof global<"u"?global:{});function Fn(e){if(G(e)){const t={};for(let n=0;n{if(n){const r=n.split(Ql);r.length>1&&(t[r[0].trim()]=r[1].trim())}}),t}function We(e){let t="";if(pe(e))t=e;else if(G(e))for(let n=0;npe(e)?e:e==null?"":G(e)||xe(e)&&(e.toString===ii||!se(e.toString))?JSON.stringify(e,ci,2):String(e),ci=(e,t)=>t&&t.__v_isRef?ci(e,t.value):rn(t)?{[`Map(${t.size})`]:[...t.entries()].reduce((n,[r,o])=>(n[`${r} =>`]=o,n),{})}:oi(t)?{[`Set(${t.size})`]:[...t.values()]}:xe(t)&&!G(t)&&!li(t)?String(t):t;let Ye;class na{constructor(t=!1){this.detached=t,this._active=!0,this.effects=[],this.cleanups=[],this.parent=Ye,!t&&Ye&&(this.index=(Ye.scopes||(Ye.scopes=[])).push(this)-1)}get active(){return this._active}run(t){if(this._active){const n=Ye;try{return Ye=this,t()}finally{Ye=n}}}on(){Ye=this}off(){Ye=this.parent}stop(t){if(this._active){let n,r;for(n=0,r=this.effects.length;n{const t=new Set(e);return t.w=0,t.n=0,t},fi=e=>(e.w&Ot)>0,di=e=>(e.n&Ot)>0,sa=({deps:e})=>{if(e.length)for(let t=0;t{const{deps:t}=e;if(t.length){let n=0;for(let r=0;r{(u==="length"||u>=a)&&l.push(c)})}else switch(n!==void 0&&l.push(i.get(n)),t){case"add":G(e)?go(n)&&l.push(i.get("length")):(l.push(i.get(Ut)),rn(e)&&l.push(i.get(Zr)));break;case"delete":G(e)||(l.push(i.get(Ut)),rn(e)&&l.push(i.get(Zr)));break;case"set":rn(e)&&l.push(i.get(Ut));break}if(l.length===1)l[0]&&Xr(l[0]);else{const a=[];for(const c of l)c&&a.push(...c);Xr(_o(a))}}function Xr(e,t){const n=G(e)?e:[...e];for(const r of n)r.computed&&Go(r);for(const r of n)r.computed||Go(r)}function Go(e,t){(e!==nt||e.allowRecurse)&&(e.scheduler?e.scheduler():e.run())}function la(e,t){var n;return(n=lr.get(e))==null?void 0:n.get(t)}const aa=ho("__proto__,__v_isRef,__isVue"),mi=new Set(Object.getOwnPropertyNames(Symbol).filter(e=>e!=="arguments"&&e!=="caller").map(e=>Symbol[e]).filter(vo)),ca=yo(),ua=yo(!1,!0),fa=yo(!0),Qo=da();function da(){const e={};return["includes","indexOf","lastIndexOf"].forEach(t=>{e[t]=function(...n){const r=de(this);for(let s=0,i=this.length;s{e[t]=function(...n){mn();const r=de(this)[t].apply(this,n);return vn(),r}}),e}function ha(e){const t=de(this);return qe(t,"has",e),t.hasOwnProperty(e)}function yo(e=!1,t=!1){return function(r,o,s){if(o==="__v_isReactive")return!e;if(o==="__v_isReadonly")return e;if(o==="__v_isShallow")return t;if(o==="__v_raw"&&s===(e?t?Aa:yi:t?bi:_i).get(r))return r;const i=G(r);if(!e){if(i&&ue(Qo,o))return Reflect.get(Qo,o,s);if(o==="hasOwnProperty")return ha}const l=Reflect.get(r,o,s);return(vo(o)?mi.has(o):aa(o))||(e||qe(r,"get",o),t)?l:$e(l)?i&&go(o)?l:l.value:xe(l)?e?Cr(l):jn(l):l}}const pa=vi(),ma=vi(!0);function vi(e=!1){return function(n,r,o,s){let i=n[r];if(an(i)&&$e(i)&&!$e(o))return!1;if(!e&&(!ar(o)&&!an(o)&&(i=de(i),o=de(o)),!G(n)&&$e(i)&&!$e(o)))return i.value=o,!0;const l=G(n)&&go(r)?Number(r)e,wr=e=>Reflect.getPrototypeOf(e);function Wn(e,t,n=!1,r=!1){e=e.__v_raw;const o=de(e),s=de(t);n||(t!==s&&qe(o,"get",t),qe(o,"get",s));const{has:i}=wr(o),l=r?Eo:n?xo:On;if(i.call(o,t))return l(e.get(t));if(i.call(o,s))return l(e.get(s));e!==o&&e.get(t)}function qn(e,t=!1){const n=this.__v_raw,r=de(n),o=de(e);return t||(e!==o&&qe(r,"has",e),qe(r,"has",o)),e===o?n.has(e):n.has(e)||n.has(o)}function Kn(e,t=!1){return e=e.__v_raw,!t&&qe(de(e),"iterate",Ut),Reflect.get(e,"size",e)}function Zo(e){e=de(e);const t=de(this);return wr(t).has.call(t,e)||(t.add(e),_t(t,"add",e,e)),this}function Xo(e,t){t=de(t);const n=de(this),{has:r,get:o}=wr(n);let s=r.call(n,e);s||(e=de(e),s=r.call(n,e));const i=o.call(n,e);return n.set(e,t),s?Rn(t,i)&&_t(n,"set",e,t):_t(n,"add",e,t),this}function es(e){const t=de(this),{has:n,get:r}=wr(t);let o=n.call(t,e);o||(e=de(e),o=n.call(t,e)),r&&r.call(t,e);const s=t.delete(e);return o&&_t(t,"delete",e,void 0),s}function ts(){const e=de(this),t=e.size!==0,n=e.clear();return t&&_t(e,"clear",void 0,void 0),n}function Yn(e,t){return function(r,o){const s=this,i=s.__v_raw,l=de(i),a=t?Eo:e?xo:On;return!e&&qe(l,"iterate",Ut),i.forEach((c,u)=>r.call(o,a(c),a(u),s))}}function Jn(e,t,n){return function(...r){const o=this.__v_raw,s=de(o),i=rn(s),l=e==="entries"||e===Symbol.iterator&&i,a=e==="keys"&&i,c=o[e](...r),u=n?Eo:t?xo:On;return!t&&qe(s,"iterate",a?Zr:Ut),{next(){const{value:f,done:h}=c.next();return h?{value:f,done:h}:{value:l?[u(f[0]),u(f[1])]:u(f),done:h}},[Symbol.iterator](){return this}}}}function Ct(e){return function(...t){return e==="delete"?!1:this}}function Ea(){const e={get(s){return Wn(this,s)},get size(){return Kn(this)},has:qn,add:Zo,set:Xo,delete:es,clear:ts,forEach:Yn(!1,!1)},t={get(s){return Wn(this,s,!1,!0)},get size(){return Kn(this)},has:qn,add:Zo,set:Xo,delete:es,clear:ts,forEach:Yn(!1,!0)},n={get(s){return Wn(this,s,!0)},get size(){return Kn(this,!0)},has(s){return qn.call(this,s,!0)},add:Ct("add"),set:Ct("set"),delete:Ct("delete"),clear:Ct("clear"),forEach:Yn(!0,!1)},r={get(s){return Wn(this,s,!0,!0)},get size(){return Kn(this,!0)},has(s){return qn.call(this,s,!0)},add:Ct("add"),set:Ct("set"),delete:Ct("delete"),clear:Ct("clear"),forEach:Yn(!0,!0)};return["keys","values","entries",Symbol.iterator].forEach(s=>{e[s]=Jn(s,!1,!1),n[s]=Jn(s,!0,!1),t[s]=Jn(s,!1,!0),r[s]=Jn(s,!0,!0)}),[e,n,t,r]}const[wa,Ca,xa,La]=Ea();function wo(e,t){const n=t?e?La:xa:e?Ca:wa;return(r,o,s)=>o==="__v_isReactive"?!e:o==="__v_isReadonly"?e:o==="__v_raw"?r:Reflect.get(ue(n,o)&&o in r?n:r,o,s)}const Ta={get:wo(!1,!1)},ka={get:wo(!1,!0)},Sa={get:wo(!0,!1)},_i=new WeakMap,bi=new WeakMap,yi=new WeakMap,Aa=new WeakMap;function Pa(e){switch(e){case"Object":case"Array":return 1;case"Map":case"Set":case"WeakMap":case"WeakSet":return 2;default:return 0}}function Ra(e){return e.__v_skip||!Object.isExtensible(e)?0:Pa(Wl(e))}function jn(e){return an(e)?e:Co(e,!1,gi,Ta,_i)}function Ei(e){return Co(e,!1,ya,ka,bi)}function Cr(e){return Co(e,!0,ba,Sa,yi)}function Co(e,t,n,r,o){if(!xe(e)||e.__v_raw&&!(t&&e.__v_isReactive))return e;const s=o.get(e);if(s)return s;const i=Ra(e);if(i===0)return e;const l=new Proxy(e,i===2?r:n);return o.set(e,l),l}function on(e){return an(e)?on(e.__v_raw):!!(e&&e.__v_isReactive)}function an(e){return!!(e&&e.__v_isReadonly)}function ar(e){return!!(e&&e.__v_isShallow)}function wi(e){return on(e)||an(e)}function de(e){const t=e&&e.__v_raw;return t?de(t):e}function Ci(e){return ir(e,"__v_skip",!0),e}const On=e=>xe(e)?jn(e):e,xo=e=>xe(e)?Cr(e):e;function xi(e){Pt&&nt&&(e=de(e),pi(e.dep||(e.dep=_o())))}function Li(e,t){e=de(e);const n=e.dep;n&&Xr(n)}function $e(e){return!!(e&&e.__v_isRef===!0)}function Ce(e){return ki(e,!1)}function Ti(e){return ki(e,!0)}function ki(e,t){return $e(e)?e:new Oa(e,t)}class Oa{constructor(t,n){this.__v_isShallow=n,this.dep=void 0,this.__v_isRef=!0,this._rawValue=n?t:de(t),this._value=n?t:On(t)}get value(){return xi(this),this._value}set value(t){const n=this.__v_isShallow||ar(t)||an(t);t=n?t:de(t),Rn(t,this._rawValue)&&(this._rawValue=t,this._value=n?t:On(t),Li(this))}}function X(e){return $e(e)?e.value:e}const Ia={get:(e,t,n)=>X(Reflect.get(e,t,n)),set:(e,t,n,r)=>{const o=e[t];return $e(o)&&!$e(n)?(o.value=n,!0):Reflect.set(e,t,n,r)}};function Si(e){return on(e)?e:new Proxy(e,Ia)}function Lo(e){const t=G(e)?new Array(e.length):{};for(const n in e)t[n]=Ma(e,n);return t}class $a{constructor(t,n,r){this._object=t,this._key=n,this._defaultValue=r,this.__v_isRef=!0}get value(){const t=this._object[this._key];return t===void 0?this._defaultValue:t}set value(t){this._object[this._key]=t}get dep(){return la(de(this._object),this._key)}}function Ma(e,t,n){const r=e[t];return $e(r)?r:new $a(e,t,n)}class Na{constructor(t,n,r,o){this._setter=n,this.dep=void 0,this.__v_isRef=!0,this.__v_isReadonly=!1,this._dirty=!0,this.effect=new bo(t,()=>{this._dirty||(this._dirty=!0,Li(this))}),this.effect.computed=this,this.effect.active=this._cacheable=!o,this.__v_isReadonly=r}get value(){const t=de(this);return xi(t),(t._dirty||!t._cacheable)&&(t._dirty=!1,t._value=t.effect.run()),t._value}set value(t){this._setter(t)}}function Da(e,t,n=!1){let r,o;const s=se(e);return s?(r=e,o=ot):(r=e.get,o=e.set),new Na(r,o,s||!o,n)}function Rt(e,t,n,r){let o;try{o=r?e(...r):e()}catch(s){Bn(s,t,n)}return o}function Ze(e,t,n,r){if(se(e)){const s=Rt(e,t,n,r);return s&&si(s)&&s.catch(i=>{Bn(i,t,n)}),s}const o=[];for(let s=0;s>>1;$n(je[r])ut&&je.splice(t,1)}function Ba(e){G(e)?sn.push(...e):(!mt||!mt.includes(e,e.allowRecurse?jt+1:jt))&&sn.push(e),Pi()}function ns(e,t=In?ut+1:0){for(;t$n(n)-$n(r)),jt=0;jte.id==null?1/0:e.id,za=(e,t)=>{const n=$n(e)-$n(t);if(n===0){if(e.pre&&!t.pre)return-1;if(t.pre&&!e.pre)return 1}return n};function Ri(e){eo=!1,In=!0,je.sort(za);const t=ot;try{for(ut=0;utpe(v)?v.trim():v)),f&&(o=n.map(Yl))}let l,a=r[l=Mr(t)]||r[l=Mr(ft(t))];!a&&s&&(a=r[l=Mr(Yt(t))]),a&&Ze(a,e,6,o);const c=r[l+"Once"];if(c){if(!e.emitted)e.emitted={};else if(e.emitted[l])return;e.emitted[l]=!0,Ze(c,e,6,o)}}function Oi(e,t,n=!1){const r=t.emitsCache,o=r.get(e);if(o!==void 0)return o;const s=e.emits;let i={},l=!1;if(!se(e)){const a=c=>{const u=Oi(c,t,!0);u&&(l=!0,Pe(i,u))};!n&&t.mixins.length&&t.mixins.forEach(a),e.extends&&a(e.extends),e.mixins&&e.mixins.forEach(a)}return!s&&!l?(xe(e)&&r.set(e,null),null):(G(s)?s.forEach(a=>i[a]=null):Pe(i,s),xe(e)&&r.set(e,i),i)}function Tr(e,t){return!e||!Hn(t)?!1:(t=t.slice(2).replace(/Once$/,""),ue(e,t[0].toLowerCase()+t.slice(1))||ue(e,Yt(t))||ue(e,t))}let De=null,Ii=null;function ur(e){const t=De;return De=e,Ii=e&&e.type.__scopeId||null,t}function Ne(e,t=De,n){if(!t||e._n)return e;const r=(...o)=>{r._d&&ps(-1);const s=ur(t);let i;try{i=e(...o)}finally{ur(s),r._d&&ps(1)}return i};return r._n=!0,r._c=!0,r._d=!0,r}function Dr(e){const{type:t,vnode:n,proxy:r,withProxy:o,props:s,propsOptions:[i],slots:l,attrs:a,emit:c,render:u,renderCache:f,data:h,setupState:v,ctx:y,inheritAttrs:w}=e;let T,g;const b=ur(e);try{if(n.shapeFlag&4){const A=o||r;T=tt(u.call(A,A,f,s,v,h,y)),g=a}else{const A=t;T=tt(A.length>1?A(s,{attrs:a,slots:l,emit:c}):A(s,null)),g=t.props?a:Ua(a)}}catch(A){Sn.length=0,Bn(A,e,1),T=te(Je)}let P=T;if(g&&w!==!1){const A=Object.keys(g),{shapeFlag:W}=P;A.length&&W&7&&(i&&A.some(po)&&(g=Wa(g,i)),P=$t(P,g))}return n.dirs&&(P=$t(P),P.dirs=P.dirs?P.dirs.concat(n.dirs):n.dirs),n.transition&&(P.transition=n.transition),T=P,ur(b),T}const Ua=e=>{let t;for(const n in e)(n==="class"||n==="style"||Hn(n))&&((t||(t={}))[n]=e[n]);return t},Wa=(e,t)=>{const n={};for(const r in e)(!po(r)||!(r.slice(9)in t))&&(n[r]=e[r]);return n};function qa(e,t,n){const{props:r,children:o,component:s}=e,{props:i,children:l,patchFlag:a}=t,c=s.emitsOptions;if(t.dirs||t.transition)return!0;if(n&&a>=0){if(a&1024)return!0;if(a&16)return r?rs(r,i,c):!!i;if(a&8){const u=t.dynamicProps;for(let f=0;fe.__isSuspense;function $i(e,t){t&&t.pendingBranch?G(e)?t.effects.push(...e):t.effects.push(e):Ba(e)}function Ja(e,t){return ko(e,null,t)}const Gn={};function st(e,t,n){return ko(e,t,n)}function ko(e,t,{immediate:n,deep:r,flush:o,onTrack:s,onTrigger:i}=we){var l;const a=ui()===((l=Re)==null?void 0:l.scope)?Re:null;let c,u=!1,f=!1;if($e(e)?(c=()=>e.value,u=ar(e)):on(e)?(c=()=>e,r=!0):G(e)?(f=!0,u=e.some(A=>on(A)||ar(A)),c=()=>e.map(A=>{if($e(A))return A.value;if(on(A))return Vt(A);if(se(A))return Rt(A,a,2)})):se(e)?t?c=()=>Rt(e,a,2):c=()=>{if(!(a&&a.isUnmounted))return h&&h(),Ze(e,a,3,[v])}:c=ot,t&&r){const A=c;c=()=>Vt(A())}let h,v=A=>{h=b.onStop=()=>{Rt(A,a,4)}},y;if(fn)if(v=ot,t?n&&Ze(t,a,3,[c(),f?[]:void 0,v]):c(),o==="sync"){const A=Uc();y=A.__watcherHandles||(A.__watcherHandles=[])}else return ot;let w=f?new Array(e.length).fill(Gn):Gn;const T=()=>{if(b.active)if(t){const A=b.run();(r||u||(f?A.some((W,ee)=>Rn(W,w[ee])):Rn(A,w)))&&(h&&h(),Ze(t,a,3,[A,w===Gn?void 0:f&&w[0]===Gn?[]:w,v]),w=A)}else b.run()};T.allowRecurse=!!t;let g;o==="sync"?g=T:o==="post"?g=()=>Ue(T,a&&a.suspense):(T.pre=!0,a&&(T.id=a.uid),g=()=>Lr(T));const b=new bo(c,g);t?n?T():w=b.run():o==="post"?Ue(b.run.bind(b),a&&a.suspense):b.run();const P=()=>{b.stop(),a&&a.scope&&mo(a.scope.effects,b)};return y&&y.push(P),P}function Ga(e,t,n){const r=this.proxy,o=pe(e)?e.includes(".")?Mi(r,e):()=>r[e]:e.bind(r,r);let s;se(t)?s=t:(s=t.handler,n=t);const i=Re;un(this);const l=ko(o,s.bind(r),n);return i?un(i):qt(),l}function Mi(e,t){const n=t.split(".");return()=>{let r=e;for(let o=0;o{Vt(n,t)});else if(li(e))for(const n in e)Vt(e[n],t);return e}function fr(e,t){const n=De;if(n===null)return e;const r=Rr(n)||n.proxy,o=e.dirs||(e.dirs=[]);for(let s=0;s{e.isMounted=!0}),Sr(()=>{e.isUnmounting=!0}),e}const Ge=[Function,Array],Ni={mode:String,appear:Boolean,persisted:Boolean,onBeforeEnter:Ge,onEnter:Ge,onAfterEnter:Ge,onEnterCancelled:Ge,onBeforeLeave:Ge,onLeave:Ge,onAfterLeave:Ge,onLeaveCancelled:Ge,onBeforeAppear:Ge,onAppear:Ge,onAfterAppear:Ge,onAppearCancelled:Ge},Za={name:"BaseTransition",props:Ni,setup(e,{slots:t}){const n=Zi(),r=Qa();let o;return()=>{const s=t.default&&Hi(t.default(),!0);if(!s||!s.length)return;let i=s[0];if(s.length>1){for(const w of s)if(w.type!==Je){i=w;break}}const l=de(e),{mode:a}=l;if(r.isLeaving)return Hr(i);const c=os(i);if(!c)return Hr(i);const u=to(c,l,r,n);no(c,u);const f=n.subTree,h=f&&os(f);let v=!1;const{getTransitionKey:y}=c.type;if(y){const w=y();o===void 0?o=w:w!==o&&(o=w,v=!0)}if(h&&h.type!==Je&&(!Bt(c,h)||v)){const w=to(h,l,r,n);if(no(h,w),a==="out-in")return r.isLeaving=!0,w.afterLeave=()=>{r.isLeaving=!1,n.update.active!==!1&&n.update()},Hr(i);a==="in-out"&&c.type!==Je&&(w.delayLeave=(T,g,b)=>{const P=Di(r,h);P[String(h.key)]=h,T._leaveCb=()=>{g(),T._leaveCb=void 0,delete u.delayedLeave},u.delayedLeave=b})}return i}}},Xa=Za;function Di(e,t){const{leavingVNodes:n}=e;let r=n.get(t.type);return r||(r=Object.create(null),n.set(t.type,r)),r}function to(e,t,n,r){const{appear:o,mode:s,persisted:i=!1,onBeforeEnter:l,onEnter:a,onAfterEnter:c,onEnterCancelled:u,onBeforeLeave:f,onLeave:h,onAfterLeave:v,onLeaveCancelled:y,onBeforeAppear:w,onAppear:T,onAfterAppear:g,onAppearCancelled:b}=t,P=String(e.key),A=Di(n,e),W=(m,B)=>{m&&Ze(m,r,9,B)},ee=(m,B)=>{const D=B[1];W(m,B),G(m)?m.every(K=>K.length<=1)&&D():m.length<=1&&D()},N={mode:s,persisted:i,beforeEnter(m){let B=l;if(!n.isMounted)if(o)B=w||l;else return;m._leaveCb&&m._leaveCb(!0);const D=A[P];D&&Bt(e,D)&&D.el._leaveCb&&D.el._leaveCb(),W(B,[m])},enter(m){let B=a,D=c,K=u;if(!n.isMounted)if(o)B=T||a,D=g||c,K=b||u;else return;let L=!1;const O=m._enterCb=I=>{L||(L=!0,I?W(K,[m]):W(D,[m]),N.delayedLeave&&N.delayedLeave(),m._enterCb=void 0)};B?ee(B,[m,O]):O()},leave(m,B){const D=String(e.key);if(m._enterCb&&m._enterCb(!0),n.isUnmounting)return B();W(f,[m]);let K=!1;const L=m._leaveCb=O=>{K||(K=!0,B(),O?W(y,[m]):W(v,[m]),m._leaveCb=void 0,A[D]===e&&delete A[D])};A[D]=e,h?ee(h,[m,L]):L()},clone(m){return to(m,t,n,r)}};return N}function Hr(e){if(zn(e))return e=$t(e),e.children=null,e}function os(e){return zn(e)?e.children?e.children[0]:void 0:e}function no(e,t){e.shapeFlag&6&&e.component?no(e.component.subTree,t):e.shapeFlag&128?(e.ssContent.transition=t.clone(e.ssContent),e.ssFallback.transition=t.clone(e.ssFallback)):e.transition=t}function Hi(e,t=!1,n){let r=[],o=0;for(let s=0;s1)for(let s=0;sPe({name:e.name},t,{setup:e}))():e}const ln=e=>!!e.type.__asyncLoader;function be(e){se(e)&&(e={loader:e});const{loader:t,loadingComponent:n,errorComponent:r,delay:o=200,timeout:s,suspensible:i=!0,onError:l}=e;let a=null,c,u=0;const f=()=>(u++,a=null,h()),h=()=>{let v;return a||(v=a=t().catch(y=>{if(y=y instanceof Error?y:new Error(String(y)),l)return new Promise((w,T)=>{l(y,()=>w(f()),()=>T(y),u+1)});throw y}).then(y=>v!==a&&a?a:(y&&(y.__esModule||y[Symbol.toStringTag]==="Module")&&(y=y.default),c=y,y)))};return he({name:"AsyncComponentWrapper",__asyncLoader:h,get __asyncResolved(){return c},setup(){const v=Re;if(c)return()=>Fr(c,v);const y=b=>{a=null,Bn(b,v,13,!r)};if(i&&v.suspense||fn)return h().then(b=>()=>Fr(b,v)).catch(b=>(y(b),()=>r?te(r,{error:b}):null));const w=Ce(!1),T=Ce(),g=Ce(!!o);return o&&setTimeout(()=>{g.value=!1},o),s!=null&&setTimeout(()=>{if(!w.value&&!T.value){const b=new Error(`Async component timed out after ${s}ms.`);y(b),T.value=b}},s),h().then(()=>{w.value=!0,v.parent&&zn(v.parent.vnode)&&Lr(v.parent.update)}).catch(b=>{y(b),T.value=b}),()=>{if(w.value&&c)return Fr(c,v);if(T.value&&r)return te(r,{error:T.value});if(n&&!g.value)return te(n)}}})}function Fr(e,t){const{ref:n,props:r,children:o,ce:s}=t.vnode,i=te(e,r,o);return i.ref=n,i.ce=s,delete t.vnode.ce,i}const zn=e=>e.type.__isKeepAlive;function ec(e,t){Fi(e,"a",t)}function tc(e,t){Fi(e,"da",t)}function Fi(e,t,n=Re){const r=e.__wdc||(e.__wdc=()=>{let o=n;for(;o;){if(o.isDeactivated)return;o=o.parent}return e()});if(kr(t,r,n),n){let o=n.parent;for(;o&&o.parent;)zn(o.parent.vnode)&&nc(r,t,n,o),o=o.parent}}function nc(e,t,n,r){const o=kr(t,e,r,!0);Ar(()=>{mo(r[t],o)},n)}function kr(e,t,n=Re,r=!1){if(n){const o=n[e]||(n[e]=[]),s=t.__weh||(t.__weh=(...i)=>{if(n.isUnmounted)return;mn(),un(n);const l=Ze(t,n,e,i);return qt(),vn(),l});return r?o.unshift(s):o.push(s),s}}const yt=e=>(t,n=Re)=>(!fn||e==="sp")&&kr(e,(...r)=>t(...r),n),rc=yt("bm"),Xe=yt("m"),oc=yt("bu"),sc=yt("u"),Sr=yt("bum"),Ar=yt("um"),ic=yt("sp"),lc=yt("rtg"),ac=yt("rtc");function cc(e,t=Re){kr("ec",e,t)}const ji="components";function bt(e,t){return fc(ji,e,!0,t)||e}const uc=Symbol.for("v-ndc");function fc(e,t,n=!0,r=!1){const o=De||Re;if(o){const s=o.type;if(e===ji){const l=Bc(s,!1);if(l&&(l===t||l===ft(t)||l===Er(ft(t))))return s}const i=ss(o[e]||s[e],t)||ss(o.appContext[e],t);return!i&&r?s:i}}function ss(e,t){return e&&(e[t]||e[ft(t)]||e[Er(ft(t))])}function It(e,t,n,r){let o;const s=n&&n[r];if(G(e)||pe(e)){o=new Array(e.length);for(let i=0,l=e.length;it(i,l,void 0,s&&s[l]));else{const i=Object.keys(e);o=new Array(i.length);for(let l=0,a=i.length;lmr(t)?!(t.type===Je||t.type===Ee&&!Bi(t.children)):!0)?e:null}const ro=e=>e?Xi(e)?Rr(e)||e.proxy:ro(e.parent):null,Tn=Pe(Object.create(null),{$:e=>e,$el:e=>e.vnode.el,$data:e=>e.data,$props:e=>e.props,$attrs:e=>e.attrs,$slots:e=>e.slots,$refs:e=>e.refs,$parent:e=>ro(e.parent),$root:e=>ro(e.root),$emit:e=>e.emit,$options:e=>So(e),$forceUpdate:e=>e.f||(e.f=()=>Lr(e.update)),$nextTick:e=>e.n||(e.n=xr.bind(e.proxy)),$watch:e=>Ga.bind(e)}),jr=(e,t)=>e!==we&&!e.__isScriptSetup&&ue(e,t),dc={get({_:e},t){const{ctx:n,setupState:r,data:o,props:s,accessCache:i,type:l,appContext:a}=e;let c;if(t[0]!=="$"){const v=i[t];if(v!==void 0)switch(v){case 1:return r[t];case 2:return o[t];case 4:return n[t];case 3:return s[t]}else{if(jr(r,t))return i[t]=1,r[t];if(o!==we&&ue(o,t))return i[t]=2,o[t];if((c=e.propsOptions[0])&&ue(c,t))return i[t]=3,s[t];if(n!==we&&ue(n,t))return i[t]=4,n[t];oo&&(i[t]=0)}}const u=Tn[t];let f,h;if(u)return t==="$attrs"&&qe(e,"get",t),u(e);if((f=l.__cssModules)&&(f=f[t]))return f;if(n!==we&&ue(n,t))return i[t]=4,n[t];if(h=a.config.globalProperties,ue(h,t))return h[t]},set({_:e},t,n){const{data:r,setupState:o,ctx:s}=e;return jr(o,t)?(o[t]=n,!0):r!==we&&ue(r,t)?(r[t]=n,!0):ue(e.props,t)||t[0]==="$"&&t.slice(1)in e?!1:(s[t]=n,!0)},has({_:{data:e,setupState:t,accessCache:n,ctx:r,appContext:o,propsOptions:s}},i){let l;return!!n[i]||e!==we&&ue(e,i)||jr(t,i)||(l=s[0])&&ue(l,i)||ue(r,i)||ue(Tn,i)||ue(o.config.globalProperties,i)},defineProperty(e,t,n){return n.get!=null?e._.accessCache[t]=0:ue(n,"value")&&this.set(e,t,n.value,null),Reflect.defineProperty(e,t,n)}};function is(e){return G(e)?e.reduce((t,n)=>(t[n]=null,t),{}):e}let oo=!0;function hc(e){const t=So(e),n=e.proxy,r=e.ctx;oo=!1,t.beforeCreate&&ls(t.beforeCreate,e,"bc");const{data:o,computed:s,methods:i,watch:l,provide:a,inject:c,created:u,beforeMount:f,mounted:h,beforeUpdate:v,updated:y,activated:w,deactivated:T,beforeDestroy:g,beforeUnmount:b,destroyed:P,unmounted:A,render:W,renderTracked:ee,renderTriggered:N,errorCaptured:m,serverPrefetch:B,expose:D,inheritAttrs:K,components:L,directives:O,filters:I}=t;if(c&&pc(c,r,null),i)for(const re in i){const oe=i[re];se(oe)&&(r[re]=oe.bind(n))}if(o){const re=o.call(n,n);xe(re)&&(e.data=jn(re))}if(oo=!0,s)for(const re in s){const oe=s[re],He=se(oe)?oe.bind(n,n):se(oe.get)?oe.get.bind(n,n):ot,Me=!se(oe)&&se(oe.set)?oe.set.bind(n):ot,Ve=z({get:He,set:Me});Object.defineProperty(r,re,{enumerable:!0,configurable:!0,get:()=>Ve.value,set:Fe=>Ve.value=Fe})}if(l)for(const re in l)zi(l[re],r,n,re);if(a){const re=se(a)?a.call(n):a;Reflect.ownKeys(re).forEach(oe=>{Wt(oe,re[oe])})}u&&ls(u,e,"c");function V(re,oe){G(oe)?oe.forEach(He=>re(He.bind(n))):oe&&re(oe.bind(n))}if(V(rc,f),V(Xe,h),V(oc,v),V(sc,y),V(ec,w),V(tc,T),V(cc,m),V(ac,ee),V(lc,N),V(Sr,b),V(Ar,A),V(ic,B),G(D))if(D.length){const re=e.exposed||(e.exposed={});D.forEach(oe=>{Object.defineProperty(re,oe,{get:()=>n[oe],set:He=>n[oe]=He})})}else e.exposed||(e.exposed={});W&&e.render===ot&&(e.render=W),K!=null&&(e.inheritAttrs=K),L&&(e.components=L),O&&(e.directives=O)}function pc(e,t,n=ot){G(e)&&(e=so(e));for(const r in e){const o=e[r];let s;xe(o)?"default"in o?s=Ae(o.from||r,o.default,!0):s=Ae(o.from||r):s=Ae(o),$e(s)?Object.defineProperty(t,r,{enumerable:!0,configurable:!0,get:()=>s.value,set:i=>s.value=i}):t[r]=s}}function ls(e,t,n){Ze(G(e)?e.map(r=>r.bind(t.proxy)):e.bind(t.proxy),t,n)}function zi(e,t,n,r){const o=r.includes(".")?Mi(n,r):()=>n[r];if(pe(e)){const s=t[e];se(s)&&st(o,s)}else if(se(e))st(o,e.bind(n));else if(xe(e))if(G(e))e.forEach(s=>zi(s,t,n,r));else{const s=se(e.handler)?e.handler.bind(n):t[e.handler];se(s)&&st(o,s,e)}}function So(e){const t=e.type,{mixins:n,extends:r}=t,{mixins:o,optionsCache:s,config:{optionMergeStrategies:i}}=e.appContext,l=s.get(t);let a;return l?a=l:!o.length&&!n&&!r?a=t:(a={},o.length&&o.forEach(c=>dr(a,c,i,!0)),dr(a,t,i)),xe(t)&&s.set(t,a),a}function dr(e,t,n,r=!1){const{mixins:o,extends:s}=t;s&&dr(e,s,n,!0),o&&o.forEach(i=>dr(e,i,n,!0));for(const i in t)if(!(r&&i==="expose")){const l=mc[i]||n&&n[i];e[i]=l?l(e[i],t[i]):t[i]}return e}const mc={data:as,props:cs,emits:cs,methods:xn,computed:xn,beforeCreate:Be,created:Be,beforeMount:Be,mounted:Be,beforeUpdate:Be,updated:Be,beforeDestroy:Be,beforeUnmount:Be,destroyed:Be,unmounted:Be,activated:Be,deactivated:Be,errorCaptured:Be,serverPrefetch:Be,components:xn,directives:xn,watch:gc,provide:as,inject:vc};function as(e,t){return t?e?function(){return Pe(se(e)?e.call(this,this):e,se(t)?t.call(this,this):t)}:t:e}function vc(e,t){return xn(so(e),so(t))}function so(e){if(G(e)){const t={};for(let n=0;n1)return n&&se(t)?t.call(r&&r.proxy):t}}function yc(e,t,n,r=!1){const o={},s={};ir(s,Pr,1),e.propsDefaults=Object.create(null),Ui(e,t,o,s);for(const i in e.propsOptions[0])i in o||(o[i]=void 0);n?e.props=r?o:Ei(o):e.type.props?e.props=o:e.props=s,e.attrs=s}function Ec(e,t,n,r){const{props:o,attrs:s,vnode:{patchFlag:i}}=e,l=de(o),[a]=e.propsOptions;let c=!1;if((r||i>0)&&!(i&16)){if(i&8){const u=e.vnode.dynamicProps;for(let f=0;f{a=!0;const[h,v]=Wi(f,t,!0);Pe(i,h),v&&l.push(...v)};!n&&t.mixins.length&&t.mixins.forEach(u),e.extends&&u(e.extends),e.mixins&&e.mixins.forEach(u)}if(!s&&!a)return xe(e)&&r.set(e,nn),nn;if(G(s))for(let u=0;u-1,v[1]=w<0||y-1||ue(v,"default"))&&l.push(f)}}}const c=[i,l];return xe(e)&&r.set(e,c),c}function us(e){return e[0]!=="$"}function fs(e){const t=e&&e.toString().match(/^\s*(function|class) (\w+)/);return t?t[2]:e===null?"null":""}function ds(e,t){return fs(e)===fs(t)}function hs(e,t){return G(t)?t.findIndex(n=>ds(n,e)):se(t)&&ds(t,e)?0:-1}const qi=e=>e[0]==="_"||e==="$stable",Ao=e=>G(e)?e.map(tt):[tt(e)],wc=(e,t,n)=>{if(t._n)return t;const r=Ne((...o)=>Ao(t(...o)),n);return r._c=!1,r},Ki=(e,t,n)=>{const r=e._ctx;for(const o in e){if(qi(o))continue;const s=e[o];if(se(s))t[o]=wc(o,s,r);else if(s!=null){const i=Ao(s);t[o]=()=>i}}},Yi=(e,t)=>{const n=Ao(t);e.slots.default=()=>n},Cc=(e,t)=>{if(e.vnode.shapeFlag&32){const n=t._;n?(e.slots=de(t),ir(t,"_",n)):Ki(t,e.slots={})}else e.slots={},t&&Yi(e,t);ir(e.slots,Pr,1)},xc=(e,t,n)=>{const{vnode:r,slots:o}=e;let s=!0,i=we;if(r.shapeFlag&32){const l=t._;l?n&&l===1?s=!1:(Pe(o,t),!n&&l===1&&delete o._):(s=!t.$stable,Ki(t,o)),i=t}else t&&(Yi(e,t),i={default:1});if(s)for(const l in o)!qi(l)&&!(l in i)&&delete o[l]};function pr(e,t,n,r,o=!1){if(G(e)){e.forEach((h,v)=>pr(h,t&&(G(t)?t[v]:t),n,r,o));return}if(ln(r)&&!o)return;const s=r.shapeFlag&4?Rr(r.component)||r.component.proxy:r.el,i=o?null:s,{i:l,r:a}=e,c=t&&t.r,u=l.refs===we?l.refs={}:l.refs,f=l.setupState;if(c!=null&&c!==a&&(pe(c)?(u[c]=null,ue(f,c)&&(f[c]=null)):$e(c)&&(c.value=null)),se(a))Rt(a,l,12,[i,u]);else{const h=pe(a),v=$e(a);if(h||v){const y=()=>{if(e.f){const w=h?ue(f,a)?f[a]:u[a]:a.value;o?G(w)&&mo(w,s):G(w)?w.includes(s)||w.push(s):h?(u[a]=[s],ue(f,a)&&(f[a]=u[a])):(a.value=[s],e.k&&(u[e.k]=a.value))}else h?(u[a]=i,ue(f,a)&&(f[a]=i)):v&&(a.value=i,e.k&&(u[e.k]=i))};i?(y.id=-1,Ue(y,n)):y()}}}let xt=!1;const Qn=e=>/svg/.test(e.namespaceURI)&&e.tagName!=="foreignObject",Zn=e=>e.nodeType===8;function Lc(e){const{mt:t,p:n,o:{patchProp:r,createText:o,nextSibling:s,parentNode:i,remove:l,insert:a,createComment:c}}=e,u=(g,b)=>{if(!b.hasChildNodes()){n(null,g,b),cr(),b._vnode=g;return}xt=!1,f(b.firstChild,g,null,null,null),cr(),b._vnode=g,xt&&console.error("Hydration completed but contains mismatches.")},f=(g,b,P,A,W,ee=!1)=>{const N=Zn(g)&&g.data==="[",m=()=>w(g,b,P,A,W,N),{type:B,ref:D,shapeFlag:K,patchFlag:L}=b;let O=g.nodeType;b.el=g,L===-2&&(ee=!1,b.dynamicChildren=null);let I=null;switch(B){case cn:O!==3?b.children===""?(a(b.el=o(""),i(g),g),I=g):I=m():(g.data!==b.children&&(xt=!0,g.data=b.children),I=s(g));break;case Je:O!==8||N?I=m():I=s(g);break;case kn:if(N&&(g=s(g),O=g.nodeType),O===1||O===3){I=g;const ie=!b.children.length;for(let V=0;V{ee=ee||!!b.dynamicChildren;const{type:N,props:m,patchFlag:B,shapeFlag:D,dirs:K}=b,L=N==="input"&&K||N==="option";if(L||B!==-1){if(K&&ct(b,null,P,"created"),m)if(L||!ee||B&48)for(const I in m)(L&&I.endsWith("value")||Hn(I)&&!Ln(I))&&r(g,I,null,m[I],!1,void 0,P);else m.onClick&&r(g,"onClick",null,m.onClick,!1,void 0,P);let O;if((O=m&&m.onVnodeBeforeMount)&&Qe(O,P,b),K&&ct(b,null,P,"beforeMount"),((O=m&&m.onVnodeMounted)||K)&&$i(()=>{O&&Qe(O,P,b),K&&ct(b,null,P,"mounted")},A),D&16&&!(m&&(m.innerHTML||m.textContent))){let I=v(g.firstChild,b,g,P,A,W,ee);for(;I;){xt=!0;const ie=I;I=I.nextSibling,l(ie)}}else D&8&&g.textContent!==b.children&&(xt=!0,g.textContent=b.children)}return g.nextSibling},v=(g,b,P,A,W,ee,N)=>{N=N||!!b.dynamicChildren;const m=b.children,B=m.length;for(let D=0;D{const{slotScopeIds:N}=b;N&&(W=W?W.concat(N):N);const m=i(g),B=v(s(g),b,m,P,A,W,ee);return B&&Zn(B)&&B.data==="]"?s(b.anchor=B):(xt=!0,a(b.anchor=c("]"),m,B),B)},w=(g,b,P,A,W,ee)=>{if(xt=!0,b.el=null,ee){const B=T(g);for(;;){const D=s(g);if(D&&D!==B)l(D);else break}}const N=s(g),m=i(g);return l(g),n(null,b,m,N,P,A,Qn(m),W),N},T=g=>{let b=0;for(;g;)if(g=s(g),g&&Zn(g)&&(g.data==="["&&b++,g.data==="]")){if(b===0)return s(g);b--}return g};return[u,f]}const Ue=$i;function Tc(e){return kc(e,Lc)}function kc(e,t){const n=Gr();n.__VUE__=!0;const{insert:r,remove:o,patchProp:s,createElement:i,createText:l,createComment:a,setText:c,setElementText:u,parentNode:f,nextSibling:h,setScopeId:v=ot,insertStaticContent:y}=e,w=(d,p,_,E=null,x=null,k=null,H=!1,R=null,M=!!p.dynamicChildren)=>{if(d===p)return;d&&!Bt(d,p)&&(E=C(d),Fe(d,x,k,!0),d=null),p.patchFlag===-2&&(M=!1,p.dynamicChildren=null);const{type:S,ref:Y,shapeFlag:U}=p;switch(S){case cn:T(d,p,_,E);break;case Je:g(d,p,_,E);break;case kn:d==null&&b(p,_,E,H);break;case Ee:L(d,p,_,E,x,k,H,R,M);break;default:U&1?W(d,p,_,E,x,k,H,R,M):U&6?O(d,p,_,E,x,k,H,R,M):(U&64||U&128)&&S.process(d,p,_,E,x,k,H,R,M,$)}Y!=null&&x&&pr(Y,d&&d.ref,k,p||d,!p)},T=(d,p,_,E)=>{if(d==null)r(p.el=l(p.children),_,E);else{const x=p.el=d.el;p.children!==d.children&&c(x,p.children)}},g=(d,p,_,E)=>{d==null?r(p.el=a(p.children||""),_,E):p.el=d.el},b=(d,p,_,E)=>{[d.el,d.anchor]=y(d.children,p,_,E,d.el,d.anchor)},P=({el:d,anchor:p},_,E)=>{let x;for(;d&&d!==p;)x=h(d),r(d,_,E),d=x;r(p,_,E)},A=({el:d,anchor:p})=>{let _;for(;d&&d!==p;)_=h(d),o(d),d=_;o(p)},W=(d,p,_,E,x,k,H,R,M)=>{H=H||p.type==="svg",d==null?ee(p,_,E,x,k,H,R,M):B(d,p,x,k,H,R,M)},ee=(d,p,_,E,x,k,H,R)=>{let M,S;const{type:Y,props:U,shapeFlag:J,transition:ne,dirs:le}=d;if(M=d.el=i(d.type,k,U&&U.is,U),J&8?u(M,d.children):J&16&&m(d.children,M,null,E,x,k&&Y!=="foreignObject",H,R),le&&ct(d,null,E,"created"),N(M,d,d.scopeId,H,E),U){for(const ve in U)ve!=="value"&&!Ln(ve)&&s(M,ve,null,U[ve],k,d.children,E,x,Ie);"value"in U&&s(M,"value",null,U.value),(S=U.onVnodeBeforeMount)&&Qe(S,E,d)}le&&ct(d,null,E,"beforeMount");const _e=(!x||x&&!x.pendingBranch)&&ne&&!ne.persisted;_e&&ne.beforeEnter(M),r(M,p,_),((S=U&&U.onVnodeMounted)||_e||le)&&Ue(()=>{S&&Qe(S,E,d),_e&&ne.enter(M),le&&ct(d,null,E,"mounted")},x)},N=(d,p,_,E,x)=>{if(_&&v(d,_),E)for(let k=0;k{for(let S=M;S{const R=p.el=d.el;let{patchFlag:M,dynamicChildren:S,dirs:Y}=p;M|=d.patchFlag&16;const U=d.props||we,J=p.props||we;let ne;_&&Nt(_,!1),(ne=J.onVnodeBeforeUpdate)&&Qe(ne,_,p,d),Y&&ct(p,d,_,"beforeUpdate"),_&&Nt(_,!0);const le=x&&p.type!=="foreignObject";if(S?D(d.dynamicChildren,S,R,_,E,le,k):H||oe(d,p,R,null,_,E,le,k,!1),M>0){if(M&16)K(R,p,U,J,_,E,x);else if(M&2&&U.class!==J.class&&s(R,"class",null,J.class,x),M&4&&s(R,"style",U.style,J.style,x),M&8){const _e=p.dynamicProps;for(let ve=0;ve<_e.length;ve++){const ke=_e[ve],et=U[ke],Qt=J[ke];(Qt!==et||ke==="value")&&s(R,ke,et,Qt,x,d.children,_,E,Ie)}}M&1&&d.children!==p.children&&u(R,p.children)}else!H&&S==null&&K(R,p,U,J,_,E,x);((ne=J.onVnodeUpdated)||Y)&&Ue(()=>{ne&&Qe(ne,_,p,d),Y&&ct(p,d,_,"updated")},E)},D=(d,p,_,E,x,k,H)=>{for(let R=0;R{if(_!==E){if(_!==we)for(const R in _)!Ln(R)&&!(R in E)&&s(d,R,_[R],null,H,p.children,x,k,Ie);for(const R in E){if(Ln(R))continue;const M=E[R],S=_[R];M!==S&&R!=="value"&&s(d,R,S,M,H,p.children,x,k,Ie)}"value"in E&&s(d,"value",_.value,E.value)}},L=(d,p,_,E,x,k,H,R,M)=>{const S=p.el=d?d.el:l(""),Y=p.anchor=d?d.anchor:l("");let{patchFlag:U,dynamicChildren:J,slotScopeIds:ne}=p;ne&&(R=R?R.concat(ne):ne),d==null?(r(S,_,E),r(Y,_,E),m(p.children,_,Y,x,k,H,R,M)):U>0&&U&64&&J&&d.dynamicChildren?(D(d.dynamicChildren,J,_,x,k,H,R),(p.key!=null||x&&p===x.subTree)&&Ji(d,p,!0)):oe(d,p,_,Y,x,k,H,R,M)},O=(d,p,_,E,x,k,H,R,M)=>{p.slotScopeIds=R,d==null?p.shapeFlag&512?x.ctx.activate(p,_,E,H,M):I(p,_,E,x,k,H,M):ie(d,p,M)},I=(d,p,_,E,x,k,H)=>{const R=d.component=Nc(d,E,x);if(zn(d)&&(R.ctx.renderer=$),Dc(R),R.asyncDep){if(x&&x.registerDep(R,V),!d.el){const M=R.subTree=te(Je);g(null,M,p,_)}return}V(R,d,p,_,x,k,H)},ie=(d,p,_)=>{const E=p.component=d.component;if(qa(d,p,_))if(E.asyncDep&&!E.asyncResolved){re(E,p,_);return}else E.next=p,ja(E.update),E.update();else p.el=d.el,E.vnode=p},V=(d,p,_,E,x,k,H)=>{const R=()=>{if(d.isMounted){let{next:Y,bu:U,u:J,parent:ne,vnode:le}=d,_e=Y,ve;Nt(d,!1),Y?(Y.el=le.el,re(d,Y,H)):Y=le,U&&Nr(U),(ve=Y.props&&Y.props.onVnodeBeforeUpdate)&&Qe(ve,ne,Y,le),Nt(d,!0);const ke=Dr(d),et=d.subTree;d.subTree=ke,w(et,ke,f(et.el),C(et),d,x,k),Y.el=ke.el,_e===null&&Ka(d,ke.el),J&&Ue(J,x),(ve=Y.props&&Y.props.onVnodeUpdated)&&Ue(()=>Qe(ve,ne,Y,le),x)}else{let Y;const{el:U,props:J}=p,{bm:ne,m:le,parent:_e}=d,ve=ln(p);if(Nt(d,!1),ne&&Nr(ne),!ve&&(Y=J&&J.onVnodeBeforeMount)&&Qe(Y,_e,p),Nt(d,!0),U&&ae){const ke=()=>{d.subTree=Dr(d),ae(U,d.subTree,d,x,null)};ve?p.type.__asyncLoader().then(()=>!d.isUnmounted&&ke()):ke()}else{const ke=d.subTree=Dr(d);w(null,ke,_,E,d,x,k),p.el=ke.el}if(le&&Ue(le,x),!ve&&(Y=J&&J.onVnodeMounted)){const ke=p;Ue(()=>Qe(Y,_e,ke),x)}(p.shapeFlag&256||_e&&ln(_e.vnode)&&_e.vnode.shapeFlag&256)&&d.a&&Ue(d.a,x),d.isMounted=!0,p=_=E=null}},M=d.effect=new bo(R,()=>Lr(S),d.scope),S=d.update=()=>M.run();S.id=d.uid,Nt(d,!0),S()},re=(d,p,_)=>{p.component=d;const E=d.vnode.props;d.vnode=p,d.next=null,Ec(d,p.props,E,_),xc(d,p.children,_),mn(),ns(),vn()},oe=(d,p,_,E,x,k,H,R,M=!1)=>{const S=d&&d.children,Y=d?d.shapeFlag:0,U=p.children,{patchFlag:J,shapeFlag:ne}=p;if(J>0){if(J&128){Me(S,U,_,E,x,k,H,R,M);return}else if(J&256){He(S,U,_,E,x,k,H,R,M);return}}ne&8?(Y&16&&Ie(S,x,k),U!==S&&u(_,U)):Y&16?ne&16?Me(S,U,_,E,x,k,H,R,M):Ie(S,x,k,!0):(Y&8&&u(_,""),ne&16&&m(U,_,E,x,k,H,R,M))},He=(d,p,_,E,x,k,H,R,M)=>{d=d||nn,p=p||nn;const S=d.length,Y=p.length,U=Math.min(S,Y);let J;for(J=0;JY?Ie(d,x,k,!0,!1,U):m(p,_,E,x,k,H,R,M,U)},Me=(d,p,_,E,x,k,H,R,M)=>{let S=0;const Y=p.length;let U=d.length-1,J=Y-1;for(;S<=U&&S<=J;){const ne=d[S],le=p[S]=M?kt(p[S]):tt(p[S]);if(Bt(ne,le))w(ne,le,_,null,x,k,H,R,M);else break;S++}for(;S<=U&&S<=J;){const ne=d[U],le=p[J]=M?kt(p[J]):tt(p[J]);if(Bt(ne,le))w(ne,le,_,null,x,k,H,R,M);else break;U--,J--}if(S>U){if(S<=J){const ne=J+1,le=neJ)for(;S<=U;)Fe(d[S],x,k,!0),S++;else{const ne=S,le=S,_e=new Map;for(S=le;S<=J;S++){const Ke=p[S]=M?kt(p[S]):tt(p[S]);Ke.key!=null&&_e.set(Ke.key,S)}let ve,ke=0;const et=J-le+1;let Qt=!1,Uo=0;const _n=new Array(et);for(S=0;S=et){Fe(Ke,x,k,!0);continue}let at;if(Ke.key!=null)at=_e.get(Ke.key);else for(ve=le;ve<=J;ve++)if(_n[ve-le]===0&&Bt(Ke,p[ve])){at=ve;break}at===void 0?Fe(Ke,x,k,!0):(_n[at-le]=S+1,at>=Uo?Uo=at:Qt=!0,w(Ke,p[at],_,null,x,k,H,R,M),ke++)}const Wo=Qt?Sc(_n):nn;for(ve=Wo.length-1,S=et-1;S>=0;S--){const Ke=le+S,at=p[Ke],qo=Ke+1{const{el:k,type:H,transition:R,children:M,shapeFlag:S}=d;if(S&6){Ve(d.component.subTree,p,_,E);return}if(S&128){d.suspense.move(p,_,E);return}if(S&64){H.move(d,p,_,$);return}if(H===Ee){r(k,p,_);for(let U=0;UR.enter(k),x);else{const{leave:U,delayLeave:J,afterLeave:ne}=R,le=()=>r(k,p,_),_e=()=>{U(k,()=>{le(),ne&&ne()})};J?J(k,le,_e):_e()}else r(k,p,_)},Fe=(d,p,_,E=!1,x=!1)=>{const{type:k,props:H,ref:R,children:M,dynamicChildren:S,shapeFlag:Y,patchFlag:U,dirs:J}=d;if(R!=null&&pr(R,null,_,d,!0),Y&256){p.ctx.deactivate(d);return}const ne=Y&1&&J,le=!ln(d);let _e;if(le&&(_e=H&&H.onVnodeBeforeUnmount)&&Qe(_e,p,d),Y&6)lt(d.component,_,E);else{if(Y&128){d.suspense.unmount(_,E);return}ne&&ct(d,null,p,"beforeUnmount"),Y&64?d.type.remove(d,p,_,x,$,E):S&&(k!==Ee||U>0&&U&64)?Ie(S,p,_,!1,!0):(k===Ee&&U&384||!x&&Y&16)&&Ie(M,p,_),E&&Et(d)}(le&&(_e=H&&H.onVnodeUnmounted)||ne)&&Ue(()=>{_e&&Qe(_e,p,d),ne&&ct(d,null,p,"unmounted")},_)},Et=d=>{const{type:p,el:_,anchor:E,transition:x}=d;if(p===Ee){wt(_,E);return}if(p===kn){A(d);return}const k=()=>{o(_),x&&!x.persisted&&x.afterLeave&&x.afterLeave()};if(d.shapeFlag&1&&x&&!x.persisted){const{leave:H,delayLeave:R}=x,M=()=>H(_,k);R?R(d.el,k,M):M()}else k()},wt=(d,p)=>{let _;for(;d!==p;)_=h(d),o(d),d=_;o(p)},lt=(d,p,_)=>{const{bum:E,scope:x,update:k,subTree:H,um:R}=d;E&&Nr(E),x.stop(),k&&(k.active=!1,Fe(H,d,p,_)),R&&Ue(R,p),Ue(()=>{d.isUnmounted=!0},p),p&&p.pendingBranch&&!p.isUnmounted&&d.asyncDep&&!d.asyncResolved&&d.suspenseId===p.pendingId&&(p.deps--,p.deps===0&&p.resolve())},Ie=(d,p,_,E=!1,x=!1,k=0)=>{for(let H=k;Hd.shapeFlag&6?C(d.component.subTree):d.shapeFlag&128?d.suspense.next():h(d.anchor||d.el),j=(d,p,_)=>{d==null?p._vnode&&Fe(p._vnode,null,null,!0):w(p._vnode||null,d,p,null,null,null,_),ns(),cr(),p._vnode=d},$={p:w,um:Fe,m:Ve,r:Et,mt:I,mc:m,pc:oe,pbc:D,n:C,o:e};let q,ae;return t&&([q,ae]=t($)),{render:j,hydrate:q,createApp:bc(j,q)}}function Nt({effect:e,update:t},n){e.allowRecurse=t.allowRecurse=n}function Ji(e,t,n=!1){const r=e.children,o=t.children;if(G(r)&&G(o))for(let s=0;s>1,e[n[l]]0&&(t[r]=n[s-1]),n[s]=r)}}for(s=n.length,i=n[s-1];s-- >0;)n[s]=i,i=t[i];return n}const Ac=e=>e.__isTeleport,Ee=Symbol.for("v-fgt"),cn=Symbol.for("v-txt"),Je=Symbol.for("v-cmt"),kn=Symbol.for("v-stc"),Sn=[];let rt=null;function F(e=!1){Sn.push(rt=e?null:[])}function Pc(){Sn.pop(),rt=Sn[Sn.length-1]||null}let Mn=1;function ps(e){Mn+=e}function Gi(e){return e.dynamicChildren=Mn>0?rt||nn:null,Pc(),Mn>0&&rt&&rt.push(e),e}function Z(e,t,n,r,o,s){return Gi(fe(e,t,n,r,o,s,!0))}function Se(e,t,n,r,o){return Gi(te(e,t,n,r,o,!0))}function mr(e){return e?e.__v_isVNode===!0:!1}function Bt(e,t){return e.type===t.type&&e.key===t.key}const Pr="__vInternal",Qi=({key:e})=>e??null,or=({ref:e,ref_key:t,ref_for:n})=>(typeof e=="number"&&(e=""+e),e!=null?pe(e)||$e(e)||se(e)?{i:De,r:e,k:t,f:!!n}:e:null);function fe(e,t=null,n=null,r=0,o=null,s=e===Ee?0:1,i=!1,l=!1){const a={__v_isVNode:!0,__v_skip:!0,type:e,props:t,key:t&&Qi(t),ref:t&&or(t),scopeId:Ii,slotScopeIds:null,children:n,component:null,suspense:null,ssContent:null,ssFallback:null,dirs:null,transition:null,el:null,anchor:null,target:null,targetAnchor:null,staticCount:0,shapeFlag:s,patchFlag:r,dynamicProps:o,dynamicChildren:null,appContext:null,ctx:De};return l?(Po(a,n),s&128&&e.normalize(a)):n&&(a.shapeFlag|=pe(n)?8:16),Mn>0&&!i&&rt&&(a.patchFlag>0||s&6)&&a.patchFlag!==32&&rt.push(a),a}const te=Rc;function Rc(e,t=null,n=null,r=0,o=null,s=!1){if((!e||e===uc)&&(e=Je),mr(e)){const l=$t(e,t,!0);return n&&Po(l,n),Mn>0&&!s&&rt&&(l.shapeFlag&6?rt[rt.indexOf(e)]=l:rt.push(l)),l.patchFlag|=-2,l}if(zc(e)&&(e=e.__vccOpts),t){t=Oc(t);let{class:l,style:a}=t;l&&!pe(l)&&(t.class=We(l)),xe(a)&&(wi(a)&&!G(a)&&(a=Pe({},a)),t.style=Fn(a))}const i=pe(e)?1:Ya(e)?128:Ac(e)?64:xe(e)?4:se(e)?2:0;return fe(e,t,n,r,o,i,s,!0)}function Oc(e){return e?wi(e)||Pr in e?Pe({},e):e:null}function $t(e,t,n=!1){const{props:r,ref:o,patchFlag:s,children:i}=e,l=t?lo(r||{},t):r;return{__v_isVNode:!0,__v_skip:!0,type:e.type,props:l,key:l&&Qi(l),ref:t&&t.ref?n&&o?G(o)?o.concat(or(t)):[o,or(t)]:or(t):o,scopeId:e.scopeId,slotScopeIds:e.slotScopeIds,children:i,target:e.target,targetAnchor:e.targetAnchor,staticCount:e.staticCount,shapeFlag:e.shapeFlag,patchFlag:t&&e.type!==Ee?s===-1?16:s|16:s,dynamicProps:e.dynamicProps,dynamicChildren:e.dynamicChildren,appContext:e.appContext,dirs:e.dirs,transition:e.transition,component:e.component,suspense:e.suspense,ssContent:e.ssContent&&$t(e.ssContent),ssFallback:e.ssFallback&&$t(e.ssFallback),el:e.el,anchor:e.anchor,ctx:e.ctx,ce:e.ce}}function Mt(e=" ",t=0){return te(cn,null,e,t)}function Ic(e,t){const n=te(kn,null,e);return n.staticCount=t,n}function Te(e="",t=!1){return t?(F(),Se(Je,null,e)):te(Je,null,e)}function tt(e){return e==null||typeof e=="boolean"?te(Je):G(e)?te(Ee,null,e.slice()):typeof e=="object"?kt(e):te(cn,null,String(e))}function kt(e){return e.el===null&&e.patchFlag!==-1||e.memo?e:$t(e)}function Po(e,t){let n=0;const{shapeFlag:r}=e;if(t==null)t=null;else if(G(t))n=16;else if(typeof t=="object")if(r&65){const o=t.default;o&&(o._c&&(o._d=!1),Po(e,o()),o._c&&(o._d=!0));return}else{n=32;const o=t._;!o&&!(Pr in t)?t._ctx=De:o===3&&De&&(De.slots._===1?t._=1:(t._=2,e.patchFlag|=1024))}else se(t)?(t={default:t,_ctx:De},n=32):(t=String(t),r&64?(n=16,t=[Mt(t)]):n=8);e.children=t,e.shapeFlag|=n}function lo(...e){const t={};for(let n=0;nRe||De;let Ro,Zt,ms="__VUE_INSTANCE_SETTERS__";(Zt=Gr()[ms])||(Zt=Gr()[ms]=[]),Zt.push(e=>Re=e),Ro=e=>{Zt.length>1?Zt.forEach(t=>t(e)):Zt[0](e)};const un=e=>{Ro(e),e.scope.on()},qt=()=>{Re&&Re.scope.off(),Ro(null)};function Xi(e){return e.vnode.shapeFlag&4}let fn=!1;function Dc(e,t=!1){fn=t;const{props:n,children:r}=e.vnode,o=Xi(e);yc(e,n,o,t),Cc(e,r);const s=o?Hc(e,t):void 0;return fn=!1,s}function Hc(e,t){const n=e.type;e.accessCache=Object.create(null),e.proxy=Ci(new Proxy(e.ctx,dc));const{setup:r}=n;if(r){const o=e.setupContext=r.length>1?jc(e):null;un(e),mn();const s=Rt(r,e,0,[e.props,o]);if(vn(),qt(),si(s)){if(s.then(qt,qt),t)return s.then(i=>{vs(e,i,t)}).catch(i=>{Bn(i,e,0)});e.asyncDep=s}else vs(e,s,t)}else el(e,t)}function vs(e,t,n){se(t)?e.type.__ssrInlineRender?e.ssrRender=t:e.render=t:xe(t)&&(e.setupState=Si(t)),el(e,n)}let gs;function el(e,t,n){const r=e.type;if(!e.render){if(!t&&gs&&!r.render){const o=r.template||So(e).template;if(o){const{isCustomElement:s,compilerOptions:i}=e.appContext.config,{delimiters:l,compilerOptions:a}=r,c=Pe(Pe({isCustomElement:s,delimiters:l},i),a);r.render=gs(o,c)}}e.render=r.render||ot}un(e),mn(),hc(e),vn(),qt()}function Fc(e){return e.attrsProxy||(e.attrsProxy=new Proxy(e.attrs,{get(t,n){return qe(e,"get","$attrs"),t[n]}}))}function jc(e){const t=n=>{e.exposed=n||{}};return{get attrs(){return Fc(e)},slots:e.slots,emit:e.emit,expose:t}}function Rr(e){if(e.exposed)return e.exposeProxy||(e.exposeProxy=new Proxy(Si(Ci(e.exposed)),{get(t,n){if(n in t)return t[n];if(n in Tn)return Tn[n](e)},has(t,n){return n in t||n in Tn}}))}function Bc(e,t=!0){return se(e)?e.displayName||e.name:e.name||t&&e.__name}function zc(e){return se(e)&&"__vccOpts"in e}const z=(e,t)=>Da(e,t,fn);function ge(e,t,n){const r=arguments.length;return r===2?xe(t)&&!G(t)?mr(t)?te(e,null,[t]):te(e,t):te(e,null,t):(r>3?n=Array.prototype.slice.call(arguments,2):r===3&&mr(n)&&(n=[n]),te(e,t,n))}const Vc=Symbol.for("v-scx"),Uc=()=>Ae(Vc),Wc="3.3.4",qc="http://www.w3.org/2000/svg",zt=typeof document<"u"?document:null,_s=zt&&zt.createElement("template"),Kc={insert:(e,t,n)=>{t.insertBefore(e,n||null)},remove:e=>{const t=e.parentNode;t&&t.removeChild(e)},createElement:(e,t,n,r)=>{const o=t?zt.createElementNS(qc,e):zt.createElement(e,n?{is:n}:void 0);return e==="select"&&r&&r.multiple!=null&&o.setAttribute("multiple",r.multiple),o},createText:e=>zt.createTextNode(e),createComment:e=>zt.createComment(e),setText:(e,t)=>{e.nodeValue=t},setElementText:(e,t)=>{e.textContent=t},parentNode:e=>e.parentNode,nextSibling:e=>e.nextSibling,querySelector:e=>zt.querySelector(e),setScopeId(e,t){e.setAttribute(t,"")},insertStaticContent(e,t,n,r,o,s){const i=n?n.previousSibling:t.lastChild;if(o&&(o===s||o.nextSibling))for(;t.insertBefore(o.cloneNode(!0),n),!(o===s||!(o=o.nextSibling)););else{_s.innerHTML=r?`${e}`:e;const l=_s.content;if(r){const a=l.firstChild;for(;a.firstChild;)l.appendChild(a.firstChild);l.removeChild(a)}t.insertBefore(l,n)}return[i?i.nextSibling:t.firstChild,n?n.previousSibling:t.lastChild]}};function Yc(e,t,n){const r=e._vtc;r&&(t=(t?[t,...r]:[...r]).join(" ")),t==null?e.removeAttribute("class"):n?e.setAttribute("class",t):e.className=t}function Jc(e,t,n){const r=e.style,o=pe(n);if(n&&!o){if(t&&!pe(t))for(const s in t)n[s]==null&&ao(r,s,"");for(const s in n)ao(r,s,n[s])}else{const s=r.display;o?t!==n&&(r.cssText=n):t&&e.removeAttribute("style"),"_vod"in e&&(r.display=s)}}const bs=/\s*!important$/;function ao(e,t,n){if(G(n))n.forEach(r=>ao(e,t,r));else if(n==null&&(n=""),t.startsWith("--"))e.setProperty(t,n);else{const r=Gc(e,t);bs.test(n)?e.setProperty(Yt(r),n.replace(bs,""),"important"):e[r]=n}}const ys=["Webkit","Moz","ms"],Br={};function Gc(e,t){const n=Br[t];if(n)return n;let r=ft(t);if(r!=="filter"&&r in e)return Br[t]=r;r=Er(r);for(let o=0;ozr||(ru.then(()=>zr=0),zr=Date.now());function su(e,t){const n=r=>{if(!r._vts)r._vts=Date.now();else if(r._vts<=n.attached)return;Ze(iu(r,n.value),t,5,[r])};return n.value=e,n.attached=ou(),n}function iu(e,t){if(G(t)){const n=e.stopImmediatePropagation;return e.stopImmediatePropagation=()=>{n.call(e),e._stopped=!0},t.map(r=>o=>!o._stopped&&r&&r(o))}else return t}const Cs=/^on[a-z]/,lu=(e,t,n,r,o=!1,s,i,l,a)=>{t==="class"?Yc(e,r,o):t==="style"?Jc(e,n,r):Hn(t)?po(t)||tu(e,t,n,r,i):(t[0]==="."?(t=t.slice(1),!0):t[0]==="^"?(t=t.slice(1),!1):au(e,t,r,o))?Zc(e,t,r,s,i,l,a):(t==="true-value"?e._trueValue=r:t==="false-value"&&(e._falseValue=r),Qc(e,t,r,o))};function au(e,t,n,r){return r?!!(t==="innerHTML"||t==="textContent"||t in e&&Cs.test(t)&&se(n)):t==="spellcheck"||t==="draggable"||t==="translate"||t==="form"||t==="list"&&e.tagName==="INPUT"||t==="type"&&e.tagName==="TEXTAREA"||Cs.test(t)&&pe(n)?!1:t in e}const Lt="transition",bn="animation",Vn=(e,{slots:t})=>ge(Xa,cu(e),t);Vn.displayName="Transition";const tl={name:String,type:String,css:{type:Boolean,default:!0},duration:[String,Number,Object],enterFromClass:String,enterActiveClass:String,enterToClass:String,appearFromClass:String,appearActiveClass:String,appearToClass:String,leaveFromClass:String,leaveActiveClass:String,leaveToClass:String};Vn.props=Pe({},Ni,tl);const Dt=(e,t=[])=>{G(e)?e.forEach(n=>n(...t)):e&&e(...t)},xs=e=>e?G(e)?e.some(t=>t.length>1):e.length>1:!1;function cu(e){const t={};for(const L in e)L in tl||(t[L]=e[L]);if(e.css===!1)return t;const{name:n="v",type:r,duration:o,enterFromClass:s=`${n}-enter-from`,enterActiveClass:i=`${n}-enter-active`,enterToClass:l=`${n}-enter-to`,appearFromClass:a=s,appearActiveClass:c=i,appearToClass:u=l,leaveFromClass:f=`${n}-leave-from`,leaveActiveClass:h=`${n}-leave-active`,leaveToClass:v=`${n}-leave-to`}=e,y=uu(o),w=y&&y[0],T=y&&y[1],{onBeforeEnter:g,onEnter:b,onEnterCancelled:P,onLeave:A,onLeaveCancelled:W,onBeforeAppear:ee=g,onAppear:N=b,onAppearCancelled:m=P}=t,B=(L,O,I)=>{Ht(L,O?u:l),Ht(L,O?c:i),I&&I()},D=(L,O)=>{L._isLeaving=!1,Ht(L,f),Ht(L,v),Ht(L,h),O&&O()},K=L=>(O,I)=>{const ie=L?N:b,V=()=>B(O,L,I);Dt(ie,[O,V]),Ls(()=>{Ht(O,L?a:s),Tt(O,L?u:l),xs(ie)||Ts(O,r,w,V)})};return Pe(t,{onBeforeEnter(L){Dt(g,[L]),Tt(L,s),Tt(L,i)},onBeforeAppear(L){Dt(ee,[L]),Tt(L,a),Tt(L,c)},onEnter:K(!1),onAppear:K(!0),onLeave(L,O){L._isLeaving=!0;const I=()=>D(L,O);Tt(L,f),hu(),Tt(L,h),Ls(()=>{L._isLeaving&&(Ht(L,f),Tt(L,v),xs(A)||Ts(L,r,T,I))}),Dt(A,[L,I])},onEnterCancelled(L){B(L,!1),Dt(P,[L])},onAppearCancelled(L){B(L,!0),Dt(m,[L])},onLeaveCancelled(L){D(L),Dt(W,[L])}})}function uu(e){if(e==null)return null;if(xe(e))return[Vr(e.enter),Vr(e.leave)];{const t=Vr(e);return[t,t]}}function Vr(e){return Jl(e)}function Tt(e,t){t.split(/\s+/).forEach(n=>n&&e.classList.add(n)),(e._vtc||(e._vtc=new Set)).add(t)}function Ht(e,t){t.split(/\s+/).forEach(r=>r&&e.classList.remove(r));const{_vtc:n}=e;n&&(n.delete(t),n.size||(e._vtc=void 0))}function Ls(e){requestAnimationFrame(()=>{requestAnimationFrame(e)})}let fu=0;function Ts(e,t,n,r){const o=e._endId=++fu,s=()=>{o===e._endId&&r()};if(n)return setTimeout(s,n);const{type:i,timeout:l,propCount:a}=du(e,t);if(!i)return r();const c=i+"end";let u=0;const f=()=>{e.removeEventListener(c,h),s()},h=v=>{v.target===e&&++u>=a&&f()};setTimeout(()=>{u(n[y]||"").split(", "),o=r(`${Lt}Delay`),s=r(`${Lt}Duration`),i=ks(o,s),l=r(`${bn}Delay`),a=r(`${bn}Duration`),c=ks(l,a);let u=null,f=0,h=0;t===Lt?i>0&&(u=Lt,f=i,h=s.length):t===bn?c>0&&(u=bn,f=c,h=a.length):(f=Math.max(i,c),u=f>0?i>c?Lt:bn:null,h=u?u===Lt?s.length:a.length:0);const v=u===Lt&&/\b(transform|all)(,|$)/.test(r(`${Lt}Property`).toString());return{type:u,timeout:f,propCount:h,hasTransform:v}}function ks(e,t){for(;e.lengthSs(n)+Ss(e[r])))}function Ss(e){return Number(e.slice(0,-1).replace(",","."))*1e3}function hu(){return document.body.offsetHeight}const pu={esc:"escape",space:" ",up:"arrow-up",left:"arrow-left",right:"arrow-right",down:"arrow-down",delete:"backspace"},mu=(e,t)=>n=>{if(!("key"in n))return;const r=Yt(n.key);if(t.some(o=>o===r||pu[o]===r))return e(n)},vr={beforeMount(e,{value:t},{transition:n}){e._vod=e.style.display==="none"?"":e.style.display,n&&t?n.beforeEnter(e):yn(e,t)},mounted(e,{value:t},{transition:n}){n&&t&&n.enter(e)},updated(e,{value:t,oldValue:n},{transition:r}){!t!=!n&&(r?t?(r.beforeEnter(e),yn(e,!0),r.enter(e)):r.leave(e,()=>{yn(e,!1)}):yn(e,t))},beforeUnmount(e,{value:t}){yn(e,t)}};function yn(e,t){e.style.display=t?e._vod:"none"}const vu=Pe({patchProp:lu},Kc);let Ur,As=!1;function gu(){return Ur=As?Ur:Tc(vu),As=!0,Ur}const _u=(...e)=>{const t=gu().createApp(...e),{mount:n}=t;return t.mount=r=>{const o=bu(r);if(o)return n(o,!0,o instanceof SVGElement)},t};function bu(e){return pe(e)?document.querySelector(e):e}const yu={"v-8daa1a0e":()=>Q(()=>import("./index.html-b7b4e1e2.js"),[]).then(({data:e})=>e),"v-23cb31b3":()=>Q(()=>import("./index.html-0cc578c7.js"),[]).then(({data:e})=>e),"v-0fceabfe":()=>Q(()=>import("./index.html-d7674d58.js"),[]).then(({data:e})=>e),"v-5791b9ab":()=>Q(()=>import("./index.html-beb4b1c4.js"),[]).then(({data:e})=>e),"v-02dcbf8e":()=>Q(()=>import("./index.html-71b8f972.js"),[]).then(({data:e})=>e),"v-01b06704":()=>Q(()=>import("./index.html-394fa650.js"),[]).then(({data:e})=>e),"v-60936db8":()=>Q(()=>import("./index.html-e05eac13.js"),[]).then(({data:e})=>e),"v-415e2210":()=>Q(()=>import("./01.html-4acb4a17.js"),[]).then(({data:e})=>e),"v-4312faaf":()=>Q(()=>import("./02.html-9d224a5a.js"),[]).then(({data:e})=>e),"v-44c7d34e":()=>Q(()=>import("./03.html-6e4a25f7.js"),[]).then(({data:e})=>e),"v-467cabed":()=>Q(()=>import("./04.html-cf0be750.js"),[]).then(({data:e})=>e),"v-4831848c":()=>Q(()=>import("./05.html-ddabefdf.js"),[]).then(({data:e})=>e),"v-49e65d2b":()=>Q(()=>import("./06.html-907b0bed.js"),[]).then(({data:e})=>e),"v-4b9b35ca":()=>Q(()=>import("./07.html-04baef94.js"),[]).then(({data:e})=>e),"v-4d500e69":()=>Q(()=>import("./08.html-ce11059a.js"),[]).then(({data:e})=>e),"v-4f04e708":()=>Q(()=>import("./09.html-e58cbdae.js"),[]).then(({data:e})=>e),"v-748f84b2":()=>Q(()=>import("./10.html-92f88bd8.js"),[]).then(({data:e})=>e),"v-a5e72730":()=>Q(()=>import("./index.html-80dba622.js"),[]).then(({data:e})=>e),"v-17a33721":()=>Q(()=>import("./index.html-3757d620.js"),[]).then(({data:e})=>e),"v-4525d36b":()=>Q(()=>import("./ali-iconfont.html-7f7a09e2.js"),[]).then(({data:e})=>e),"v-7e5c3b44":()=>Q(()=>import("./git.html-3b96bfc9.js"),[]).then(({data:e})=>e),"v-11ee2517":()=>Q(()=>import("./index.html-0a28c9d4.js"),[]).then(({data:e})=>e),"v-c7ffc5b0":()=>Q(()=>import("./json-server.html-c81eb9f6.js"),[]).then(({data:e})=>e),"v-7c9faf39":()=>Q(()=>import("./index.html-380c9dcf.js"),[]).then(({data:e})=>e),"v-b7245cd4":()=>Q(()=>import("./index.html-3a7a3c33.js"),[]).then(({data:e})=>e),"v-3706649a":()=>Q(()=>import("./404.html-f9875e7b.js"),[]).then(({data:e})=>e)},Eu=JSON.parse('{"base":"/study-notes/","lang":"zh-CN","title":"瓢儿白施肥记","description":"记录一些学习内容","head":[],"locales":{}}');var wu=([e,t,n])=>e==="meta"&&t.name?`${e}.${t.name}`:["title","base"].includes(e)?e:e==="template"&&t.id?`${e}.${t.id}`:JSON.stringify([e,t,n]),Cu=e=>{const t=new Set,n=[];return e.forEach(r=>{const o=wu(r);t.has(o)||(t.add(o),n.push(r))}),n},Un=e=>/^(https?:)?\/\//.test(e),xu=e=>/^mailto:/.test(e),Lu=e=>/^tel:/.test(e),Oo=e=>Object.prototype.toString.call(e)==="[object Object]",nl=e=>e[e.length-1]==="/"?e.slice(0,-1):e,rl=e=>e[0]==="/"?e.slice(1):e,ol=(e,t)=>{const n=Object.keys(e).sort((r,o)=>{const s=o.split("/").length-r.split("/").length;return s!==0?s:o.length-r.length});for(const r of n)if(t.startsWith(r))return r;return"/"};const sl={"v-8daa1a0e":be(()=>Q(()=>import("./index.html-d7ee44df.js"),[])),"v-23cb31b3":be(()=>Q(()=>import("./index.html-f4b28a94.js"),[])),"v-0fceabfe":be(()=>Q(()=>import("./index.html-947c70c9.js"),[])),"v-5791b9ab":be(()=>Q(()=>import("./index.html-94a8ea89.js"),[])),"v-02dcbf8e":be(()=>Q(()=>import("./index.html-864b3258.js"),[])),"v-01b06704":be(()=>Q(()=>import("./index.html-78eca336.js"),[])),"v-60936db8":be(()=>Q(()=>import("./index.html-e7ad04f1.js"),[])),"v-415e2210":be(()=>Q(()=>import("./01.html-9c89b4ec.js"),[])),"v-4312faaf":be(()=>Q(()=>import("./02.html-aee8f56b.js"),[])),"v-44c7d34e":be(()=>Q(()=>import("./03.html-2aeb7c67.js"),[])),"v-467cabed":be(()=>Q(()=>import("./04.html-e5c01e6e.js"),[])),"v-4831848c":be(()=>Q(()=>import("./05.html-4cfb85ff.js"),[])),"v-49e65d2b":be(()=>Q(()=>import("./06.html-4c3bc427.js"),[])),"v-4b9b35ca":be(()=>Q(()=>import("./07.html-66107e55.js"),[])),"v-4d500e69":be(()=>Q(()=>import("./08.html-8eebe63e.js"),[])),"v-4f04e708":be(()=>Q(()=>import("./09.html-4f635d79.js"),[])),"v-748f84b2":be(()=>Q(()=>import("./10.html-530a222f.js"),[])),"v-a5e72730":be(()=>Q(()=>import("./index.html-e21578be.js"),[])),"v-17a33721":be(()=>Q(()=>import("./index.html-a0bd810b.js"),[])),"v-4525d36b":be(()=>Q(()=>import("./ali-iconfont.html-e6785a18.js"),[])),"v-7e5c3b44":be(()=>Q(()=>import("./git.html-b7ea5d64.js"),[])),"v-11ee2517":be(()=>Q(()=>import("./index.html-aff49fb2.js"),[])),"v-c7ffc5b0":be(()=>Q(()=>import("./json-server.html-0e424c53.js"),[])),"v-7c9faf39":be(()=>Q(()=>import("./index.html-dd9cba0d.js"),[])),"v-b7245cd4":be(()=>Q(()=>import("./index.html-39df15d0.js"),[])),"v-3706649a":be(()=>Q(()=>import("./404.html-b2dd1c29.js"),[]))};var Tu=Symbol(""),ku=Ce(yu),il=Cr({key:"",path:"",title:"",lang:"",frontmatter:{},headers:[]}),St=Ce(il),Kt=()=>St,ll=Symbol(""),vt=()=>{const e=Ae(ll);if(!e)throw new Error("usePageFrontmatter() is called without provider.");return e},al=Symbol(""),Su=()=>{const e=Ae(al);if(!e)throw new Error("usePageHead() is called without provider.");return e},Au=Symbol(""),cl=Symbol(""),Pu=()=>{const e=Ae(cl);if(!e)throw new Error("usePageLang() is called without provider.");return e},ul=Symbol(""),Ru=()=>{const e=Ae(ul);if(!e)throw new Error("usePageLayout() is called without provider.");return e},Io=Symbol(""),Or=()=>{const e=Ae(Io);if(!e)throw new Error("useRouteLocale() is called without provider.");return e},tn=Ce(Eu),fl=()=>tn,dl=Symbol(""),$o=()=>{const e=Ae(dl);if(!e)throw new Error("useSiteLocaleData() is called without provider.");return e},Ou=Symbol(""),Iu="Layout",$u="NotFound",ht=jn({resolveLayouts:e=>e.reduce((t,n)=>({...t,...n.layouts}),{}),resolvePageData:async e=>{const t=ku.value[e];return await(t==null?void 0:t())??il},resolvePageFrontmatter:e=>e.frontmatter,resolvePageHead:(e,t,n)=>{const r=pe(t.description)?t.description:n.description,o=[...G(t.head)?t.head:[],...n.head,["title",{},e],["meta",{name:"description",content:r}]];return Cu(o)},resolvePageHeadTitle:(e,t)=>[e.title,t.title].filter(n=>!!n).join(" | "),resolvePageLang:e=>e.lang||"en",resolvePageLayout:(e,t)=>{let n;if(e.path){const r=e.frontmatter.layout;pe(r)?n=r:n=Iu}else n=$u;return t[n]},resolveRouteLocale:(e,t)=>ol(e,t),resolveSiteLocaleData:(e,t)=>({...e,...e.locales[t]})}),Mo=he({name:"ClientOnly",setup(e,t){const n=Ce(!1);return Xe(()=>{n.value=!0}),()=>{var r,o;return n.value?(o=(r=t.slots).default)==null?void 0:o.call(r):null}}}),Mu=he({name:"Content",props:{pageKey:{type:String,required:!1,default:""}},setup(e){const t=Kt(),n=z(()=>sl[e.pageKey||t.value.key]);return()=>n.value?ge(n.value):ge("div","404 Not Found")}}),Jt=(e={})=>e,No=e=>Un(e)?e:`/study-notes/${rl(e)}`;function hl(e,t,n){var r,o,s;t===void 0&&(t=50),n===void 0&&(n={});var i=(r=n.isImmediate)!=null&&r,l=(o=n.callback)!=null&&o,a=n.maxWait,c=Date.now(),u=[];function f(){if(a!==void 0){var v=Date.now()-c;if(v+t>=a)return a-v}return t}var h=function(){var v=[].slice.call(arguments),y=this;return new Promise(function(w,T){var g=i&&s===void 0;if(s!==void 0&&clearTimeout(s),s=setTimeout(function(){if(s=void 0,c=Date.now(),!i){var P=e.apply(y,v);l&&l(P),u.forEach(function(A){return(0,A.resolve)(P)}),u=[]}},f()),g){var b=e.apply(y,v);return l&&l(b),w(b)}u.push({resolve:w,reject:T})})};return h.cancel=function(v){s!==void 0&&clearTimeout(s),u.forEach(function(y){return(0,y.reject)(v)}),u=[]},h}/*! + * vue-router v4.2.4 + * (c) 2023 Eduardo San Martin Morote + * @license MIT + */const en=typeof window<"u";function Nu(e){return e.__esModule||e[Symbol.toStringTag]==="Module"}const me=Object.assign;function Wr(e,t){const n={};for(const r in t){const o=t[r];n[r]=it(o)?o.map(e):e(o)}return n}const An=()=>{},it=Array.isArray,Du=/\/$/,Hu=e=>e.replace(Du,"");function qr(e,t,n="/"){let r,o={},s="",i="";const l=t.indexOf("#");let a=t.indexOf("?");return l=0&&(a=-1),a>-1&&(r=t.slice(0,a),s=t.slice(a+1,l>-1?l:t.length),o=e(s)),l>-1&&(r=r||t.slice(0,l),i=t.slice(l,t.length)),r=zu(r??t,n),{fullPath:r+(s&&"?")+s+i,path:r,query:o,hash:i}}function Fu(e,t){const n=t.query?e(t.query):"";return t.path+(n&&"?")+n+(t.hash||"")}function Ps(e,t){return!t||!e.toLowerCase().startsWith(t.toLowerCase())?e:e.slice(t.length)||"/"}function ju(e,t,n){const r=t.matched.length-1,o=n.matched.length-1;return r>-1&&r===o&&dn(t.matched[r],n.matched[o])&&pl(t.params,n.params)&&e(t.query)===e(n.query)&&t.hash===n.hash}function dn(e,t){return(e.aliasOf||e)===(t.aliasOf||t)}function pl(e,t){if(Object.keys(e).length!==Object.keys(t).length)return!1;for(const n in e)if(!Bu(e[n],t[n]))return!1;return!0}function Bu(e,t){return it(e)?Rs(e,t):it(t)?Rs(t,e):e===t}function Rs(e,t){return it(t)?e.length===t.length&&e.every((n,r)=>n===t[r]):e.length===1&&e[0]===t}function zu(e,t){if(e.startsWith("/"))return e;if(!e)return t;const n=t.split("/"),r=e.split("/"),o=r[r.length-1];(o===".."||o===".")&&r.push("");let s=n.length-1,i,l;for(i=0;i1&&s--;else break;return n.slice(0,s).join("/")+"/"+r.slice(i-(i===r.length?1:0)).join("/")}var Nn;(function(e){e.pop="pop",e.push="push"})(Nn||(Nn={}));var Pn;(function(e){e.back="back",e.forward="forward",e.unknown=""})(Pn||(Pn={}));function Vu(e){if(!e)if(en){const t=document.querySelector("base");e=t&&t.getAttribute("href")||"/",e=e.replace(/^\w+:\/\/[^\/]+/,"")}else e="/";return e[0]!=="/"&&e[0]!=="#"&&(e="/"+e),Hu(e)}const Uu=/^[^#]+#/;function Wu(e,t){return e.replace(Uu,"#")+t}function qu(e,t){const n=document.documentElement.getBoundingClientRect(),r=e.getBoundingClientRect();return{behavior:t.behavior,left:r.left-n.left-(t.left||0),top:r.top-n.top-(t.top||0)}}const Ir=()=>({left:window.pageXOffset,top:window.pageYOffset});function Ku(e){let t;if("el"in e){const n=e.el,r=typeof n=="string"&&n.startsWith("#"),o=typeof n=="string"?r?document.getElementById(n.slice(1)):document.querySelector(n):n;if(!o)return;t=qu(o,e)}else t=e;"scrollBehavior"in document.documentElement.style?window.scrollTo(t):window.scrollTo(t.left!=null?t.left:window.pageXOffset,t.top!=null?t.top:window.pageYOffset)}function Os(e,t){return(history.state?history.state.position-t:-1)+e}const co=new Map;function Yu(e,t){co.set(e,t)}function Ju(e){const t=co.get(e);return co.delete(e),t}let Gu=()=>location.protocol+"//"+location.host;function ml(e,t){const{pathname:n,search:r,hash:o}=t,s=e.indexOf("#");if(s>-1){let l=o.includes(e.slice(s))?e.slice(s).length:1,a=o.slice(l);return a[0]!=="/"&&(a="/"+a),Ps(a,"")}return Ps(n,e)+r+o}function Qu(e,t,n,r){let o=[],s=[],i=null;const l=({state:h})=>{const v=ml(e,location),y=n.value,w=t.value;let T=0;if(h){if(n.value=v,t.value=h,i&&i===y){i=null;return}T=w?h.position-w.position:0}else r(v);o.forEach(g=>{g(n.value,y,{delta:T,type:Nn.pop,direction:T?T>0?Pn.forward:Pn.back:Pn.unknown})})};function a(){i=n.value}function c(h){o.push(h);const v=()=>{const y=o.indexOf(h);y>-1&&o.splice(y,1)};return s.push(v),v}function u(){const{history:h}=window;h.state&&h.replaceState(me({},h.state,{scroll:Ir()}),"")}function f(){for(const h of s)h();s=[],window.removeEventListener("popstate",l),window.removeEventListener("beforeunload",u)}return window.addEventListener("popstate",l),window.addEventListener("beforeunload",u,{passive:!0}),{pauseListeners:a,listen:c,destroy:f}}function Is(e,t,n,r=!1,o=!1){return{back:e,current:t,forward:n,replaced:r,position:window.history.length,scroll:o?Ir():null}}function Zu(e){const{history:t,location:n}=window,r={value:ml(e,n)},o={value:t.state};o.value||s(r.value,{back:null,current:r.value,forward:null,position:t.length-1,replaced:!0,scroll:null},!0);function s(a,c,u){const f=e.indexOf("#"),h=f>-1?(n.host&&document.querySelector("base")?e:e.slice(f))+a:Gu()+e+a;try{t[u?"replaceState":"pushState"](c,"",h),o.value=c}catch(v){console.error(v),n[u?"replace":"assign"](h)}}function i(a,c){const u=me({},t.state,Is(o.value.back,a,o.value.forward,!0),c,{position:o.value.position});s(a,u,!0),r.value=a}function l(a,c){const u=me({},o.value,t.state,{forward:a,scroll:Ir()});s(u.current,u,!0);const f=me({},Is(r.value,a,null),{position:u.position+1},c);s(a,f,!1),r.value=a}return{location:r,state:o,push:l,replace:i}}function Xu(e){e=Vu(e);const t=Zu(e),n=Qu(e,t.state,t.location,t.replace);function r(s,i=!0){i||n.pauseListeners(),history.go(s)}const o=me({location:"",base:e,go:r,createHref:Wu.bind(null,e)},t,n);return Object.defineProperty(o,"location",{enumerable:!0,get:()=>t.location.value}),Object.defineProperty(o,"state",{enumerable:!0,get:()=>t.state.value}),o}function ef(e){return typeof e=="string"||e&&typeof e=="object"}function vl(e){return typeof e=="string"||typeof e=="symbol"}const pt={path:"/",name:void 0,params:{},query:{},hash:"",fullPath:"/",matched:[],meta:{},redirectedFrom:void 0},gl=Symbol("");var $s;(function(e){e[e.aborted=4]="aborted",e[e.cancelled=8]="cancelled",e[e.duplicated=16]="duplicated"})($s||($s={}));function hn(e,t){return me(new Error,{type:e,[gl]:!0},t)}function dt(e,t){return e instanceof Error&&gl in e&&(t==null||!!(e.type&t))}const Ms="[^/]+?",tf={sensitive:!1,strict:!1,start:!0,end:!0},nf=/[.+*?^${}()[\]/\\]/g;function rf(e,t){const n=me({},tf,t),r=[];let o=n.start?"^":"";const s=[];for(const c of e){const u=c.length?[]:[90];n.strict&&!c.length&&(o+="/");for(let f=0;ft.length?t.length===1&&t[0]===40+40?1:-1:0}function sf(e,t){let n=0;const r=e.score,o=t.score;for(;n0&&t[t.length-1]<0}const lf={type:0,value:""},af=/[a-zA-Z0-9_]/;function cf(e){if(!e)return[[]];if(e==="/")return[[lf]];if(!e.startsWith("/"))throw new Error(`Invalid path "${e}"`);function t(v){throw new Error(`ERR (${n})/"${c}": ${v}`)}let n=0,r=n;const o=[];let s;function i(){s&&o.push(s),s=[]}let l=0,a,c="",u="";function f(){c&&(n===0?s.push({type:0,value:c}):n===1||n===2||n===3?(s.length>1&&(a==="*"||a==="+")&&t(`A repeatable param (${c}) must be alone in its segment. eg: '/:ids+.`),s.push({type:1,value:c,regexp:u,repeatable:a==="*"||a==="+",optional:a==="*"||a==="?"})):t("Invalid state to consume buffer"),c="")}function h(){c+=a}for(;l{i(b)}:An}function i(u){if(vl(u)){const f=r.get(u);f&&(r.delete(u),n.splice(n.indexOf(f),1),f.children.forEach(i),f.alias.forEach(i))}else{const f=n.indexOf(u);f>-1&&(n.splice(f,1),u.record.name&&r.delete(u.record.name),u.children.forEach(i),u.alias.forEach(i))}}function l(){return n}function a(u){let f=0;for(;f=0&&(u.record.path!==n[f].record.path||!_l(u,n[f]));)f++;n.splice(f,0,u),u.record.name&&!Hs(u)&&r.set(u.record.name,u)}function c(u,f){let h,v={},y,w;if("name"in u&&u.name){if(h=r.get(u.name),!h)throw hn(1,{location:u});w=h.record.name,v=me(Ds(f.params,h.keys.filter(b=>!b.optional).map(b=>b.name)),u.params&&Ds(u.params,h.keys.map(b=>b.name))),y=h.stringify(v)}else if("path"in u)y=u.path,h=n.find(b=>b.re.test(y)),h&&(v=h.parse(y),w=h.record.name);else{if(h=f.name?r.get(f.name):n.find(b=>b.re.test(f.path)),!h)throw hn(1,{location:u,currentLocation:f});w=h.record.name,v=me({},f.params,u.params),y=h.stringify(v)}const T=[];let g=h;for(;g;)T.unshift(g.record),g=g.parent;return{name:w,path:y,params:v,matched:T,meta:pf(T)}}return e.forEach(u=>s(u)),{addRoute:s,resolve:c,removeRoute:i,getRoutes:l,getRecordMatcher:o}}function Ds(e,t){const n={};for(const r of t)r in e&&(n[r]=e[r]);return n}function df(e){return{path:e.path,redirect:e.redirect,name:e.name,meta:e.meta||{},aliasOf:void 0,beforeEnter:e.beforeEnter,props:hf(e),children:e.children||[],instances:{},leaveGuards:new Set,updateGuards:new Set,enterCallbacks:{},components:"components"in e?e.components||null:e.component&&{default:e.component}}}function hf(e){const t={},n=e.props||!1;if("component"in e)t.default=n;else for(const r in e.components)t[r]=typeof n=="object"?n[r]:n;return t}function Hs(e){for(;e;){if(e.record.aliasOf)return!0;e=e.parent}return!1}function pf(e){return e.reduce((t,n)=>me(t,n.meta),{})}function Fs(e,t){const n={};for(const r in e)n[r]=r in t?t[r]:e[r];return n}function _l(e,t){return t.children.some(n=>n===e||_l(e,n))}const bl=/#/g,mf=/&/g,vf=/\//g,gf=/=/g,_f=/\?/g,yl=/\+/g,bf=/%5B/g,yf=/%5D/g,El=/%5E/g,Ef=/%60/g,wl=/%7B/g,wf=/%7C/g,Cl=/%7D/g,Cf=/%20/g;function Do(e){return encodeURI(""+e).replace(wf,"|").replace(bf,"[").replace(yf,"]")}function xf(e){return Do(e).replace(wl,"{").replace(Cl,"}").replace(El,"^")}function uo(e){return Do(e).replace(yl,"%2B").replace(Cf,"+").replace(bl,"%23").replace(mf,"%26").replace(Ef,"`").replace(wl,"{").replace(Cl,"}").replace(El,"^")}function Lf(e){return uo(e).replace(gf,"%3D")}function Tf(e){return Do(e).replace(bl,"%23").replace(_f,"%3F")}function kf(e){return e==null?"":Tf(e).replace(vf,"%2F")}function gr(e){try{return decodeURIComponent(""+e)}catch{}return""+e}function Sf(e){const t={};if(e===""||e==="?")return t;const r=(e[0]==="?"?e.slice(1):e).split("&");for(let o=0;os&&uo(s)):[r&&uo(r)]).forEach(s=>{s!==void 0&&(t+=(t.length?"&":"")+n,s!=null&&(t+="="+s))})}return t}function Af(e){const t={};for(const n in e){const r=e[n];r!==void 0&&(t[n]=it(r)?r.map(o=>o==null?null:""+o):r==null?r:""+r)}return t}const Pf=Symbol(""),Bs=Symbol(""),$r=Symbol(""),Ho=Symbol(""),fo=Symbol("");function En(){let e=[];function t(r){return e.push(r),()=>{const o=e.indexOf(r);o>-1&&e.splice(o,1)}}function n(){e=[]}return{add:t,list:()=>e.slice(),reset:n}}function At(e,t,n,r,o){const s=r&&(r.enterCallbacks[o]=r.enterCallbacks[o]||[]);return()=>new Promise((i,l)=>{const a=f=>{f===!1?l(hn(4,{from:n,to:t})):f instanceof Error?l(f):ef(f)?l(hn(2,{from:t,to:f})):(s&&r.enterCallbacks[o]===s&&typeof f=="function"&&s.push(f),i())},c=e.call(r&&r.instances[o],t,n,a);let u=Promise.resolve(c);e.length<3&&(u=u.then(a)),u.catch(f=>l(f))})}function Kr(e,t,n,r){const o=[];for(const s of e)for(const i in s.components){let l=s.components[i];if(!(t!=="beforeRouteEnter"&&!s.instances[i]))if(Rf(l)){const c=(l.__vccOpts||l)[t];c&&o.push(At(c,n,r,s,i))}else{let a=l();o.push(()=>a.then(c=>{if(!c)return Promise.reject(new Error(`Couldn't resolve component "${i}" at "${s.path}"`));const u=Nu(c)?c.default:c;s.components[i]=u;const h=(u.__vccOpts||u)[t];return h&&At(h,n,r,s,i)()}))}}return o}function Rf(e){return typeof e=="object"||"displayName"in e||"props"in e||"__vccOpts"in e}function zs(e){const t=Ae($r),n=Ae(Ho),r=z(()=>t.resolve(X(e.to))),o=z(()=>{const{matched:a}=r.value,{length:c}=a,u=a[c-1],f=n.matched;if(!u||!f.length)return-1;const h=f.findIndex(dn.bind(null,u));if(h>-1)return h;const v=Vs(a[c-2]);return c>1&&Vs(u)===v&&f[f.length-1].path!==v?f.findIndex(dn.bind(null,a[c-2])):h}),s=z(()=>o.value>-1&&Mf(n.params,r.value.params)),i=z(()=>o.value>-1&&o.value===n.matched.length-1&&pl(n.params,r.value.params));function l(a={}){return $f(a)?t[X(e.replace)?"replace":"push"](X(e.to)).catch(An):Promise.resolve()}return{route:r,href:z(()=>r.value.href),isActive:s,isExactActive:i,navigate:l}}const Of=he({name:"RouterLink",compatConfig:{MODE:3},props:{to:{type:[String,Object],required:!0},replace:Boolean,activeClass:String,exactActiveClass:String,custom:Boolean,ariaCurrentValue:{type:String,default:"page"}},useLink:zs,setup(e,{slots:t}){const n=jn(zs(e)),{options:r}=Ae($r),o=z(()=>({[Us(e.activeClass,r.linkActiveClass,"router-link-active")]:n.isActive,[Us(e.exactActiveClass,r.linkExactActiveClass,"router-link-exact-active")]:n.isExactActive}));return()=>{const s=t.default&&t.default(n);return e.custom?s:ge("a",{"aria-current":n.isExactActive?e.ariaCurrentValue:null,href:n.href,onClick:n.navigate,class:o.value},s)}}}),If=Of;function $f(e){if(!(e.metaKey||e.altKey||e.ctrlKey||e.shiftKey)&&!e.defaultPrevented&&!(e.button!==void 0&&e.button!==0)){if(e.currentTarget&&e.currentTarget.getAttribute){const t=e.currentTarget.getAttribute("target");if(/\b_blank\b/i.test(t))return}return e.preventDefault&&e.preventDefault(),!0}}function Mf(e,t){for(const n in t){const r=t[n],o=e[n];if(typeof r=="string"){if(r!==o)return!1}else if(!it(o)||o.length!==r.length||r.some((s,i)=>s!==o[i]))return!1}return!0}function Vs(e){return e?e.aliasOf?e.aliasOf.path:e.path:""}const Us=(e,t,n)=>e??t??n,Nf=he({name:"RouterView",inheritAttrs:!1,props:{name:{type:String,default:"default"},route:Object},compatConfig:{MODE:3},setup(e,{attrs:t,slots:n}){const r=Ae(fo),o=z(()=>e.route||r.value),s=Ae(Bs,0),i=z(()=>{let c=X(s);const{matched:u}=o.value;let f;for(;(f=u[c])&&!f.components;)c++;return c}),l=z(()=>o.value.matched[i.value]);Wt(Bs,z(()=>i.value+1)),Wt(Pf,l),Wt(fo,o);const a=Ce();return st(()=>[a.value,l.value,e.name],([c,u,f],[h,v,y])=>{u&&(u.instances[f]=c,v&&v!==u&&c&&c===h&&(u.leaveGuards.size||(u.leaveGuards=v.leaveGuards),u.updateGuards.size||(u.updateGuards=v.updateGuards))),c&&u&&(!v||!dn(u,v)||!h)&&(u.enterCallbacks[f]||[]).forEach(w=>w(c))},{flush:"post"}),()=>{const c=o.value,u=e.name,f=l.value,h=f&&f.components[u];if(!h)return Ws(n.default,{Component:h,route:c});const v=f.props[u],y=v?v===!0?c.params:typeof v=="function"?v(c):v:null,T=ge(h,me({},y,t,{onVnodeUnmounted:g=>{g.component.isUnmounted&&(f.instances[u]=null)},ref:a}));return Ws(n.default,{Component:T,route:c})||T}}});function Ws(e,t){if(!e)return null;const n=e(t);return n.length===1?n[0]:n}const xl=Nf;function Df(e){const t=ff(e.routes,e),n=e.parseQuery||Sf,r=e.stringifyQuery||js,o=e.history,s=En(),i=En(),l=En(),a=Ti(pt);let c=pt;en&&e.scrollBehavior&&"scrollRestoration"in history&&(history.scrollRestoration="manual");const u=Wr.bind(null,C=>""+C),f=Wr.bind(null,kf),h=Wr.bind(null,gr);function v(C,j){let $,q;return vl(C)?($=t.getRecordMatcher(C),q=j):q=C,t.addRoute(q,$)}function y(C){const j=t.getRecordMatcher(C);j&&t.removeRoute(j)}function w(){return t.getRoutes().map(C=>C.record)}function T(C){return!!t.getRecordMatcher(C)}function g(C,j){if(j=me({},j||a.value),typeof C=="string"){const _=qr(n,C,j.path),E=t.resolve({path:_.path},j),x=o.createHref(_.fullPath);return me(_,E,{params:h(E.params),hash:gr(_.hash),redirectedFrom:void 0,href:x})}let $;if("path"in C)$=me({},C,{path:qr(n,C.path,j.path).path});else{const _=me({},C.params);for(const E in _)_[E]==null&&delete _[E];$=me({},C,{params:f(_)}),j.params=f(j.params)}const q=t.resolve($,j),ae=C.hash||"";q.params=u(h(q.params));const d=Fu(r,me({},C,{hash:xf(ae),path:q.path})),p=o.createHref(d);return me({fullPath:d,hash:ae,query:r===js?Af(C.query):C.query||{}},q,{redirectedFrom:void 0,href:p})}function b(C){return typeof C=="string"?qr(n,C,a.value.path):me({},C)}function P(C,j){if(c!==C)return hn(8,{from:j,to:C})}function A(C){return N(C)}function W(C){return A(me(b(C),{replace:!0}))}function ee(C){const j=C.matched[C.matched.length-1];if(j&&j.redirect){const{redirect:$}=j;let q=typeof $=="function"?$(C):$;return typeof q=="string"&&(q=q.includes("?")||q.includes("#")?q=b(q):{path:q},q.params={}),me({query:C.query,hash:C.hash,params:"path"in q?{}:C.params},q)}}function N(C,j){const $=c=g(C),q=a.value,ae=C.state,d=C.force,p=C.replace===!0,_=ee($);if(_)return N(me(b(_),{state:typeof _=="object"?me({},ae,_.state):ae,force:d,replace:p}),j||$);const E=$;E.redirectedFrom=j;let x;return!d&&ju(r,q,$)&&(x=hn(16,{to:E,from:q}),Ve(q,q,!0,!1)),(x?Promise.resolve(x):D(E,q)).catch(k=>dt(k)?dt(k,2)?k:Me(k):oe(k,E,q)).then(k=>{if(k){if(dt(k,2))return N(me({replace:p},b(k.to),{state:typeof k.to=="object"?me({},ae,k.to.state):ae,force:d}),j||E)}else k=L(E,q,!0,p,ae);return K(E,q,k),k})}function m(C,j){const $=P(C,j);return $?Promise.reject($):Promise.resolve()}function B(C){const j=wt.values().next().value;return j&&typeof j.runWithContext=="function"?j.runWithContext(C):C()}function D(C,j){let $;const[q,ae,d]=Hf(C,j);$=Kr(q.reverse(),"beforeRouteLeave",C,j);for(const _ of q)_.leaveGuards.forEach(E=>{$.push(At(E,C,j))});const p=m.bind(null,C,j);return $.push(p),Ie($).then(()=>{$=[];for(const _ of s.list())$.push(At(_,C,j));return $.push(p),Ie($)}).then(()=>{$=Kr(ae,"beforeRouteUpdate",C,j);for(const _ of ae)_.updateGuards.forEach(E=>{$.push(At(E,C,j))});return $.push(p),Ie($)}).then(()=>{$=[];for(const _ of d)if(_.beforeEnter)if(it(_.beforeEnter))for(const E of _.beforeEnter)$.push(At(E,C,j));else $.push(At(_.beforeEnter,C,j));return $.push(p),Ie($)}).then(()=>(C.matched.forEach(_=>_.enterCallbacks={}),$=Kr(d,"beforeRouteEnter",C,j),$.push(p),Ie($))).then(()=>{$=[];for(const _ of i.list())$.push(At(_,C,j));return $.push(p),Ie($)}).catch(_=>dt(_,8)?_:Promise.reject(_))}function K(C,j,$){l.list().forEach(q=>B(()=>q(C,j,$)))}function L(C,j,$,q,ae){const d=P(C,j);if(d)return d;const p=j===pt,_=en?history.state:{};$&&(q||p?o.replace(C.fullPath,me({scroll:p&&_&&_.scroll},ae)):o.push(C.fullPath,ae)),a.value=C,Ve(C,j,$,p),Me()}let O;function I(){O||(O=o.listen((C,j,$)=>{if(!lt.listening)return;const q=g(C),ae=ee(q);if(ae){N(me(ae,{replace:!0}),q).catch(An);return}c=q;const d=a.value;en&&Yu(Os(d.fullPath,$.delta),Ir()),D(q,d).catch(p=>dt(p,12)?p:dt(p,2)?(N(p.to,q).then(_=>{dt(_,20)&&!$.delta&&$.type===Nn.pop&&o.go(-1,!1)}).catch(An),Promise.reject()):($.delta&&o.go(-$.delta,!1),oe(p,q,d))).then(p=>{p=p||L(q,d,!1),p&&($.delta&&!dt(p,8)?o.go(-$.delta,!1):$.type===Nn.pop&&dt(p,20)&&o.go(-1,!1)),K(q,d,p)}).catch(An)}))}let ie=En(),V=En(),re;function oe(C,j,$){Me(C);const q=V.list();return q.length?q.forEach(ae=>ae(C,j,$)):console.error(C),Promise.reject(C)}function He(){return re&&a.value!==pt?Promise.resolve():new Promise((C,j)=>{ie.add([C,j])})}function Me(C){return re||(re=!C,I(),ie.list().forEach(([j,$])=>C?$(C):j()),ie.reset()),C}function Ve(C,j,$,q){const{scrollBehavior:ae}=e;if(!en||!ae)return Promise.resolve();const d=!$&&Ju(Os(C.fullPath,0))||(q||!$)&&history.state&&history.state.scroll||null;return xr().then(()=>ae(C,j,d)).then(p=>p&&Ku(p)).catch(p=>oe(p,C,j))}const Fe=C=>o.go(C);let Et;const wt=new Set,lt={currentRoute:a,listening:!0,addRoute:v,removeRoute:y,hasRoute:T,getRoutes:w,resolve:g,options:e,push:A,replace:W,go:Fe,back:()=>Fe(-1),forward:()=>Fe(1),beforeEach:s.add,beforeResolve:i.add,afterEach:l.add,onError:V.add,isReady:He,install(C){const j=this;C.component("RouterLink",If),C.component("RouterView",xl),C.config.globalProperties.$router=j,Object.defineProperty(C.config.globalProperties,"$route",{enumerable:!0,get:()=>X(a)}),en&&!Et&&a.value===pt&&(Et=!0,A(o.location).catch(ae=>{}));const $={};for(const ae in pt)Object.defineProperty($,ae,{get:()=>a.value[ae],enumerable:!0});C.provide($r,j),C.provide(Ho,Ei($)),C.provide(fo,a);const q=C.unmount;wt.add(C),C.unmount=function(){wt.delete(C),wt.size<1&&(c=pt,O&&O(),O=null,a.value=pt,Et=!1,re=!1),q()}}};function Ie(C){return C.reduce((j,$)=>j.then(()=>B($)),Promise.resolve())}return lt}function Hf(e,t){const n=[],r=[],o=[],s=Math.max(t.matched.length,e.matched.length);for(let i=0;idn(c,l))?r.push(l):n.push(l));const a=e.matched[i];a&&(t.matched.find(c=>dn(c,a))||o.push(a))}return[n,r,o]}function gn(){return Ae($r)}function Gt(){return Ae(Ho)}const Ff=({headerLinkSelector:e,headerAnchorSelector:t,delay:n,offset:r=5})=>{const o=gn(),i=hl(()=>{var w,T;const l=Math.max(window.scrollY,document.documentElement.scrollTop,document.body.scrollTop);if(Math.abs(l-0)h.some(b=>b.hash===g.hash));for(let g=0;g=(((w=b.parentElement)==null?void 0:w.offsetTop)??0)-r,W=!P||l<(((T=P.parentElement)==null?void 0:T.offsetTop)??0)-r;if(!(A&&W))continue;const N=decodeURIComponent(o.currentRoute.value.hash),m=decodeURIComponent(b.hash);if(N===m)return;if(f){for(let B=g+1;B{window.addEventListener("scroll",i)}),Sr(()=>{window.removeEventListener("scroll",i)})},qs=async(e,t)=>{const{scrollBehavior:n}=e.options;e.options.scrollBehavior=void 0,await e.replace({query:e.currentRoute.value.query,hash:t,force:!0}).finally(()=>e.options.scrollBehavior=n)},jf="a.sidebar-item",Bf=".header-anchor",zf=300,Vf=5,Uf=Jt({setup(){Ff({headerLinkSelector:jf,headerAnchorSelector:Bf,delay:zf,offset:Vf})}}),Ks=()=>window.pageYOffset||document.documentElement.scrollTop||document.body.scrollTop||0,Wf=()=>window.scrollTo({top:0,behavior:"smooth"});const qf=he({name:"BackToTop",setup(){const e=Ce(0),t=z(()=>e.value>300),n=hl(()=>{e.value=Ks()},100);Xe(()=>{e.value=Ks(),window.addEventListener("scroll",()=>n())});const r=ge("div",{class:"back-to-top",onClick:Wf});return()=>ge(Vn,{name:"back-to-top"},()=>t.value?r:null)}}),Kf=Jt({rootComponents:[qf]});const Yf=ge("svg",{class:"external-link-icon",xmlns:"http://www.w3.org/2000/svg","aria-hidden":"true",focusable:"false",x:"0px",y:"0px",viewBox:"0 0 100 100",width:"15",height:"15"},[ge("path",{fill:"currentColor",d:"M18.8,85.1h56l0,0c2.2,0,4-1.8,4-4v-32h-8v28h-48v-48h28v-8h-32l0,0c-2.2,0-4,1.8-4,4v56C14.8,83.3,16.6,85.1,18.8,85.1z"}),ge("polygon",{fill:"currentColor",points:"45.7,48.7 51.3,54.3 77.2,28.5 77.2,37.2 85.2,37.2 85.2,14.9 62.8,14.9 62.8,22.9 71.5,22.9"})]),Jf=he({name:"ExternalLinkIcon",props:{locales:{type:Object,required:!1,default:()=>({})}},setup(e){const t=Or(),n=z(()=>e.locales[t.value]??{openInNewWindow:"open in new window"});return()=>ge("span",[Yf,ge("span",{class:"external-link-icon-sr-only"},n.value.openInNewWindow)])}}),Gf={"/":{openInNewWindow:"open in new window"}},Qf=Jt({enhance({app:e}){e.component("ExternalLinkIcon",ge(Jf,{locales:Gf}))}});/*! medium-zoom 1.0.8 | MIT License | https://github.com/francoischalifour/medium-zoom */var Ft=Object.assign||function(e){for(var t=1;t1&&arguments[1]!==void 0?arguments[1]:{},r=window.Promise||function(L){function O(){}L(O,O)},o=function(L){var O=L.target;if(O===B){y();return}P.indexOf(O)!==-1&&w({target:O})},s=function(){if(!(W||!m.original)){var L=window.pageYOffset||document.documentElement.scrollTop||document.body.scrollTop||0;Math.abs(ee-L)>N.scrollOffset&&setTimeout(y,150)}},i=function(L){var O=L.key||L.keyCode;(O==="Escape"||O==="Esc"||O===27)&&y()},l=function(){var L=arguments.length>0&&arguments[0]!==void 0?arguments[0]:{},O=L;if(L.background&&(B.style.background=L.background),L.container&&L.container instanceof Object&&(O.container=Ft({},N.container,L.container)),L.template){var I=sr(L.template)?L.template:document.querySelector(L.template);O.template=I}return N=Ft({},N,O),P.forEach(function(ie){ie.dispatchEvent(Xt("medium-zoom:update",{detail:{zoom:D}}))}),D},a=function(){var L=arguments.length>0&&arguments[0]!==void 0?arguments[0]:{};return e(Ft({},N,L))},c=function(){for(var L=arguments.length,O=Array(L),I=0;I0?O.reduce(function(V,re){return[].concat(V,Js(re))},[]):P;return ie.forEach(function(V){V.classList.remove("medium-zoom-image"),V.dispatchEvent(Xt("medium-zoom:detach",{detail:{zoom:D}}))}),P=P.filter(function(V){return ie.indexOf(V)===-1}),D},f=function(L,O){var I=arguments.length>2&&arguments[2]!==void 0?arguments[2]:{};return P.forEach(function(ie){ie.addEventListener("medium-zoom:"+L,O,I)}),A.push({type:"medium-zoom:"+L,listener:O,options:I}),D},h=function(L,O){var I=arguments.length>2&&arguments[2]!==void 0?arguments[2]:{};return P.forEach(function(ie){ie.removeEventListener("medium-zoom:"+L,O,I)}),A=A.filter(function(ie){return!(ie.type==="medium-zoom:"+L&&ie.listener.toString()===O.toString())}),D},v=function(){var L=arguments.length>0&&arguments[0]!==void 0?arguments[0]:{},O=L.target,I=function(){var V={width:document.documentElement.clientWidth,height:document.documentElement.clientHeight,left:0,top:0,right:0,bottom:0},re=void 0,oe=void 0;if(N.container)if(N.container instanceof Object)V=Ft({},V,N.container),re=V.width-V.left-V.right-N.margin*2,oe=V.height-V.top-V.bottom-N.margin*2;else{var He=sr(N.container)?N.container:document.querySelector(N.container),Me=He.getBoundingClientRect(),Ve=Me.width,Fe=Me.height,Et=Me.left,wt=Me.top;V=Ft({},V,{width:Ve,height:Fe,left:Et,top:wt})}re=re||V.width-N.margin*2,oe=oe||V.height-N.margin*2;var lt=m.zoomedHd||m.original,Ie=Ys(lt)?re:lt.naturalWidth||re,C=Ys(lt)?oe:lt.naturalHeight||oe,j=lt.getBoundingClientRect(),$=j.top,q=j.left,ae=j.width,d=j.height,p=Math.min(Math.max(ae,Ie),re)/ae,_=Math.min(Math.max(d,C),oe)/d,E=Math.min(p,_),x=(-q+(re-ae)/2+N.margin+V.left)/E,k=(-$+(oe-d)/2+N.margin+V.top)/E,H="scale("+E+") translate3d("+x+"px, "+k+"px, 0)";m.zoomed.style.transform=H,m.zoomedHd&&(m.zoomedHd.style.transform=H)};return new r(function(ie){if(O&&P.indexOf(O)===-1){ie(D);return}var V=function Ve(){W=!1,m.zoomed.removeEventListener("transitionend",Ve),m.original.dispatchEvent(Xt("medium-zoom:opened",{detail:{zoom:D}})),ie(D)};if(m.zoomed){ie(D);return}if(O)m.original=O;else if(P.length>0){var re=P;m.original=re[0]}else{ie(D);return}if(m.original.dispatchEvent(Xt("medium-zoom:open",{detail:{zoom:D}})),ee=window.pageYOffset||document.documentElement.scrollTop||document.body.scrollTop||0,W=!0,m.zoomed=ed(m.original),document.body.appendChild(B),N.template){var oe=sr(N.template)?N.template:document.querySelector(N.template);m.template=document.createElement("div"),m.template.appendChild(oe.content.cloneNode(!0)),document.body.appendChild(m.template)}if(m.original.parentElement&&m.original.parentElement.tagName==="PICTURE"&&m.original.currentSrc&&(m.zoomed.src=m.original.currentSrc),document.body.appendChild(m.zoomed),window.requestAnimationFrame(function(){document.body.classList.add("medium-zoom--opened")}),m.original.classList.add("medium-zoom-image--hidden"),m.zoomed.classList.add("medium-zoom-image--opened"),m.zoomed.addEventListener("click",y),m.zoomed.addEventListener("transitionend",V),m.original.getAttribute("data-zoom-src")){m.zoomedHd=m.zoomed.cloneNode(),m.zoomedHd.removeAttribute("srcset"),m.zoomedHd.removeAttribute("sizes"),m.zoomedHd.removeAttribute("loading"),m.zoomedHd.src=m.zoomed.getAttribute("data-zoom-src"),m.zoomedHd.onerror=function(){clearInterval(He),console.warn("Unable to reach the zoom image target "+m.zoomedHd.src),m.zoomedHd=null,I()};var He=setInterval(function(){m.zoomedHd.complete&&(clearInterval(He),m.zoomedHd.classList.add("medium-zoom-image--opened"),m.zoomedHd.addEventListener("click",y),document.body.appendChild(m.zoomedHd),I())},10)}else if(m.original.hasAttribute("srcset")){m.zoomedHd=m.zoomed.cloneNode(),m.zoomedHd.removeAttribute("sizes"),m.zoomedHd.removeAttribute("loading");var Me=m.zoomedHd.addEventListener("load",function(){m.zoomedHd.removeEventListener("load",Me),m.zoomedHd.classList.add("medium-zoom-image--opened"),m.zoomedHd.addEventListener("click",y),document.body.appendChild(m.zoomedHd),I()})}else I()})},y=function(){return new r(function(L){if(W||!m.original){L(D);return}var O=function I(){m.original.classList.remove("medium-zoom-image--hidden"),document.body.removeChild(m.zoomed),m.zoomedHd&&document.body.removeChild(m.zoomedHd),document.body.removeChild(B),m.zoomed.classList.remove("medium-zoom-image--opened"),m.template&&document.body.removeChild(m.template),W=!1,m.zoomed.removeEventListener("transitionend",I),m.original.dispatchEvent(Xt("medium-zoom:closed",{detail:{zoom:D}})),m.original=null,m.zoomed=null,m.zoomedHd=null,m.template=null,L(D)};W=!0,document.body.classList.remove("medium-zoom--opened"),m.zoomed.style.transform="",m.zoomedHd&&(m.zoomedHd.style.transform=""),m.template&&(m.template.style.transition="opacity 150ms",m.template.style.opacity=0),m.original.dispatchEvent(Xt("medium-zoom:close",{detail:{zoom:D}})),m.zoomed.addEventListener("transitionend",O)})},w=function(){var L=arguments.length>0&&arguments[0]!==void 0?arguments[0]:{},O=L.target;return m.original?y():v({target:O})},T=function(){return N},g=function(){return P},b=function(){return m.original},P=[],A=[],W=!1,ee=0,N=n,m={original:null,zoomed:null,zoomedHd:null,template:null};Object.prototype.toString.call(t)==="[object Object]"?N=t:(t||typeof t=="string")&&c(t),N=Ft({margin:0,background:"#fff",scrollOffset:40,container:null,template:null},N);var B=Xf(N.background);document.addEventListener("click",o),document.addEventListener("keyup",i),document.addEventListener("scroll",s),window.addEventListener("resize",y);var D={open:v,close:y,toggle:w,update:l,clone:a,attach:c,detach:u,on:f,off:h,getOptions:T,getImages:g,getZoomedImage:b};return D};function nd(e,t){t===void 0&&(t={});var n=t.insertAt;if(!(!e||typeof document>"u")){var r=document.head||document.getElementsByTagName("head")[0],o=document.createElement("style");o.type="text/css",n==="top"&&r.firstChild?r.insertBefore(o,r.firstChild):r.appendChild(o),o.styleSheet?o.styleSheet.cssText=e:o.appendChild(document.createTextNode(e))}}var rd=".medium-zoom-overlay{position:fixed;top:0;right:0;bottom:0;left:0;opacity:0;transition:opacity .3s;will-change:opacity}.medium-zoom--opened .medium-zoom-overlay{cursor:pointer;cursor:zoom-out;opacity:1}.medium-zoom-image{cursor:pointer;cursor:zoom-in;transition:transform .3s cubic-bezier(.2,0,.2,1)!important}.medium-zoom-image--hidden{visibility:hidden}.medium-zoom-image--opened{position:relative;cursor:pointer;cursor:zoom-out;will-change:transform}";nd(rd);const od=td,sd=Symbol("mediumZoom");const id=".theme-default-content > img, .theme-default-content :not(a) > img",ld={},ad=300,cd=Jt({enhance({app:e,router:t}){const n=od(ld);n.refresh=(r=id)=>{n.detach(),n.attach(r)},e.provide(sd,n),t.afterEach(()=>{setTimeout(()=>n.refresh(),ad)})}});/** + * NProgress, (c) 2013, 2014 Rico Sta. Cruz - http://ricostacruz.com/nprogress + * @license MIT + */const ce={settings:{minimum:.08,easing:"ease",speed:200,trickle:!0,trickleRate:.02,trickleSpeed:800,barSelector:'[role="bar"]',parent:"body",template:'
'},status:null,set:e=>{const t=ce.isStarted();e=Yr(e,ce.settings.minimum,1),ce.status=e===1?null:e;const n=ce.render(!t),r=n.querySelector(ce.settings.barSelector),o=ce.settings.speed,s=ce.settings.easing;return n.offsetWidth,ud(i=>{er(r,{transform:"translate3d("+Gs(e)+"%,0,0)",transition:"all "+o+"ms "+s}),e===1?(er(n,{transition:"none",opacity:"1"}),n.offsetWidth,setTimeout(function(){er(n,{transition:"all "+o+"ms linear",opacity:"0"}),setTimeout(function(){ce.remove(),i()},o)},o)):setTimeout(()=>i(),o)}),ce},isStarted:()=>typeof ce.status=="number",start:()=>{ce.status||ce.set(0);const e=()=>{setTimeout(()=>{ce.status&&(ce.trickle(),e())},ce.settings.trickleSpeed)};return ce.settings.trickle&&e(),ce},done:e=>!e&&!ce.status?ce:ce.inc(.3+.5*Math.random()).set(1),inc:e=>{let t=ce.status;return t?(typeof e!="number"&&(e=(1-t)*Yr(Math.random()*t,.1,.95)),t=Yr(t+e,0,.994),ce.set(t)):ce.start()},trickle:()=>ce.inc(Math.random()*ce.settings.trickleRate),render:e=>{if(ce.isRendered())return document.getElementById("nprogress");Qs(document.documentElement,"nprogress-busy");const t=document.createElement("div");t.id="nprogress",t.innerHTML=ce.settings.template;const n=t.querySelector(ce.settings.barSelector),r=e?"-100":Gs(ce.status||0),o=document.querySelector(ce.settings.parent);return er(n,{transition:"all 0 linear",transform:"translate3d("+r+"%,0,0)"}),o!==document.body&&Qs(o,"nprogress-custom-parent"),o==null||o.appendChild(t),t},remove:()=>{Zs(document.documentElement,"nprogress-busy"),Zs(document.querySelector(ce.settings.parent),"nprogress-custom-parent");const e=document.getElementById("nprogress");e&&fd(e)},isRendered:()=>!!document.getElementById("nprogress")},Yr=(e,t,n)=>en?n:e,Gs=e=>(-1+e)*100,ud=function(){const e=[];function t(){const n=e.shift();n&&n(t)}return function(n){e.push(n),e.length===1&&t()}}(),er=function(){const e=["Webkit","O","Moz","ms"],t={};function n(i){return i.replace(/^-ms-/,"ms-").replace(/-([\da-z])/gi,function(l,a){return a.toUpperCase()})}function r(i){const l=document.body.style;if(i in l)return i;let a=e.length;const c=i.charAt(0).toUpperCase()+i.slice(1);let u;for(;a--;)if(u=e[a]+c,u in l)return u;return i}function o(i){return i=n(i),t[i]??(t[i]=r(i))}function s(i,l,a){l=o(l),i.style[l]=a}return function(i,l){for(const a in l){const c=l[a];c!==void 0&&Object.prototype.hasOwnProperty.call(l,a)&&s(i,a,c)}}}(),Ll=(e,t)=>(typeof e=="string"?e:Fo(e)).indexOf(" "+t+" ")>=0,Qs=(e,t)=>{const n=Fo(e),r=n+t;Ll(n,t)||(e.className=r.substring(1))},Zs=(e,t)=>{const n=Fo(e);if(!Ll(e,t))return;const r=n.replace(" "+t+" "," ");e.className=r.substring(1,r.length-1)},Fo=e=>(" "+(e.className||"")+" ").replace(/\s+/gi," "),fd=e=>{e&&e.parentNode&&e.parentNode.removeChild(e)};const dd=()=>{Xe(()=>{const e=gn(),t=new Set;t.add(e.currentRoute.value.path),e.beforeEach(n=>{t.has(n.path)||ce.start()}),e.afterEach(n=>{t.add(n.path),ce.done()})})},hd=Jt({setup(){dd()}}),pd=JSON.parse(`{"logo":"/favicon.ico","repo":"https://github.com/carla-cn/study-notes","docsDir":"docs","sidebarDepth":4,"navbar":[{"text":"前端","children":[{"text":"JavaScript","children":[{"text":"红宝书","link":"/frontend/js/red-book"}]},{"text":"框架","children":[{"text":"小程序","link":"/frontend/framework/we-app"}]},{"text":"工程化","children":[{"text":"umi框架","link":"/frontend/engineering/umi"}]},{"text":"其他","children":[{"text":"小工具","link":"/frontend/other/tools"},{"text":"错误收集","link":"/frontend/other/errors"}]}]},{"text":"后端","children":[{"text":"Go","link":"/backend/go"}]},{"text":"通用","children":[{"text":"计算机网络","link":"/general/network"}]}],"editLink":false,"contributors":false,"backToHome":"返回首页","toggleColorMode":"","locales":{"/":{"selectLanguageName":"English"}},"colorMode":"auto","colorModeSwitch":true,"selectLanguageText":"Languages","selectLanguageAriaLabel":"Select language","sidebar":"auto","editLinkText":"Edit this page","lastUpdated":true,"lastUpdatedText":"Last Updated","contributorsText":"Contributors","notFound":["There's nothing here.","How did we get here?","That's a Four-Oh-Four.","Looks like we've got some broken links."],"openInNewWindow":"open in new window","toggleSidebar":"toggle sidebar"}`),md=Ce(pd),Tl=()=>md,kl=Symbol(""),vd=()=>{const e=Ae(kl);if(!e)throw new Error("useThemeLocaleData() is called without provider.");return e},gd=(e,t)=>{const{locales:n,...r}=e;return{...r,...n==null?void 0:n[t]}},_d=Jt({enhance({app:e}){const t=Tl(),n=e._context.provides[Io],r=z(()=>gd(t.value,n.value));e.provide(kl,r),Object.defineProperties(e.config.globalProperties,{$theme:{get(){return t.value}},$themeLocale:{get(){return r.value}}})}}),bd=he({__name:"Badge",props:{type:{type:String,required:!1,default:"tip"},text:{type:String,required:!1,default:""},vertical:{type:String,required:!1,default:void 0}},setup(e){return(t,n)=>(F(),Z("span",{class:We(["badge",e.type]),style:Fn({verticalAlign:e.vertical})},[ye(t.$slots,"default",{},()=>[Mt(Oe(e.text),1)])],6))}}),Le=(e,t)=>{const n=e.__vccOpts||e;for(const[r,o]of t)n[r]=o;return n},yd=Le(bd,[["__file","Badge.vue"]]),Ed=he({name:"CodeGroup",slots:Object,setup(e,{slots:t}){const n=Ce(-1),r=Ce([]),o=(l=n.value)=>{l{l>0?n.value=l-1:n.value=r.value.length-1,r.value[n.value].focus()},i=(l,a)=>{l.key===" "||l.key==="Enter"?(l.preventDefault(),n.value=a):l.key==="ArrowRight"?(l.preventDefault(),o(a)):l.key==="ArrowLeft"&&(l.preventDefault(),s(a))};return()=>{var a;const l=(((a=t.default)==null?void 0:a.call(t))||[]).filter(c=>c.type.name==="CodeGroupItem").map(c=>(c.props===null&&(c.props={}),c));return l.length===0?null:(n.value<0||n.value>l.length-1?(n.value=l.findIndex(c=>c.props.active===""||c.props.active===!0),n.value===-1&&(n.value=0)):l.forEach((c,u)=>{c.props.active=u===n.value}),ge("div",{class:"code-group"},[ge("div",{class:"code-group__nav"},ge("ul",{class:"code-group__ul"},l.map((c,u)=>{const f=u===n.value;return ge("li",{class:"code-group__li"},ge("button",{ref:h=>{h&&(r.value[u]=h)},class:{"code-group__nav-tab":!0,"code-group__nav-tab-active":f},ariaPressed:f,ariaExpanded:f,onClick:()=>n.value=u,onKeydown:h=>i(h,u)},c.props.title))}))),l]))}}}),wd=["aria-selected"],Cd=he({name:"CodeGroupItem"}),xd=he({...Cd,props:{title:{type:String,required:!0},active:{type:Boolean,required:!1,default:!1}},setup(e){return(t,n)=>(F(),Z("div",{class:We(["code-group-item",{"code-group-item__active":e.active}]),"aria-selected":e.active},[ye(t.$slots,"default")],10,wd))}}),Ld=Le(xd,[["__file","CodeGroupItem.vue"]]);function Sl(e){return ui()?(oa(e),!0):!1}function pn(e){return typeof e=="function"?e():X(e)}const Td=typeof window<"u"&&typeof document<"u",kd=Object.prototype.toString,Sd=e=>kd.call(e)==="[object Object]",Ad=()=>{};function Pd(e,t){function n(...r){return new Promise((o,s)=>{Promise.resolve(e(()=>t.apply(this,r),{fn:t,thisArg:this,args:r})).then(o).catch(s)})}return n}const Al=e=>e();function Rd(e=Al){const t=Ce(!0);function n(){t.value=!1}function r(){t.value=!0}const o=(...s)=>{t.value&&e(...s)};return{isActive:Cr(t),pause:n,resume:r,eventFilter:o}}function Od(e,t,n={}){const{eventFilter:r=Al,...o}=n;return st(e,Pd(r,t),o)}function Id(e,t,n={}){const{eventFilter:r,...o}=n,{eventFilter:s,pause:i,resume:l,isActive:a}=Rd(r);return{stop:Od(e,t,{...o,eventFilter:s}),pause:i,resume:l,isActive:a}}function $d(e=!1,t={}){const{truthyValue:n=!0,falsyValue:r=!1}=t,o=$e(e),s=Ce(e);function i(l){if(arguments.length)return s.value=l,s.value;{const a=pn(n);return s.value=s.value===a?pn(r):a,s.value}}return o?i:[s,i]}function Md(e){var t;const n=pn(e);return(t=n==null?void 0:n.$el)!=null?t:n}const _r=Td?window:void 0;function Xs(...e){let t,n,r,o;if(typeof e[0]=="string"||Array.isArray(e[0])?([n,r,o]=e,t=_r):[t,n,r,o]=e,!t)return Ad;Array.isArray(n)||(n=[n]),Array.isArray(r)||(r=[r]);const s=[],i=()=>{s.forEach(u=>u()),s.length=0},l=(u,f,h,v)=>(u.addEventListener(f,h,v),()=>u.removeEventListener(f,h,v)),a=st(()=>[Md(t),pn(o)],([u,f])=>{if(i(),!u)return;const h=Sd(f)?{...f}:f;s.push(...n.flatMap(v=>r.map(y=>l(u,v,y,h))))},{immediate:!0,flush:"post"}),c=()=>{a(),i()};return Sl(c),c}function Nd(){const e=Ce(!1);return Zi()&&Xe(()=>{e.value=!0}),e}function Dd(e){const t=Nd();return z(()=>(t.value,!!e()))}function Hd(e,t={}){const{window:n=_r}=t,r=Dd(()=>n&&"matchMedia"in n&&typeof n.matchMedia=="function");let o;const s=Ce(!1),i=c=>{s.value=c.matches},l=()=>{o&&("removeEventListener"in o?o.removeEventListener("change",i):o.removeListener(i))},a=Ja(()=>{r.value&&(l(),o=n.matchMedia(pn(e)),"addEventListener"in o?o.addEventListener("change",i):o.addListener(i),s.value=o.matches)});return Sl(()=>{a(),l(),o=void 0}),s}const tr=typeof globalThis<"u"?globalThis:typeof window<"u"?window:typeof global<"u"?global:typeof self<"u"?self:{},nr="__vueuse_ssr_handlers__",Fd=jd();function jd(){return nr in tr||(tr[nr]=tr[nr]||{}),tr[nr]}function Bd(e,t){return Fd[e]||t}function zd(e){return e==null?"any":e instanceof Set?"set":e instanceof Map?"map":e instanceof Date?"date":typeof e=="boolean"?"boolean":typeof e=="string"?"string":typeof e=="object"?"object":Number.isNaN(e)?"any":"number"}const Vd={boolean:{read:e=>e==="true",write:e=>String(e)},object:{read:e=>JSON.parse(e),write:e=>JSON.stringify(e)},number:{read:e=>Number.parseFloat(e),write:e=>String(e)},any:{read:e=>e,write:e=>String(e)},string:{read:e=>e,write:e=>String(e)},map:{read:e=>new Map(JSON.parse(e)),write:e=>JSON.stringify(Array.from(e.entries()))},set:{read:e=>new Set(JSON.parse(e)),write:e=>JSON.stringify(Array.from(e))},date:{read:e=>new Date(e),write:e=>e.toISOString()}},ei="vueuse-storage";function Ud(e,t,n,r={}){var o;const{flush:s="pre",deep:i=!0,listenToStorageChanges:l=!0,writeDefaults:a=!0,mergeDefaults:c=!1,shallow:u,window:f=_r,eventFilter:h,onError:v=m=>{console.error(m)}}=r,y=(u?Ti:Ce)(t);if(!n)try{n=Bd("getDefaultStorage",()=>{var m;return(m=_r)==null?void 0:m.localStorage})()}catch(m){v(m)}if(!n)return y;const w=pn(t),T=zd(w),g=(o=r.serializer)!=null?o:Vd[T],{pause:b,resume:P}=Id(y,()=>A(y.value),{flush:s,deep:i,eventFilter:h});return f&&l&&(Xs(f,"storage",N),Xs(f,ei,ee)),N(),y;function A(m){try{if(m==null)n.removeItem(e);else{const B=g.write(m),D=n.getItem(e);D!==B&&(n.setItem(e,B),f&&f.dispatchEvent(new CustomEvent(ei,{detail:{key:e,oldValue:D,newValue:B,storageArea:n}})))}}catch(B){v(B)}}function W(m){const B=m?m.newValue:n.getItem(e);if(B==null)return a&&w!==null&&n.setItem(e,g.write(w)),w;if(!m&&c){const D=g.read(B);return typeof c=="function"?c(D,w):T==="object"&&!Array.isArray(D)?{...w,...D}:D}else return typeof B!="string"?B:g.read(B)}function ee(m){N(m.detail)}function N(m){if(!(m&&m.storageArea!==n)){if(m&&m.key==null){y.value=w;return}if(!(m&&m.key!==e)){b();try{(m==null?void 0:m.newValue)!==g.write(y.value)&&(y.value=W(m))}catch(B){v(B)}finally{m?xr(P):P()}}}}}function Wd(e){return Hd("(prefers-color-scheme: dark)",e)}const qd=()=>Tl(),ze=()=>vd(),Pl=Symbol(""),jo=()=>{const e=Ae(Pl);if(!e)throw new Error("useDarkMode() is called without provider.");return e},Kd=()=>{const e=ze(),t=Wd(),n=Ud("vuepress-color-scheme",e.value.colorMode),r=z({get(){return e.value.colorModeSwitch?n.value==="auto"?t.value:n.value==="dark":e.value.colorMode==="dark"},set(o){o===t.value?n.value="auto":n.value=o?"dark":"light"}});Wt(Pl,r),Yd(r)},Yd=e=>{const t=(n=e.value)=>{const r=window==null?void 0:window.document.querySelector("html");r==null||r.classList.toggle("dark",n)};Xe(()=>{st(e,t,{immediate:!0})}),Ar(()=>t())},Rl=(...e)=>{const n=gn().resolve(...e),r=n.matched[n.matched.length-1];if(!(r!=null&&r.redirect))return n;const{redirect:o}=r,s=se(o)?o(n):o,i=pe(s)?{path:s}:s;return Rl({hash:n.hash,query:n.query,params:n.params,...i})},Bo=e=>{const t=Rl(encodeURI(e));return{text:t.meta.title||e,link:t.name==="404"?e:t.fullPath}};let Jr=null,wn=null;const Jd={wait:()=>Jr,pending:()=>{Jr=new Promise(e=>wn=e)},resolve:()=>{wn==null||wn(),Jr=null,wn=null}},Ol=()=>Jd,Il=Symbol("sidebarItems"),zo=()=>{const e=Ae(Il);if(!e)throw new Error("useSidebarItems() is called without provider.");return e},Gd=()=>{const e=ze(),t=vt(),n=z(()=>Qd(t.value,e.value));Wt(Il,n)},Qd=(e,t)=>{const n=e.sidebar??t.sidebar??"auto",r=e.sidebarDepth??t.sidebarDepth??2;return e.home||n===!1?[]:n==="auto"?Xd(r):G(n)?$l(n,r):Oo(n)?eh(n,r):[]},Zd=(e,t)=>({text:e.title,link:e.link,children:Vo(e.children,t)}),Vo=(e,t)=>t>0?e.map(n=>Zd(n,t-1)):[],Xd=e=>{const t=Kt();return[{text:t.value.title,children:Vo(t.value.headers,e)}]},$l=(e,t)=>{const n=Gt(),r=Kt(),o=s=>{var l;let i;if(pe(s)?i=Bo(s):i=s,i.children)return{...i,children:i.children.map(a=>o(a))};if(i.link===n.path){const a=((l=r.value.headers[0])==null?void 0:l.level)===1?r.value.headers[0].children:r.value.headers;return{...i,children:Vo(a,t)}}return i};return e.map(s=>o(s))},eh=(e,t)=>{const n=Gt(),r=ol(e,n.path),o=e[r]??[];return $l(o,t)},th="719px",nh={mobile:th};var Dn;(function(e){e.MOBILE="mobile"})(Dn||(Dn={}));var ri;const rh={[Dn.MOBILE]:Number.parseInt((ri=nh.mobile)==null?void 0:ri.replace("px",""),10)},Ml=(e,t)=>{const n=rh[e];Number.isInteger(n)&&Xe(()=>{t(n),window.addEventListener("resize",()=>t(n),!1),window.addEventListener("orientationchange",()=>t(n),!1)})},oh={},sh={class:"theme-default-content"};function ih(e,t){const n=bt("Content");return F(),Z("div",sh,[te(n)])}const lh=Le(oh,[["render",ih],["__file","HomeContent.vue"]]),ah={key:0,class:"features"},ch=he({__name:"HomeFeatures",setup(e){const t=vt(),n=z(()=>G(t.value.features)?t.value.features:[]);return(r,o)=>n.value.length?(F(),Z("div",ah,[(F(!0),Z(Ee,null,It(n.value,s=>(F(),Z("div",{key:s.title,class:"feature"},[fe("h2",null,Oe(s.title),1),fe("p",null,Oe(s.details),1)]))),128))])):Te("v-if",!0)}}),uh=Le(ch,[["__file","HomeFeatures.vue"]]),fh=["innerHTML"],dh=["textContent"],hh=he({__name:"HomeFooter",setup(e){const t=vt(),n=z(()=>t.value.footer),r=z(()=>t.value.footerHtml);return(o,s)=>n.value?(F(),Z(Ee,{key:0},[Te(" eslint-disable-next-line vue/no-v-html "),r.value?(F(),Z("div",{key:0,class:"footer",innerHTML:n.value},null,8,fh)):(F(),Z("div",{key:1,class:"footer",textContent:Oe(n.value)},null,8,dh))],64)):Te("v-if",!0)}}),ph=Le(hh,[["__file","HomeFooter.vue"]]),mh=["href","rel","target","aria-label"],vh=he({inheritAttrs:!1}),gh=he({...vh,__name:"AutoLink",props:{item:{type:Object,required:!0}},setup(e){const t=e,n=Gt(),r=fl(),{item:o}=Lo(t),s=z(()=>Un(o.value.link)),i=z(()=>xu(o.value.link)||Lu(o.value.link)),l=z(()=>{if(!i.value){if(o.value.target)return o.value.target;if(s.value)return"_blank"}}),a=z(()=>l.value==="_blank"),c=z(()=>!s.value&&!i.value&&!a.value),u=z(()=>{if(!i.value){if(o.value.rel)return o.value.rel;if(a.value)return"noopener noreferrer"}}),f=z(()=>o.value.ariaLabel||o.value.text),h=z(()=>{const w=Object.keys(r.value.locales);return w.length?!w.some(T=>T===o.value.link):o.value.link!=="/"}),v=z(()=>h.value?n.path.startsWith(o.value.link):!1),y=z(()=>c.value?o.value.activeMatch?new RegExp(o.value.activeMatch).test(n.path):v.value:!1);return(w,T)=>{const g=bt("RouterLink"),b=bt("AutoLinkExternalIcon");return c.value?(F(),Se(g,lo({key:0,class:{"router-link-active":y.value},to:X(o).link,"aria-label":f.value},w.$attrs),{default:Ne(()=>[ye(w.$slots,"before"),Mt(" "+Oe(X(o).text)+" ",1),ye(w.$slots,"after")]),_:3},16,["class","to","aria-label"])):(F(),Z("a",lo({key:1,class:"external-link",href:X(o).link,rel:u.value,target:l.value,"aria-label":f.value},w.$attrs),[ye(w.$slots,"before"),Mt(" "+Oe(X(o).text)+" ",1),a.value?(F(),Se(b,{key:0})):Te("v-if",!0),ye(w.$slots,"after")],16,mh))}}}),gt=Le(gh,[["__file","AutoLink.vue"]]),_h={class:"hero"},bh={key:0,id:"main-title"},yh={key:1,class:"description"},Eh={key:2,class:"actions"},wh=he({__name:"HomeHero",setup(e){const t=vt(),n=$o(),r=jo(),o=z(()=>r.value&&t.value.heroImageDark!==void 0?t.value.heroImageDark:t.value.heroImage),s=z(()=>t.value.heroAlt||l.value||"hero"),i=z(()=>t.value.heroHeight||280),l=z(()=>t.value.heroText===null?null:t.value.heroText||n.value.title||"Hello"),a=z(()=>t.value.tagline===null?null:t.value.tagline||n.value.description||"Welcome to your VuePress site"),c=z(()=>G(t.value.actions)?t.value.actions.map(({text:f,link:h,type:v="primary"})=>({text:f,link:h,type:v})):[]),u=()=>{if(!o.value)return null;const f=ge("img",{src:No(o.value),alt:s.value,height:i.value});return t.value.heroImageDark===void 0?f:ge(Mo,()=>f)};return(f,h)=>(F(),Z("header",_h,[te(u),l.value?(F(),Z("h1",bh,Oe(l.value),1)):Te("v-if",!0),a.value?(F(),Z("p",yh,Oe(a.value),1)):Te("v-if",!0),c.value.length?(F(),Z("p",Eh,[(F(!0),Z(Ee,null,It(c.value,v=>(F(),Se(gt,{key:v.text,class:We(["action-button",[v.type]]),item:v},null,8,["class","item"]))),128))])):Te("v-if",!0)]))}}),Ch=Le(wh,[["__file","HomeHero.vue"]]),xh={class:"home"},Lh=he({__name:"Home",setup(e){return(t,n)=>(F(),Z("main",xh,[te(Ch),te(uh),te(lh),te(ph)]))}}),Th=Le(Lh,[["__file","Home.vue"]]),kh=he({__name:"NavbarBrand",setup(e){const t=Or(),n=$o(),r=ze(),o=jo(),s=z(()=>r.value.home||t.value),i=z(()=>n.value.title),l=z(()=>o.value&&r.value.logoDark!==void 0?r.value.logoDark:r.value.logo),a=()=>{if(!l.value)return null;const c=ge("img",{class:"logo",src:No(l.value),alt:i.value});return r.value.logoDark===void 0?c:ge(Mo,()=>c)};return(c,u)=>{const f=bt("RouterLink");return F(),Se(f,{to:s.value},{default:Ne(()=>[te(a),i.value?(F(),Z("span",{key:0,class:We(["site-name",{"can-hide":l.value}])},Oe(i.value),3)):Te("v-if",!0)]),_:1},8,["to"])}}}),Sh=Le(kh,[["__file","NavbarBrand.vue"]]),Ah=he({__name:"DropdownTransition",setup(e){const t=r=>{r.style.height=r.scrollHeight+"px"},n=r=>{r.style.height=""};return(r,o)=>(F(),Se(Vn,{name:"dropdown",onEnter:t,onAfterEnter:n,onBeforeLeave:t},{default:Ne(()=>[ye(r.$slots,"default")]),_:3}))}}),Nl=Le(Ah,[["__file","DropdownTransition.vue"]]),Ph=["aria-label"],Rh={class:"title"},Oh=fe("span",{class:"arrow down"},null,-1),Ih=["aria-label"],$h={class:"title"},Mh={class:"navbar-dropdown"},Nh={class:"navbar-dropdown-subtitle"},Dh={key:1},Hh={class:"navbar-dropdown-subitem-wrapper"},Fh=he({__name:"NavbarDropdown",props:{item:{type:Object,required:!0}},setup(e){const t=e,{item:n}=Lo(t),r=z(()=>n.value.ariaLabel||n.value.text),o=Ce(!1),s=Gt();st(()=>s.path,()=>{o.value=!1});const i=a=>{a.detail===0?o.value=!o.value:o.value=!1},l=(a,c)=>c[c.length-1]===a;return(a,c)=>(F(),Z("div",{class:We(["navbar-dropdown-wrapper",{open:o.value}])},[fe("button",{class:"navbar-dropdown-title",type:"button","aria-label":r.value,onClick:i},[fe("span",Rh,Oe(X(n).text),1),Oh],8,Ph),fe("button",{class:"navbar-dropdown-title-mobile",type:"button","aria-label":r.value,onClick:c[0]||(c[0]=u=>o.value=!o.value)},[fe("span",$h,Oe(X(n).text),1),fe("span",{class:We(["arrow",o.value?"down":"right"])},null,2)],8,Ih),te(Nl,null,{default:Ne(()=>[fr(fe("ul",Mh,[(F(!0),Z(Ee,null,It(X(n).children,u=>(F(),Z("li",{key:u.text,class:"navbar-dropdown-item"},[u.children?(F(),Z(Ee,{key:0},[fe("h4",Nh,[u.link?(F(),Se(gt,{key:0,item:u,onFocusout:f=>l(u,X(n).children)&&u.children.length===0&&(o.value=!1)},null,8,["item","onFocusout"])):(F(),Z("span",Dh,Oe(u.text),1))]),fe("ul",Hh,[(F(!0),Z(Ee,null,It(u.children,f=>(F(),Z("li",{key:f.link,class:"navbar-dropdown-subitem"},[te(gt,{item:f,onFocusout:h=>l(f,u.children)&&l(u,X(n).children)&&(o.value=!1)},null,8,["item","onFocusout"])]))),128))])],64)):(F(),Se(gt,{key:1,item:u,onFocusout:f=>l(u,X(n).children)&&(o.value=!1)},null,8,["item","onFocusout"]))]))),128))],512),[[vr,o.value]])]),_:1})],2))}}),jh=Le(Fh,[["__file","NavbarDropdown.vue"]]),ti=e=>decodeURI(e).replace(/#.*$/,"").replace(/(index)?\.(md|html)$/,""),Bh=(e,t)=>{if(t.hash===e)return!0;const n=ti(t.path),r=ti(e);return n===r},Dl=(e,t)=>e.link&&Bh(e.link,t)?!0:e.children?e.children.some(n=>Dl(n,t)):!1,Hl=e=>!Un(e)||/github\.com/.test(e)?"GitHub":/bitbucket\.org/.test(e)?"Bitbucket":/gitlab\.com/.test(e)?"GitLab":/gitee\.com/.test(e)?"Gitee":null,zh={GitHub:":repo/edit/:branch/:path",GitLab:":repo/-/edit/:branch/:path",Gitee:":repo/edit/:branch/:path",Bitbucket:":repo/src/:branch/:path?mode=edit&spa=0&at=:branch&fileviewer=file-view-default"},Vh=({docsRepo:e,editLinkPattern:t})=>{if(t)return t;const n=Hl(e);return n!==null?zh[n]:null},Uh=({docsRepo:e,docsBranch:t,docsDir:n,filePathRelative:r,editLinkPattern:o})=>{if(!r)return null;const s=Vh({docsRepo:e,editLinkPattern:o});return s?s.replace(/:repo/,Un(e)?e:`https://github.com/${e}`).replace(/:branch/,t).replace(/:path/,rl(`${nl(n)}/${r}`)):null},Wh={key:0,class:"navbar-items"},qh=he({__name:"NavbarItems",setup(e){const t=()=>{const u=gn(),f=Or(),h=fl(),v=$o(),y=qd(),w=ze();return z(()=>{const T=Object.keys(h.value.locales);if(T.length<2)return[];const g=u.currentRoute.value.path,b=u.currentRoute.value.fullPath;return[{text:`${w.value.selectLanguageText}`,ariaLabel:`${w.value.selectLanguageAriaLabel??w.value.selectLanguageText}`,children:T.map(A=>{var D,K;const W=((D=h.value.locales)==null?void 0:D[A])??{},ee=((K=y.value.locales)==null?void 0:K[A])??{},N=`${W.lang}`,m=ee.selectLanguageName??N;let B;if(N===v.value.lang)B=b;else{const L=g.replace(f.value,A);u.getRoutes().some(O=>O.path===L)?B=b.replace(g,L):B=ee.home??A}return{text:m,link:B}})}]})},n=()=>{const u=ze(),f=z(()=>u.value.repo),h=z(()=>f.value?Hl(f.value):null),v=z(()=>f.value&&!Un(f.value)?`https://github.com/${f.value}`:f.value),y=z(()=>v.value?u.value.repoLabel?u.value.repoLabel:h.value===null?"Source":h.value:null);return z(()=>!v.value||!y.value?[]:[{text:y.value,link:v.value}])},r=u=>pe(u)?Bo(u):u.children?{...u,children:u.children.map(r)}:u,o=()=>{const u=ze();return z(()=>(u.value.navbar||[]).map(r))},s=Ce(!1),i=o(),l=t(),a=n(),c=z(()=>[...i.value,...l.value,...a.value]);return Ml(Dn.MOBILE,u=>{window.innerWidthc.value.length?(F(),Z("nav",Wh,[(F(!0),Z(Ee,null,It(c.value,h=>(F(),Z("div",{key:h.text,class:"navbar-item"},[h.children?(F(),Se(jh,{key:0,item:h,class:We(s.value?"mobile":"")},null,8,["item","class"])):(F(),Se(gt,{key:1,item:h},null,8,["item"]))]))),128))])):Te("v-if",!0)}}),Fl=Le(qh,[["__file","NavbarItems.vue"]]),Kh=["title"],Yh={class:"icon",focusable:"false",viewBox:"0 0 32 32"},Jh=Ic('',9),Gh=[Jh],Qh={class:"icon",focusable:"false",viewBox:"0 0 32 32"},Zh=fe("path",{d:"M13.502 5.414a15.075 15.075 0 0 0 11.594 18.194a11.113 11.113 0 0 1-7.975 3.39c-.138 0-.278.005-.418 0a11.094 11.094 0 0 1-3.2-21.584M14.98 3a1.002 1.002 0 0 0-.175.016a13.096 13.096 0 0 0 1.825 25.981c.164.006.328 0 .49 0a13.072 13.072 0 0 0 10.703-5.555a1.01 1.01 0 0 0-.783-1.565A13.08 13.08 0 0 1 15.89 4.38A1.015 1.015 0 0 0 14.98 3z",fill:"currentColor"},null,-1),Xh=[Zh],ep=he({__name:"ToggleColorModeButton",setup(e){const t=ze(),n=jo(),r=()=>{n.value=!n.value};return(o,s)=>(F(),Z("button",{class:"toggle-color-mode-button",title:X(t).toggleColorMode,onClick:r},[fr((F(),Z("svg",Yh,Gh,512)),[[vr,!X(n)]]),fr((F(),Z("svg",Qh,Xh,512)),[[vr,X(n)]])],8,Kh))}}),tp=Le(ep,[["__file","ToggleColorModeButton.vue"]]),np=["title"],rp=fe("div",{class:"icon","aria-hidden":"true"},[fe("span"),fe("span"),fe("span")],-1),op=[rp],sp=he({__name:"ToggleSidebarButton",emits:["toggle"],setup(e){const t=ze();return(n,r)=>(F(),Z("div",{class:"toggle-sidebar-button",title:X(t).toggleSidebar,"aria-expanded":"false",role:"button",tabindex:"0",onClick:r[0]||(r[0]=o=>n.$emit("toggle"))},op,8,np))}}),ip=Le(sp,[["__file","ToggleSidebarButton.vue"]]),lp=he({__name:"Navbar",emits:["toggle-sidebar"],setup(e){const t=ze(),n=Ce(null),r=Ce(null),o=Ce(0),s=z(()=>o.value?{maxWidth:o.value+"px"}:{});Ml(Dn.MOBILE,l=>{var c;const a=i(n.value,"paddingLeft")+i(n.value,"paddingRight");window.innerWidth{const c=bt("NavbarSearch");return F(),Z("header",{ref_key:"navbar",ref:n,class:"navbar"},[te(ip,{onToggle:a[0]||(a[0]=u=>l.$emit("toggle-sidebar"))}),fe("span",{ref_key:"navbarBrand",ref:r},[te(Sh)],512),fe("div",{class:"navbar-items-wrapper",style:Fn(s.value)},[ye(l.$slots,"before"),te(Fl,{class:"can-hide"}),ye(l.$slots,"after"),X(t).colorModeSwitch?(F(),Se(tp,{key:0})):Te("v-if",!0),te(c)],4)],512)}}}),ap=Le(lp,[["__file","Navbar.vue"]]),cp={class:"page-meta"},up={key:0,class:"meta-item edit-link"},fp={key:1,class:"meta-item last-updated"},dp={class:"meta-item-label"},hp={class:"meta-item-info"},pp={key:2,class:"meta-item contributors"},mp={class:"meta-item-label"},vp={class:"meta-item-info"},gp=["title"],_p=he({__name:"PageMeta",setup(e){const t=()=>{const a=ze(),c=Kt(),u=vt();return z(()=>{if(!(u.value.editLink??a.value.editLink??!0))return null;const{repo:h,docsRepo:v=h,docsBranch:y="main",docsDir:w="",editLinkText:T}=a.value;if(!v)return null;const g=Uh({docsRepo:v,docsBranch:y,docsDir:w,filePathRelative:c.value.filePathRelative,editLinkPattern:u.value.editLinkPattern??a.value.editLinkPattern});return g?{text:T??"Edit this page",link:g}:null})},n=()=>{const a=ze(),c=Kt(),u=vt();return z(()=>{var v,y;return!(u.value.lastUpdated??a.value.lastUpdated??!0)||!((v=c.value.git)!=null&&v.updatedTime)?null:new Date((y=c.value.git)==null?void 0:y.updatedTime).toLocaleString()})},r=()=>{const a=ze(),c=Kt(),u=vt();return z(()=>{var h;return u.value.contributors??a.value.contributors??!0?((h=c.value.git)==null?void 0:h.contributors)??null:null})},o=ze(),s=t(),i=n(),l=r();return(a,c)=>{const u=bt("ClientOnly");return F(),Z("footer",cp,[X(s)?(F(),Z("div",up,[te(gt,{class:"meta-item-label",item:X(s)},null,8,["item"])])):Te("v-if",!0),X(i)?(F(),Z("div",fp,[fe("span",dp,Oe(X(o).lastUpdatedText)+": ",1),te(u,null,{default:Ne(()=>[fe("span",hp,Oe(X(i)),1)]),_:1})])):Te("v-if",!0),X(l)&&X(l).length?(F(),Z("div",pp,[fe("span",mp,Oe(X(o).contributorsText)+": ",1),fe("span",vp,[(F(!0),Z(Ee,null,It(X(l),(f,h)=>(F(),Z(Ee,{key:h},[fe("span",{class:"contributor",title:`email: ${f.email}`},Oe(f.name),9,gp),h!==X(l).length-1?(F(),Z(Ee,{key:0},[Mt(", ")],64)):Te("v-if",!0)],64))),128))])])):Te("v-if",!0)])}}}),bp=Le(_p,[["__file","PageMeta.vue"]]),yp={key:0,class:"page-nav"},Ep={class:"inner"},wp={key:0,class:"prev"},Cp={key:1,class:"next"},xp=he({__name:"PageNav",setup(e){const t=a=>a===!1?null:pe(a)?Bo(a):Oo(a)?a:!1,n=(a,c,u)=>{const f=a.findIndex(h=>h.link===c);if(f!==-1){const h=a[f+u];return h!=null&&h.link?h:null}for(const h of a)if(h.children){const v=n(h.children,c,u);if(v)return v}return null},r=vt(),o=zo(),s=Gt(),i=z(()=>{const a=t(r.value.prev);return a!==!1?a:n(o.value,s.path,-1)}),l=z(()=>{const a=t(r.value.next);return a!==!1?a:n(o.value,s.path,1)});return(a,c)=>i.value||l.value?(F(),Z("nav",yp,[fe("p",Ep,[i.value?(F(),Z("span",wp,[te(gt,{item:i.value},null,8,["item"])])):Te("v-if",!0),l.value?(F(),Z("span",Cp,[te(gt,{item:l.value},null,8,["item"])])):Te("v-if",!0)])])):Te("v-if",!0)}}),Lp=Le(xp,[["__file","PageNav.vue"]]),Tp={class:"page"},kp={class:"theme-default-content"},Sp=he({__name:"Page",setup(e){return(t,n)=>{const r=bt("Content");return F(),Z("main",Tp,[ye(t.$slots,"top"),fe("div",kp,[ye(t.$slots,"content-top"),te(r),ye(t.$slots,"content-bottom")]),te(bp),te(Lp),ye(t.$slots,"bottom")])}}}),Ap=Le(Sp,[["__file","Page.vue"]]),Pp=["onKeydown"],Rp={class:"sidebar-item-children"},Op=he({__name:"SidebarItem",props:{item:{type:Object,required:!0},depth:{type:Number,required:!1,default:0}},setup(e){const t=e,{item:n,depth:r}=Lo(t),o=Gt(),s=gn(),i=z(()=>Dl(n.value,o)),l=z(()=>({"sidebar-item":!0,"sidebar-heading":r.value===0,active:i.value,collapsible:n.value.collapsible})),a=z(()=>n.value.collapsible?i.value:!0),[c,u]=$d(a.value),f=v=>{n.value.collapsible&&(v.preventDefault(),u())},h=s.afterEach(v=>{xr(()=>{c.value=a.value})});return Sr(()=>{h()}),(v,y)=>{var T;const w=bt("SidebarItem",!0);return F(),Z("li",null,[X(n).link?(F(),Se(gt,{key:0,class:We(l.value),item:X(n)},null,8,["class","item"])):(F(),Z("p",{key:1,tabindex:"0",class:We(l.value),onClick:f,onKeydown:mu(f,["enter"])},[Mt(Oe(X(n).text)+" ",1),X(n).collapsible?(F(),Z("span",{key:0,class:We(["arrow",X(c)?"down":"right"])},null,2)):Te("v-if",!0)],42,Pp)),(T=X(n).children)!=null&&T.length?(F(),Se(Nl,{key:2},{default:Ne(()=>[fr(fe("ul",Rp,[(F(!0),Z(Ee,null,It(X(n).children,g=>(F(),Se(w,{key:`${X(r)}${g.text}${g.link}`,item:g,depth:X(r)+1},null,8,["item","depth"]))),128))],512),[[vr,X(c)]])]),_:1})):Te("v-if",!0)])}}}),Ip=Le(Op,[["__file","SidebarItem.vue"]]),$p={key:0,class:"sidebar-items"},Mp=he({__name:"SidebarItems",setup(e){const t=Gt(),n=zo();return Xe(()=>{st(()=>t.hash,r=>{const o=document.querySelector(".sidebar");if(!o)return;const s=document.querySelector(`.sidebar a.sidebar-item[href="${t.path}${r}"]`);if(!s)return;const{top:i,height:l}=o.getBoundingClientRect(),{top:a,height:c}=s.getBoundingClientRect();ai+l&&s.scrollIntoView(!1)})}),(r,o)=>X(n).length?(F(),Z("ul",$p,[(F(!0),Z(Ee,null,It(X(n),s=>(F(),Se(Ip,{key:`${s.text}${s.link}`,item:s},null,8,["item"]))),128))])):Te("v-if",!0)}}),Np=Le(Mp,[["__file","SidebarItems.vue"]]),Dp={class:"sidebar"},Hp=he({__name:"Sidebar",setup(e){return(t,n)=>(F(),Z("aside",Dp,[te(Fl),ye(t.$slots,"top"),te(Np),ye(t.$slots,"bottom")]))}}),Fp=Le(Hp,[["__file","Sidebar.vue"]]),jp=he({__name:"Layout",setup(e){const t=Kt(),n=vt(),r=ze(),o=z(()=>n.value.navbar!==!1&&r.value.navbar!==!1),s=zo(),i=Ce(!1),l=T=>{i.value=typeof T=="boolean"?T:!i.value},a={x:0,y:0},c=T=>{a.x=T.changedTouches[0].clientX,a.y=T.changedTouches[0].clientY},u=T=>{const g=T.changedTouches[0].clientX-a.x,b=T.changedTouches[0].clientY-a.y;Math.abs(g)>Math.abs(b)&&Math.abs(g)>40&&(g>0&&a.x<=80?l(!0):l(!1))},f=z(()=>[{"no-navbar":!o.value,"no-sidebar":!s.value.length,"sidebar-open":i.value},n.value.pageClass]);let h;Xe(()=>{h=gn().afterEach(()=>{l(!1)})}),Ar(()=>{h()});const v=Ol(),y=v.resolve,w=v.pending;return(T,g)=>(F(),Z("div",{class:We(["theme-container",f.value]),onTouchstart:c,onTouchend:u},[ye(T.$slots,"navbar",{},()=>[o.value?(F(),Se(ap,{key:0,onToggleSidebar:l},{before:Ne(()=>[ye(T.$slots,"navbar-before")]),after:Ne(()=>[ye(T.$slots,"navbar-after")]),_:3})):Te("v-if",!0)]),fe("div",{class:"sidebar-mask",onClick:g[0]||(g[0]=b=>l(!1))}),ye(T.$slots,"sidebar",{},()=>[te(Fp,null,{top:Ne(()=>[ye(T.$slots,"sidebar-top")]),bottom:Ne(()=>[ye(T.$slots,"sidebar-bottom")]),_:3})]),ye(T.$slots,"page",{},()=>[X(n).home?(F(),Se(Th,{key:0})):(F(),Se(Vn,{key:1,name:"fade-slide-y",mode:"out-in",onBeforeEnter:X(y),onBeforeLeave:X(w)},{default:Ne(()=>[(F(),Se(Ap,{key:X(t).path},{top:Ne(()=>[ye(T.$slots,"page-top")]),"content-top":Ne(()=>[ye(T.$slots,"page-content-top")]),"content-bottom":Ne(()=>[ye(T.$slots,"page-content-bottom")]),bottom:Ne(()=>[ye(T.$slots,"page-bottom")]),_:3}))]),_:3},8,["onBeforeEnter","onBeforeLeave"]))])],34))}}),Bp=Le(jp,[["__file","Layout.vue"]]),zp={class:"theme-container"},Vp={class:"page"},Up={class:"theme-default-content"},Wp=fe("h1",null,"404",-1),qp=he({__name:"NotFound",setup(e){const t=Or(),n=ze(),r=n.value.notFound??["Not Found"],o=()=>r[Math.floor(Math.random()*r.length)],s=n.value.home??t.value,i=n.value.backToHome??"Back to home";return(l,a)=>{const c=bt("RouterLink");return F(),Z("div",zp,[fe("main",Vp,[fe("div",Up,[Wp,fe("blockquote",null,Oe(o()),1),te(c,{to:X(s)},{default:Ne(()=>[Mt(Oe(X(i)),1)]),_:1},8,["to"])])])])}}}),Kp=Le(qp,[["__file","NotFound.vue"]]);const Yp=Jt({enhance({app:e,router:t}){e.component("Badge",yd),e.component("CodeGroup",Ed),e.component("CodeGroupItem",Ld),e.component("AutoLinkExternalIcon",()=>{const r=e.component("ExternalLinkIcon");return r?ge(r):null}),e.component("NavbarSearch",()=>{const r=e.component("Docsearch")||e.component("SearchBox");return r?ge(r):null});const n=t.options.scrollBehavior;t.options.scrollBehavior=async(...r)=>(await Ol().wait(),n(...r))},setup(){Kd(),Gd()},layouts:{Layout:Bp,NotFound:Kp}}),rr=[Uf,Kf,Qf,cd,hd,_d,Yp],Jp=[["v-8daa1a0e","/",{title:""},["/index.html","/README.md"]],["v-23cb31b3","/backend/go/",{title:""},["/backend/go/index.html","/backend/go/index.md"]],["v-0fceabfe","/general/network/",{title:"计算机网络"},["/general/network/index.html","/general/network/README.md"]],["v-5791b9ab","/backend/go/base/",{title:""},["/backend/go/base/index.html","/backend/go/base/index.md"]],["v-02dcbf8e","/backend/go/web/",{title:""},["/backend/go/web/index.html","/backend/go/web/index.md"]],["v-01b06704","/frontend/engineering/umi/",{title:""},["/frontend/engineering/umi/index.html","/frontend/engineering/umi/index.md"]],["v-60936db8","/frontend/framework/we-app/",{title:""},["/frontend/framework/we-app/index.html","/frontend/framework/we-app/index.md"]],["v-415e2210","/frontend/js/red-book/01.html",{title:"红宝书"},["/frontend/js/red-book/01","/frontend/js/red-book/01.md"]],["v-4312faaf","/frontend/js/red-book/02.html",{title:"红宝书"},["/frontend/js/red-book/02","/frontend/js/red-book/02.md"]],["v-44c7d34e","/frontend/js/red-book/03.html",{title:"红宝书"},["/frontend/js/red-book/03","/frontend/js/red-book/03.md"]],["v-467cabed","/frontend/js/red-book/04.html",{title:"红宝书"},["/frontend/js/red-book/04","/frontend/js/red-book/04.md"]],["v-4831848c","/frontend/js/red-book/05.html",{title:"红宝书"},["/frontend/js/red-book/05","/frontend/js/red-book/05.md"]],["v-49e65d2b","/frontend/js/red-book/06.html",{title:"红宝书"},["/frontend/js/red-book/06","/frontend/js/red-book/06.md"]],["v-4b9b35ca","/frontend/js/red-book/07.html",{title:"红宝书"},["/frontend/js/red-book/07","/frontend/js/red-book/07.md"]],["v-4d500e69","/frontend/js/red-book/08.html",{title:"红宝书"},["/frontend/js/red-book/08","/frontend/js/red-book/08.md"]],["v-4f04e708","/frontend/js/red-book/09.html",{title:"红宝书"},["/frontend/js/red-book/09","/frontend/js/red-book/09.md"]],["v-748f84b2","/frontend/js/red-book/10.html",{title:"红宝书"},["/frontend/js/red-book/10","/frontend/js/red-book/10.md"]],["v-a5e72730","/frontend/js/red-book/",{title:""},["/frontend/js/red-book/index.html","/frontend/js/red-book/index.md"]],["v-17a33721","/frontend/other/errors/",{title:""},["/frontend/other/errors/index.html","/frontend/other/errors/index.md"]],["v-4525d36b","/frontend/other/tools/ali-iconfont.html",{title:"小工具"},["/frontend/other/tools/ali-iconfont","/frontend/other/tools/ali-iconfont.md"]],["v-7e5c3b44","/frontend/other/tools/git.html",{title:"小工具"},["/frontend/other/tools/git","/frontend/other/tools/git.md"]],["v-11ee2517","/frontend/other/tools/",{title:""},["/frontend/other/tools/index.html","/frontend/other/tools/index.md"]],["v-c7ffc5b0","/frontend/other/tools/json-server.html",{title:"小工具"},["/frontend/other/tools/json-server","/frontend/other/tools/json-server.md"]],["v-7c9faf39","/frontend/framework/react/redux-mobx/",{title:""},["/frontend/framework/react/redux-mobx/index.html","/frontend/framework/react/redux-mobx/index.md"]],["v-b7245cd4","/frontend/framework/we-app/01/",{title:"小程序"},["/frontend/framework/we-app/01/index.html","/frontend/framework/we-app/01/index.md"]],["v-3706649a","/404.html",{title:""},["/404"]]];var ni=he({name:"Vuepress",setup(){const e=Ru();return()=>ge(e.value)}}),Gp=()=>Jp.reduce((e,[t,n,r,o])=>(e.push({name:t,path:n,component:ni,meta:r},...o.map(s=>({path:s,redirect:n}))),e),[{name:"404",path:"/:catchAll(.*)",component:ni}]),Qp=Xu,Zp=()=>{const e=Df({history:Qp(nl("/study-notes/")),routes:Gp(),scrollBehavior:(t,n,r)=>r||(t.hash?{el:t.hash}:{top:0})});return e.beforeResolve(async(t,n)=>{var r;(t.path!==n.path||n===pt)&&([St.value]=await Promise.all([ht.resolvePageData(t.name),(r=sl[t.name])==null?void 0:r.__asyncLoader()]))}),e},Xp=e=>{e.component("ClientOnly",Mo),e.component("Content",Mu)},em=(e,t,n)=>{const r=Ce(t.currentRoute.value.path);st(()=>t.currentRoute.value.path,h=>r.value=h);const o=z(()=>ht.resolveLayouts(n)),s=z(()=>ht.resolveRouteLocale(tn.value.locales,r.value)),i=z(()=>ht.resolveSiteLocaleData(tn.value,s.value)),l=z(()=>ht.resolvePageFrontmatter(St.value)),a=z(()=>ht.resolvePageHeadTitle(St.value,i.value)),c=z(()=>ht.resolvePageHead(a.value,l.value,i.value)),u=z(()=>ht.resolvePageLang(St.value)),f=z(()=>ht.resolvePageLayout(St.value,o.value));return e.provide(Tu,o),e.provide(ll,l),e.provide(Au,a),e.provide(al,c),e.provide(cl,u),e.provide(ul,f),e.provide(Io,s),e.provide(dl,i),Object.defineProperties(e.config.globalProperties,{$frontmatter:{get:()=>l.value},$head:{get:()=>c.value},$headTitle:{get:()=>a.value},$lang:{get:()=>u.value},$page:{get:()=>St.value},$routeLocale:{get:()=>s.value},$site:{get:()=>tn.value},$siteLocale:{get:()=>i.value},$withBase:{get:()=>No}}),{layouts:o,pageData:St,pageFrontmatter:l,pageHead:c,pageHeadTitle:a,pageLang:u,pageLayout:f,routeLocale:s,siteData:tn,siteLocaleData:i}},tm=()=>{const e=Su(),t=Pu(),n=Ce([]),r=()=>{e.value.forEach(s=>{const i=nm(s);i&&n.value.push(i)})},o=()=>{document.documentElement.lang=t.value,n.value.forEach(s=>{s.parentNode===document.head&&document.head.removeChild(s)}),n.value.splice(0,n.value.length),e.value.forEach(s=>{const i=rm(s);i!==null&&(document.head.appendChild(i),n.value.push(i))})};Wt(Ou,o),Xe(()=>{r(),o(),st(()=>e.value,()=>o())})},nm=([e,t,n=""])=>{const r=Object.entries(t).map(([l,a])=>pe(a)?`[${l}=${JSON.stringify(a)}]`:a===!0?`[${l}]`:"").join(""),o=`head > ${e}${r}`;return Array.from(document.querySelectorAll(o)).find(l=>l.innerText===n)||null},rm=([e,t,n])=>{if(!pe(e))return null;const r=document.createElement(e);return Oo(t)&&Object.entries(t).forEach(([o,s])=>{pe(s)?r.setAttribute(o,s):s===!0&&r.setAttribute(o,"")}),pe(n)&&r.appendChild(document.createTextNode(n)),r},om=_u,sm=async()=>{var n;const e=om({name:"VuepressApp",setup(){var r;tm();for(const o of rr)(r=o.setup)==null||r.call(o);return()=>[ge(xl),...rr.flatMap(({rootComponents:o=[]})=>o.map(s=>ge(s)))]}}),t=Zp();Xp(e),em(e,t,rr);for(const r of rr)await((n=r.enhance)==null?void 0:n.call(r,{app:e,router:t,siteData:tn}));return e.use(t),{app:e,router:t}};sm().then(({app:e,router:t})=>{t.isReady().then(()=>{e.mount("#app")})});export{Le as _,fe as a,te as b,Z as c,sm as createVueApp,Mt as d,Ic as e,F as o,bt as r,Oe as t,Ne as w}; diff --git a/assets/assembly-line-544d32be.png b/assets/assembly-line-544d32be.png new file mode 100644 index 0000000..6418252 Binary files /dev/null and b/assets/assembly-line-544d32be.png differ diff --git a/assets/back-to-top-8efcbe56.svg b/assets/back-to-top-8efcbe56.svg new file mode 100644 index 0000000..8323678 --- /dev/null +++ b/assets/back-to-top-8efcbe56.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/assets/default-serve-mux-01211d08.png b/assets/default-serve-mux-01211d08.png new file mode 100644 index 0000000..5a0ad20 Binary files /dev/null and b/assets/default-serve-mux-01211d08.png differ diff --git a/assets/file-server-demo-b798ce94.png b/assets/file-server-demo-b798ce94.png new file mode 100644 index 0000000..0653393 Binary files /dev/null and b/assets/file-server-demo-b798ce94.png differ diff --git a/assets/git.html-3b96bfc9.js b/assets/git.html-3b96bfc9.js new file mode 100644 index 0000000..9430b13 --- /dev/null +++ b/assets/git.html-3b96bfc9.js @@ -0,0 +1 @@ +const t=JSON.parse('{"key":"v-7e5c3b44","path":"/frontend/other/tools/git.html","title":"小工具","lang":"zh-CN","frontmatter":{"title":"小工具"},"headers":[{"level":2,"title":"git","slug":"git","link":"#git","children":[{"level":3,"title":"常用命令","slug":"常用命令","link":"#常用命令","children":[]},{"level":3,"title":"报错","slug":"报错","link":"#报错","children":[]}]}],"git":{"updatedTime":1693643793000},"filePathRelative":"frontend/other/tools/git.md"}');export{t as data}; diff --git a/assets/git.html-b7ea5d64.js b/assets/git.html-b7ea5d64.js new file mode 100644 index 0000000..822c6f1 --- /dev/null +++ b/assets/git.html-b7ea5d64.js @@ -0,0 +1,24 @@ +import{_ as e,r as t,o as i,c,a as n,d as s,b as l,e as o}from"./app-b4fb6edd.js";const d={},p=n("h2",{id:"git",tabindex:"-1"},[n("a",{class:"header-anchor",href:"#git","aria-hidden":"true"},"#"),s(" git")],-1),r={href:"https://backlog.com/git-tutorial/cn/intro/intro1_1.html",target:"_blank",rel:"noopener noreferrer"},u=o(`

常用命令

revert

# 回退一个commit
+git revert commit号
+
+# 回退多个连续的commit (后面的,前面的]
+git revert 后面的...前面的
+

添加远程仓库并取别名

git remote add upstream xxxxxx(上游仓库地址)
+

基于远程分支新建一个分支并切换过去

git checkout upstream/dev -b xxxx(分支名)
+

stash

# 暂存现在的内容
+git stash [save '描述']
+# 查看所有的暂存
+git stash list
+# 清空所有的暂存
+git stash clear
+# 删除某一个暂存,默认删除 stash@{0}
+git stash drop [stash@{某一个序号}]
+# 恢复某一个暂存并删掉它,默认恢复 stash@{0}
+git stash pop [stash@{某一个序号}]
+# 同上恢复,但是不删掉它
+git stash apply [stash@{某一个序号}]
+

clone

git clone 加 --single-branch 是下载单个分支, --depth=1 是下载单个 commit 这俩配置项可以提高拉取代码的速度

 git clone --depth=1 --single-branch git @github.com:ant-design/ant-design.git
+

设置命令别名

# git lg
+git config --global alias.lg "log --color --graph --pretty=format:'%Cred%h%Creset -%C(yellow)%d%Creset %s %Cgreen(%cr) %C(bold blue)<%an>%Creset' --abbrev-commit"
+

报错

LF will be replaced by CRLF the next time Git touches it

git config --global core.autocrlf false
+
`,17);function m(v,g){const a=t("ExternalLinkIcon");return i(),c("div",null,[p,n("p",null,[s("好使的教程 --> "),n("a",r,[s("猴子都能懂的 git 入门"),l(a)])]),u])}const h=e(d,[["render",m],["__file","git.html.vue"]]);export{h as default}; diff --git a/assets/handler-dbd08f93.png b/assets/handler-dbd08f93.png new file mode 100644 index 0000000..c2d5ba6 Binary files /dev/null and b/assets/handler-dbd08f93.png differ diff --git a/assets/index.html-0a28c9d4.js b/assets/index.html-0a28c9d4.js new file mode 100644 index 0000000..b0b815e --- /dev/null +++ b/assets/index.html-0a28c9d4.js @@ -0,0 +1 @@ +const e=JSON.parse('{"key":"v-11ee2517","path":"/frontend/other/tools/","title":"","lang":"zh-CN","frontmatter":{"sidebar":false},"headers":[{"level":2,"title":"小工具","slug":"小工具","link":"#小工具","children":[]}],"git":{"updatedTime":1694268022000},"filePathRelative":"frontend/other/tools/index.md"}');export{e as data}; diff --git a/assets/index.html-0cc578c7.js b/assets/index.html-0cc578c7.js new file mode 100644 index 0000000..3565104 --- /dev/null +++ b/assets/index.html-0cc578c7.js @@ -0,0 +1 @@ +const e=JSON.parse('{"key":"v-23cb31b3","path":"/backend/go/","title":"","lang":"zh-CN","frontmatter":{},"headers":[{"level":2,"title":"Go","slug":"go","link":"#go","children":[]}],"git":{"updatedTime":1694564237000},"filePathRelative":"backend/go/index.md"}');export{e as data}; diff --git a/assets/index.html-3757d620.js b/assets/index.html-3757d620.js new file mode 100644 index 0000000..f1c428b --- /dev/null +++ b/assets/index.html-3757d620.js @@ -0,0 +1 @@ +const e=JSON.parse('{"key":"v-17a33721","path":"/frontend/other/errors/","title":"","lang":"zh-CN","frontmatter":{},"headers":[{"level":3,"title":"终端错误","slug":"终端错误","link":"#终端错误","children":[{"level":4,"title":"1. BUILD_ENV=XXX 命令不支持","slug":"_1-build-env-xxx-命令不支持","link":"#_1-build-env-xxx-命令不支持","children":[]}]},{"level":3,"title":"npm","slug":"npm","link":"#npm","children":[{"level":4,"title":"1. Unable to authenticate, need: Basic realm=\\"aliyun\\"","slug":"_1-unable-to-authenticate-need-basic-realm-aliyun","link":"#_1-unable-to-authenticate-need-basic-realm-aliyun","children":[]},{"level":4,"title":"2. cb() never called!","slug":"_2-cb-never-called","link":"#_2-cb-never-called","children":[]}]},{"level":3,"title":"编译","slug":"编译","link":"#编译","children":[{"level":4,"title":"1. “xxxx”不能用作 JSX 组件","slug":"_1-xxxx-不能用作-jsx-组件","link":"#_1-xxxx-不能用作-jsx-组件","children":[]}]}],"git":{"updatedTime":1694229902000},"filePathRelative":"frontend/other/errors/index.md"}');export{e as data}; diff --git a/assets/index.html-380c9dcf.js b/assets/index.html-380c9dcf.js new file mode 100644 index 0000000..e2da7cd --- /dev/null +++ b/assets/index.html-380c9dcf.js @@ -0,0 +1 @@ +const e=JSON.parse('{"key":"v-7c9faf39","path":"/frontend/framework/react/redux-mobx/","title":"","lang":"zh-CN","frontmatter":{},"headers":[{"level":2,"title":"了解","slug":"了解","link":"#了解","children":[{"level":3,"title":"状态","slug":"状态","link":"#状态","children":[]},{"level":3,"title":"软件究竟在做什么","slug":"软件究竟在做什么","link":"#软件究竟在做什么","children":[]},{"level":3,"title":"性能优化的两种方式","slug":"性能优化的两种方式","link":"#性能优化的两种方式","children":[]}]},{"level":2,"title":"状态管理","slug":"状态管理","link":"#状态管理","children":[{"level":3,"title":"状态机应该具备的能力","slug":"状态机应该具备的能力","link":"#状态机应该具备的能力","children":[]},{"level":3,"title":"状态管理简易实现","slug":"状态管理简易实现","link":"#状态管理简易实现","children":[]}]}],"git":{"updatedTime":1698712189000},"filePathRelative":"frontend/framework/react/redux-mobx/index.md"}');export{e as data}; diff --git a/assets/index.html-394fa650.js b/assets/index.html-394fa650.js new file mode 100644 index 0000000..63ca232 --- /dev/null +++ b/assets/index.html-394fa650.js @@ -0,0 +1 @@ +const e=JSON.parse('{"key":"v-01b06704","path":"/frontend/engineering/umi/","title":"","lang":"zh-CN","frontmatter":{},"headers":[{"level":2,"title":"使用 umi 搭建一体机项目","slug":"使用-umi-搭建一体机项目","link":"#使用-umi-搭建一体机项目","children":[{"level":3,"title":"UI 组件库","slug":"ui-组件库","link":"#ui-组件库","children":[]},{"level":3,"title":"目录结构","slug":"目录结构","link":"#目录结构","children":[]},{"level":3,"title":"sass-resources-loader","slug":"sass-resources-loader","link":"#sass-resources-loader","children":[]},{"level":3,"title":"尺寸适配","slug":"尺寸适配","link":"#尺寸适配","children":[]},{"level":3,"title":"静态资源","slug":"静态资源","link":"#静态资源","children":[]},{"level":3,"title":"手写的工具","slug":"手写的工具","link":"#手写的工具","children":[{"level":4,"title":"页面跳转","slug":"页面跳转","link":"#页面跳转","children":[]},{"level":4,"title":"无用数据过滤","slug":"无用数据过滤","link":"#无用数据过滤","children":[]}]}]}],"git":{"updatedTime":1697030106000},"filePathRelative":"frontend/engineering/umi/index.md"}');export{e as data}; diff --git a/assets/index.html-39df15d0.js b/assets/index.html-39df15d0.js new file mode 100644 index 0000000..fe74f1d --- /dev/null +++ b/assets/index.html-39df15d0.js @@ -0,0 +1 @@ +import{_ as o,r as a,o as l,c as i,a as e,d as n,b as t}from"./app-b4fb6edd.js";const s={},c=e("h2",{id:"微信小程序",tabindex:"-1"},[e("a",{class:"header-anchor",href:"#微信小程序","aria-hidden":"true"},"#"),n(" 微信小程序")],-1),d=e("h3",{id:"开发准备",tabindex:"-1"},[e("a",{class:"header-anchor",href:"#开发准备","aria-hidden":"true"},"#"),n(" 开发准备")],-1),_={href:"https://developers.weixin.qq.com/miniprogram/dev/framework/quickstart/getstart.html#%E7%94%B3%E8%AF%B7%E8%B4%A6%E5%8F%B7",target:"_blank",rel:"noopener noreferrer"},h=e("ul",null,[e("li",null,"登录小程序后台[https://mp.weixin.qq.com/wxamp/basicprofile/index?token=741939644&lang=zh_CN] ,在菜单 “开发”-“开发设置” 可看到小程序的 AppID 和 AppSecret")],-1),p={href:"https://developers.weixin.qq.com/miniprogram/dev/devtools/download.html",target:"_blank",rel:"noopener noreferrer"};function m(f,x){const r=a("ExternalLinkIcon");return l(),i("div",null,[c,d,e("ol",null,[e("li",null,[n("申请一个 APPID "),e("a",_,[n("申请账号"),t(r)]),h]),e("li",null,[n("下载并安装"),e("a",p,[n("小程序开发工具"),t(r)])])])])}const k=o(s,[["render",m],["__file","index.html.vue"]]);export{k as default}; diff --git a/assets/index.html-3a7a3c33.js b/assets/index.html-3a7a3c33.js new file mode 100644 index 0000000..7402cbc --- /dev/null +++ b/assets/index.html-3a7a3c33.js @@ -0,0 +1 @@ +const e=JSON.parse('{"key":"v-b7245cd4","path":"/frontend/framework/we-app/01/","title":"小程序","lang":"zh-CN","frontmatter":{"title":"小程序","prev":{"text":"目录","link":"./README.md"},"next":{"text":"开发准备","link":"./01.md"}},"headers":[{"level":2,"title":"微信小程序","slug":"微信小程序","link":"#微信小程序","children":[{"level":3,"title":"开发准备","slug":"开发准备","link":"#开发准备","children":[]}]}],"git":{"updatedTime":1694528949000},"filePathRelative":"frontend/framework/we-app/01/index.md"}');export{e as data}; diff --git a/assets/index.html-71b8f972.js b/assets/index.html-71b8f972.js new file mode 100644 index 0000000..2154340 --- /dev/null +++ b/assets/index.html-71b8f972.js @@ -0,0 +1 @@ +const e=JSON.parse('{"key":"v-02dcbf8e","path":"/backend/go/web/","title":"","lang":"zh-CN","frontmatter":{},"headers":[{"level":2,"title":"使用 Go 创建 Web 应用","slug":"使用-go-创建-web-应用","link":"#使用-go-创建-web-应用","children":[{"level":3,"title":"处理(Handle)请求","slug":"处理-handle-请求","link":"#处理-handle-请求","children":[{"level":4,"title":"创建 Web Server","slug":"创建-web-server","link":"#创建-web-server","children":[]},{"level":4,"title":"Handler","slug":"handler","link":"#handler","children":[]},{"level":4,"title":"DefaultServeMux","slug":"defaultservemux","link":"#defaultservemux","children":[]},{"level":4,"title":"多个 Handler - http.Handle","slug":"多个-handler-http-handle","link":"#多个-handler-http-handle","children":[]},{"level":4,"title":"Handler 函数 - http.HandleFunc","slug":"handler-函数-http-handlefunc","link":"#handler-函数-http-handlefunc","children":[]},{"level":4,"title":"内置的 handlers","slug":"内置的-handlers","link":"#内置的-handlers","children":[]}]},{"level":3,"title":"请求","slug":"请求","link":"#请求","children":[{"level":4,"title":"HTTP 消息","slug":"http-消息","link":"#http-消息","children":[]},{"level":4,"title":"请求 Request","slug":"请求-request","link":"#请求-request","children":[]},{"level":4,"title":"查询参数(Query Parameters)","slug":"查询参数-query-parameters","link":"#查询参数-query-parameters","children":[]},{"level":4,"title":"Form","slug":"form","link":"#form","children":[]},{"level":4,"title":"POST 请求 - JSON Body","slug":"post-请求-json-body","link":"#post-请求-json-body","children":[]}]},{"level":3,"title":"响应","slug":"响应","link":"#响应","children":[{"level":4,"title":"ResponseWriter","slug":"responsewriter","link":"#responsewriter","children":[]},{"level":4,"title":"WriteHeader 方法","slug":"writeheader-方法","link":"#writeheader-方法","children":[]},{"level":4,"title":"Header 方法","slug":"header-方法","link":"#header-方法","children":[]},{"level":4,"title":"内置的 Response","slug":"内置的-response","link":"#内置的-response","children":[]}]},{"level":3,"title":"模板","slug":"模板","link":"#模板","children":[{"level":4,"title":"解析模板","slug":"解析模板","link":"#解析模板","children":[]},{"level":4,"title":"执行模板","slug":"执行模板","link":"#执行模板","children":[]},{"level":4,"title":"Action","slug":"action","link":"#action","children":[]},{"level":4,"title":"函数和管道","slug":"函数和管道","link":"#函数和管道","children":[]}]}]}],"git":{"updatedTime":1698149997000},"filePathRelative":"backend/go/web/index.md"}');export{e as data}; diff --git a/assets/index.html-78eca336.js b/assets/index.html-78eca336.js new file mode 100644 index 0000000..f33b7cb --- /dev/null +++ b/assets/index.html-78eca336.js @@ -0,0 +1,105 @@ +import{_ as e,r as p,o,c,a as n,d as s,b as i,e as a}from"./app-b4fb6edd.js";const l={},u=a(`

使用 umi 搭建一体机项目

设计稿 1080 x 1920,不确定实际屏幕的宽高比

UI 组件库

考虑到一体机触摸体验,选用 Ant Design Mobile 高清适配,项目从 antd-mobile/2x 导入组件

  1. 为了导入方便,做以下配置:
// config.ts 中,配置一个从 antd-mobile 到 antd-mobile/2x 的别名
+alias: {
+  "antd-mobile": require.resolve("antd-mobile/2x")
+}
+
  1. 设置主题色
// 正好 umi 中 a 标签的 color: var(--adm-color-primary)
+:root:root {
+  --adm-color-primary: #{$primary-color};
+}
+
export const primaryColor = "#44bb55";
+

目录结构

📁owners-meeting
+├─ 📁config
+│  ├─ 📄config.ts # umi 配置文件
+│  └─ 📄routes.ts # 从配置文件中提取出来的路由配置
+├─ 📁public # 可能扔进服务器的静态资源
+└─ 📁src
+   ├─ 📁constants
+   │  └─ 📄color.ts # 如主题色等常量
+   ├─ 📁pages # 如果是一个单独的页面组件则采用大驼峰命名文件夹,否则采用小驼峰命名文件夹
+   ├─ 📁styles # 里面所有的mixin、变量、函数都是全局的(index.scss 是被 sass-resources-loader 处理过的)
+   ├─ 📁utils
+   │  ├─ 📄format.ts # 用于格式化的一些函数
+   │  └─ 📄px2.ts # 用于将 px 转换为 vw/vh 的函数
+   ├─ 📁layouts
+   └─ 📄global.scss # 全局样式文件
+

sass-resources-loader

该 loader 处理过的 mixin、变量、函数等可以在任意 .scss 文件中不经导入使用

config.ts 中配置:

chainWebpack(config) {
+  config.module
+    .rule("scss")
+    .test(/\\.scss$/)
+    .use("sass-loader")
+    .loader("sass-loader")
+    .end()
+    .use("sass-resources-loader")
+    .loader("sass-resources-loader")
+    .options({
+      resources: [path.resolve(__dirname, "../src/styles/index.scss")]
+    });
+}
+

注意:install sass-loader & sass-resources-loader

尺寸适配

基于 1080 x 1920 的设计稿,使用 vwvh 单位进行适配。

@use "sass:math";
+
+// 默认设计稿的宽度
+$designWidth: 1080;
+// 默认设计稿的高度
+$designHeight: 1920;
+
+// px 转为 vw 的函数
+@function vw($px) {
+  @return math.div($px, $designWidth) * 100vw;
+}
+
+// px 转为 vh 的函数
+@function vh($px) {
+  @return math.div($px, $designHeight) * 100vh;
+}
+
.a {
+  width: vw(100);
+  height: vh(100);
+}
+
// 定义设计稿的宽高
+const designWidth = 1080;
+const designHeight = 1920;
+
+export const px2vw = (_px: number) => {
+  return (_px * 100.0) / designWidth + "vw";
+};
+
+export const px2vh = (_px: number) => {
+  return (_px * 100.0) / designHeight + "vh";
+};
+
import { px2vw, px2vh } from "@/utils/px2.ts";
+
/**
+* 设置字体在特定屏幕中整体缩放
+*/
+html {
+  font-size: 100px !important;
+}
+
+@media (max-width: 800px) {
+  html {
+    font-size: 80px !important;
+  }
+}
+
  • 字体使用 rem 单位,方便整体缩放或者根据屏幕大小进行缩放
    • 1rem = 100px,即 20px 字体大小为 0.2rem

静态资源

  • 图片:/public/imgs/
    • scss 文件中 url(/imgs/xxx.png),ts 文件中部分使用 import xxx from "/public/imgs/xxx.png"(否则就尝试删掉 /public)

手写的工具

页面跳转

`,28),r=n("li",null,[s("项目中有一个可以获取路径的函数,getPath "),n("ul",null,[n("li",null,[s("页面路径跟组件名称有对应关系,思路是 "),n("code",null,"getPath(所有页面组件 => 所有页面组件.跳转的页面组件名称, {参数1, 参数2, ...})"),s(" 即可拿到要跳转的路径")])])],-1),d={href:"https://umijs.org/docs/api/api#usenavigate",target:"_blank",rel:"noopener noreferrer"},k=a(`
import { getPath } from "@/utils/get";
+import { useNavigate } from "umi";
+
+const navigate = useNavigate();
+const handleClick = () => {
+  navigate(
+    getPath((component) => component.Step1_2),
+    { param: "a", query1: "b", query2: "c" }
+  ); // /xxx/step1-2/a?query1=b&query2=c
+};
+

无用数据过滤

export const formatData = (
+  data: any,
+  format?: (v: any) => any,
+  options?: { sign?: number | string }
+) => {
+  const { sign } = options || {};
+  const uselessData = [undefined, null, ""];
+  const isEmptyArr = Array.isArray(data) && data.length === 0;
+  const isEmptyObj = typeof data === "object" && Object.keys(data).length === 0;
+  if (uselessData.includes(data) || isEmptyArr || isEmptyObj)
+    return sign || "-";
+  return typeof format === "function" ? format(data) : data;
+};
+
`,3);function v(m,b){const t=p("ExternalLinkIcon");return o(),c("div",null,[u,n("ul",null,[r,n("li",null,[s("umi 提供的跳转的 hook,"),n("a",d,[s("useNavigate"),i(t)])])]),k])}const h=e(l,[["render",v],["__file","index.html.vue"]]);export{h as default}; diff --git a/assets/index.html-80dba622.js b/assets/index.html-80dba622.js new file mode 100644 index 0000000..1603026 --- /dev/null +++ b/assets/index.html-80dba622.js @@ -0,0 +1 @@ +const e=JSON.parse('{"key":"v-a5e72730","path":"/frontend/js/red-book/","title":"","lang":"zh-CN","frontmatter":{"sidebar":false},"headers":[{"level":2,"title":"红宝书","slug":"红宝书","link":"#红宝书","children":[]}],"git":{"updatedTime":1694229902000},"filePathRelative":"frontend/js/red-book/index.md"}');export{e as data}; diff --git a/assets/index.html-864b3258.js b/assets/index.html-864b3258.js new file mode 100644 index 0000000..b008dc1 --- /dev/null +++ b/assets/index.html-864b3258.js @@ -0,0 +1,526 @@ +import{_ as t,o as p,c as e,a as n,t as o,e as s,d as l}from"./app-b4fb6edd.js";const c="/study-notes/assets/handler-dbd08f93.png",i="/study-notes/assets/more-handler-e3085276.png",u="/study-notes/assets/default-serve-mux-01211d08.png",r="/study-notes/assets/file-server-demo-b798ce94.png",d={},k=s(`

使用 Go 创建 Web 应用

  • 处理请求
  • 模板
  • 中间件
  • 存储数据
  • HTTPS,HTTP2
  • 测试
  • 部署
package main
+
+import "net/http"
+
+func main() {
+	http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
+		w.Write([]byte("Hell world"))
+	})
+
+	http.ListenAndServe("localhost:8080", nil) // 传入 nil,即 DefaultServeMux
+}
+

处理(Handle)请求

  • 如何处理(Handle)Web 请求
    • http.Handle 函数
    • http.HandleFunc 函数

handler

创建 Web Server

http.ListenAndServe(addr string, handler Handler) error

  • addr:监听的地址,如果为空字符串,则使用 ":http",即监听 80 端口
  • handler:处理请求的 Handler,如果为空,则使用 DefaultServeMux

DefaultServeMux 是一个 multiplexer,即多路复用器,用于将请求分发到不同的处理器(可以看作是路由器)

http.ListenAndServe("localhost:8080", nil)
+

http.Server 是一个 struct

  • Addr 字段表示网络地址
    • 如果为 "",则使用 ":http",即监听所有网络接口的 80 端口
  • Handler 字段
    • 如果为 nil,则使用 DefaultServeMux
  • ListenAndServe 方法
// serve := &http.Server{
+serve := http.Server{
+	Addr:    "localhost:8080",
+	Handler: nil,
+}
+
+serve.ListenAndServe()
+

上面两种创建 Web Server 的方式,都只能使用 http。如果要用 https,则需要使用同理的 http.ListenAndServeTLS() 和 server.ListenAndServeTLS() 方法

Handler

Handler 是一个接口

type Handler interface {
+	ServeHTTP(ResponseWriter, *Request)
+}
+

自己实现 Handler 接口

type myHandler struct{}
+
+func (m *myHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
+	w.Write([]byte("Hello world"))
+}
+
+func main() {
+	mh := myHandler{}
+	server := http.Server{
+		Addr:    "localhost:8080",
+		Handler: &mh,
+	}
+	server.ListenAndServe()
+}
+

handler

DefaultServeMux

DefaultServeMux 是一个 multiplexer,即多路复用器,用于将请求分发到不同的处理器(可以看作是路由器)

DefaultServeMux

多个 Handler - http.Handle

func Handle(pattern string, handler Handler)
+

不指定 Server struct 里面的 Handler 字段值(指定为 nil)

可以使用 http.Handle 将某个 Handler 附加到 DefaultServeMux 上

  • http 包有一个 Handle 函数
  • ServerMux struct 也有一个 Handle 方法

如果调用 http.Handle,实际上调用的是 DefaultServeMux 的 Handle 方法

  • DefaultServeMux 就是 ServerMux 的指针变量
type helloHandler struct{}
+
+func (h *helloHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
+	w.Write([]byte("Hello world"))
+}
+
+type aboutHandler struct{}
+
+func (a *aboutHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
+	w.Write([]byte("About!"))
+}
+
+func main() {
+	hello := helloHandler{}
+	about := aboutHandler{}
+	server := http.Server{
+		Addr:    "localhost:8080",
+		Handler: nil, // DefaultServeMux
+	}
+	http.Handle("/hello", &hello)
+	http.Handle("/about", &about)
+	server.ListenAndServe()
+}
+

Handler 函数 - http.HandleFunc

Handler 函数就是那些行为与 handler 类似的函数:

  • Handler 函数的签名与 ServeHTTP 方法的签名一样,接收
    • http.ResponseWriter
    • 指向 http.Request 的指针

http.HandleFunc 原理

  • Go 有一个函数类型 HandlerFunc。可以将某个具有适当签名的函数 f,适配成为一个 Handler,而这个 Handler 就是调用 f 本身
type helloHandler struct{}
+
+func (h *helloHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
+	w.Write([]byte("Hello world"))
+}
+
+type aboutHandler struct{}
+
+func (a *aboutHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
+	w.Write([]byte("About!"))
+}
+
+func welcome(w http.ResponseWriter, r *http.Request) {
+	w.Write([]byte("Welcome!"))
+}
+
+func main() {
+	hello := helloHandler{}
+	about := aboutHandler{}
+	server := http.Server{
+		Addr:    "localhost:8081",
+		Handler: nil, // DefaultServeMux
+	}
+
+	http.Handle("/hello", &hello)
+	http.Handle("/about", &about)
+
+	http.HandleFunc("/home", func(w http.ResponseWriter, r *http.Request) {
+		w.Write([]byte("Home!"))
+	})
+	// http.HandleFunc("/welcome", welcome)
+
+	http.Handle("/welcome", http.HandlerFunc(welcome))
+
+	server.ListenAndServe()
+}
+

http.HandleFunc

  • http.HandleFunc(pattern string, handler func(ResponseWriter, *Request))
  • type HandlerFunc func(ResponseWriter, *Request)

内置的 handlers

  • NotFoundHandler
  • RedirectHandler
  • StripPrefix
  • TimeoutHandler
  • FileServer

http.NotFoundHandler

  • func NotFoundHandler() Handler
  • 返回一个 handler,它给每个请求的响应都是 “404 page not found”

http.RedirectHandler

  • func RedirectHandler(url string, code int) Handler
  • 返回一个 handler,它把每个请求使用给定的状态码跳转到指定的 URL
    • url,要跳转到的 URL
    • code,跳转的状态码(3xx),常见的:StatusMovedPermanently,StatusFound,StatusSeeOther,StatusTemporaryRedirect,StatusPermanentRedirect

http.StripPrefix

  • func StripPrefix(prefix string, h Handler) Handler
  • 返回一个 handler,它从请求的 URL 中去掉指定的前缀,然后再调用另一个 handler
    • 如果请求的 URL 与提供的前缀不符,那么 404
  • 略像中间件
    • prefix,URL 将要被移除的字符串前缀
    • h,是一个 handler,在移除字符串前缀之后,这个 handler 将会收到请求
  • 修饰了另一个 handler

http.TimeoutHandler

  • func TimeoutHandler(h Handler, dt time.Duration, msg string) Handler
  • 返回一个 handler,它在指定的时间内处理请求,如果超时,就返回一个错误信息
  • 也相当于是一个修饰器
    • h,将要被修饰的 handler
    • dt,第一个 handler 允许的处理时间
    • msg,如果超时,那么就把 msg 返回给请求,表示响应时间过长

http.FileServer

  • func FileServer(root FileSystem) Handler
  • 返回一个 handler,它会在 root 中寻找文件,并将其提供给请求
type FileSystem interface {
+	Open(name string) (File, error)
+}
+
  • 使用时需要用到操作系统的文件系统,所以还需要委托给
    • type Dir string
    • func (d Dir) Open(name string) (File, error)

例子

FileServer

通过 localhost:8081/ 访问 index.html

/* 方法1 */
+// http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
+// 	http.ServeFile(w, r, "wwwroot" + r.URL.Path)
+// })
+// http.ListenAndServe(":8081", nil)
+
+/* 方法2 */
+http.ListenAndServe(":8081", http.FileServer(http.Dir("wwwroot")))
+

请求

  • HTTP 请求
  • Request
  • URL
  • Header
  • Body

HTTP 消息

  • HTTP Request 和 HTTP Response(请求和响应)
  • 它们具有相同的结构
    • 请求(响应)行
    • 0 个或多个 Header
    • 空行
    • 可选的消息体(Body)
  • net/http 包提供了用于表示 HTTP 消息的结构

请求 Request

  • Request 是个 struct,代表了客户端发送的 HTTP 请求消息
  • 重要的字段
    • URL
    • Header
    • Body
    • Form、PostForm、MultipartForm
  • 也可以通过 Request 的方法访问请求中的 Cookie、URL、User Agent 等信息
  • Request 既可以代表发送到服务器的请求,又可代表客户端发出的请求

请求的 URL

  • Request 的 URL 字段就代表了请求行(请求信息第一行)里面的部分内容
  • URL 字段是指向 url.URL 类型的一个指针,url.URL 是一个 struct:

URL 的通用格式:scheme://[userinfo@]host/path[?query][#fragment] 不以斜杠开头的 URL 被解释为:scheme:opaque[?query][#fragment]

type URL struct {
+	Scheme		string
+	Opaque		string		// 编码后的不透明数据
+	User		*Userinfo	// 用户名和密码信息
+	Host		string		// host 或 host:port
+	Path		string
+	RawPath		string		// 编码后的 path,保留了转义符
+	ForceQuery	bool		// 是否在 URL 中添加 ? 强制添加查询参数
+	RawQuery	string		// 编码后的查询字符串,没有 '?'
+	Fragment	string		// 引用的片段(文档位置),没有 '#'
+}
+

URL Query

  • RawQuery 会提供实际查询的字符串
  • 例如:http://localhost:8080/?name=abc&age=18
    • RawQuery 为:name=abc&age=18

URL Fragment

  • 如果从浏览器发出的请求,就无法提取出 Fragment 字段的值
    • 浏览器在发送请求时会把 fragment 部分去掉
  • 但不是所有的请求都是从浏览器发出的(例如从 http 客户端包)

Request Header

  • 请求和响应(Request、Response)的 headers 是通过 Header 类型来描述的,它是一个 map,用来表述 HTTP Header 里的 Key-Value 对
  • Header map 的 key 是 string 类型,value 是一个字符串切片 []string
  • 设置 key 的时候会创建一个空的 []string 作为 value,value 里面第一个元素就是新 header 的值
  • 为指定的 key 添加一个新的 header 值,执行 append 操作即可
  • r.Header,返回 map
  • r.Header["Accept-Encoding"],返回 [gzip, deflate],[]string 类型
  • r.Header.Get("Accept-Encoding"),返回 gzip, deflate,string 类型
server := http.Server{
+	Addr: "localhost:8081",
+}
+
+http.HandleFunc("/header", func(w http.ResponseWriter, r *http.Request) {
+	fmt.Fprintln(w, r.Header)
+	fmt.Fprintln(w, r.Header["Accept-Encoding"])
+	fmt.Fprintln(w, r.Header.Get("Accept-Encoding"))
+})
+
+server.ListenAndServe()
+

Request Body

  • 请求和响应的 bodies 都是使用 Body 字段来表示的
  • Body 是一个 io.ReadCloser 接口
type ReadCloser interface {
+	Reader
+	Closer
+}
+
  • Reader 接口定义了一个 Open() 方法
    • 参数:[]byte
    • 返回:byte 的数量、可选的错误
  • Closer 接口定义了一个 Close() 方法
    • 返回:可选的错误
  • 想要读取请求 Body 的内容,可以调用 Body 的 Read 方法
server := http.Server{
+	Addr: "localhost:8081",
+}
+
+http.HandleFunc("/post", func(w http.ResponseWriter, r *http.Request) {
+	length := r.ContentLength
+	body := make([]byte, length)
+	r.Body.Read(body)
+	fmt.Fprintln(w, body)
+	fmt.Fprintln(w, string(body))
+})
+
+server.ListenAndServe()
+

查询参数(Query Parameters)

URL Query

  • http://localhost:8080/?name=abc&age=18
    • r.URL.RawQuery 为:name=abc&age=18(实际查询的原始字符串)
    • r.URL.Query() 方法返回 map[string][]string
      • map 的 key 是 string 类型
      • map 的 value 是 []string 类型
// http://localhost:8081/query?id=123&name=张三&id=466&name=李四
+	server := http.Server{
+		Addr: "localhost:8081",
+	}
+
+	http.HandleFunc("/query", func(w http.ResponseWriter, r *http.Request) {
+		url := r.URL
+		query := url.Query()
+
+		id := query["id"]
+		log.Println(id)
+
+		name := query.Get("name")
+		log.Println(name)
+	})
+
+	server.ListenAndServe()
+

Form

Request 上的函数允许从 URL 或 / 和 Body 中提取数据,通过如下字段

  • Form
  • PostForm
  • MultipartForm
  • FormValue
  • PostFormValue
  • FormFile
  • MultiPartReader

Form 里面的数据是 key-value 对

通常的做法是:

  • 先调用 ParseForm 或 ParseMultipartForm 来解析 Request
  • 然后相应地访问 Form、PostForm、MultipartForm 字段
<form
+  action="http://localhost:8080/process"
+  method="post"
+  enctype="application/x-www-form-urlencoded"
+>
+  <input type="text" name="name" placeholder="Name" />
+  <input type="text" name="email" placeholder="Email" />
+  <button type="submit">Submit</button>
+</form>
+
server := http.Server{
+	Addr: "localhost:8080",
+}
+http.HandleFunc("/process", func(w http.ResponseWriter, r *http.Request) {
+	r.ParseForm()
+	fmt.Fprintln(w, r.Form) // map[email:[2439639832@qq.com] name:[客户1号]]
+})
+server.ListenAndServe()
+

PostForm 字段

  • 上例中,如果只想得到 name 这个 Key 的 Value,可以使用 r.Form["name"],它返回含有一个元素的 slice:["客户 1 号"]
  • 如果表单和 URL 里有同样的 Key,那么它们都会放在一个 slice 里:表单里的值靠前,URL 的值靠后
  • 如果只想要表单的 key-value 对,不要 URL 的,可以使用 PostForm 字段
<form
+  action="http://localhost:8080/process?name=客户2号"
+  method="post"
+  enctype="application/x-www-form-urlencoded"
+>
+  <input type="text" name="name" placeholder="Name" />
+  <input type="text" name="email" placeholder="Email" />
+  <button type="submit">Submit</button>
+</form>
+

 







server := http.Server{
+	Addr: "localhost:8080",
+}
+http.HandleFunc("/process", func(w http.ResponseWriter, r *http.Request) {
+	r.ParseForm()
+	fmt.Fprintln(w, r.Form) // map[email:[2439639832@qq.com] name:[客户1号 客户2号]]
+	fmt.Fprintln(w, r.PostForm) // map[email:[2439639832@qq.com] name:[客户1号]]
+})
+server.ListenAndServe()
+





 
 


MultipartForm 字段

  • PostForm 只支持 application/x-www-form-urlencoded 编码
  • 要想得到 multipart/form-data 对,必须使用 MultipartForm 字段
  • 要想使用 MultiPartForm 字段,必须先调用 ParseMultipartForm 方法
    • 该方法会在必要时调用 ParseForm 方法
    • 参数是需要读取数据的长度
  • MultipartForm 只包含表单的 key-value 对
  • 返回类型是一个 struct,这个 struct 里面有两个 map:
    • key 是 string,value 是 []string
    • key 是 string,value 是 文件
<form
+  action="http://localhost:8080/process?name=客户2号"
+  method="post"
+  enctype="multipart/form-data"
+>
+  <input type="text" name="name" placeholder="Name" />
+  <input type="text" name="email" placeholder="Email" />
+  <button type="submit">Submit</button>
+</form>
+



 





server := http.Server{
+	Addr: "localhost:8080",
+}
+http.HandleFunc("/process", func(w http.ResponseWriter, r *http.Request) {
+	r.ParseMultipartForm(1024)
+	fmt.Fprintln(w, r.MultipartForm) // &{map[email:[2439639832@qq.com] name:[客户1号]] map[]}
+})
+server.ListenAndServe()
+





 


FormValue 和 PostFormValue 方法

  • FormValue 会返回 Form 字段中指定 key 对应的第一个 value
    • 无需调用 ParseForm 或 ParseMultipartForm
  • PostFormValue 也一样,但只能读取 PostForm 字段
  • FormValue 和 PostFormValue 都会调用 ParseMultipartForm 方法

上传文件

multipart/form-data 最常见的应用场景就是上传文件

  • 首先调用 ParseMultiPartForm 方法
  • 从 File 字段获得 FileHeader,调用其 Open 方法来获得文件
  • 可以使用 io.ReadAll 函数把文件内容读取到 byte 切片里
<form
+  action="http://localhost:8080/process?name=客户2号"
+  method="post"
+  enctype="multipart/form-data"
+>
+  <input type="text" name="name" placeholder="Name" />
+  <input type="text" name="email" placeholder="Email" />
+  <input type="file" name="file" />
+  <button type="submit">Submit</button>
+</form>
+
func process(w http.ResponseWriter, r *http.Request) {
+	r.ParseMultipartForm(1024)
+
+	fileHeader := r.MultipartForm.File["file"][0]
+	file, err := fileHeader.Open()
+	if err == nil {
+		data, err := io.ReadAll(file)
+		if err == nil {
+			fmt.Fprintln(w, string(data))
+		}
+	}
+}
+
+func main() {
+	server := http.Server{
+		Addr: "localhost:8080",
+	}
+
+	http.HandleFunc("/process", process)
+
+	server.ListenAndServe()
+}
+
+

FormFile 方法

  • 上传文件还有一个简便方法:FormFile
    • 无需调用 ParseMultipartForm 方法
    • 返回指定 key 对应的第一个 value
    • 同时返回 File 和 FileHeader,以及错误信息
    • 如果只上传一个文件,那么这种方式会快一些
func process(w http.ResponseWriter, r *http.Request) {
+	// r.ParseMultipartForm(1024)
+
+	// fileHeader := r.MultipartForm.File["file"][0]
+	// file, err := fileHeader.Open()
+
+	file, _, err := r.FormFile("file")
+
+	if err == nil {
+		data, err := io.ReadAll(file)
+		if err == nil {
+			fmt.Fprintln(w, string(data))
+		}
+	}
+}
+

MultiPartReader()

  • func (r *Request) MultiPartReader() (*multipart.Reader, error)
  • 如果是 multipart/form-data 或 multipart 混合的 POST 请求
    • MultiPartReader 方法会返回一个 MIME multipart reader
    • 否则返回一个错误
  • 可以使用该函数代替 ParseMultipartForm,来把请求的 body 作为 stream 进行处理
    • 不是把表单作为一个对象来处理的,不是一次性获得整个 map
    • 逐个检查来自表单的值,然后每次处理一个

POST 请求 - JSON Body

  • 不是所有的 POST 请求都来自 Form
  • 客户端框架(例如 Angular 等)会议不同的方式对 POST 请求编码:
    • jQuery 通常使用 application/x-www-form-urlencoded
    • Angular 通常使用 application/json
  • ParseForm 方法无法处理 application/json

响应

ResponseWriter

  • 从服务器向客户端返回响应需要使用 ResponseWriter
  • ResponseWriter 是一个接口,handler 用它来返回响应
  • 真正支撑 ResponseWriter 的幕后 struct 是非导出的 http.response

写入到 ResponseWriter

  • Write 方法接收一个 byte 切片作为参数,然后把它写入到 HTTP 响应的 body 里
  • 如果在 Write 方法被调用时, header 里面没有设定 content-type,那么数据的前 512 字节就会用来被检测 content type
func writeExample(w http.ResponseWriter, r *http.Request) {
+	str := \`<html>
+<head><title>Go Web</title></head>
+<body><h1>Hello World</h1></body>
+</html>
+\`
+	w.Write([]byte(str))
+}
+
+
+func main() {
+	server := http.Server{
+		Addr: "localhost:8080",
+	}
+	http.HandleFunc("/write", writeExample)
+	server.ListenAndServe()
+}
+
curl -i localhost:8080/write
+
+# HTTP/1.1 200 OK
+# Date: Sun, 15 Oct 2023 08:38:27 GMT
+# Content-Length: 84
+# Content-Type: text/html; charset=utf-8
+
+# <html>
+# <head><title>Go Web</title></head>
+# <body><h1>Hello World</h1></body>
+# </html>
+

WriteHeader 方法

  • WriteHeader 方法接收一个整数类型(HTTP 状态码)作为参数,并把它作为 HTTP 响应的状态码返回
  • 如果该方法没有显示调用,那么在第一次调用 Write 方法前,会隐式地调用 WriteHeader(http.StatusOK)
    • 所以 WriteHeader 主要用来发送错误类的 HTTP 状态码
  • 调用完 WriteHeader 方法之后,仍然可以写入到 ResponseWriter,但无法再修改 header 了
func writeHeaderExample(w http.ResponseWriter, r *http.Request) {
+	w.WriteHeader(501)
+	fmt.Fprintln(w, "No such service, try next door")
+}
+
+
+func main() {
+	server := http.Server{
+		Addr: "localhost:8080",
+	}
+	http.HandleFunc("/writeheader", writeHeaderExample)
+	server.ListenAndServe()
+}
+
curl -i localhost:8080/writeheader
+
+# HTTP/1.1 501 Not Implemented
+# Date: Sun, 15 Oct 2023 12:44:53 GMT
+# Content-Length: 31
+# Content-Type: text/plain; charset=utf-8
+
+# No such service, try next door
+

Header 方法

  • Header 方法返回 headers 的 map,可以进行修改
  • 修改后的 headers 将会体现在返回给客户端的 HTTP 响应里
type Post struct {
+	User string
+	Threads []string
+}
+
+func headerExample(w http.ResponseWriter, r *http.Request) {
+	w.Header().Set("Location", "http://google.com")
+	w.WriteHeader(302)
+}
+
+func jsonExample(w http.ResponseWriter, r *http.Request) {
+	w.Header().Set("Content-Type", "application/json")
+	post := &Post{
+		User: "Sau Sheong",
+		Threads: []string{"first", "second", "third"},
+	}
+	json, _ := json.Marshal(post)
+	w.Write(json)
+}
+
+
+func main() {
+	server := http.Server{
+		Addr: "localhost:8080",
+	}
+	http.HandleFunc("/header", headerExample)
+	http.HandleFunc("/json", jsonExample)
+	server.ListenAndServe()
+}
+
curl -i localhost:8080/header
+
+# HTTP/1.1 302 Found
+# Location: http://google.com
+# Date: Sun, 15 Oct 2023 12:50:47 GMT
+# Content-Length: 0
+
+curl -i localhost:8080/json
+
+# HTTP/1.1 200 OK
+# Content-Type: application/json
+# Date: Sun, 15 Oct 2023 12:56:51 GMT
+# Content-Length: 58
+
+# {"User":"Sau Sheong","Threads":["first","second","third"]}
+

内置的 Response

  • NotFound 函数,包装一个 404 状态码和一个额外的信息
  • ServeFile 函数,从文件系统提供文件,返回给请求者
  • ServeContent 函数,它可以把实现了 io.ReadSeeker 接口的任何东西里面的内容返回给请求者
    • 还可以处理 Range 请求(范围请求),如果只请求了资源的一部分内容,那么 ServeContent 就可以如此响应。而 ServeFile 或 io.Copy 就不行
  • Redirect 函数,告诉客户端重定向到另一个 URL

模板

  • Web 模板就是预先设计好的 HTML 页面,它可以被模板引擎反复的使用,来产生 HTML 页面
  • Go 的标准库提供了 text/template 和 html/template 两个模板库
    • 大多数 Go 的 Web 框架都使用这些库作为默认的模板引擎

模板与模板引擎

模板引擎可以合并模板与上下文数据,产生最终的 HTML

Go 的模板引擎

  • 主要使用的是 text/template,HTML 相关的部分使用了 html/template,是个混合体
  • 模板可以完全无逻辑,但又具有足够的嵌入特性
  • 和大多数模板引擎一样,Go Web 的模板位于无逻辑和嵌入逻辑之间的某个地方

关于模板

  • 模板必须是可读的文本格式,扩展名任意。对于 Web 应用通常就是 HTML
    • 里面会内嵌一些命令(叫作 action)
  • text/template 是通用模板引擎,html/template 是 HTML 模板引擎
  • action 位于双层花括号之间:{{ . }}
    • 这里的 . 就是一个 action
    • 它可以命令模板引擎将其替换成一个值

使用模板引擎

  1. 解析模板源(可以是字符串或模板文件),从而创建一个解析好的模板的 struct
  2. 执行解析好的模板,并传入 ResponseWriter 和数据
    • 这会触发模板引擎组合解析好的模板和数据,来产生最终的 HTML,并将它传递给 ResponseWriter
<!DOCTYPE html>
+<html>
+  <head>
+    <meta charset="UTF-8" />
+    <title>Document</title>
+    <style></style>
+  </head>
+
+  <body>
+    {{ . }}
+  </body>
+</html>
+
func process(w http.ResponseWriter, r *http.Request) {
+	t, _ := template.ParseFiles("tmpl.html")
+	t.Execute(w, "Hello World")
+}
+
+
+func main() {
+	server := http.Server{
+		Addr: "localhost:8080",
+	}
+	http.HandleFunc("/process", process)
+	server.ListenAndServe()
+}
+

解析模板

  • ParseFiles
  • ParseGlob
  • Parse

ParseFiles

  • 解析模板文件,并创建一个解析好的模板 struct,后续可以被执行
  • ParseFiles 函数是 Template struct 上 ParseFiles 方法的简便调用
  • 调用 ParseFiles 后,会创建一个新的模板,模板名字是文件名
  • New 函数
  • ParseFiles 的参数数量可变,但只返回一个模板
    • 当解析多个文件时,第一个文件作为返回的模板(名,内容),其余的作为 map,供后续执行使用
// t, _ := template.ParseFiles("tmpl.html")
+t := template.New("tmpl.html")
+t, _ = t.ParseFiles("tmpl.html")
+

ParseGlob

  • 使用模式匹配来解析特定的文件
t, _ := template.ParseGlob("*.html")
+

Parse

  • 可以解析字符串模板,其他方式最终都会调用 Parse

Lookup

  • 通过模板名来寻找模板,如果没找到就返回 nil

Must

  • 可以包裹一个函数,返回到一个模板的指针和一个错误
    • 如果错误不为 nil,那么就 panic

执行模板

  • Execute
    • 参数是 ResponseWriter、数据
    • 单模板:很适用
    • 模板集:只用第一个模板
  • ExecuteTemplate
    • 参数是 ResponseWriter、模板名、数据
    • 模板集:很适用
func process(w http.ResponseWriter, r *http.Request) {
+	t, _ := template.ParseFiles("t1.html")
+	t.Execute(w, "Hello World")
+
+	ts, _ := template.ParseFiles("t2.html", "t3.html")
+	ts.ExecuteTemplate(w, "t2.html", "Hello World")
+}
+

Action

`,161),v=n("li",null,". 就是一个 Action,而且是最重要的一个。它代表了传入模板的数据",-1),m=n("li",null,[l("Action 主要可以分为 5 类 "),n("ul",null,[n("li",null,"条件类"),n("li",null,"迭代/遍历类"),n("li",null,"设置类"),n("li",null,"包含类"),n("li",null,"定义类")])],-1),g=s(`

条件 Action

语法

{{ if arg }}
+	some content
+{{ end }}
+
+{{ if arg }}
+	some content
+{{ else }}
+	some content
+{{ end }}
+
+{{ if arg }}
+	some content
+{{ else if arg }}
+	some content
+{{ else }}
+	some content
+{{ end }}
+

demo

<body>
+  {{ if . }} Number is greater than 5 {{ else }} Number is 5 or less {{ end }}
+</body>
+
func main() {
+	http.HandleFunc("/process", process)
+	http.ListenAndServe("localhost:8080", nil)
+}
+
+var rng = rand.New(rand.NewSource(time.Now().UnixNano()))
+
+func process(w http.ResponseWriter, r *http.Request) {
+	t, _ := template.ParseFiles("tmpl.html")
+	t.Execute(w, rng.Intn(10) > 5)
+}
+

迭代/遍历 Action

语法

{{ range array }}
+	some content {{ . }}
+{{ end }}
+
  • 这类 Action 用来遍历数组、slice、map 或 channel 等数据结构
    • “.” 用来表示每次迭代循环中的元素
<ul>
+  {{range .}}
+  <li>{{.}}</li>
+  <!-- 回落机制 -->
+  {{ else }}
+  <li>Empty list</li>
+  {{end}}
+</ul>
+
func main() {
+	http.HandleFunc("/process", process)
+	http.ListenAndServe("localhost:8080", nil)
+}
+
+func process(w http.ResponseWriter, r *http.Request) {
+	t, _ := template.ParseFiles("tmpl.html")
+	daysOfWeek := []string{"Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun"}
+	t.Execute(w, daysOfWeek)
+}
+

设置 Action

语法

{{ with arg }}
+.
+{{ end }}
+
  • 它允许在指定范围内,让“.”来表示其它指定的值(arg)

demo

<body>
+  <div>The dot is set to {{ . }}</div>
+  <div>{{ with "world" }} Now the dot is set to {{ . }} {{ end }}</div>
+  <div>The dot is {{ . }} again</div>
+</body>
+
+<!-- 回落机制 -->
+
+<body>
+  <div>The dot is set to {{ . }}</div>
+  <div>
+    {{ with "" }} Now the dot is set to {{ . }} {{ else }} The dot is still {{ .
+    }} {{ end }}
+  </div>
+  <div>The dot is {{ . }} again</div>
+</body>
+
func process(w http.ResponseWriter, r *http.Request) {
+	t, _ := template.ParseFiles("tmpl.html")
+	t.Execute(w, "hello")
+}
+

包含 Action

语法

{{ template "name"}}
+
+// 给被包含的模板传递参数
+{{ template "name" arg }}
+
  • 它允许在模板中包含其他的模板
<!DOCTYPE html>
+<html>
+  <head>
+    <meta charset="UTF-8" />
+    <title>Document</title>
+    <style></style>
+  </head>
+
+  <body>
+    <div>this is t1.html</div>
+    <div>This is the value of the dot in t1.html - [{{ . }}]</div>
+    <hr />
+    {{ template "t2.html" . }}
+    <hr />
+    <div>This is t1.html after</div>
+  </body>
+</html>
+
<div style="background-color: yellow">
+  This is t2.html <br />
+  This is the value of the dot in t2.html - [{{ . }}]
+</div>
+
func process(w http.ResponseWriter, r *http.Request) {
+	t, _ := template.ParseFiles("tmpl.html", "t2.html")
+	t.Execute(w, "hello")
+}
+

定义 Action

define action

函数和管道

参数

  • 参数就是模板里面用到的值
  • 模板里面的参数可以是任何类型的值
  • 参数可以是变量、方法(返回单个值或返回一个值和一个错误)或函数
  • 参数可以是一个“.”,也就是传入到模板引擎的那个值

在 Action 中设置变量

  • 可以在 action 中设置变量,变量以 $ 开头:
    • $variable := value
  • 一个迭代 action 的例子
{{ range $key, $value := . }}
+{{ $key }}: {{ $value }}
+{{ end }}
+

管道

  • 管道是按顺序连接到一起的参数、函数和方法
    • 和 Unix 的管道类似
  • 例如:{{ p1 | p2 | p3 }}
    • p1、p2、p3 要么是参数,要么是函数
  • 管道允许把参数的输出发给下一个参数,下一个参数由管道(|)分隔开
<body>
+  <!-- 展示 12.35 -->
+  {{ 12.3456 | printf "%.2f" }}
+  <!-- 等价于 -->
+  {{ printf "%.2f" 12.3456 }}
+</body>
+

函数

  • 参数可以是一个函数
  • Go 模板引擎提供了一些基本的内置函数,功能比较有限。例如 fmt.Sprint 的各类变体
  • 开发者可以自定义函数
    • 可以接收任意数量的输入参数
    • 返回:
      • 一个值
      • 一个值 + 一个错误

内置函数

  • define\\template\\block
  • html\\js\\urlquery。对字符串进行转义,防止安全问题
    • 如果是 Web 模板,那么不会需要经常使用这些函数
  • index
  • print/printf/println
  • len
  • with

自定义函数

template.Funcs(funcMap FuncMap) *Template

type FuncMap map[string]interface{}

创建自定义函数的步骤:

  1. 创建一个 FuncMap 类型的变量
    • key 是函数名
    • value 就是函数
  2. 把 FuncMap 附加到模板
<body>
+  <!-- 自定义函数可以在管道中使用,更强大灵活 -->
+  {{ . | fdate }}
+  <!-- 自定义函数也可以作为正常函数使用 -->
+  {{ fdate . }}
+</body>
+
func process(w http.ResponseWriter, r *http.Request) {
+	// 常见用法:
+	// template.New("").Funcs(funcMap).Parse(...)
+	// 调用顺序十分重要
+	funcMap := template.FuncMap{"fdate": formatDate}
+	t:= template.New("index.html").Funcs(funcMap)
+	t.ParseFiles("index.html")
+	t.Execute(w, time.Now())
+}
+
+func formatDate(t time.Time) string {
+	layout := "2006-01-02"
+	return t.Format(layout)
+}
+
`,48);function b(a,h){return p(),e("div",null,[k,n("ul",null,[n("li",null,"Action 就是 Go 模板中嵌入的命令,位于两组花括号之间 "+o(a.xxx),1),v,m]),g])}const f=t(d,[["render",b],["__file","index.html.vue"]]);export{f as default}; diff --git a/assets/index.html-947c70c9.js b/assets/index.html-947c70c9.js new file mode 100644 index 0000000..c941292 --- /dev/null +++ b/assets/index.html-947c70c9.js @@ -0,0 +1 @@ +import{_ as e,o as c,c as t}from"./app-b4fb6edd.js";const n={};function _(o,r){return c(),t("div")}const a=e(n,[["render",_],["__file","index.html.vue"]]);export{a as default}; diff --git a/assets/index.html-94a8ea89.js b/assets/index.html-94a8ea89.js new file mode 100644 index 0000000..6bb1edb --- /dev/null +++ b/assets/index.html-94a8ea89.js @@ -0,0 +1,1496 @@ +import{_ as n,o as s,c as a,e as t}from"./app-b4fb6edd.js";const p="/study-notes/assets/assembly-line-544d32be.png",e={},o=t(`

Go

编译型语言

Go 是静态类型语言,一旦某个变量被声明,那么它的类型就无法再改变了

搭建环境

vscode:

  1. 插件

  2. Go install/update tools:安装/更新工具

    Go 代理

    go env -w GO111MODULE=on
    +go env -w GOPROXY=https://goproxy.cn,direct
    +

变量及其作用域

作用域的范围就是 {} 之间的部分

短声明

// 使用 var 声明变量
+var a = 1
+// 也可以使用短声明,效果同上,但可以在无法使用 var 的地方使用
+a := 1
+
+for i := 0; i < 10; i++ {
+   // i 在 for 循环中声明,作用域只在 for 循环中
+}
+
+if a := 1; a > 0 {
+   // a 在 if 语句中声明,作用域只在 if 语句中
+}
+
+switch a := 1; a {
+case 1:
+   // a 在 switch 语句中声明,作用域只在 switch 语句中
+default:
+   //...
+}
+
+

main 函数外声明的变量拥有 package 作用域,短声明不能用来声明 package 作用域的变量

实数

声明浮点型变量

只要数字含有小数部分,那么它的类型就是 float64

/* 下面三个语句的效果是一样的 */
+days := 365.2425
+var days = 365.2425
+var days float64 = 365.2425
+/* 如果使用一个整数来初始化某个变量,则必须指定它的类型为 float64,否则它就是一个整数类型 */
+var answer float32 = 42
+

Go 语言里有两种浮点数类型:

默认是 float64

  • 64 位的浮点类型
  • 占用 8 字节

float32

  • 占用 4 字节
  • 精度比 float64 低
  • 有时叫做单精度浮点数类型

想使用单精度类型,必须再声明变量的时候指定该类型:

var pi64 = math.Pi
+var pi32 float32 = math.Pi
+fmt.Println(pi64) // 3.141592653589793
+fmt.Println(pi32) // 3.1415927
+
  • 当处理大量数据时,例如 3D 游戏中的数千个顶点,使用 float32 牺牲精度来节省内存是很有意义的
  • math 包里面的函数操作都是 float64 类型,所以应该首选使用 float 64 类型,除非有足够的理由不去使用它

零值

Go 里面的每个类型都有一个默认值,它称作零值

当声明变量却不对它进行初始化的时候,它的值就是零值

var price float64
+fmt.Println(price) // 0
+

显示浮点类型

  • Print 或 Println 打印浮点类型的时候,默认的行为是尽可能地多显示几位小数
  • Printf 函数,结合 %f 格式化动词来指定显示小数的位数
third := 1.0 / 3
+fmt.Println(third) // 0.3333333333333333
+fmt.Printf("%v\\n", third) // 0.3333333333333333
+fmt.Printf("%f\\n", third) // 0.333333
+fmt.Printf("%.3f\\n", third) // 0.333
+fmt.Printf("%4.2f\\n", third) // 0.33
+fmt.Printf("%05.2f\\n", third) // 00.33,默认是空格填充
+

浮点类型不适合用于金融类计算,为了尽量最小化舍入错误,建议先做乘法,再做除法

如何比较浮点类型

piggyBank := 0.1
+piggyBank += 0.2
+fmt.Println(piggyBank == 0.3) // false
+
+fmt.Println(math.Abs(piggyBank - 0.3) < 0.0001)
+

整数类型

Go 提供了 10 种整数类型(不可以存小数部分,范围有限,通常根据数值范围来选取整数类型)

  • 5 种整数类型是有符号的,能表示正数、0、负数
  • 5 种整数类型是无符号的,能表示正数、0
// 最常用的整数类型是 int
+var year int = 2018
+// 无符号整数类型是 uint
+var month uint = 12
+

下面三个语句是等价的:

year := 2018
+var year = 2018
+var year int = 2018
+

int 和 uint 是针对目标设备优化的类型

  • 在树莓派 2、比较老的移动设备上,int 和 int32 都是 32 位的
  • 在比较新的计算机上,int 和 int64 都是 64 位的

如果在比较老的 32 位设备上,使用了超过 20 亿的整数,并且代码还能运行,那么最好使用 int64 和 uint64 来代替 int 和 uint

打印数据类型

在 Printf 函数里面,可以使用 %T 格式化动词来打印变量的类型

year := 2018
+fmt.Printf("Type %T for %v\\n", year, year) // Type int for 2018
+a := "text"
+fmt.Printf("Type %T for %[1]v\\n", a) // Type string for text
+b := 3.14
+fmt.Printf("Type %T for %[1]v\\n", b) // Type float64 for 3.14
+c := true
+fmt.Printf("Type %T from %[1]v\\n", c) // Type bool from true
+

uint8

取值范围 0-255

  • unit8 可以用来表示 8 位的颜色(红绿蓝:0-255)
    var red, green, blue unit8 = 0, 141, 213
    +

十六进制表示法

Go 语言里,在数前面加上 0x 前缀,就可以用十六进制的形式来表示

var red, green, blue unit8 = 0, 141, 213
+var red, green, blue unit8 = 0x00, 0x8d, 0xd5
+

打印十六进制

打印十六进制的数,用 %x 格式化动词

fmt.Printf("%x %x %x", red, green, blue)
+// 也可以指定最小宽度和填充
+fmt.Printf("color: #%02x%02x%02x;", red, green, blue)
+

整数环绕

所有的整数都有一个取值范围,超出这个范围,就会发生“环绕”

var red uint8 = 255
+red++
+fmt.Println(red) // 0
+
+var number int8 = 127
+number++
+fmt.Println(number) // -128
+

如何避免时间发生环绕?

Unix 系统里,时间是以 1970 年 1 月 1 日至今的秒数来表示的

但是在 2038 年,这个数就会超过 20 多亿,也就是超过了 int32 的范围

应使用:int64 或 uint64

future := time.Unix(12622780800, 0)
+fmt.Println(future) // 2370-01-01 08:00:00 +0800 CST
+

打印每个 bit

使用 %b 格式化动词

var green uint8 = 3
+fmt.Printf("%08b\\n", green) // 00000011
+green++
+fmt.Printf("%08b\\n", green) // 00000100
+

整数类型的最大值、最小值

  • math 包里,为与架构无关的整数类型,定义了最大、最小值常量
    math.MaxInt16
    +math.MinInt64
    +
  • 而 int 和 uint,可能是 32 位 或 64 位的

比较大的数

浮点类型可以存储非常大的数值,但是精度不高

整型很精确,但是取值范围有限

使用指数表示的数,默认就是 float64 类型

var distance = 24e2
+fmt.Printf("%T", distance) // float64
+

如果需要存储非常大的整数,可以使用 math/big 包

  • 对于比较大的整数(超过 10^18),big.Int
  • 对于任意精度的浮点类型,big.Float
  • 对于分数,big.Rat
lightSpeed := big.NewInt(299792)
+secondsPerDay := big.NewInt(86400)
+
+distance := new(big.Int)
+distance.SetString("24000000000000000000000", 10)
+fmt.Println(distance) // 24000000000000000000000
+
+seconds := new(big.Int)
+seconds.Div(distance, lightSpeed) // seconds = distance / lightSpeed
+
+days := new(big.Int)
+days.Div(seconds, secondsPerDay) // days = seconds / secondsPerDay
+
+fmt.Println("That is", days, "days of travel at light speed.")
+

一旦使用了 big.Int,那么等式里其他部分也必须使用 big.Int

NewInt() 函数可以把 int64 转化为 big.Int 类型

缺点:用起来繁琐,速度较慢

较大数值的常量

// 会报错
+const distance unit64 = 24000000000000000000000
+// 但在 Go 里面,常量是可以无类型的(untyped),下面就不会报错
+const distance = 24000000000000000000000 // untyped int
+fmt.Printf("%T", distance) // 报错
+

常量使用 const 关键字来声明,程序里每个字面值都是常量,这意味着比较大的数值可以直接使用(作为字面值)

fmt.Println(24000000000000000000000/299792/86400) // 926568346646
+

针对字面值和常量的计算是在编译阶段完成的

Go 的编译器是用 Go 编写的,这种无类型的数值字面值就是由 big 包所支持的,这使得可以操作很大的数(超过 18 的 10^18)

多语言文本

声明字符串

声明

peace := "peace"
+var peace = "peace"
+var peace string = "peace"
+

字符串的零值

var empty string
+fmt.Println(empty == "") // true
+

字符串字面值/原始字符串字面值

字符串字面值(string literal)可以包含转义字符,例如 \\n

但如果想得到 \\n 而不是换行的话,可以使用 \` 来代替 ",这叫做原始字符串字面值(raw string literal)

fmt.Println("peace be upon you\\nupon you be peace")
+fmt.Println(\`strings can span multiple lines with the \\n escape sequence\`)
+fmt.Println(\`
+peace be upon you
+upon you be peace
+\`)
+

字符,code points,runes,bytes

Unicode 联盟为超过 100 万个字符分配了相应的数值,这个数叫做 code point

  • 例如:65 代表 A,128515 代表 😃

为了表示这样的 unicode code point,Go 提供了 rune 类型,它是 int32 的别名

byte 是 unit 8 类型的别名,目的是用于二进制数据

  • byte 倒是可以表示由 ASCII 定义的英语字符,它是 Unicode 的一个子集(共 128 个字符)

类型别名

类型别名就是同一个类型的另一个名字

  • 所以,rune 和 int32 可以互换使用

也可以自定义类型别名,语法如下

type byte = uint8
+type rune = int32
+

打印

如果想打印字符而不是数值,使用 c% 格式化动词

fmt.Printf("%c", 128515) // 😃
+

任何整数类型都可以使用 %c 打印,但是 rune 意味着该数值表示了一个字符

字符

字符字面值使用 '' 括起来,例如 'A'

如果没有指定字符类型的话,Go 会推断它的类型为 rune

grade := 'A'
+var grade1 = 'A'
+var grade2 rune = 'A'
+

这里的 grade 仍然包含一个数值,本例中就是 65,它是 A 的 code point

字符字面值也可以用 byte 类型

var star byte = '*'
+

string

可以给某个变量赋予不同的 string 值,但是 string 本身是不可变的

message := "shalom"
+c := message[5]
+fmt.Printf("%c\\n", c) // m
+message[5] = 'd' // 报错
+

Caesar cipher 凯撒加密法

凯撒加密法是一种简单的加密方法,它是通过将每个字符移动固定数目的位置来实现的

c := 'a'
+c = c + 3
+fmt.Printf("%c", c) // d
+if c > 'z' {
+  c = c - 26
+}
+

ROT13

ROT13 (旋转 13) 是凯撒加密在 20 世纪的变体, 它会把字母替换成 +13 后对应的字母

originalMessage := "uv vagreangvbany fcnpr fgngvba"
+for i := 0; i < len(originalMessage); i++ {
+  c := originalMessage[i]
+  if c >= 'a' && c <= 'z' {
+    c = c + 13
+    if c > 'z' {
+      c = c - 26
+    }
+  }
+  fmt.Printf("%c", c)
+}
+

Go 的内置函数

len 是 Go 语言的一个内置函数

message := "uv vagreangvbany fcnpr fgngvba"
+fmt.Println(len(message)) // 32
+

本例中 len 返回 message 所占的 byte 数

UTF-8

Go 中的字符串是用 UTF-8 编码的,UTF-8 是 Unicode Code Point 的几种编码之一

UTF8 是一种有效率的可变长度的编码,每个 code point 可以是 8 位、16 位或 32 位的

通过使用可变长度编码,UTF-8 使得从 ASCII 的转换变得简单明了,因为 ASCII 字符与其 UTF-8 编码对应的字符是相同的

UTF-8 是万维网的主要字符编码,它是由 Ken Thompson(Go 语言的设计者之一) 于 1992 年发明的

question := "¿Cómo estás?"
+fmt.Println(len(question), "bytes") // 15
+fmt.Println(utf8.RuneCountInString(question), "runes") // 12
+
+c,size := utf8.DecodeRuneInString(question)
+fmt.Printf("First rune: %c %v bytes", c, size) // First rune: ¿ 2 bytes
+

range

使用 range 关键字,可以遍历各种集合

question := "¿Cómo estás?"
+for i, c := range question {
+  fmt.Printf("%v %c\\n", i, c)
+}
+

类型不能混用

连接两个字符串,使用 + 运算符

countdown := "Launch in T minus " + "10 seconds."
+

如果想连接字符串和数值,是会报错的

countdown := "Launch in T minus " + 10 + " seconds."
+

整型和浮点类型也不能混着用

age := 41
+marsDays := 687
+earthDays := 365.2425
+fmt.Println("I am", age * earthDays / marsDays, "years old on Mars.") // invalid operation: age * earthDays (mismatched types int and float64)
+

数值间类型转换

整数类型转换为浮点类型

age := 41
+// 将 age 转换为浮点类型
+marsAge := float64(age)
+

浮点类型转换为整数类型,小数点后边的部分会被截断,而不是舍入

earthDays := 365.2425
+// 将 earthDays 转换为整数类型
+fmt.Println(int(earthDays)) // 365
+

无符号和有符号整数类型之间也需要转换

不同大小的整数类型之间也需要转换

类型转换时需谨慎

环绕行为

var bh float64 = 32768
+var h = int16(bh)
+fmt.Println(h) // -32768
+

可以通过 math 包提供的 max、min 常量,来判断是否超过最大最小值

var bh float64 = 32768
+if bh < math.MinInt16 || bh > math.MaxInt16 {
+   // handle out of range error
+}
+

字符串转换

把 rune、byte 转换为 string

var pi rune = 960
+var alpha rune = 940
+var omega rune = 969
+var bang byte = 33
+fmt.Printf("%v %v %v %v\\n", string(pi), string(alpha), string(omega), string(bang)) // π ά ω !
+

想把数值转化为有意义的字符串,它的值必须能转化为 code point

countdown := 10
+str := "Launch in T minus " + strconv.Itoa(countdown) + " seconds."
+fmt.Println(str) // Launch in T minus 10 seconds.
+

Itoa 是 Integer to ASCII 的意思

Unicode 是 ASCII 的超集,它们前 128 个 code points 是一样的(数字、英文字母、常用标点)

另外一种把数值转化为 string 的方式是使用 Sprintf 函数,和 Printf 略类似,但是会返回一个 string

countdown := 9
+str := fmt.Sprintf("Launch in T minus %v seconds.", countdown)
+fmt.Println(str) // Launch in T minus 9 seconds.
+

strconv 包中的 Atoi 函数(ASCII to Integer),由于字符串里面可能包含任意字符,或者要转换的数字字符串太大,所以 Atoi 函数可能会发生错误

countdown, err := strconv.Atoi("10ds")
+if err != nil {
+  // handle error
+  fmt.Println(err.Error())
+}
+fmt.Println(countdown) // 10
+

布尔类型的转换

Print 家族函数,会把 bool 类型的值打印成 true/false 文本

launch := false
+launchText := fmt.Sprintf("%v", launch)
+fmt.Println("Ready for launch:", launchText) // Ready for launch: false
+
+var yesNo string
+if launch {
+   yesNo = "yes"
+} else {
+   yesNo = "no"
+}
+fmt.Println("Ready for launch:", yesNo) // Ready for launch: no
+

如果想使用 string(false),int(false);bool(1), bool("yes") 等类似的方法进行转换,那么 Go 编译器会报错

函数

使用 func 关键字

大写字母开头的函数、变量或其他标识符都会被导出,对其他包可用;小写字母开头的则不会

实参(argument)和形参(parameter)

函数声明时,如果多个形参类型相同,那么该类型只写一次即可:

// func Unix(sec int64, nsec int64) Time
+func Unix(sec, nsec int64) Time
+

Go 的函数可以返回多个值

countdown, err := strconv.Atoi("10")
+

上面的函数声明为 func Atoi(s string) (i int, err error)

函数的多个返回值需要用括号括起来,每个返回值名字在前,类型在后。声明函数时可以把名字去掉,只保留类型:

func Atoi(s string) (int, error)
+

Println 是一个特殊的函数,它可以接收一个、两个甚至多个参数,参数类型还可以不同。其声明如下:

func Println(a ...interface{}) (n int, err error)
+

... 表示函数的参数数量是可变的

参数 a 的类型为 interface{},是一个空接口

编写函数

func kelvinToCelsius(k float64) float64 {
+  k -= 273.15
+  return k
+}
+
+func main() {
+  k := 294.0
+  c := kelvinToCelsius(k)
+  fmt.Println(k, "°K is", c, "°C")
+}
+

函数按值传递

同一个包中声明的函数在调用时彼此不需要加上包名

方法

声明新类型

关键字 type 用来声明新类型

type celsius float64
+const degrees = 20
+var temperature celsius = degrees
+temperature += 10
+fmt.Println(temperature) // 30
+

为什么声明新类型?极大地提高代码可读性和可靠性

不同的类型是无法混用的

声明函数类型

type sensor func() kelvin
+

通过方法添加行为

在 Go 里,它提供了方法,但是没提供类和对象

Go 比其他语言的方法要灵活

可以将方法与同包中声明的任何类型相关联,但不可以是 int、float32 等预声明的类型

type celsius float64
+type kelvin float64
+
+func kelvinToCelsius(k kelvin) celsius {
+  return celsius(k - 273.15)
+}
+
+func (k kelvin) celsius() celsius {
+  return celsius(k - 273.15)
+}
+

celsius 方法虽然没有参数,但它前面却有一个类型参数的接收者

每个方法可以有多个参数,但只能有一个接收者

接收者的行为和其他参数是一样的

方法调用

变量.方法名(参数)

一等函数

在 Go 里,函数是头等的,它可以用在整数、字符串或其他类型能用的地方:

  • 将函数赋给变量
  • 将函数作为参数传递给函数
  • 将函数作为函数的返回类型

闭包和匿名函数

匿名函数就是没有名字的函数,在 Go 里也称作函数字面值

var f = func() {
+  fmt.Println("Dress up for the masquerade.")
+}
+
+f := func() {
+  fmt.Println("Dress up for the masquerade.")
+}
+
+func() {
+  fmt.Println("Dress up for the masquerade.")
+}()
+

因为函数字面值需要保留外部作用域的变量引用,所以函数字面值都是闭包的

闭包就是由于匿名函数封闭并包围作用域中的变量而得名

数组

数组是一种固定长度且有序的元素集合

var colors [3]string
+colors[0] = "Red"
+colors[1] = "Green"
+
+color := colors[1]
+fmt.Println(color) // Green
+fmt.Println(colors[2] == "") // true
+fmt.Println(len(colors)) // 3
+

数组的长度可以由内置函数 len 确定

在声明数组时,未被赋值元素的值是对应类型的零值

数组越界

var colors [3]string
+colors[3] = "Red"
+i := 3
+fmt.Println(colors[i]) // panic: runtime error: index out of range
+

Go 编译器在检测到对越界元素的访问时会报错

如果 Go 编译器在编译时未能发现越界错误,那么程序在运行时会出现 Panic

Panic 会导致程序崩溃

使用复合字面值初始化数组

复合字面值(composite literal)是一种用于初始化复合类型(数组、切片、字典和结构体)的紧凑语法

只用一步就完成数组声明和数组初始化

colors := [3]string{"Red", "Green", "Blue"}
+

可以在复合字面值里使用 ... 作为数组的长度,这样 Go 编译器会自动算出数组的元素数量

colors := [...]string{"Red", "Green", "Blue"}
+

无论哪种方式,数组的长度都是固定的

遍历数组

for 循环

dwarfs := [5]string{"Ceres", "Pluto", "Haumea", "Makemake", "Eris"}
+for i := 0; i < len(dwarfs); i++ {
+  fmt.Println(i, dwarfs[i])
+}
+

range 关键字

dwarfs := [5]string{"Ceres", "Pluto", "Haumea", "Makemake", "Eris"}
+for i, dwarf := range dwarfs {
+  fmt.Println(i, dwarf)
+}
+

数组的复制

无论数组赋值给新的变量还是将它传递给函数,都会产生一个完整的数组副本

planets := [...]string{"Mercury", "Venus", "Earth", "Mars", "Jupiter", "Saturn", "Uranus", "Neptune"}
+
+planetsMarkII := planets
+planets[2] = "whoops"
+fmt.Println(planets) // [Mercury Venus whoops Mars Jupiter Saturn Uranus Neptune]
+fmt.Println(planetsMarkII) // [Mercury Venus Earth Mars Jupiter Saturn Uranus Neptune]
+fmt.Println(planets == planetsMarkII) // false
+

数组也是一种值,函数通过值传递来接受参数,所以数组作为函数的参数就非常低效

数组的长度也是数组类型的一部分,将长度不符的数组作为参数传递会报错

函数一般使用 slice 而不是数组作为参数

数组的数组

二维数组

var board [8][8]string
+
+board[0][0] = "r"
+board[0][7] = "r"
+
+for column := range board[1] {
+  board[1][column] = "p"
+}
+
+fmt.Println(board)
+

切片 Slice

Slice 指向数组的窗口

假设 planets 是一个数组,那么 planets[0:4] 就是一个切片,它指向 planets 数组的前 4 个元素

切分数组不会导致数组被修改,它只是创建了指向数组的一个窗口或视图,这种视图就是 slice 类型

planets := [...]string{"Mercury", "Venus", "Earth", "Mars", "Jupiter", "Saturn", "Uranus", "Neptune"}
+
+// terrestrial := planets[0:4]
+terrestrial := planets[:4]
+gasGiants := planets[4:6]
+// iceGiants := planets[6:8]
+iceGiants := planets[6:]
+
+allPlanets := planets[:]
+
+fmt.Println(terrestrial) // [Mercury Venus Earth Mars]
+fmt.Println(gasGiants) // [Jupiter Saturn]
+fmt.Println(iceGiants) // [Uranus Neptune]
+fmt.Println(allPlanets) // [Mercury Venus Earth Mars Jupiter Saturn Uranus Neptune]
+

忽略掉 slice 的起始索引,表示从数组的起始位置进行切分

忽略掉 slice 的结束索引,相当于使用数组的长度作为结束索引

注意:slice 的索引不能是负数

切分数组的语法也可以用于切分字符串

s := "hello, world"
+c := s[0:5]
+s = "1111111"
+fmt.Println((c)) // hello
+

切分字符串时,索引代表的时字节数而非 rune 数

question := "¿Cómo estás?"
+fmt.Println(question[:6]) // ¿Cómo
+

Slice 的复合字面值

切分数组并不是创建 slice 的唯一方法,可以直接声明 slice

dwarfArray := [...]string{"Ceres", "Pluto", "Haumea", "Makemake", "Eris"}
+dwarfs := dwarfArray[:]
+
+// 直接声明 slice,不需要指定长度
+dwarfs := []string{"Ceres", "Pluto", "Haumea", "Makemake", "Eris"}
+

切片应用

func hyperspace(worlds []string) {
+  for i := range worlds {
+    worlds[i] = strings.TrimSpace(worlds[i])
+  }
+}
+
+func main() {
+  planets := []string{" Venus  ", "Earth ", " Mars"}
+  hyperspace(planets)
+  fmt.Println(strings.Join(planets, "")) // VenusEarthMars
+}
+

带有方法的切片

在 Go 里,可以将 slice 或数组作为底层类型,然后绑定其它方法

planets := []string{"Venus", "Earth", "Mars", "Jupiter", "Saturn", "Uranus", "Neptune"}
+
+sort.StringSlice(planets).Sort()
+fmt.Println(planets) // [Earth Jupiter Mars Neptune Saturn Uranus Venus]
+

更大的 slice

append 函数也是内置函数,它用于向 slice 里追加元素

dwarfs := []string{"Ceres", "Pluto", "Haumea", "Makemake", "Eris"}
+dwarfs = append(dwarfs, "Orcus")
+fmt.Println(dwarfs) // [Ceres Pluto Haumea Makemake Eris Orcus]
+

长度和容量(length & capacity)

Slice 中元素的个数决定了 slice 的长度

如果 slice 的底层数组比 slice 还大,那么就说 slice 还有容量可供增长

func dump(label string, slice []string) {
+  fmt.Printf("%v: length %v, capacity %v %v\\n", label, len(slice), cap(slice), slice)
+}
+
+func main() {
+  dwarfs := []string{"Ceres", "Pluto", "Haumea", "Makemake", "Eris"}
+  dump("dwarfs", dwarfs) // dwarfs: length 5, capacity 5 [Ceres Pluto Haumea Makemake Eris]
+  dump("dwarfs[1:2]", dwarfs[1:2]) // dwarfs[1:2]: length 1, capacity 4 [Pluto]
+}
+

再结合 append 函数看一看

func dump(label string, slice []string) {
+  fmt.Printf("%v: length %v, capacity %v %v\\n", label, len(slice), cap(slice), slice)
+}
+
+func main() {
+  dwarfs1 := []string{"Ceres", "Pluto", "Haumea", "Makemake", "Eris"}
+  dwarfs2 := append(dwarfs1, "Orcus")
+  dwarfs3 := append(dwarfs2, "Salacia", "Quaoar", "Sedna")
+
+  dump("dwarfs1", dwarfs1) // dwarfs1: length 5, capacity 5 [Ceres Pluto Haumea Makemake Eris]
+  dump("dwarfs2", dwarfs2) // dwarfs2: length 6, capacity 10 [Ceres Pluto Haumea Makemake Eris Orcus]
+  dump("dwarfs3", dwarfs3) // dwarfs3: length 9, capacity 10 [Ceres Pluto Haumea Makemake Eris Orcus Salacia Quaoar Sedna]
+
+  dwarfs3[1] = "Pluto!"
+  fmt.Println(dwarfs1) // [Ceres Pluto Haumea Makemake Eris]
+	/* 下面两个切片的底层数组是相同的 */
+  fmt.Println(dwarfs2) // [Ceres Pluto! Haumea Makemake Eris Orcus]
+  fmt.Println(dwarfs3) // [Ceres Pluto! Haumea Makemake Eris Orcus Salacia Quaoar Sedna]
+}
+

三个索引的切分操作

Go 1.2 中引入了能够限制新建切片容量的三索引切分操作

func dump(label string, slice []string) {
+  fmt.Printf("%v: length %v, capacity %v %v\\n", label, len(slice), cap(slice), slice)
+}
+
+func main() {
+  planets := []string{"Mercury", "Venus", "Earth", "Mars", "Jupiter", "Saturn", "Uranus", "Neptune"}
+
+  terrestrials := planets[0:4:4] // 又新分配了一个数组,长度为 4,容量为 4
+  worlds := append(terrestrials, "Ceres") // 又新分配了一个数组,长度为 4,容量为 8
+  dump("terrestrials", terrestrials) // terrestrials: length 4, capacity 4 [Mercury Venus Earth Mars]
+  dump("worlds", worlds) // worlds: length 5, capacity 8 [Mercury Venus Earth Mars Ceres]
+
+  worlds2 := append(terrestrials, "Ceres", "Pluto", "Haumea", "Makemake", "Eris")
+  dump("worlds2", worlds2) // worlds2: length 9, capacity 12 [Mercury Venus Earth Mars Ceres Pluto Haumea Makemake Eris]
+}
+

使用 make 函数对 slice 进行预分配

当 slice 的容量不足以执行 append 操作时,Go 必须创建新数组并复制旧数组中的内容

但通过内置的 make 函数,可以对 slice 进行预分配策略

  • 尽量避免额外的内存分配和数组重复操作
func dump(label string, slice []string) {
+  fmt.Printf("%v: length %v, capacity %v %v\\n", label, len(slice), cap(slice), slice)
+}
+
+func main() {
+  dwarfs := make([]string, 0, 10) // 预分配了一个长度为 0,容量为 10 的 slice。如果省略第三个参数,则第二个参数即规定长度也规定容量
+
+  dump("dwarfs", dwarfs) // dwarfs: length 0, capacity 10 []
+
+  dwarfs = append(dwarfs, "Ceres", "Pluto", "Haumea", "Makemake", "Eris")
+
+  dump("dwarfs", dwarfs) // dwarfs: length 5, capacity 10 [Ceres Pluto Haumea Makemake Eris]
+}
+

声明可变参数的函数

声明 Printf、append 这样的可变参数函数,需要在函数的最后一个参数前面加上 ... 符号

func terraform(prefix string, worlds ...string) []string {
+  newWorlds := make([]string, len(worlds))
+  for i := range worlds {
+    newWorlds[i] = prefix + " " + worlds[i]
+  }
+  return newWorlds
+}
+
+func main() {
+  twoWorlds := terraform("New", "Venus", "Mars")
+  fmt.Println(twoWorlds) // [New Venus New Mars]
+
+  planets := []string{"Venus", "Mars", "Jupiter"}
+  newPlanets := terraform("New", planets...)
+  fmt.Println(newPlanets) // [New Venus New Mars New Jupiter]
+}
+

map

map 是 Go 提供的另外一种集合

  • 它可以将 key 映射到 value
  • 它快速通过 key 找到对应的 value
  • 它的 key 几乎可以是任何类型

声明 map

声明 map 必须指定 key 和 value 的类型

temperature := map[string]int{
+"Earth": 15,
+"Mars": -65,
+}
+
+temp := temperature["Earth"]
+
+fmt.Println("On average the Earth is", temp, "Celsius.")
+
+temperature["Earth"] = 16
+temperature["Venus"] = 464
+
+fmt.Println(temperature) // map[Earth:16 Mars:-65 Venus:464]
+
+moon := temperature["Moon"]
+fmt.Println(moon) // 0
+

, 与 ok 写法

temperature := map[string]int{
+  "Earth": 15,
+  "Mars": -65,
+}
+
+temp, ok := temperature["Earth"]
+fmt.Println(temp, ok) // 15 true
+
+if moon, ok := temperature["Moon"]; ok {
+  fmt.Println(moon)
+} else {
+  fmt.Println("Where is the Moon?") // Where is the Moon?
+}
+

map 不会复制

数组、int、float64 等类型在赋值给新变量或传递至函数/方法时会创建相应的副本

但 map 不会

planets := map[string]string{
+  "Earth": "Sector ZZ9",
+  "Mars": "Sector ZZ9",
+}
+
+planetsMarkII := planets
+planets["Earth"] = "whoops"
+
+fmt.Println(planets) // map[Earth:whoops Mars:Sector ZZ9]
+fmt.Println(planetsMarkII) // map[Earth:whoops Mars:Sector ZZ9]
+
+delete(planets, "Earth")
+fmt.Println(planetsMarkII) // map[Mars:Sector ZZ9]
+

使用 make 函数对 map 进行预分配

除非使用复合字面值来初始化 map,否则必须使用内置的 make 函数来为 map 分配空间

创建 map 时,make 函数可以接收一个或两个参数

  • 第一个参数是 map 的类型
  • 第二个参数是可选的,用于指定 map 的初始容量(为指定数量的 key 预先分配空间)

使用 make 函数创建的 map 初始长度是 0

temperature := make(map[float64]int, 8)
+fmt.Println(len(temperature)) // 0
+

使用 map 作计数器

temperature := []float64{
+  -28.0, 32.0, -31.0, -29.0, -23.0, -29.0, -28.0, -33.0,
+}
+
+frequency := make(map[float64]int)
+
+for _, t := range temperature {
+  frequency[t]++
+}
+
+/* range 遍历 map 时是无法保证顺序的 */
+for t, num := range frequency {
+  fmt.Printf("%+.2f occurs %d times\\n", t, num)
+}
+

使用 map 和 slice 实现数据分组

temperature := []float64{
+  -28.0, 32.0, -31.0, -29.0, -23.0, -29.0, -28.0, -33.0,
+}
+
+groups := make(map[float64][]float64)
+
+for _, t := range temperature {
+  g := math.Trunc(t/10) * 10
+  groups[g] = append(groups[g], t)
+}
+
+for g, temperatures := range groups {
+  fmt.Printf("%v: %v\\n", g, temperatures)
+}
+

将 map 用作 set

Set 这种集合与数组类似,但元素不会重复

Go 语言里没有提供 set 集合

但可以使用 map 来实现 set 集合

var temperatures = []float64{
+  -28.0, 32.0, -31.0, -29.0, -23.0, -29.0, -28.0, -33.0,
+}
+
+/* 去重 */
+
+set := make(map[float64]bool)
+
+for _, t := range temperatures {
+  set[t] = true
+}
+
+if set[-28.0] {
+  fmt.Println("set member") // set member
+}
+
+fmt.Println(set)
+
+/* 排序 */
+
+unique := make([]float64, 0, len(set))
+
+for t := range set {
+  unique = append(unique, t)
+}
+
+sort.Float64s(unique)
+
+fmt.Println(unique) // [-33 -31 -29 -28 -23 32]
+

结构 struct

为了将分散的零件组成一个完整的结构体,Go 提供了 struct 类型

声明结构

var curiosity struct {
+  lat float64
+  long float64
+}
+
+curiosity.lat = -4.5895
+curiosity.long = 137.4417
+
+fmt.Println(curiosity.lat, curiosity.long) // -4.5895 137.4417
+fmt.Println(curiosity) // { -4.5895 137.4417}
+

通过类型复用结构体

type location struct {
+  lat float64
+  long float64
+}
+
+var spirit location
+spirit.lat = -14.5684
+spirit.long = 175.472636
+
+/* 通过成对的字段和值进行初始化 */
+opportunity := location{lat: -1.9462, long: 354.4734}
+
+/* 按照字段声明的顺序初始化 */
+insight := location{-4.5, 135.9}
+
+fmt.Printf("%v\\n", insight) // {-4.5 135.9}
+fmt.Printf("%+v\\n", insight) // {lat:-4.5 long:135.9}
+
+fmt.Println(spirit, opportunity) // {-14.5684 175.472636} {-1.9462 354.4734}
+

struct 的复制

type location struct {
+  lat, long float64
+}
+
+bradbury := location{-4.5895, 137.4417}
+curiosity := bradbury // 两个不同的实例
+
+curiosity.long += 0.0106
+
+fmt.Println(bradbury, curiosity) // {-4.5895 137.4417} {-4.5895 137.4523}
+

由结构体组成的 slice

type location struct {
+  lat, long float64
+  name string
+}
+
+lats := []float64{-4.5895, -14.5684, -1.9462}
+longs := []float64{137.4417, 175.472636, 354.4734}
+
+locations := []location{
+  {lat: -4.5895, long: 137.4417, name: "Bradbury Landing"},
+  {lat: -14.5684, long: 175.472636, name: "Columbia Memorial Station"},
+  {lat: -1.9462, long: 354.4734, name: "Challenger Memorial Station"},
+}
+
+fmt.Println(locations) // [{-4.5895 137.4417 Bradbury Landing} {-14.5684 175.472636 Columbia Memorial Station} {-1.9462 354.4734 Challenger Memorial Station}]
+

将 struct 编码为 JSON

JSON (JavaScript Object Notation,JavaScript 对象表示法)

常用于 Web API

json 包中的 Marshal 函数可以将 struct 编码为 JSON

type location struct {
+  Lat, Long float64
+  // lat, long float64
+}
+
+func main() {
+  curiosity := location{-4.5895, 137.4417}
+
+  bytes, err := json.Marshal(curiosity)
+  exitOnError(err)
+
+  fmt.Println(string(bytes)) // {"lat":-4.5895,"long":137.4417}
+}
+
+func exitOnError(err error) {
+  if err != nil {
+    fmt.Println(err)
+    os.Exit(1)
+  }
+}
+

Marshal 函数只会编码 struct 中被导出的字段(首字母大写)

使用 struct 标签来定义 JSON

Go 语言中 json 包要求 struct 中的字段必须以大写字母开头(类似 CamelCase 大驼峰),但如果需要 snake_case 蛇形命名规范,可以为字段注明标签,使得 json 包在进行编码的时候能够按照标签里的样式修改字段名


+type location struct {
+  Lat float64 \`json:"latitude"xml:"latitude"\`
+  Long float64 \`json:"longitude"\`
+}
+
+func main() {
+  curiosity := location{-4.5895, 137.4417}
+
+  bytes, err := json.Marshal(curiosity)
+  exitOnError(err)
+
+  fmt.Println(string(bytes)) // {"latitude":-4.5895,"longitude":137.4417}
+}
+
+func exitOnError(err error) {
+  if err != nil {
+    fmt.Println(err)
+    os.Exit(1)
+  }
+}
+

Go 语言里没有 class

Go 和其他经典语言不同,它没有 class,没有对象,也没有继承

但是 Go 提供了 struct 和方法

type coordinate struct {
+  d, m, s float64
+  h rune
+}
+
+func (c coordinate) decimal() float64 {
+  sign := 1.0
+
+  switch c.h {
+  case 'S', 'W', 's', 'w':
+    sign = -1
+  }
+
+  return sign * (c.d + c.m / 60 + c.s / 3600)
+}
+
+func main() {
+  lat := coordinate{4, 35, 22.2, 'S'}
+  long := coordinate{137, 26, 30.12, 'E'}
+  fmt.Println(lat.decimal(), long.decimal()) // -4.5895 137.4417
+}
+

构造函数

可以使用 struct 复合字面值来初始化想要的数据;但如果 struct 初始化的时候还有做很多事情,那就可以考虑写一个构造用的函数:

type coordinate struct {
+  d, m, s float64
+  h rune
+}
+
+func (c coordinate) decimal() float64 {
+  sign := 1.0
+
+  switch c.h {
+  case 'S', 'W', 's', 'w':
+    sign = -1
+  }
+
+  return sign * (c.d + c.m / 60 + c.s / 3600)
+}
+
+type location struct {
+  lat, long float64
+}
+
+/* new/New 开头,后面跟一个类型的名称,通常就代表这个类型的构造函数(约定) */
+func newLocation(lat, long coordinate) location {
+  return location{lat.decimal(), long.decimal()}
+}
+
+

Go 语言没有专门的构造函数,但以 new 或者 New 开头的函数,通常是用来构造数据的

有一些构造函数的名称就是 New(例如 errors 包里面的 New 函数),errors.New()

class 的替代方案

Go 语言中没有 class,但是可以使用 struct 和方法来实现类似的功能

type location struct {
+  lat, long float64
+}
+
+type world struct {
+  radius float64
+}
+
+func rad(deg float64) float64 {
+  return deg * math.Pi / 180
+}
+
+func (w world) distance(p1, p2 location) float64 {
+  s1, c1 := math.Sincos(rad(p1.lat))
+  s2, c2 := math.Sincos(rad(p2.lat))
+  clong := math.Cos(rad(p1.long - p2.long))
+  return w.radius * math.Acos(s1 * s2 + c1 * c2 * clong)
+}
+
+func main() {
+  mars := world{radius: 3389.5}
+  spirit := location{-14.5684, 175.472636}
+  opportunity := location{-1.9462, 354.4734}
+  fmt.Printf("%.2f km\\n", mars.distance(spirit, opportunity)) // 9669.71 km
+}
+

组合与转发(composition & forwarding)

Go 通过结构体实现组合

Go 提供了“嵌入”(embedding)特性,它可以实现方法的转发(forwarding)

组合相对于继承更简单、灵活

// type report struct {
+// 	sol         int
+// 	temperature temperature
+// 	location    location
+// }
+
+type sol int
+
+type report struct {
+	sol
+	temperature
+	location
+}
+
+type temperature struct {
+	high, low celsius
+}
+
+type location struct {
+	lat, long float64
+}
+
+type celsius float64
+
+func (t temperature) average() celsius {
+	return (t.high + t.low) / 2
+}
+
+func (s sol) days(s2 sol) int {
+	days := int(s2 - s)
+	if days < 0 {
+		return -days
+	}
+	return days
+}
+
+// func (r report) average() celsius {
+// 	return r.temperature.average()
+// }
+
+func main() {
+	t := temperature{high: -1.0, low: -78.0}
+	fmt.Printf("%v\\n", t.average()) // -39.5
+	loc := location{-4.5895, 137.4417}
+	rep := report{sol: 15, temperature: t, location: loc}
+
+	fmt.Println(rep.temperature.average()) // -39.5
+	fmt.Println(rep.average())             // -39.5
+	fmt.Println(rep.high)                  // -1
+
+	fmt.Println(rep.sol.days(1446)) // 1431
+	fmt.Println(rep.days(1446))     // 1431
+
+	fmt.Printf("%+v\\n", rep) // {sol:15 temperature:{high:-1 low:-78} location:{lat:-4.5895 long:137.4417}}
+}
+

Go 可以通过 struct 嵌入来实现方法的转发

在 struct 中只给定字段类型,不给定字段名即可

在 struct 中可以转发任意类型

Go 语言中,如果两个字段名字相同,那么在访问的时候就必须使用完整的路径

接口

类型关注于可以做什么,而不是存储了什么

接口通过列举类型必须满足的一组方法来进行声明

在 Go 语言中,不需要显示声明接口

var t interface {
+  talk() string
+}
+
+type martian struct {}
+
+func (m martian) talk() string {
+  return "nack nack"
+}
+
+type laser int
+
+func (l laser) talk() string {
+  return strings.Repeat("pew ", int(l))
+}
+
+func main() {
+  t = martian{}
+  fmt.Println(t.talk()) // nack nack
+
+  t = laser(3)
+  fmt.Println(t.talk()) // pew pew pew
+}
+

为了复用,通常会把接口声明为类型

按约定,接口名称通常以 er 结尾

type talker interface {
+  talk() string
+}
+
+type martian struct {}
+
+func (m martian) talk() string {
+  return "nack nack"
+}
+
+type laser int
+
+func (l laser) talk() string {
+  return strings.Repeat("pew ", int(l))
+}
+
+func shout(t talker) {
+  louder := strings.ToUpper(t.talk())
+  fmt.Println(louder)
+}
+
+/* 接口配合 struct 嵌入特性一起使用 */
+type starship struct {
+  laser
+}
+
+func main() {
+   s := starship{laser(3)}
+
+  shout(martian{}) // NACK NACK
+  shout(laser(2)) // PEW PEW
+  shout(s) // PEW PEW PEW
+}
+

同时使用组合和接口将构成非常强大的设计工具

满足接口

Go 语言的接口都是隐式满足的

Go 标准库导出了很多只有单个方法的接口

例如 fmt 包声明的 Stringer 接口

type Stringer interface {
+  String() string
+}
+
type location struct {
+  lat, long float64
+}
+
+func (l location) String() string {
+  return fmt.Sprintf("%v, %v", l.lat, l.long)
+}
+
+func main() {
+  curiosity := location{-4.5895, 137.4417}
+  fmt.Println(curiosity) // -4.5895, 137.4417
+}
+

标准库中常用的接口还包括:io.Reader、io.Writer、http.Handler、json.Marshaler 等

指针

指针是指向另一个变量地址的变量

Go 语言的指针同时强调安全性,不会出现迷途指针(dangling pointers)

变量会将它们的值存储在计算机 RAM 里,存储位置就是该变量的内存地址

& 表示地址操作符,通过 & 可以获得变量的内存地址

func main() {
+  answer := 42
+  fmt.Println(&answer) // 0xc0000140a8 类似的一个地址
+}
+

& 操作符无法获得字符串/数值/布尔字面值的地址,&42,&"hello" 都会导致编译器报错

* 操作符与 & 的作用相反,它用来解引用,提供内存地址指向的值

answer := 42
+/* Go 语言不允许 address++ 这样的指针运算进行操作 */
+address := &answer
+fmt.Println(*address) // 42
+fmt.Printf("%T\\n", address) // *int
+

指针存储的是内存地址

指针类型和其他普通类型一样,出现在所有需要用到类型的地方,如变量声明、函数形参、返回值类型、结构体字段等

指针类型

canada := "Canada"
+var home *string
+fmt.Printf("home is a %T\\n", home) // home is a *string
+home = &canada
+fmt.Println(*home) // Canada
+

将 * 放在类型前面,表示声明一个指针类型

将 * 放在变量前面,表示解引用操作,获取指针指向的值

两个指针变量指向同一个内存地址,那么它们就是相等的

指向结构的指针

与字符串和数值不一样,复合字面量的前面可以放置 &

type person struct {
+  name, superpower string
+  age int
+}
+
+timmy := &person{
+  name: "Timothy",
+  age: 10,
+}
+
+timmy.superpower = "flying" // 等价于 (*timmy).superpower = "flying"
+
+fmt.Printf("%+v\\n", timmy) // &{name:Timothy superpower:flying age:10}
+

访问字段时,对结构体进行解引用并不是必须的

指向数组的指针

和结构体一样,可以把 & 放在数组的复合字面值前面来创建指向数组的指针

superpowers := &[3]string{"flight", "invisibility", "super strength"}
+
+fmt.Println(superpowers[0]) // flight
+fmt.Println(superpowers[1:2]) // [invisibility]
+

数组在执行索引或切片操作时,会自动解引用,没有必要写 (*superpowers)[0] 这种形式

Go 里面数组和指针是两种完全独立的类型

slice 和 map 的复合字面值前面也可以放置 & 操作符,但是 Go 并没有为它们提供自动解引用的功能

实现修改

Go 语言的函数和方法都是按值传递参数的,这意味着函数总是操作于被传递参数的副本

当指针被传递到函数时,函数将接受传入的内存地址的副本。之后函数可以通过解引用内存地址来修改指针指向的值

type person struct {
+  name string
+  age int
+}
+
+func birthday(p *person) {
+  p.age++
+}
+
+func main() {
+  timmy := &person{
+    name: "Timothy",
+    age: 10,
+  }
+
+  birthday(timmy)
+  fmt.Printf("%+v\\n", timmy) // &{name:Timothy superpower: age:11}
+}
+

指针接收者

方法的接收者和方法的参数在处理指针方面是很相似的

type person struct {
+  name string
+  age int
+}
+
+func (p *person) birthday() {
+  p.age++
+}
+
+func main() {
+  timmy := &person{
+    name: "Timothy",
+    age: 10,
+  }
+
+  timmy.birthday()
+  fmt.Printf("%+v\\n", timmy) // &{name:Timothy superpower: age:11}
+
+  /* Go 语言在变量通过点标记法进行调用的时候,自动使用 & 取得变量的内存地址 */
+  nathan := person{"Nathan", 18}
+  nathan.birthday() // (&nathan).birthday()
+  fmt.Printf("%+v\\n", nathan) // {name:Nathan superpower: age:19}
+}
+

使用指针作为接收者的策略应该始终如一:如果一种类型的某些方法需要用到指针作为接收者,这种类型的所有方法就应该都是用指针作为接收者

内部指针

Go 提供了内部指针这种特性,它用于确定结构体中指定字段的内存地址

type stats struct {
+  level int
+  endurance, health int
+}
+
+func levelUp(s *stats) {
+  s.level++
+  s.endurance = 42 + (14 * s.level)
+  s.health = 5 * s.endurance
+}
+
+type character struct {
+  name string
+  stats stats
+}
+
+func main() {
+  player := character{name: "Matthias"}
+  levelUp(&player.stats) // & 操作符不仅可以获得结构体的内存地址,还可以获得结构体中指定字段的内存地址
+  fmt.Printf("%+v\\n", player) // {name:Matthias stats:{level:1 endurance:56 health:280}}
+}
+

修改数组

函数通过指针对数组的元素进行修改

func reset(board *[8][8]rune) {
+  board[0][0] = 'r'
+}
+
+func main() {
+  var board [8][8]rune
+  reset(&board)
+
+  fmt.Printf("%c", board[0][0]) // r
+}
+

隐式的指针

Go 语言里的一些内置的集合类型就在暗中使用指针

  • map 在被赋值或者作为参数传递的时候不会被复制

    • func demolish(planets *map[string]string) 这种写法就是多此一举
    • map 的键和值都可以是指针类型
    • 需要将指针指向 map 的情况不多见
  • slice 在指向数组元素的时候也使用了指针

    • 每个 slice 内部都会被表示为一个包含 3 个元素的结构,它们分别指向数组的指针、slice 的长度和 slice 的容量
    • 当 slice 被直接传递至函数或方法时,slice 的内部指针就可以对底层数据进行修改
    • 指向 slice 的显示指针的唯一作用就是修改 slice 本身,slice 的长度、容量以及起始偏移量
func reclassify(planets *[]string) {
+  *planets = (*planets)[0:8]
+}
+
+func main() {
+  planets := []string{
+    "Mercury", "Venus", "Earth", "Mars",
+    "Jupiter", "Saturn", "Uranus", "Neptune", "Pluto",
+  }
+
+  reclassify(&planets)
+  fmt.Println(planets) // [Mercury Venus Earth Mars Jupiter Saturn Uranus Neptune]
+}
+

指针和接口

type talker interface {
+  talk() string
+}
+
+func shout(t talker) {
+  fmt.Println(strings.ToUpper(t.talk()))
+}
+
+type martian struct {}
+
+func (m martian) talk() string {
+  return "nack nack"
+}
+
+func main() {
+  /* 无论是 martian 还是指向 martian 的指针,都可以满足 talker 接口 */
+  shout(martian{}) // NACK NACK
+  shout(&martian{}) // NACK NACK
+}
+

上例中无论是 martian 还是指向 martian 的指针,都可以满足 talker 接口

如果方法使用的是指针接收者,那么情况会有所不同

type talker interface {
+  talk() string
+}
+
+func shout(t talker) {
+  fmt.Println(strings.ToUpper(t.talk()))
+}
+
+type laser int
+
+func (l *laser) talk() string {
+  return strings.Repeat("pew ", int(*l))
+}
+
+func main() {
+  laser := laser(2)
+  shout(&laser) // PEW PEW
+  shout(laser) // cannot use laser (type laser) as type talker in argument to shout: laser does not implement talker (talk method has pointer receiver)
+}
+

明智的使用指针

应合理使用指针,不要过度使用

nil

nil 是一个名词,表示“无”或者“零”

在 Go 里,nil 是一个零值

如果一个指针没有明确的指向,那么它的值就是 nil

除了指针,nil 还是 slice 、map、channel、interface 和函数的零值

Go 语言的 nil 比以往语言的 null 更为友好,并且用的没那么频繁,但是仍需谨慎使用

nil 会导致 panic

如果指针没有明确的指向,那么程序将无法对其实施解引用

尝试解引用一个 nil 指针将导致程序崩溃

var nowhere *int
+fmt.Println(nowhere) // <nil>
+fmt.Println(*nowhere) // panic: runtime error: invalid memory address or nil pointer dereference
+

避免 nil 引发 panic

type person struct {
+  age int
+}
+
+func (p *person) birthday() {
+  // 避免 nil 引发 panic
+  if p == nil {
+    return
+  }
+  p.age++
+}
+
+func main() {
+  var nobody *person
+  nobody.birthday()
+}
+

因为值为 nil 的接收者和值为 nil 的参数在行为上并没有区别,所以 Go 语言即使在接收者为 nil 的情况下,也会继续调用方法

nil 函数值

当变量被声明为函数类型时,它的默认值是 nil

var fn func(a, b int) int
+fmt.Println(fn == nil) // true
+

检查函数值是否为 nil,并在有需要时提供默认行为

nil slice

如果 slice 在声明之后没有使用复合字面值或内置的 make 函数进行初始化,那么它的值就是 nil

幸运的是,range\\len\\append 等内置函数都可以安全地处理值为 nil 的 slice

var soup []string
+fmt.Println(soup == nil) // true
+
+for _, ingredient := range soup {
+  fmt.Println(ingredient) // 不会执行
+}
+
+fmt.Println(len(soup)) // 0
+
+soup = append(soup, "onion", "carrot", "celery")
+fmt.Println(soup) // [onion carrot celery]
+

虽然空 slice 和值为 nil 的 slice 并不相等,但它们通常可以替换使用

nil map

和 slice 一样,如果 map 在声明之后没有使用复合字面值或内置的 make 函数进行初始化,那么它的值就是 nil

var soup map[string]int
+fmt.Println(soup == nil) // true
+
+measurements, ok := soup["onion"]
+if ok {
+  fmt.Println(measurements) // 不会执行
+}
+
+for ingredient, measurement := range soup {
+  fmt.Println(ingredient, measurement) // 不会执行
+}
+

nil 接口

声明为接口类型的变量在未被赋值时,它的零值是 nil

对于一个未被赋值的接口变量来说,它的接口类型和值都是 nil,并且变量本身也等于 nil

var v interface{}
+fmt.Printf("%T %v %v\\n", v, v, v == nil) // <nil> <nil> true
+

当接口类型的变量被赋值后,接口就会在内部指向该变量的类型和值

var v interface{}
+fmt.Printf("%T %v %v\\n", v, v, v == nil) // <nil> <nil> true
+var p *int
+v = p
+fmt.Printf("%T %v %v\\n", v, v, v == nil) // *int <nil> false
+
+// 检验接口变量的内部表示
+fmt.Printf("%#v\\n", v) // (*int)(nil)
+

在 Go 中,接口类型的变量只有在类型和值都为 nil 时才等于 nil

nil 之外的另一个选择

type number struct {
+  value int
+  valid bool
+}
+
+func newNumber(v int) number {
+  return number{value: v, valid: true}
+}
+
+func (n number) String() string {
+  if !n.valid {
+    return "未知"
+  }
+  return strconv.Itoa(n.value)
+}
+
+func main() {
+  n := newNumber(42)
+  fmt.Println(n) // 42
+  n = number{}
+  fmt.Println(n) // 未知
+}
+

错误

处理错误

Go 语言允许函数和方法同时返回多个值

按照惯例,函数在返回错误时,最后边的返回值应用来表示错误

调用函数后,应立即检查是否发生错误

  • 如果没有错误发生,那么返回的错误值为 nil
files, err := ioutil.ReadDir(".")
+if err != nil {
+  fmt.Println(err)
+  os.Exit(1)
+}
+for _, file := range files {
+  fmt.Println(file.Name())
+}
+

注意:当错误发生时,函数返回的其他值通常就不再可信

优雅的错误处理

减少错误处理代码的一种策略是:将程序中不会出错的部分和包含潜在错误隐患的部分隔离开来

对于不得不返回错误的代码,应尽力简化相应的错误处理代码

文件写入

写入文件的时候可能出错:

  • 路径不正确
  • 权限不够
  • 磁盘空间不足
  • ...

文件写入完毕后,必须被关闭,确保文件被刷到磁盘上,避免资源的泄露

// 内置类型 error 用来表示错误
+func proverbs(name string) error {
+  f, err := os.Create(name)
+  if err != nil {
+    return err
+  }
+
+  _, err = fmt.Fprintln(f, "Errors are values.")
+  if err != nil {
+    f.Close()
+    return err
+  }
+
+  _, err = fmt.Fprintln(f, "Don't just check errors, handle them gracefully.")
+  f.Close()
+  return err
+}
+
+func main() {
+  err := proverbs("proverbs.txt")
+  if err != nil {
+    fmt.Println(err)
+    os.Exit(1)
+  }
+}
+

defer 关键字

使用 defer 关键字,Go 可以确保所有 deferred 的动作可以在函数返回前执行

可以 defer 任意的函数和方法

func proverbs(name string) (err error) {
+  f, err := os.Create(name)
+  if err != nil {
+    return err
+  }
+  defer f.Close()
+
+  _, err = fmt.Fprintln(f, "Errors are values.")
+  if err != nil {
+    return err
+  }
+
+  _, err = fmt.Fprintln(f, "Don't just check errors, handle them gracefully.")
+  return err
+}
+

defer 并不是专门做错误处理的

defer 可以消除必须时刻惦记执行资源释放的负担

有创意的错误处理

type safeWriter struct {
+	w   io.Writer
+	err error
+}
+
+func (sw *safeWriter) writeln(s string) {
+	if sw.err != nil {
+		return
+	}
+	_, sw.err = fmt.Fprintln(sw.w, s)
+}
+
+func proverbs(name string) error {
+	f, err := os.Create(name)
+	if err != nil {
+		return err
+	}
+	defer f.Close()
+
+	sw := safeWriter{w: f}
+	sw.writeln("Errors are values.")
+	sw.writeln("Don't just check errors, handle them gracefully.")
+	sw.writeln("Don't panic.")
+	sw.writeln("Make the zero value useful.")
+	sw.writeln("The bigger the interface, the weaker the abstraction.")
+	sw.writeln("interface{} says nothing.")
+	sw.writeln("Gofmt's style is no one's favorite, yet gofmt is everyone's favorite.")
+	sw.writeln("Documentation is for users.")
+	sw.writeln("A little copying is better than a little dependency.")
+	sw.writeln("Clear is better than clever.")
+	sw.writeln("Concurrency is not parallelism.")
+	sw.writeln("Don't communicate by sharing memory, share memory by communicating.")
+	sw.writeln("Channels orchestrate; mutexes serialize.")
+
+	return sw.err
+}
+

New error

errors 包里有一个构造用的 New 函数,它接收 string 来作为参数用来表示错误信息,该函数返回 error 类型

const rows, columns = 9, 9
+
+// Sudoku 数独
+type Grid [rows][columns]int8
+
+// Set
+func (g *Grid) Set(row, column int, digit int8) error {
+  if !inBounds(row, column) {
+    return errors.New("out of bounds")
+  }
+  g[row][column] = digit
+  return nil
+}
+
+func inBounds(row, column int) bool {
+  if row < 0 || row >= rows {
+    return false
+  }
+  if column < 0 || column >= columns {
+    return false
+  }
+  return true
+}
+
+func main() {
+  var g Grid
+  err := g.Set(10, 0, 5)
+  if err != nil {
+    fmt.Printf("An error occurred: %v.\\n", err)
+    os.Exit(1)
+  }
+}
+

错误信息应具有信息性

可以把错误信息当作用户界面的一部分,无论对最终用户还是开发者

按需返回错误

按照惯例,包含错误信息的变量名应以 Err 开头

const rows, columns = 9, 9
+
+// Sudoku 数独
+type Grid [rows][columns]int8
+
+var(
+  // ErrBounds 表示数字越界
+  ErrBounds = errors.New("out of bounds")
+  // ErrDigit 表示数字无效
+  ErrDigit = errors.New("invalid digit")
+)
+
+// Set
+func (g *Grid) Set(row, column int, digit int8) error {
+  if !inBounds(row, column) {
+    return ErrBounds
+  }
+  g[row][column] = digit
+  return nil
+}
+
+func inBounds(row, column int) bool {
+  if row < 0 || row >= rows {
+    return false
+  }
+  if column < 0 || column >= columns {
+    return false
+  }
+  return true
+}
+
+func main() {
+  var g Grid
+  err := g.Set(10, 0, 5)
+  if err != nil {
+    switch err {
+    case ErrBounds, ErrDigit:
+      fmt.Println("error!!!")
+    default:
+      fmt.Println("unknown error")
+    }
+    os.Exit(1)
+  }
+}
+

errors.New 这个构造函数是使用指针实现的,所以上例 switch 语句比较的是内存地址,而不是错误包含的文字信息

自定义错误类型

error 类型是一个内置的接口:任何类型只要实现了返回 string 的 Error() 方法就满足了该接口

可以创建新的错误类型

const rows, columns = 9, 9
+
+// Sudoku 数独
+type Grid [rows][columns]int8
+
+var (
+	// ErrBounds 表示数字越界
+	ErrBounds = errors.New("out of bounds")
+	// ErrDigit 表示数字无效
+	ErrDigit = errors.New("invalid digit")
+)
+
+type SudokuError []error
+
+func (se SudokuError) Error() string {
+	var s []string
+	for _, e := range se {
+		s = append(s, e.Error())
+	}
+	return strings.Join(s, ", ")
+}
+
+// Set
+func (g *Grid) Set(row, column int, digit int8) error {
+	var errs SudokuError
+	if !inBounds(row, column) {
+		errs = append(errs, ErrBounds)
+	}
+	if !validDigit(digit) {
+		errs = append(errs, ErrDigit)
+	}
+
+	if len(errs) > 0 {
+		return errs
+	}
+
+	g[row][column] = digit
+	return nil
+}
+
+func inBounds(row, column int) bool {
+	if row < 0 || row >= rows {
+		return false
+	}
+	if column < 0 || column >= columns {
+		return false
+	}
+	return true
+}
+
+func validDigit(digit int8) bool {
+	return digit >= 1 && digit <= 9
+}
+
+func main() {
+	var g Grid
+	err := g.Set(10, 0, 10)
+	if err != nil {
+		switch err {
+		case ErrBounds, ErrDigit:
+			fmt.Println("error!!!")
+		default:
+			fmt.Println("unknown error:", err)
+		}
+		os.Exit(1)
+	}
+}
+

按照惯例,自定义错误类型的名字应以 Error 结尾 有时候名字就是 Error,例如 url.Error

类型断言

上例中,可以使用类型断言来访问每一种错误

使用类型断言,可以把接口类型转化成底层的具体类型,例如 err.(SudokuError)

const rows, columns = 9, 9
+
+// Sudoku 数独
+type Grid [rows][columns]int8
+
+var (
+	// ErrBounds 表示数字越界
+	ErrBounds = errors.New("out of bounds")
+	// ErrDigit 表示数字无效
+	ErrDigit = errors.New("invalid digit")
+)
+
+type SudokuError []error
+
+func (se SudokuError) Error() string {
+	var s []string
+	for _, e := range se {
+		s = append(s, e.Error())
+	}
+	return strings.Join(s, ", ")
+}
+
+// Set
+func (g *Grid) Set(row, column int, digit int8) error {
+	var errs SudokuError
+	if !inBounds(row, column) {
+		errs = append(errs, ErrBounds)
+	}
+	if !validDigit(digit) {
+		errs = append(errs, ErrDigit)
+	}
+
+	if len(errs) > 0 {
+		return errs
+	}
+
+	g[row][column] = digit
+	return nil
+}
+
+func inBounds(row, column int) bool {
+	if row < 0 || row >= rows {
+		return false
+	}
+	if column < 0 || column >= columns {
+		return false
+	}
+	return true
+}
+
+func validDigit(digit int8) bool {
+	return digit >= 1 && digit <= 9
+}
+
+func main() {
+	var g Grid
+	err := g.Set(10, 0, 10)
+	if err != nil {
+		/* 相较于上例,只变动此处 */
+    if errs, ok := err.(SudokuError); ok {
+      fmt.Printf("%d error(s) occurred:\\n", len(errs))
+      for _, e := range errs {
+        fmt.Printf("- %v\\n", e)
+      }
+    }
+		os.Exit(1)
+	}
+}
+

如果类型满足多个接口,那么类型断言可使它从一个接口类型转化为另一个接口类型

如何 panic

Go 里有一个和其他语言异常类似的机制:panic

实际上,panic 很少出现

创建 panic:调用内置的 panic 函数

panic("invalid operation") // panic 的参数可以是任意类型
+

错误值、panic、os.Exit?

通常,更推荐使用错误值,其次才是 panic

panic 比 os.Exit() 更好:panic 后会执行所有 defer 操作,而 os.Exit() 不会

有时候 Go 程序会 panic 而不是返回错误值

var zero int
+fmt.Println(1 / zero) // panic: runtime error: integer divide by zero
+

保持冷静并继续

为了防止 panic 导致程序崩溃,Go 提供了 recover 函数

defer 的动作会在函数返回前执行,即使发生了 panic

但如果 defer 的函数调用了 recover,panic 就会停止,程序将继续运行

defer func() {
+  if err := recover(); err != nil {
+    log.Printf("run time panic: %v", err) // 2023/09/10 11:23:52 run time panic: I forgot my towel
+  }
+}()
+panic("I forgot my towel")
+

goroutine 和 并发(concurrent)

goroutine

在 Go 中,独立的任务叫做 goroutine

  • goroutine 和其他语言的协程、进程、线程都有相似之处,但并不完全相同
  • goroutine 创建效率非常高
  • Go 能直截了当地协同多个并发(concurrent)操作

在 Go 里,无需修改现有顺序式的代码,就可以通过 goroutine 以并发的方式运行任意数量的任务

启动 goroutine

只需在调用前加一个 go 关键字,就可以让函数/方法以 goroutine 方式运行

func sleepyGopher() {
+  time.Sleep(3 * time.Second)
+  fmt.Println("... snore ...")
+}
+
+func main() {
+  /* 分支线路 */
+  go sleepyGopher()
+  /* 主线路 */
+  fmt.Println("i'm waiting")
+  time.Sleep(4 * time.Second)
+}
+

不止一个 goroutine

每次使用 go 关键字都会产生一个新的 goroutine

表面上看,goroutine 似乎在同时运行,但由于计算机处理单元有限,其实技术上来说,这些 goroutine 不是真的在同时运行

  • 计算机处理器会使用“分时”技术,在多个 goroutine 上轮流花费一些时间
  • 在使用 goroutine 时,各个 goroutine 的执行顺序无法确定
func sleepyGopher(id int) {
+  time.Sleep(3 * time.Second)
+  fmt.Println("... snore ...", id)
+}
+
+func main() {
+  for i := 0; i < 5; i++ {
+    go sleepyGopher(i)
+  }
+  time.Sleep(4 * time.Second)
+}
+

通道 channel

channel 可以在多个 goroutine 之间安全地传值

通道可以用作变量、函数参数、结构体字段...

创建通道用 make 函数,并指定其传输数据的类型 c := make(chan int)

通道 channel 发送、接收

使用左箭头操作符 <- 向通道发送值或从通道接收值

  • 发送值:c <- 1
  • 接收值:r := <- c

发送操作会等待直到另一个 goroutine 尝试对该通道进行接收操作为止

  • 执行发送操作的 goroutine 在等待期间将无法执行其他操作
  • 未在等待通道操作的 goroutine 可以继续自由地运行

执行接收操作的 goroutine 将等待直到另一个 goroutine 尝试向该通道进行发送操作为止

func main() {
+  c := make(chan int)
+  for i := 0; i < 5; i++ {
+    go sleepyGopher(i, c)
+  }
+  for i := 0; i < 5; i++ {
+    gopherID := <-c
+    fmt.Println("gopher", gopherID, "has finished sleeping")
+  }
+}
+
+func sleepyGopher(id int, c chan int) {
+  time.Sleep(3 * time.Second)
+  fmt.Println("... snore ...", id)
+  c <- id
+}
+

使用 select 处理多个通道

等待不同类型的值

time.After 函数,返回一个通道,该通道在指定时间后会接收到一个值(发送该值的 goroutine 是 Go 运行时的一部分)

select 和 switch 有点像

  • 该语句包含的每个 case 都持有一个通道,用来发送或接收数据
  • select 会等待直到某个 case 分支的操作就绪,然后就会执行该 case 的分支
func main() {
+  c := make(chan int)
+  for i := 0; i < 5; i++ {
+    go sleepyGopher(i, c)
+  }
+  timeout := time.After(2 * time.Second)
+  for i := 0; i < 5; i++ {
+    select {
+    case gopherID := <-c:
+      fmt.Println("gopher", gopherID, "has finished sleeping")
+    case <-timeout:
+      fmt.Println("my patience ran out")
+      return
+    }
+  }
+}
+
+func sleepyGopher(id int, c chan int) {
+  time.Sleep(time.Duration(rand.Intn(4000)) * time.Millisecond) // 0~4s
+  c <- id
+}
+

注意:

即使已经停止等待 goroutine,但只要 main 函数还没返回,仍在运行的 goroutine 将会继续占用内存

select 语句在不包含任何 case 的情况下将永远等下去

nil 通道

如果不使用 make 初始化通道,那么通道变量的值就是 nil(零值)

对 nil 通道进行发送或接收不会引起 panic,但会导致永久阻塞

对 nil 哦那个到执行 close 函数,会引发 panic

nil 通道的用处:

  • 对于包含 select 语句的循环,如果不希望每次循环都等待 select 所涉及的所有的通道,那么就可以先将某些通道设为 nil,等到发送值准备就绪之后,再将通道变成一个非 nil 值并执行发送操作

阻塞和死锁

当 goroutine 在等待通道的发送或接收时,就说它被阻塞了

除了 goroutine 本身占用少量的内存外,被阻塞的 goroutine 并不消耗任何其他资源

当一个或多个 goroutine 因为某些永远无法发生的事情被阻塞时,则称这种情况为死锁。而出现死锁的程序通常会崩溃或挂起

引发死锁的例子:

func main() {
+  c := make(chan int)
+  /* 下面这行代码可以解除死锁 */
+  // go func () {c <- 2}
+  <- c // fatal error: all goroutines are asleep - deadlock!
+}
+

地鼠装配线

图片

func sourceGopher(upstream chan string) {
+	for _, v := range []string{"hello world", "a bad apple", "goodbye all"} {
+		upstream <- v
+	}
+	close(upstream)
+}
+
+func filterGopher(downstream, upstream chan string) {
+	for {
+		item, ok := <- downstream
+		if !ok {
+			close(upstream)
+			break
+		}
+		if !strings.Contains(item, "bad") {
+			upstream <- item
+		}
+	}
+}
+
+func PrintGopher(downstream chan string) {
+	for {
+		v, ok := <- downstream
+		if !ok {
+			break
+		}
+		fmt.Println(v)
+	}
+}
+
+func main() {
+	c0 := make(chan string)
+	c1 := make(chan string)
+	go sourceGopher(c0)
+	go filterGopher(c0, c1)
+	PrintGopher(c1)
+}
+

Go 允许在没有值可供发送的情况下通过 close 函数关闭通道,例如 close(c)

通道被关闭后无法写入任何值,如果尝试写入将引发 panic

尝试读取被关闭的通道会获得与通道类型对应的零值

注意:如果循环里读取一个已关闭的通道,并没检查通道是否关闭,那么该循环可能会一直运转下去,耗费大量 CPU 时间

执行以下代码可知通道是否被关闭

  • v, ok := <-c

常用模式

从通道里面读取值,直到它关闭为止

  • 可以使用 range 关键字达到该目的
func sourceGopher(upstream chan string) {
+	for _, v := range []string{"hello world", "a bad apple", "goodbye all"} {
+		upstream <- v
+	}
+	close(upstream)
+}
+
+func filterGopher(downstream, upstream chan string) {
+  for item := range downstream {
+    if !strings.Contains(item, "bad") {
+      upstream <- item
+    }
+  }
+  close(upstream)
+}
+
+func PrintGopher(downstream chan string) {
+  for v := range downstream {
+    fmt.Println(v)
+  }
+}
+
+func main() {
+	c0 := make(chan string)
+	c1 := make(chan string)
+	go sourceGopher(c0)
+	go filterGopher(c0, c1)
+	PrintGopher(c1)
+}
+

并发状态

  • 共享值
  • 竞争条件(race condition)

Go 的互斥锁(mutex = mutual exclusive)

  • Lock(),Unlock()
var mu sync.Mutex
+
+func main() {
+  mu.Lock()
+  defer mu.Unlock()
+  // The lock is held until we return from the function
+}
+

互斥锁定义在被保护的变量之上

type Visited struct {
+  mu sync.Mutex
+  visited map[string]int
+}
+
+func (v *Visited) VisitLink(url string) int {
+  v.mu.Lock()
+  defer v.mu.Unlock()
+  count := v.visited[url]
+  count++
+  v.visited[url] = count
+  return count
+}
+

互斥锁的隐患

  • 死锁

为保证互斥锁的安全使用,须遵循以下规则:

  • 尽可能地简化互斥锁保护的代码
  • 对每一份共享状态只使用一个互斥锁

长时间运行的工作进程

  • 工作进程(worker)
    • 通常会被写成包含 select 语句的 for 循环
func worker() {
+  n := 0
+  next := time.After(time.Second)
+  for {
+    select {
+      case <-next:
+        n++
+        fmt.Println(n)
+        next = time.After(time.Second)
+    }
+  }
+}
+
+func main() {
+  go worker()
+	for {
+		time.Sleep(time.Second)
+	}
+}
+

事件循环和 goroutine

  • 事件循环(event loop)
  • 中心循环(central loop)

Go 通过提供 goroutine 作为核心概念,消除了对中心循环的需求

`,557),c=[o];function i(l,u){return s(),a("div",null,c)}const k=n(e,[["render",i],["__file","index.html.vue"]]);export{k as default}; diff --git a/assets/index.html-a0bd810b.js b/assets/index.html-a0bd810b.js new file mode 100644 index 0000000..5dec984 --- /dev/null +++ b/assets/index.html-a0bd810b.js @@ -0,0 +1,13 @@ +import{_ as i,r as t,o as r,c,a as e,d as n,b as s,e as o}from"./app-b4fb6edd.js";const d={},l=o('

终端错误

1. BUILD_ENV=XXX 命令不支持

package.json  的  scripts  属性下配置命令  "BUILD_ENV=XXX,切换到  Windows  环境下报错

'BUILD_ENV' 不是内部或外部命令,也不是可运行的程序

原因: Windows  环境不支持  BUILD_ENV=XXX  命令

',5),p=e("code",null,"VSCode",-1),u=e("code",null,"wls",-1),h=e("code",null,"Linux",-1),m=e("br",null,null,-1),v={href:"https://learn.microsoft.com/zh-cn/windows/wsl/tutorials/wsl-vscode",target:"_blank",rel:"noopener noreferrer"},_=e("h3",{id:"npm",tabindex:"-1"},[e("a",{class:"header-anchor",href:"#npm","aria-hidden":"true"},"#"),n(" npm")],-1),b=e("h4",{id:"_1-unable-to-authenticate-need-basic-realm-aliyun",tabindex:"-1"},[e("a",{class:"header-anchor",href:"#_1-unable-to-authenticate-need-basic-realm-aliyun","aria-hidden":"true"},"#"),n(" 1. "),e("code",null,'Unable to authenticate, need: Basic realm="aliyun"')],-1),x=e("p",null,[n("解决:"),e("code",null,"npm login")],-1),g={href:"https://packages.aliyun.com/npm/npm-registry/guide",target:"_blank",rel:"noopener noreferrer"},f=o(`

2. cb() never called!

使用 npm i 安装依赖出现此错误

使用 rimraf 可快速删除 node_modules

npm install -g rimraf
+

解决:

依次执行如下命令

rimraf node_modules
+rimraf package-lock.json
+
+npm cache verify
+npm cache clean --force
+
+npm i
+

若上述步骤不能解决,则细看究竟是哪个包导致的问题,分批拉依赖

编译

1. “xxxx”不能用作 JSX 组件

解决:https://juejin.cn/post/7089463577634930718

通过 resolutions 指定版本

"resolutions": {
+  "@types/react": "17.0.44"
+},
+

如果用的是 npm(yarn 不需要) 的话,还需要在 package.json 的 script 中添加如下 preinstall,通过使用 npm-force-resolutions 包来根据 resolutions 进行版本限定。

"preinstall": "npm install --package-lock-only --ignore-scripts && npx npm-force-resolutions"
+
`,15);function k(j,q){const a=t("ExternalLinkIcon");return r(),c("div",null,[l,e("p",null,[n("解决:开启 "),p,n(" 的 "),u,n(" 命令行,使用 "),h,n(" 环境 "),m,e("a",v,[n("# Get started using Visual Studio Code with Windows Subsystem for Linux"),s(a)])]),_,b,x,e("p",null,[e("a",g,[n("云效账号密码"),s(a)])]),f])}const V=i(d,[["render",k],["__file","index.html.vue"]]);export{V as default}; diff --git a/assets/index.html-aff49fb2.js b/assets/index.html-aff49fb2.js new file mode 100644 index 0000000..444f7ae --- /dev/null +++ b/assets/index.html-aff49fb2.js @@ -0,0 +1 @@ +import{_ as a,r as s,o as l,c,a as e,b as n,w as r,d as t}from"./app-b4fb6edd.js";const d={},i=e("h2",{id:"小工具",tabindex:"-1"},[e("a",{class:"header-anchor",href:"#小工具","aria-hidden":"true"},"#"),t(" 小工具")],-1);function _(h,f){const o=s("RouterLink");return l(),c("div",null,[i,e("p",null,[n(o,{to:"/frontend/other/tools/git.html"},{default:r(()=>[t("git")]),_:1})]),e("p",null,[n(o,{to:"/frontend/other/tools/json-server.html"},{default:r(()=>[t("json-server")]),_:1})]),e("p",null,[n(o,{to:"/frontend/other/tools/ali-iconfont.html"},{default:r(()=>[t("阿里图标库脚本下载")]),_:1})])])}const m=a(d,[["render",_],["__file","index.html.vue"]]);export{m as default}; diff --git a/assets/index.html-b7b4e1e2.js b/assets/index.html-b7b4e1e2.js new file mode 100644 index 0000000..ded4116 --- /dev/null +++ b/assets/index.html-b7b4e1e2.js @@ -0,0 +1 @@ +const t=JSON.parse('{"key":"v-8daa1a0e","path":"/","title":"","lang":"zh-CN","frontmatter":{"home":true,"heroImage":"/imgs/logo.png","tagline":"时间的最大损失是拖延、期待和依赖将来","actions":[{"text":"开始","link":"/frontend/js/red-book","type":"primary"},{"text":"GitHub","link":"https://github.com/carla-cn/study-notes","type":"secondary"}]},"headers":[],"git":{"updatedTime":1687701684000},"filePathRelative":"README.md"}');export{t as data}; diff --git a/assets/index.html-beb4b1c4.js b/assets/index.html-beb4b1c4.js new file mode 100644 index 0000000..1baf4e1 --- /dev/null +++ b/assets/index.html-beb4b1c4.js @@ -0,0 +1 @@ +const l=JSON.parse('{"key":"v-5791b9ab","path":"/backend/go/base/","title":"","lang":"zh-CN","frontmatter":{},"headers":[{"level":2,"title":"Go","slug":"go","link":"#go","children":[{"level":3,"title":"搭建环境","slug":"搭建环境","link":"#搭建环境","children":[]},{"level":3,"title":"变量及其作用域","slug":"变量及其作用域","link":"#变量及其作用域","children":[]},{"level":3,"title":"实数","slug":"实数","link":"#实数","children":[{"level":4,"title":"声明浮点型变量","slug":"声明浮点型变量","link":"#声明浮点型变量","children":[]},{"level":4,"title":"零值","slug":"零值","link":"#零值","children":[]},{"level":4,"title":"显示浮点类型","slug":"显示浮点类型","link":"#显示浮点类型","children":[]},{"level":4,"title":"如何比较浮点类型","slug":"如何比较浮点类型","link":"#如何比较浮点类型","children":[]},{"level":4,"title":"整数类型","slug":"整数类型","link":"#整数类型","children":[]},{"level":4,"title":"打印数据类型","slug":"打印数据类型","link":"#打印数据类型","children":[]},{"level":4,"title":"uint8","slug":"uint8","link":"#uint8","children":[]},{"level":4,"title":"十六进制表示法","slug":"十六进制表示法","link":"#十六进制表示法","children":[]},{"level":4,"title":"打印十六进制","slug":"打印十六进制","link":"#打印十六进制","children":[]},{"level":4,"title":"整数环绕","slug":"整数环绕","link":"#整数环绕","children":[]},{"level":4,"title":"打印每个 bit","slug":"打印每个-bit","link":"#打印每个-bit","children":[]},{"level":4,"title":"整数类型的最大值、最小值","slug":"整数类型的最大值、最小值","link":"#整数类型的最大值、最小值","children":[]},{"level":4,"title":"比较大的数","slug":"比较大的数","link":"#比较大的数","children":[]},{"level":4,"title":"较大数值的常量","slug":"较大数值的常量","link":"#较大数值的常量","children":[]}]},{"level":3,"title":"多语言文本","slug":"多语言文本","link":"#多语言文本","children":[{"level":4,"title":"声明字符串","slug":"声明字符串","link":"#声明字符串","children":[]},{"level":4,"title":"字符串字面值/原始字符串字面值","slug":"字符串字面值-原始字符串字面值","link":"#字符串字面值-原始字符串字面值","children":[]},{"level":4,"title":"字符,code points,runes,bytes","slug":"字符-code-points-runes-bytes","link":"#字符-code-points-runes-bytes","children":[]},{"level":4,"title":"类型别名","slug":"类型别名","link":"#类型别名","children":[]},{"level":4,"title":"打印","slug":"打印","link":"#打印","children":[]},{"level":4,"title":"字符","slug":"字符","link":"#字符","children":[]},{"level":4,"title":"string","slug":"string","link":"#string","children":[]},{"level":4,"title":"Caesar cipher 凯撒加密法","slug":"caesar-cipher-凯撒加密法","link":"#caesar-cipher-凯撒加密法","children":[]},{"level":4,"title":"ROT13","slug":"rot13","link":"#rot13","children":[]},{"level":4,"title":"Go 的内置函数","slug":"go-的内置函数","link":"#go-的内置函数","children":[]},{"level":4,"title":"UTF-8","slug":"utf-8","link":"#utf-8","children":[]},{"level":4,"title":"range","slug":"range","link":"#range","children":[]},{"level":4,"title":"类型不能混用","slug":"类型不能混用","link":"#类型不能混用","children":[]},{"level":4,"title":"数值间类型转换","slug":"数值间类型转换","link":"#数值间类型转换","children":[]},{"level":4,"title":"字符串转换","slug":"字符串转换","link":"#字符串转换","children":[]},{"level":4,"title":"布尔类型的转换","slug":"布尔类型的转换","link":"#布尔类型的转换","children":[]}]},{"level":3,"title":"函数","slug":"函数","link":"#函数","children":[]},{"level":3,"title":"方法","slug":"方法","link":"#方法","children":[{"level":4,"title":"声明新类型","slug":"声明新类型","link":"#声明新类型","children":[]},{"level":4,"title":"通过方法添加行为","slug":"通过方法添加行为","link":"#通过方法添加行为","children":[]},{"level":4,"title":"方法调用","slug":"方法调用","link":"#方法调用","children":[]},{"level":4,"title":"一等函数","slug":"一等函数","link":"#一等函数","children":[]},{"level":4,"title":"闭包和匿名函数","slug":"闭包和匿名函数","link":"#闭包和匿名函数","children":[]}]},{"level":3,"title":"数组","slug":"数组","link":"#数组","children":[{"level":4,"title":"数组越界","slug":"数组越界","link":"#数组越界","children":[]},{"level":4,"title":"使用复合字面值初始化数组","slug":"使用复合字面值初始化数组","link":"#使用复合字面值初始化数组","children":[]},{"level":4,"title":"遍历数组","slug":"遍历数组","link":"#遍历数组","children":[]},{"level":4,"title":"数组的复制","slug":"数组的复制","link":"#数组的复制","children":[]},{"level":4,"title":"数组的数组","slug":"数组的数组","link":"#数组的数组","children":[]}]},{"level":3,"title":"切片 Slice","slug":"切片-slice","link":"#切片-slice","children":[{"level":4,"title":"Slice 指向数组的窗口","slug":"slice-指向数组的窗口","link":"#slice-指向数组的窗口","children":[]},{"level":4,"title":"Slice 的复合字面值","slug":"slice-的复合字面值","link":"#slice-的复合字面值","children":[]},{"level":4,"title":"带有方法的切片","slug":"带有方法的切片","link":"#带有方法的切片","children":[]},{"level":4,"title":"更大的 slice","slug":"更大的-slice","link":"#更大的-slice","children":[]},{"level":4,"title":"长度和容量(length & capacity)","slug":"长度和容量-length-capacity","link":"#长度和容量-length-capacity","children":[]},{"level":4,"title":"三个索引的切分操作","slug":"三个索引的切分操作","link":"#三个索引的切分操作","children":[]},{"level":4,"title":"使用 make 函数对 slice 进行预分配","slug":"使用-make-函数对-slice-进行预分配","link":"#使用-make-函数对-slice-进行预分配","children":[]},{"level":4,"title":"声明可变参数的函数","slug":"声明可变参数的函数","link":"#声明可变参数的函数","children":[]}]},{"level":3,"title":"map","slug":"map","link":"#map","children":[{"level":4,"title":"声明 map","slug":"声明-map","link":"#声明-map","children":[]},{"level":4,"title":", 与 ok 写法","slug":"与-ok-写法","link":"#与-ok-写法","children":[]},{"level":4,"title":"map 不会复制","slug":"map-不会复制","link":"#map-不会复制","children":[]},{"level":4,"title":"使用 make 函数对 map 进行预分配","slug":"使用-make-函数对-map-进行预分配","link":"#使用-make-函数对-map-进行预分配","children":[]},{"level":4,"title":"使用 map 作计数器","slug":"使用-map-作计数器","link":"#使用-map-作计数器","children":[]},{"level":4,"title":"使用 map 和 slice 实现数据分组","slug":"使用-map-和-slice-实现数据分组","link":"#使用-map-和-slice-实现数据分组","children":[]},{"level":4,"title":"将 map 用作 set","slug":"将-map-用作-set","link":"#将-map-用作-set","children":[]}]},{"level":3,"title":"结构 struct","slug":"结构-struct","link":"#结构-struct","children":[{"level":4,"title":"声明结构","slug":"声明结构","link":"#声明结构","children":[]},{"level":4,"title":"通过类型复用结构体","slug":"通过类型复用结构体","link":"#通过类型复用结构体","children":[]},{"level":4,"title":"struct 的复制","slug":"struct-的复制","link":"#struct-的复制","children":[]},{"level":4,"title":"由结构体组成的 slice","slug":"由结构体组成的-slice","link":"#由结构体组成的-slice","children":[]},{"level":4,"title":"将 struct 编码为 JSON","slug":"将-struct-编码为-json","link":"#将-struct-编码为-json","children":[]},{"level":4,"title":"使用 struct 标签来定义 JSON","slug":"使用-struct-标签来定义-json","link":"#使用-struct-标签来定义-json","children":[]},{"level":4,"title":"Go 语言里没有 class","slug":"go-语言里没有-class","link":"#go-语言里没有-class","children":[]},{"level":4,"title":"构造函数","slug":"构造函数","link":"#构造函数","children":[]},{"level":4,"title":"class 的替代方案","slug":"class-的替代方案","link":"#class-的替代方案","children":[]},{"level":4,"title":"组合与转发(composition & forwarding)","slug":"组合与转发-composition-forwarding","link":"#组合与转发-composition-forwarding","children":[]},{"level":4,"title":"接口","slug":"接口","link":"#接口","children":[]},{"level":4,"title":"满足接口","slug":"满足接口","link":"#满足接口","children":[]}]},{"level":3,"title":"指针","slug":"指针","link":"#指针","children":[{"level":4,"title":"指向结构的指针","slug":"指向结构的指针","link":"#指向结构的指针","children":[]},{"level":4,"title":"指向数组的指针","slug":"指向数组的指针","link":"#指向数组的指针","children":[]},{"level":4,"title":"实现修改","slug":"实现修改","link":"#实现修改","children":[]},{"level":4,"title":"指针接收者","slug":"指针接收者","link":"#指针接收者","children":[]},{"level":4,"title":"内部指针","slug":"内部指针","link":"#内部指针","children":[]},{"level":4,"title":"修改数组","slug":"修改数组","link":"#修改数组","children":[]},{"level":4,"title":"隐式的指针","slug":"隐式的指针","link":"#隐式的指针","children":[]},{"level":4,"title":"指针和接口","slug":"指针和接口","link":"#指针和接口","children":[]},{"level":4,"title":"明智的使用指针","slug":"明智的使用指针","link":"#明智的使用指针","children":[]}]},{"level":3,"title":"nil","slug":"nil","link":"#nil","children":[{"level":4,"title":"nil 会导致 panic","slug":"nil-会导致-panic","link":"#nil-会导致-panic","children":[]},{"level":4,"title":"避免 nil 引发 panic","slug":"避免-nil-引发-panic","link":"#避免-nil-引发-panic","children":[]},{"level":4,"title":"nil 函数值","slug":"nil-函数值","link":"#nil-函数值","children":[]},{"level":4,"title":"nil slice","slug":"nil-slice","link":"#nil-slice","children":[]},{"level":4,"title":"nil map","slug":"nil-map","link":"#nil-map","children":[]},{"level":4,"title":"nil 接口","slug":"nil-接口","link":"#nil-接口","children":[]},{"level":4,"title":"nil 之外的另一个选择","slug":"nil-之外的另一个选择","link":"#nil-之外的另一个选择","children":[]}]},{"level":3,"title":"错误","slug":"错误","link":"#错误","children":[{"level":4,"title":"处理错误","slug":"处理错误","link":"#处理错误","children":[]},{"level":4,"title":"优雅的错误处理","slug":"优雅的错误处理","link":"#优雅的错误处理","children":[]},{"level":4,"title":"文件写入","slug":"文件写入","link":"#文件写入","children":[]},{"level":4,"title":"defer 关键字","slug":"defer-关键字","link":"#defer-关键字","children":[]},{"level":4,"title":"有创意的错误处理","slug":"有创意的错误处理","link":"#有创意的错误处理","children":[]},{"level":4,"title":"New error","slug":"new-error","link":"#new-error","children":[]},{"level":4,"title":"按需返回错误","slug":"按需返回错误","link":"#按需返回错误","children":[]},{"level":4,"title":"自定义错误类型","slug":"自定义错误类型","link":"#自定义错误类型","children":[]},{"level":4,"title":"类型断言","slug":"类型断言","link":"#类型断言","children":[]},{"level":4,"title":"如何 panic","slug":"如何-panic","link":"#如何-panic","children":[]},{"level":4,"title":"错误值、panic、os.Exit?","slug":"错误值、panic、os-exit","link":"#错误值、panic、os-exit","children":[]},{"level":4,"title":"保持冷静并继续","slug":"保持冷静并继续","link":"#保持冷静并继续","children":[]}]},{"level":3,"title":"goroutine 和 并发(concurrent)","slug":"goroutine-和-并发-concurrent","link":"#goroutine-和-并发-concurrent","children":[{"level":4,"title":"goroutine","slug":"goroutine","link":"#goroutine","children":[]},{"level":4,"title":"启动 goroutine","slug":"启动-goroutine","link":"#启动-goroutine","children":[]},{"level":4,"title":"不止一个 goroutine","slug":"不止一个-goroutine","link":"#不止一个-goroutine","children":[]},{"level":4,"title":"通道 channel","slug":"通道-channel","link":"#通道-channel","children":[]},{"level":4,"title":"通道 channel 发送、接收","slug":"通道-channel-发送、接收","link":"#通道-channel-发送、接收","children":[]},{"level":4,"title":"使用 select 处理多个通道","slug":"使用-select-处理多个通道","link":"#使用-select-处理多个通道","children":[]},{"level":4,"title":"nil 通道","slug":"nil-通道","link":"#nil-通道","children":[]},{"level":4,"title":"阻塞和死锁","slug":"阻塞和死锁","link":"#阻塞和死锁","children":[]},{"level":4,"title":"地鼠装配线","slug":"地鼠装配线","link":"#地鼠装配线","children":[]},{"level":4,"title":"常用模式","slug":"常用模式","link":"#常用模式","children":[]}]},{"level":3,"title":"并发状态","slug":"并发状态","link":"#并发状态","children":[{"level":4,"title":"Go 的互斥锁(mutex = mutual exclusive)","slug":"go-的互斥锁-mutex-mutual-exclusive","link":"#go-的互斥锁-mutex-mutual-exclusive","children":[]},{"level":4,"title":"互斥锁的隐患","slug":"互斥锁的隐患","link":"#互斥锁的隐患","children":[]},{"level":4,"title":"长时间运行的工作进程","slug":"长时间运行的工作进程","link":"#长时间运行的工作进程","children":[]},{"level":4,"title":"事件循环和 goroutine","slug":"事件循环和-goroutine","link":"#事件循环和-goroutine","children":[]}]}]}],"git":{"updatedTime":1694528949000},"filePathRelative":"backend/go/base/index.md"}');export{l as data}; diff --git a/assets/index.html-d7674d58.js b/assets/index.html-d7674d58.js new file mode 100644 index 0000000..e08da86 --- /dev/null +++ b/assets/index.html-d7674d58.js @@ -0,0 +1 @@ +const e=JSON.parse('{"key":"v-0fceabfe","path":"/general/network/","title":"计算机网络","lang":"zh-CN","frontmatter":{"lang":"zh-CN","title":"计算机网络"},"headers":[],"git":{"updatedTime":1686817933000},"filePathRelative":"general/network/README.md"}');export{e as data}; diff --git a/assets/index.html-d7ee44df.js b/assets/index.html-d7ee44df.js new file mode 100644 index 0000000..c941292 --- /dev/null +++ b/assets/index.html-d7ee44df.js @@ -0,0 +1 @@ +import{_ as e,o as c,c as t}from"./app-b4fb6edd.js";const n={};function _(o,r){return c(),t("div")}const a=e(n,[["render",_],["__file","index.html.vue"]]);export{a as default}; diff --git a/assets/index.html-dd9cba0d.js b/assets/index.html-dd9cba0d.js new file mode 100644 index 0000000..ea770fc --- /dev/null +++ b/assets/index.html-dd9cba0d.js @@ -0,0 +1,58 @@ +import{_ as n,o as a,c as s,e as t}from"./app-b4fb6edd.js";const p={},e=t(`

了解

状态

  • 不关心过程,只关心界面处于哪个状态
  • 状态是动态数据,当前造成的结果。也就是 model 形成的 view

软件究竟在做什么

  • 软件工程的核心,就是在管理数据
  • 如果有一个功能需要开发的时候,首先考虑的是,一个数据的生命周期和作用范围

性能优化的两种方式

  • 协商缓存
  • 提前拿数据,prefetch preload prerender

状态管理

状态机应该具备的能力

  • 有独立的区域存储数据,并且能被拿到
    • 闭包
    • 单例模式
  • 有修改数据的明确方法,并且,能够让使用数据的地方感知到
    • 发布订阅
    • Proxy / Object.defineProperty
  • model 的改变触发 view 的更新
    • forceUpdate
    • $forceUpdate
    • data.x = Math.random()

状态管理简易实现

export const createData = (initialData) => {
+  let data = initialData;
+  const deps = [];
+
+  const getData = () => data;
+
+  const modifyData = (newData) => {
+    data = newData;
+    deps.forEach((fn) => fn());
+  };
+
+  const subscribeData = (handler) => {
+    deps.push(handler);
+  };
+
+  return {
+    getData,
+    modifyData,
+    subscribeData
+  };
+};
+
import { useEffect, useState } from "react";
+import { createData } from "./data";
+
+const initialData = { count: 2 };
+const { getData, modifyData, subscribeData } = createData(initialData);
+
+const Demo = () => {
+  const [state, setState] = useState(initialData);
+
+  const change0 = () => {
+    modifyData({ count: 0 });
+  };
+
+  const change1 = () => {
+    modifyData({ count: 1 });
+  };
+
+  useEffect(() => {
+    // 只需要订阅一次
+    subscribeData(() => {
+      const curData = getData();
+      console.log("the cur data is", curData);
+      setState(curData);
+    });
+  }, []);
+
+  return (
+    <div>
+      {state.count}
+      <button onClick={change0}>点击切换为0</button>
+      <button onClick={change1}>点击切换为1</button>
+    </div>
+  );
+};
+
+export default Demo;
+
`,13),o=[e];function c(i,l){return a(),s("div",null,o)}const r=n(p,[["render",c],["__file","index.html.vue"]]);export{r as default}; diff --git a/assets/index.html-e05eac13.js b/assets/index.html-e05eac13.js new file mode 100644 index 0000000..5618e99 --- /dev/null +++ b/assets/index.html-e05eac13.js @@ -0,0 +1 @@ +const e=JSON.parse('{"key":"v-60936db8","path":"/frontend/framework/we-app/","title":"","lang":"zh-CN","frontmatter":{},"headers":[{"level":2,"title":"小程序","slug":"小程序","link":"#小程序","children":[{"level":3,"title":"微信小程序","slug":"微信小程序","link":"#微信小程序","children":[]}]}],"git":{"updatedTime":1694528949000},"filePathRelative":"frontend/framework/we-app/index.md"}');export{e as data}; diff --git a/assets/index.html-e21578be.js b/assets/index.html-e21578be.js new file mode 100644 index 0000000..f463a84 --- /dev/null +++ b/assets/index.html-e21578be.js @@ -0,0 +1 @@ +import{_ as d,r,o as a,c as s,a as e,b as n,w as l,d as t}from"./app-b4fb6edd.js";const f={},u=e("h2",{id:"红宝书",tabindex:"-1"},[e("a",{class:"header-anchor",href:"#红宝书","aria-hidden":"true"},"#"),t(" 红宝书")],-1);function _(c,h){const o=r("RouterLink");return a(),s("div",null,[u,e("p",null,[n(o,{to:"/frontend/js/red-book/01.html"},{default:l(()=>[t("第 1 章 什么是 JavaScript")]),_:1})]),e("p",null,[n(o,{to:"/frontend/js/red-book/02.html"},{default:l(()=>[t("第 2 章 HTML 中的 JavaScript")]),_:1})]),e("p",null,[n(o,{to:"/frontend/js/red-book/03.html"},{default:l(()=>[t("第 3 章 语言基础")]),_:1})]),e("p",null,[n(o,{to:"/frontend/js/red-book/04.html"},{default:l(()=>[t("第 4 章 变量、作用域与内存")]),_:1})]),e("p",null,[n(o,{to:"/frontend/js/red-book/05.html"},{default:l(()=>[t("第 5 章 基本引用类型")]),_:1})]),e("p",null,[n(o,{to:"/frontend/js/red-book/06.html"},{default:l(()=>[t("第 6 章 集合引用类型")]),_:1})]),e("p",null,[n(o,{to:"/frontend/js/red-book/07.html"},{default:l(()=>[t("第 7 章 迭代器与生成器")]),_:1})]),e("p",null,[n(o,{to:"/frontend/js/red-book/08.html"},{default:l(()=>[t("第 8 章 对象、类与面向对象编程")]),_:1})]),e("p",null,[n(o,{to:"/frontend/js/red-book/09.html"},{default:l(()=>[t("第 9 章 代理与反射")]),_:1})]),e("p",null,[n(o,{to:"/frontend/js/red-book/10.html"},{default:l(()=>[t("第 10 章 函数")]),_:1})])])}const p=d(f,[["render",_],["__file","index.html.vue"]]);export{p as default}; diff --git a/assets/index.html-e7ad04f1.js b/assets/index.html-e7ad04f1.js new file mode 100644 index 0000000..1b01faf --- /dev/null +++ b/assets/index.html-e7ad04f1.js @@ -0,0 +1 @@ +import{_ as i,r as o,o as s,c,a as e,d as r,b as n,w as d}from"./app-b4fb6edd.js";const l={},_=e("h2",{id:"小程序",tabindex:"-1"},[e("a",{class:"header-anchor",href:"#小程序","aria-hidden":"true"},"#"),r(" 小程序")],-1),h=e("p",null,"小程序的本质是网页,只能使用宿主提供的 API,不能使用浏览器提供的 API -> webview",-1),m=e("h3",{id:"微信小程序",tabindex:"-1"},[e("a",{class:"header-anchor",href:"#微信小程序","aria-hidden":"true"},"#"),r(" 微信小程序")],-1),p={href:"https://developers.weixin.qq.com/miniprogram/dev/framework/quickstart/framework.html#%E6%B8%B2%E6%9F%93%E5%B1%82%E5%92%8C%E9%80%BB%E8%BE%91%E5%B1%82",target:"_blank",rel:"noopener noreferrer"},f={href:"https://developers.weixin.qq.com/miniprogram/dev/framework/runtime/operating-mechanism.html",target:"_blank",rel:"noopener noreferrer"};function u(k,x){const t=o("ExternalLinkIcon"),a=o("RouterLink");return s(),c("div",null,[_,h,m,e("p",null,[e("a",p,[r("逻辑层和渲染层"),n(t)])]),e("p",null,[e("a",f,[r("小程序运行机制(冷启动、热启动)"),n(t)])]),e("p",null,[n(a,{to:"/frontend/framework/we-app/01/"},{default:d(()=>[r("开发准备")]),_:1})])])}const E=i(l,[["render",u],["__file","index.html.vue"]]);export{E as default}; diff --git a/assets/index.html-f4b28a94.js b/assets/index.html-f4b28a94.js new file mode 100644 index 0000000..405161b --- /dev/null +++ b/assets/index.html-f4b28a94.js @@ -0,0 +1 @@ +import{_ as c,r,o as s,c as d,a as e,b as a,w as n,d as o}from"./app-b4fb6edd.js";const _={},l=e("h2",{id:"go",tabindex:"-1"},[e("a",{class:"header-anchor",href:"#go","aria-hidden":"true"},"#"),o(" Go")],-1);function i(u,f){const t=r("RouterLink");return s(),d("div",null,[l,e("p",null,[a(t,{to:"/backend/go/base/"},{default:n(()=>[o("go 基础语法")]),_:1})]),e("p",null,[a(t,{to:"/backend/go/web/"},{default:n(()=>[o("go Web")]),_:1})])])}const m=c(_,[["render",i],["__file","index.html.vue"]]);export{m as default}; diff --git a/assets/json-server.html-0e424c53.js b/assets/json-server.html-0e424c53.js new file mode 100644 index 0000000..875f779 --- /dev/null +++ b/assets/json-server.html-0e424c53.js @@ -0,0 +1,30 @@ +import{_ as s,o as a,c as e,e as n}from"./app-b4fb6edd.js";const t={},o=n(`

JSON Server

模拟数据接口

开始

安装

npm install -g json-server
+

创建一个 db.json 文件

{
+	"posts": [{ "id": 1, "title": "json-server", "author": "typicode" }],
+	"comments": [{ "id": 1, "body": "some comment", "postId": 1 }],
+	"profile": { "name": "typicode" }
+}
+

启动

json-server --watch db.json
+

注意:

  • POST, PUT, PATCH, DELETE 请求带来的改变会出现在 db.json 中
  • 请求体必须是对象
  • id 不可变。使用 PUT / PATCH 请求改变 id 的值将会被忽略,用 POST 创建的 id 也不能与已有的重复
  • POST / PUT / PATCH 请求必须设置 Content-Type: application/json

路由

默认路由

基于上述的 db.json,所有的默认路由如下:

路由描述
GET /posts所有的 posts
GET /posts/1单个 post
GET /profileprofile 对象
POST /posts创建一个 post
POST /profile创建一个 profile
PUT /posts/1替换一个 post
PUT /profile替换一个 profile
PATCH /posts/1更新一个 post
PATCH /profile更新一个 profile
DELETE /posts/1删除一个 post

过滤

. 访问深层属性

GET /posts?title=json-server&author=typicode
+GET /comments?author.name=typicode
+

分页

GET /posts?_page=7
+GET /posts?_page=7&_limit=20 # _limit 默认为 10
+

排序

默认升序,降序加 _order=desc

GET /posts?_sort=views
+GET /posts/1/comments?_sort=votes&_order=desc
+
+# 多个字段排序
+GET /posts?_sort=user,views&_order=desc,asc
+

切片

行为同 Array.slice,不包含 _end

GET /posts?_start=20&_end=30
+GET /posts/1/comments?_start=20&_end=30
+GET /posts/1/comments?_start=20&_limit=10
+

操作符

_gte _lte,取得范围内的值

GET /posts?views_gte=10&views_lte=20
+

_ne,排除值

GET /posts?id_ne=1
+

_like,模糊查询(可以使用 RegExp)

GET /posts?title_like=server
+

全文搜索

q 参数,搜索所有的字段

GET /posts?q=internet
+

关系

_embed,获取子资源

GET /posts?_embed=comments
+GET /posts/1?_embed=comments
+

_expand,获取父资源

GET /comments?_expand=post
+GET /comments/1?_expand=post
+

创建或者获取嵌套资源

GET /posts/1/comments
+POST /posts/1/comments
+
`,42),p=[o];function r(d,i){return a(),e("div",null,p)}const c=s(t,[["render",r],["__file","json-server.html.vue"]]);export{c as default}; diff --git a/assets/json-server.html-c81eb9f6.js b/assets/json-server.html-c81eb9f6.js new file mode 100644 index 0000000..72fba44 --- /dev/null +++ b/assets/json-server.html-c81eb9f6.js @@ -0,0 +1 @@ +const l=JSON.parse('{"key":"v-c7ffc5b0","path":"/frontend/other/tools/json-server.html","title":"小工具","lang":"zh-CN","frontmatter":{"title":"小工具"},"headers":[{"level":2,"title":"JSON Server","slug":"json-server","link":"#json-server","children":[{"level":3,"title":"开始","slug":"开始","link":"#开始","children":[]},{"level":3,"title":"路由","slug":"路由","link":"#路由","children":[{"level":4,"title":"默认路由","slug":"默认路由","link":"#默认路由","children":[]},{"level":4,"title":"过滤","slug":"过滤","link":"#过滤","children":[]},{"level":4,"title":"分页","slug":"分页","link":"#分页","children":[]},{"level":4,"title":"排序","slug":"排序","link":"#排序","children":[]},{"level":4,"title":"切片","slug":"切片","link":"#切片","children":[]},{"level":4,"title":"操作符","slug":"操作符","link":"#操作符","children":[]},{"level":4,"title":"全文搜索","slug":"全文搜索","link":"#全文搜索","children":[]},{"level":4,"title":"关系","slug":"关系","link":"#关系","children":[]}]}]}],"git":{"updatedTime":1691024667000},"filePathRelative":"frontend/other/tools/json-server.md"}');export{l as data}; diff --git a/assets/more-handler-e3085276.png b/assets/more-handler-e3085276.png new file mode 100644 index 0000000..4ed0bf7 Binary files /dev/null and b/assets/more-handler-e3085276.png differ diff --git a/assets/style-24736ef4.css b/assets/style-24736ef4.css new file mode 100644 index 0000000..dad5c01 --- /dev/null +++ b/assets/style-24736ef4.css @@ -0,0 +1 @@ +:root{--back-to-top-z-index: 5;--back-to-top-color: #3eaf7c;--back-to-top-color-hover: #71cda3}.back-to-top{cursor:pointer;position:fixed;bottom:2rem;right:2.5rem;width:2rem;height:1.2rem;background-color:var(--back-to-top-color);-webkit-mask:url(/study-notes/assets/back-to-top-8efcbe56.svg) no-repeat;mask:url(/study-notes/assets/back-to-top-8efcbe56.svg) no-repeat;z-index:var(--back-to-top-z-index)}.back-to-top:hover{background-color:var(--back-to-top-color-hover)}@media (max-width: 959px){.back-to-top{display:none}}@media print{.back-to-top{display:none}}.back-to-top-enter-active,.back-to-top-leave-active{transition:opacity .3s}.back-to-top-enter-from,.back-to-top-leave-to{opacity:0}:root{--external-link-icon-color: #aaa}.external-link-icon{position:relative;display:inline-block;color:var(--external-link-icon-color);vertical-align:middle;top:-1px}@media print{.external-link-icon{display:none}}.external-link-icon-sr-only{position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip:rect(0,0,0,0);white-space:nowrap;border-width:0;-webkit-user-select:none;-moz-user-select:none;user-select:none}:root{--medium-zoom-z-index: 100;--medium-zoom-bg-color: #ffffff;--medium-zoom-opacity: 1}.medium-zoom-overlay{background-color:var(--medium-zoom-bg-color)!important;z-index:var(--medium-zoom-z-index)}.medium-zoom-overlay~img{z-index:calc(var(--medium-zoom-z-index) + 1)}.medium-zoom--opened .medium-zoom-overlay{opacity:var(--medium-zoom-opacity)}:root{--nprogress-color: #29d;--nprogress-z-index: 1031}#nprogress{pointer-events:none}#nprogress .bar{background:var(--nprogress-color);position:fixed;z-index:var(--nprogress-z-index);top:0;left:0;width:100%;height:2px}:root{--c-brand: #3eaf7c;--c-brand-light: #4abf8a;--c-bg: #ffffff;--c-bg-light: #f3f4f5;--c-bg-lighter: #eeeeee;--c-bg-dark: #ebebec;--c-bg-darker: #e6e6e6;--c-bg-navbar: var(--c-bg);--c-bg-sidebar: var(--c-bg);--c-bg-arrow: #cccccc;--c-text: #2c3e50;--c-text-accent: var(--c-brand);--c-text-light: #3a5169;--c-text-lighter: #4e6e8e;--c-text-lightest: #6a8bad;--c-text-quote: #999999;--c-border: #eaecef;--c-border-dark: #dfe2e5;--c-tip: #42b983;--c-tip-bg: var(--c-bg-light);--c-tip-title: var(--c-text);--c-tip-text: var(--c-text);--c-tip-text-accent: var(--c-text-accent);--c-warning: #ffc310;--c-warning-bg: #fffae3;--c-warning-bg-light: #fff3ba;--c-warning-bg-lighter: #fff0b0;--c-warning-border-dark: #f7dc91;--c-warning-details-bg: #fff5ca;--c-warning-title: #f1b300;--c-warning-text: #746000;--c-warning-text-accent: #edb100;--c-warning-text-light: #c1971c;--c-warning-text-quote: #ccab49;--c-danger: #f11e37;--c-danger-bg: #ffe0e0;--c-danger-bg-light: #ffcfde;--c-danger-bg-lighter: #ffc9c9;--c-danger-border-dark: #f1abab;--c-danger-details-bg: #ffd4d4;--c-danger-title: #ed1e2c;--c-danger-text: #660000;--c-danger-text-accent: #bd1a1a;--c-danger-text-light: #b5474d;--c-danger-text-quote: #c15b5b;--c-details-bg: #eeeeee;--c-badge-tip: var(--c-tip);--c-badge-warning: #ecc808;--c-badge-warning-text: var(--c-bg);--c-badge-danger: #dc2626;--c-badge-danger-text: var(--c-bg);--t-color: .3s ease;--t-transform: .3s ease;--code-bg-color: #282c34;--code-hl-bg-color: rgba(0, 0, 0, .66);--code-ln-color: #9e9e9e;--code-ln-wrapper-width: 3.5rem;--font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Oxygen, Ubuntu, Cantarell, "Fira Sans", "Droid Sans", "Helvetica Neue", sans-serif;--font-family-code: Consolas, Monaco, "Andale Mono", "Ubuntu Mono", monospace;--navbar-height: 3.6rem;--navbar-padding-v: .7rem;--navbar-padding-h: 1.5rem;--sidebar-width: 20rem;--sidebar-width-mobile: calc(var(--sidebar-width) * .82);--content-width: 740px;--homepage-width: 960px}.back-to-top{--back-to-top-color: var(--c-brand);--back-to-top-color-hover: var(--c-brand-light)}.DocSearch{--docsearch-primary-color: var(--c-brand);--docsearch-text-color: var(--c-text);--docsearch-highlight-color: var(--c-brand);--docsearch-muted-color: var(--c-text-quote);--docsearch-container-background: rgba(9, 10, 17, .8);--docsearch-modal-background: var(--c-bg-light);--docsearch-searchbox-background: var(--c-bg-lighter);--docsearch-searchbox-focus-background: var(--c-bg);--docsearch-searchbox-shadow: inset 0 0 0 2px var(--c-brand);--docsearch-hit-color: var(--c-text-light);--docsearch-hit-active-color: var(--c-bg);--docsearch-hit-background: var(--c-bg);--docsearch-hit-shadow: 0 1px 3px 0 var(--c-border-dark);--docsearch-footer-background: var(--c-bg)}.external-link-icon{--external-link-icon-color: var(--c-text-quote)}.medium-zoom-overlay{--medium-zoom-bg-color: var(--c-bg)}#nprogress{--nprogress-color: var(--c-brand)}.pwa-popup{--pwa-popup-text-color: var(--c-text);--pwa-popup-bg-color: var(--c-bg);--pwa-popup-border-color: var(--c-brand);--pwa-popup-shadow: 0 4px 16px var(--c-brand);--pwa-popup-btn-text-color: var(--c-bg);--pwa-popup-btn-bg-color: var(--c-brand);--pwa-popup-btn-hover-bg-color: var(--c-brand-light)}.search-box{--search-bg-color: var(--c-bg);--search-accent-color: var(--c-brand);--search-text-color: var(--c-text);--search-border-color: var(--c-border);--search-item-text-color: var(--c-text-lighter);--search-item-focus-bg-color: var(--c-bg-light)}html.dark{--c-brand: #3aa675;--c-brand-light: #349469;--c-bg: #22272e;--c-bg-light: #2b313a;--c-bg-lighter: #262c34;--c-bg-dark: #343b44;--c-bg-darker: #37404c;--c-text: #adbac7;--c-text-light: #96a7b7;--c-text-lighter: #8b9eb0;--c-text-lightest: #8094a8;--c-border: #3e4c5a;--c-border-dark: #34404c;--c-tip: #318a62;--c-warning: #e0ad15;--c-warning-bg: #2d2f2d;--c-warning-bg-light: #423e2a;--c-warning-bg-lighter: #44442f;--c-warning-border-dark: #957c35;--c-warning-details-bg: #39392d;--c-warning-title: #fdca31;--c-warning-text: #d8d96d;--c-warning-text-accent: #ffbf00;--c-warning-text-light: #ddb84b;--c-warning-text-quote: #ccab49;--c-danger: #fc1e38;--c-danger-bg: #39232c;--c-danger-bg-light: #4b2b35;--c-danger-bg-lighter: #553040;--c-danger-border-dark: #a25151;--c-danger-details-bg: #482936;--c-danger-title: #fc2d3b;--c-danger-text: #ea9ca0;--c-danger-text-accent: #fd3636;--c-danger-text-light: #d9777c;--c-danger-text-quote: #d56b6b;--c-details-bg: #323843;--c-badge-warning: var(--c-warning);--c-badge-warning-text: #3c2e05;--c-badge-danger: var(--c-danger);--c-badge-danger-text: #401416;--code-hl-bg-color: #363b46}html.dark .DocSearch{--docsearch-logo-color: var(--c-text);--docsearch-modal-shadow: inset 1px 1px 0 0 #2c2e40, 0 3px 8px 0 #000309;--docsearch-key-shadow: inset 0 -2px 0 0 #282d55, inset 0 0 1px 1px #51577d, 0 2px 2px 0 rgba(3, 4, 9, .3);--docsearch-key-gradient: linear-gradient(-225deg, #444950, #1c1e21);--docsearch-footer-shadow: inset 0 1px 0 0 rgba(73, 76, 106, .5), 0 -4px 8px 0 rgba(0, 0, 0, .2)}html,body{padding:0;margin:0;background-color:var(--c-bg);transition:background-color var(--t-color)}html.dark{color-scheme:dark}html{font-size:16px}body{font-family:var(--font-family);-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale;font-size:1rem;color:var(--c-text)}a{font-weight:500;color:var(--c-text-accent);text-decoration:none;overflow-wrap:break-word}p a code{font-weight:400;color:var(--c-text-accent)}kbd{font-family:var(--font-family-code);color:var(--c-text);background:var(--c-bg-lighter);border:solid .15rem var(--c-border-dark);border-bottom:solid .25rem var(--c-border-dark);border-radius:.15rem;padding:0 .15em}code{font-family:var(--font-family-code);color:var(--c-text-lighter);padding:.25rem .5rem;margin:0;font-size:.85em;background-color:var(--c-bg-light);border-radius:3px;overflow-wrap:break-word;transition:background-color var(--t-color)}blockquote{font-size:1rem;color:var(--c-text-quote);border-left:.2rem solid var(--c-border-dark);margin:1rem 0;padding:.25rem 0 .25rem 1rem;overflow-wrap:break-word}blockquote>p{margin:0}ul,ol{padding-left:1.2em}strong{font-weight:600}h1,h2,h3,h4,h5,h6{font-weight:600;line-height:1.25;overflow-wrap:break-word}h1:focus-visible,h2:focus-visible,h3:focus-visible,h4:focus-visible,h5:focus-visible,h6:focus-visible{outline:none}h1:hover .header-anchor,h2:hover .header-anchor,h3:hover .header-anchor,h4:hover .header-anchor,h5:hover .header-anchor,h6:hover .header-anchor{opacity:1}h1{font-size:2.2rem}h2{font-size:1.65rem;padding-bottom:.3rem;border-bottom:1px solid var(--c-border);transition:border-color var(--t-color)}h3{font-size:1.35rem}h4{font-size:1.15rem}h5{font-size:1.05rem}h6{font-size:1rem}a.header-anchor{font-size:.85em;float:left;margin-left:-.87em;padding-right:.23em;margin-top:.125em;opacity:0;-webkit-user-select:none;-moz-user-select:none;user-select:none}@media print{a.header-anchor{display:none}}a.header-anchor:hover{text-decoration:none}a.header-anchor:focus-visible{opacity:1}@media print{a[href^="http://"]:after,a[href^="https://"]:after{content:" (" attr(href) ") "}}p,ul,ol{line-height:1.7;overflow-wrap:break-word}hr{border:0;border-top:1px solid var(--c-border)}table{border-collapse:collapse;margin:1rem 0;display:block;overflow-x:auto;transition:border-color var(--t-color)}tr{border-top:1px solid var(--c-border-dark);transition:border-color var(--t-color)}tr:nth-child(2n){background-color:var(--c-bg-light);transition:background-color var(--t-color)}tr:nth-child(2n) code{background-color:var(--c-bg-dark)}th,td{padding:.6em 1em;border:1px solid var(--c-border-dark);transition:border-color var(--t-color)}.arrow{display:inline-block;width:0;height:0}.arrow.up{border-left:4px solid transparent;border-right:4px solid transparent;border-bottom:6px solid var(--c-bg-arrow)}.arrow.down{border-left:4px solid transparent;border-right:4px solid transparent;border-top:6px solid var(--c-bg-arrow)}.arrow.right{border-top:4px solid transparent;border-bottom:4px solid transparent;border-left:6px solid var(--c-bg-arrow)}.arrow.left{border-top:4px solid transparent;border-bottom:4px solid transparent;border-right:6px solid var(--c-bg-arrow)}.badge{display:inline-block;font-size:14px;font-weight:600;height:18px;line-height:18px;border-radius:3px;padding:0 6px;color:var(--c-bg);vertical-align:top;transition:color var(--t-color),background-color var(--t-color)}.badge.tip{background-color:var(--c-badge-tip)}.badge.warning{background-color:var(--c-badge-warning);color:var(--c-badge-warning-text)}.badge.danger{background-color:var(--c-badge-danger);color:var(--c-badge-danger-text)}.badge+.badge{margin-left:5px}code[class*=language-],pre[class*=language-]{color:#ccc;background:none;font-family:var(--font-family-code);font-size:1em;text-align:left;white-space:pre;word-spacing:normal;word-break:normal;word-wrap:normal;line-height:1.5;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-hyphens:none;hyphens:none}pre[class*=language-]{padding:1em;margin:.5em 0;overflow:auto}:not(pre)>code[class*=language-],pre[class*=language-]{background:#2d2d2d}:not(pre)>code[class*=language-]{padding:.1em;border-radius:.3em;white-space:normal}.token.comment,.token.block-comment,.token.prolog,.token.doctype,.token.cdata{color:#999}.token.punctuation{color:#ccc}.token.tag,.token.attr-name,.token.namespace,.token.deleted{color:#ec5975}.token.function-name{color:#6196cc}.token.boolean,.token.number,.token.function{color:#f08d49}.token.property,.token.class-name,.token.constant,.token.symbol{color:#f8c555}.token.selector,.token.important,.token.atrule,.token.keyword,.token.builtin{color:#cc99cd}.token.string,.token.char,.token.attr-value,.token.regex,.token.variable{color:#7ec699}.token.operator,.token.entity,.token.url{color:#67cdcc}.token.important,.token.bold{font-weight:700}.token.italic{font-style:italic}.token.entity{cursor:help}.token.inserted{color:#3eaf7c}.theme-default-content pre,.theme-default-content pre[class*=language-]{line-height:1.375;padding:1.3rem 1.5rem;margin:.85rem 0;border-radius:6px;overflow:auto}.theme-default-content pre code,.theme-default-content pre[class*=language-] code{color:#fff;padding:0;background-color:transparent!important;border-radius:0;overflow-wrap:unset;-webkit-font-smoothing:auto;-moz-osx-font-smoothing:auto}.theme-default-content .line-number{font-family:var(--font-family-code)}div[class*=language-]{position:relative;background-color:var(--code-bg-color);border-radius:6px}div[class*=language-]:before{content:attr(data-ext);position:absolute;z-index:3;top:.8em;right:1em;font-size:.75rem;color:var(--code-ln-color)}div[class*=language-] pre,div[class*=language-] pre[class*=language-]{background:transparent!important;position:relative;z-index:1}div[class*=language-] .highlight-lines{-webkit-user-select:none;-moz-user-select:none;user-select:none;padding-top:1.3rem;position:absolute;top:0;left:0;width:100%;line-height:1.375}div[class*=language-] .highlight-lines .highlight-line{background-color:var(--code-hl-bg-color)}div[class*=language-]:not(.line-numbers-mode) .line-numbers{display:none}div[class*=language-].line-numbers-mode .highlight-lines .highlight-line{position:relative}div[class*=language-].line-numbers-mode .highlight-lines .highlight-line:before{content:" ";position:absolute;z-index:2;left:0;top:0;display:block;width:var(--code-ln-wrapper-width);height:100%}div[class*=language-].line-numbers-mode pre{margin-left:var(--code-ln-wrapper-width);padding-left:1rem;vertical-align:middle}div[class*=language-].line-numbers-mode .line-numbers{position:absolute;top:0;width:var(--code-ln-wrapper-width);text-align:center;color:var(--code-ln-color);padding-top:1.25rem;line-height:1.375;counter-reset:line-number}div[class*=language-].line-numbers-mode .line-numbers .line-number{position:relative;z-index:3;-webkit-user-select:none;-moz-user-select:none;user-select:none;height:1.375em}div[class*=language-].line-numbers-mode .line-numbers .line-number:before{counter-increment:line-number;content:counter(line-number);font-size:.85em}div[class*=language-].line-numbers-mode:after{content:"";position:absolute;top:0;left:0;width:var(--code-ln-wrapper-width);height:100%;border-radius:6px 0 0 6px;border-right:1px solid var(--code-hl-bg-color)}@media (max-width: 419px){.theme-default-content div[class*=language-]{margin:.85rem -1.5rem;border-radius:0}}.code-group__nav{margin-top:.85rem;margin-bottom:calc(-1.7rem - 6px);padding-bottom:calc(1.7rem - 6px);padding-left:10px;padding-top:10px;border-top-left-radius:6px;border-top-right-radius:6px;background-color:var(--code-bg-color)}.code-group__ul{margin:auto 0;padding-left:0;display:inline-flex;list-style:none}.code-group__nav-tab{border:0;padding:5px;cursor:pointer;background-color:transparent;font-size:.85em;line-height:1.4;color:#ffffffe6;font-weight:600}.code-group__nav-tab:focus{outline:none}.code-group__nav-tab:focus-visible{outline:1px solid rgba(255,255,255,.9)}.code-group__nav-tab-active{border-bottom:var(--c-brand) 1px solid}@media (max-width: 419px){.code-group__nav{margin-left:-1.5rem;margin-right:-1.5rem;border-radius:0}}.code-group-item{display:none}.code-group-item__active{display:block}.code-group-item>pre{background-color:orange}.custom-container{transition:color var(--t-color),border-color var(--t-color),background-color var(--t-color)}.custom-container .custom-container-title{font-weight:600}.custom-container .custom-container-title:not(:only-child){margin-bottom:-.4rem}.custom-container.tip,.custom-container.warning,.custom-container.danger{padding:.1rem 1.5rem;border-left-width:.5rem;border-left-style:solid;margin:1rem 0}.custom-container.tip{border-color:var(--c-tip);background-color:var(--c-tip-bg);color:var(--c-tip-text)}.custom-container.tip .custom-container-title{color:var(--c-tip-title)}.custom-container.tip a{color:var(--c-tip-text-accent)}.custom-container.tip code{background-color:var(--c-bg-dark)}.custom-container.warning{border-color:var(--c-warning);background-color:var(--c-warning-bg);color:var(--c-warning-text)}.custom-container.warning .custom-container-title{color:var(--c-warning-title)}.custom-container.warning a{color:var(--c-warning-text-accent)}.custom-container.warning blockquote{border-left-color:var(--c-warning-border-dark);color:var(--c-warning-text-quote)}.custom-container.warning code{color:var(--c-warning-text-light);background-color:var(--c-warning-bg-light)}.custom-container.warning details{background-color:var(--c-warning-details-bg)}.custom-container.warning details code{background-color:var(--c-warning-bg-lighter)}.custom-container.warning .external-link-icon{--external-link-icon-color: var(--c-warning-text-quote)}.custom-container.danger{border-color:var(--c-danger);background-color:var(--c-danger-bg);color:var(--c-danger-text)}.custom-container.danger .custom-container-title{color:var(--c-danger-title)}.custom-container.danger a{color:var(--c-danger-text-accent)}.custom-container.danger blockquote{border-left-color:var(--c-danger-border-dark);color:var(--c-danger-text-quote)}.custom-container.danger code{color:var(--c-danger-text-light);background-color:var(--c-danger-bg-light)}.custom-container.danger details{background-color:var(--c-danger-details-bg)}.custom-container.danger details code{background-color:var(--c-danger-bg-lighter)}.custom-container.danger .external-link-icon{--external-link-icon-color: var(--c-danger-text-quote)}.custom-container.details{display:block;position:relative;border-radius:2px;margin:1.6em 0;padding:1.6em;background-color:var(--c-details-bg)}.custom-container.details code{background-color:var(--c-bg-darker)}.custom-container.details h4{margin-top:0}.custom-container.details figure:last-child,.custom-container.details p:last-child{margin-bottom:0;padding-bottom:0}.custom-container.details summary{outline:none;cursor:pointer}.home{padding:var(--navbar-height) 2rem 0;max-width:var(--homepage-width);margin:0 auto;display:block}.home .hero{text-align:center}.home .hero img{max-width:100%;max-height:280px;display:block;margin:3rem auto 1.5rem}.home .hero h1{font-size:3rem}.home .hero h1,.home .hero .description,.home .hero .actions{margin:1.8rem auto}.home .hero .actions{display:flex;flex-wrap:wrap;gap:1rem;justify-content:center}.home .hero .description{max-width:35rem;font-size:1.6rem;line-height:1.3;color:var(--c-text-lightest)}.home .hero .action-button{display:inline-block;font-size:1.2rem;padding:.8rem 1.6rem;border-width:2px;border-style:solid;border-radius:4px;transition:background-color var(--t-color);box-sizing:border-box}.home .hero .action-button.primary{color:var(--c-bg);background-color:var(--c-brand);border-color:var(--c-brand)}.home .hero .action-button.primary:hover{background-color:var(--c-brand-light)}.home .hero .action-button.secondary{color:var(--c-brand);background-color:var(--c-bg);border-color:var(--c-brand)}.home .hero .action-button.secondary:hover{color:var(--c-bg);background-color:var(--c-brand-light)}.home .features{border-top:1px solid var(--c-border);transition:border-color var(--t-color);padding:1.2rem 0;margin-top:2.5rem;display:flex;flex-wrap:wrap;align-items:flex-start;align-content:stretch;justify-content:space-between}.home .feature{flex-grow:1;flex-basis:30%;max-width:30%}.home .feature h2{font-size:1.4rem;font-weight:500;border-bottom:none;padding-bottom:0;color:var(--c-text-light)}.home .feature p{color:var(--c-text-lighter)}.home .theme-default-content{padding:0;margin:0}.home .footer{padding:2.5rem;border-top:1px solid var(--c-border);text-align:center;color:var(--c-text-lighter);transition:border-color var(--t-color)}@media (max-width: 719px){.home .features{flex-direction:column}.home .feature{max-width:100%;padding:0 2.5rem}}@media (max-width: 419px){.home{padding-left:1.5rem;padding-right:1.5rem}.home .hero img{max-height:210px;margin:2rem auto 1.2rem}.home .hero h1{font-size:2rem}.home .hero h1,.home .hero .description,.home .hero .actions{margin:1.2rem auto}.home .hero .description{font-size:1.2rem}.home .hero .action-button{font-size:1rem;padding:.6rem 1.2rem}.home .feature h2{font-size:1.25rem}}.page{padding-top:var(--navbar-height);padding-left:var(--sidebar-width)}.navbar{position:fixed;z-index:20;top:0;left:0;right:0;height:var(--navbar-height);box-sizing:border-box;border-bottom:1px solid var(--c-border);background-color:var(--c-bg-navbar);transition:background-color var(--t-color),border-color var(--t-color)}.sidebar{font-size:16px;width:var(--sidebar-width);position:fixed;z-index:10;margin:0;top:var(--navbar-height);left:0;bottom:0;box-sizing:border-box;border-right:1px solid var(--c-border);overflow-y:auto;scrollbar-width:thin;scrollbar-color:var(--c-brand) var(--c-border);background-color:var(--c-bg-sidebar);transition:transform var(--t-transform),background-color var(--t-color),border-color var(--t-color)}.sidebar::-webkit-scrollbar{width:7px}.sidebar::-webkit-scrollbar-track{background-color:var(--c-border)}.sidebar::-webkit-scrollbar-thumb{background-color:var(--c-brand)}.sidebar-mask{position:fixed;z-index:9;top:0;left:0;width:100vw;height:100vh;display:none}.theme-container.sidebar-open .sidebar-mask{display:block}.theme-container.sidebar-open .navbar>.toggle-sidebar-button .icon span:nth-child(1){transform:rotate(45deg) translate3d(5.5px,5.5px,0)}.theme-container.sidebar-open .navbar>.toggle-sidebar-button .icon span:nth-child(2){transform:scale3d(0,1,1)}.theme-container.sidebar-open .navbar>.toggle-sidebar-button .icon span:nth-child(3){transform:rotate(-45deg) translate3d(6px,-6px,0)}.theme-container.sidebar-open .navbar>.toggle-sidebar-button .icon span:nth-child(1),.theme-container.sidebar-open .navbar>.toggle-sidebar-button .icon span:nth-child(3){transform-origin:center}.theme-container.no-navbar .theme-default-content h1,.theme-container.no-navbar .theme-default-content h2,.theme-container.no-navbar .theme-default-content h3,.theme-container.no-navbar .theme-default-content h4,.theme-container.no-navbar .theme-default-content h5,.theme-container.no-navbar .theme-default-content h6{margin-top:1.5rem;padding-top:0}.theme-container.no-navbar .page{padding-top:0}.theme-container.no-navbar .sidebar{top:0}.theme-container.no-sidebar .sidebar{display:none}@media (max-width: 719px){.theme-container.no-sidebar .sidebar{display:block}}.theme-container.no-sidebar .page{padding-left:0}.theme-default-content a:hover{text-decoration:underline}.theme-default-content img{max-width:100%}.theme-default-content h1,.theme-default-content h2,.theme-default-content h3,.theme-default-content h4,.theme-default-content h5,.theme-default-content h6{margin-top:calc(.5rem - var(--navbar-height));padding-top:calc(1rem + var(--navbar-height));margin-bottom:0}.theme-default-content h1:first-child,.theme-default-content h2:first-child,.theme-default-content h3:first-child,.theme-default-content h4:first-child,.theme-default-content h5:first-child,.theme-default-content h6:first-child{margin-bottom:1rem}.theme-default-content h1:first-child+p,.theme-default-content h1:first-child+pre,.theme-default-content h1:first-child+.custom-container,.theme-default-content h2:first-child+p,.theme-default-content h2:first-child+pre,.theme-default-content h2:first-child+.custom-container,.theme-default-content h3:first-child+p,.theme-default-content h3:first-child+pre,.theme-default-content h3:first-child+.custom-container,.theme-default-content h4:first-child+p,.theme-default-content h4:first-child+pre,.theme-default-content h4:first-child+.custom-container,.theme-default-content h5:first-child+p,.theme-default-content h5:first-child+pre,.theme-default-content h5:first-child+.custom-container,.theme-default-content h6:first-child+p,.theme-default-content h6:first-child+pre,.theme-default-content h6:first-child+.custom-container{margin-top:2rem}@media (max-width: 959px){.sidebar{font-size:15px;width:var(--sidebar-width-mobile)}.page{padding-left:var(--sidebar-width-mobile)}}@media (max-width: 719px){.sidebar{top:0;padding-top:var(--navbar-height);transform:translate(-100%)}.page{padding-left:0}.theme-container.sidebar-open .sidebar{transform:translate(0)}.theme-container.no-navbar .sidebar{padding-top:0}}@media (max-width: 419px){h1{font-size:1.9rem}}.navbar{--navbar-line-height: calc( var(--navbar-height) - 2 * var(--navbar-padding-v) );padding:var(--navbar-padding-v) var(--navbar-padding-h);line-height:var(--navbar-line-height)}.navbar .logo{height:var(--navbar-line-height);margin-right:var(--navbar-padding-v);vertical-align:top}.navbar .site-name{font-size:1.3rem;font-weight:600;color:var(--c-text);position:relative}.navbar .navbar-items-wrapper{display:flex;position:absolute;box-sizing:border-box;top:var(--navbar-padding-v);right:var(--navbar-padding-h);height:var(--navbar-line-height);padding-left:var(--navbar-padding-h);white-space:nowrap;font-size:.9rem}.navbar .navbar-items-wrapper .search-box{flex:0 0 auto;vertical-align:top}@media screen and (max-width: 719px){.navbar{padding-left:4rem}.navbar .site-name{display:block;width:calc(100vw - 11rem);overflow:hidden;white-space:nowrap;text-overflow:ellipsis}.navbar .can-hide{display:none}}.navbar-items{display:inline-block}@media print{.navbar-items{display:none}}.navbar-items a{display:inline-block;line-height:1.4rem;color:inherit}.navbar-items a:hover,.navbar-items a.router-link-active{color:var(--c-text)}.navbar-items .navbar-item{position:relative;display:inline-block;margin-left:1.5rem;line-height:var(--navbar-line-height)}.navbar-items .navbar-item:first-child{margin-left:0}.navbar-items .navbar-item>a:hover,.navbar-items .navbar-item>a.router-link-active{margin-bottom:-2px;border-bottom:2px solid var(--c-text-accent)}@media (max-width: 719px){.navbar-items .navbar-item{margin-left:0}.navbar-items .navbar-item>a:hover,.navbar-items .navbar-item>a.router-link-active{margin-bottom:0;border-bottom:none}.navbar-items a:hover,.navbar-items a.router-link-active{color:var(--c-text-accent)}}.toggle-sidebar-button{position:absolute;top:.6rem;left:1rem;display:none;padding:.6rem;cursor:pointer}.toggle-sidebar-button .icon{display:flex;flex-direction:column;justify-content:center;align-items:center;width:1.25rem;height:1.25rem;cursor:inherit}.toggle-sidebar-button .icon span{display:inline-block;width:100%;height:2px;border-radius:2px;background-color:var(--c-text);transition:transform var(--t-transform)}.toggle-sidebar-button .icon span:nth-child(2){margin:6px 0}@media screen and (max-width: 719px){.toggle-sidebar-button{display:block}}.toggle-color-mode-button{display:flex;margin:auto;margin-left:1rem;border:0;background:none;color:var(--c-text);opacity:.8;cursor:pointer}@media print{.toggle-color-mode-button{display:none}}.toggle-color-mode-button:hover{opacity:1}.toggle-color-mode-button .icon{width:1.25rem;height:1.25rem}.DocSearch{transition:background-color var(--t-color)}.navbar-dropdown-wrapper{cursor:pointer}.navbar-dropdown-wrapper .navbar-dropdown-title,.navbar-dropdown-wrapper .navbar-dropdown-title-mobile{display:block;font-size:.9rem;font-family:inherit;cursor:inherit;padding:inherit;line-height:1.4rem;background:transparent;border:none;font-weight:500;color:var(--c-text)}.navbar-dropdown-wrapper .navbar-dropdown-title:hover,.navbar-dropdown-wrapper .navbar-dropdown-title-mobile:hover{border-color:transparent}.navbar-dropdown-wrapper .navbar-dropdown-title .arrow,.navbar-dropdown-wrapper .navbar-dropdown-title-mobile .arrow{vertical-align:middle;margin-top:-1px;margin-left:.4rem}.navbar-dropdown-wrapper .navbar-dropdown-title-mobile{display:none;font-weight:600;font-size:inherit}.navbar-dropdown-wrapper .navbar-dropdown-title-mobile:hover{color:var(--c-text-accent)}.navbar-dropdown-wrapper .navbar-dropdown .navbar-dropdown-item{color:inherit;line-height:1.7rem}.navbar-dropdown-wrapper .navbar-dropdown .navbar-dropdown-item .navbar-dropdown-subtitle{margin:.45rem 0 0;border-top:1px solid var(--c-border);padding:1rem 0 .45rem;font-size:.9rem}.navbar-dropdown-wrapper .navbar-dropdown .navbar-dropdown-item .navbar-dropdown-subtitle>span{padding:0 1.5rem 0 1.25rem}.navbar-dropdown-wrapper .navbar-dropdown .navbar-dropdown-item .navbar-dropdown-subtitle>a{font-weight:inherit}.navbar-dropdown-wrapper .navbar-dropdown .navbar-dropdown-item .navbar-dropdown-subtitle>a.router-link-active:after{display:none}.navbar-dropdown-wrapper .navbar-dropdown .navbar-dropdown-item .navbar-dropdown-subitem-wrapper{padding:0;list-style:none}.navbar-dropdown-wrapper .navbar-dropdown .navbar-dropdown-item .navbar-dropdown-subitem-wrapper .navbar-dropdown-subitem{font-size:.9em}.navbar-dropdown-wrapper .navbar-dropdown .navbar-dropdown-item a{display:block;line-height:1.7rem;position:relative;border-bottom:none;font-weight:400;margin-bottom:0;padding:0 1.5rem 0 1.25rem}.navbar-dropdown-wrapper .navbar-dropdown .navbar-dropdown-item a:hover,.navbar-dropdown-wrapper .navbar-dropdown .navbar-dropdown-item a.router-link-active{color:var(--c-text-accent)}.navbar-dropdown-wrapper .navbar-dropdown .navbar-dropdown-item a.router-link-active:after{content:"";width:0;height:0;border-left:5px solid var(--c-text-accent);border-top:3px solid transparent;border-bottom:3px solid transparent;position:absolute;top:calc(50% - 2px);left:9px}.navbar-dropdown-wrapper .navbar-dropdown .navbar-dropdown-item:first-child .navbar-dropdown-subtitle{margin-top:0;padding-top:0;border-top:0}.navbar-dropdown-wrapper.mobile.open .navbar-dropdown-title,.navbar-dropdown-wrapper.mobile.open .navbar-dropdown-title-mobile{margin-bottom:.5rem}.navbar-dropdown-wrapper.mobile .navbar-dropdown-title,.navbar-dropdown-wrapper.mobile .navbar-dropdown-title-mobile{display:none}.navbar-dropdown-wrapper.mobile .navbar-dropdown-title-mobile{display:block}.navbar-dropdown-wrapper.mobile .navbar-dropdown{transition:height .1s ease-out;overflow:hidden}.navbar-dropdown-wrapper.mobile .navbar-dropdown .navbar-dropdown-item .navbar-dropdown-subtitle{border-top:0;margin-top:0;padding-top:0;padding-bottom:0}.navbar-dropdown-wrapper.mobile .navbar-dropdown .navbar-dropdown-item .navbar-dropdown-subtitle,.navbar-dropdown-wrapper.mobile .navbar-dropdown .navbar-dropdown-item>a{font-size:15px;line-height:2rem}.navbar-dropdown-wrapper.mobile .navbar-dropdown .navbar-dropdown-item .navbar-dropdown-subitem{font-size:14px;padding-left:1rem}.navbar-dropdown-wrapper:not(.mobile){height:1.8rem}.navbar-dropdown-wrapper:not(.mobile):hover .navbar-dropdown,.navbar-dropdown-wrapper:not(.mobile).open .navbar-dropdown{display:block!important}.navbar-dropdown-wrapper:not(.mobile).open:blur{display:none}.navbar-dropdown-wrapper:not(.mobile) .navbar-dropdown{display:none;height:auto!important;box-sizing:border-box;max-height:calc(100vh - 2.7rem);overflow-y:auto;position:absolute;top:100%;right:0;background-color:var(--c-bg-navbar);padding:.6rem 0;border:1px solid var(--c-border);border-bottom-color:var(--c-border-dark);text-align:left;border-radius:.25rem;white-space:nowrap;margin:0}.page{padding-bottom:2rem;display:block}.page .theme-default-content{max-width:var(--content-width);margin:0 auto;padding:2rem 2.5rem;padding-top:0}@media (max-width: 959px){.page .theme-default-content{padding:2rem}}@media (max-width: 419px){.page .theme-default-content{padding:1.5rem}}.page-meta{max-width:var(--content-width);margin:0 auto;padding:1rem 2.5rem;overflow:auto}@media (max-width: 959px){.page-meta{padding:2rem}}@media (max-width: 419px){.page-meta{padding:1.5rem}}.page-meta .meta-item{cursor:default;margin-top:.8rem}.page-meta .meta-item .meta-item-label{font-weight:500;color:var(--c-text-lighter)}.page-meta .meta-item .meta-item-info{font-weight:400;color:var(--c-text-quote)}.page-meta .edit-link{display:inline-block;margin-right:.25rem}@media print{.page-meta .edit-link{display:none}}.page-meta .last-updated{float:right}@media (max-width: 719px){.page-meta .last-updated{font-size:.8em;float:none}.page-meta .contributors{font-size:.8em}}.page-nav{max-width:var(--content-width);margin:0 auto;padding:1rem 2.5rem 2rem;padding-bottom:0}@media (max-width: 959px){.page-nav{padding:2rem}}@media (max-width: 419px){.page-nav{padding:1.5rem}}.page-nav .inner{min-height:2rem;margin-top:0;border-top:1px solid var(--c-border);transition:border-color var(--t-color);padding-top:1rem;overflow:auto}.page-nav .prev a:before{content:"←"}.page-nav .next{float:right}.page-nav .next a:after{content:"→"}.sidebar ul{padding:0;margin:0;list-style-type:none}.sidebar a{display:inline-block}.sidebar .navbar-items{display:none;border-bottom:1px solid var(--c-border);transition:border-color var(--t-color);padding:.5rem 0 .75rem}.sidebar .navbar-items a{font-weight:600}.sidebar .navbar-items .navbar-item{display:block;line-height:1.25rem;font-size:1.1em;padding:.5rem 0 .5rem 1.5rem}.sidebar .sidebar-items{padding:1.5rem 0}@media (max-width: 719px){.sidebar .navbar-items{display:block}.sidebar .navbar-items .navbar-dropdown-wrapper .navbar-dropdown .navbar-dropdown-item a.router-link-active:after{top:calc(1rem - 2px)}.sidebar .sidebar-items{padding:1rem 0}}.sidebar-item{cursor:default;border-left:.25rem solid transparent;color:var(--c-text)}.sidebar-item:focus-visible{outline-width:1px;outline-offset:-1px}.sidebar-item.active:not(p.sidebar-heading){font-weight:600;color:var(--c-text-accent);border-left-color:var(--c-text-accent)}.sidebar-item.sidebar-heading{transition:color .15s ease;font-size:1.1em;font-weight:700;padding:.35rem 1.5rem .35rem 1.25rem;width:100%;box-sizing:border-box;margin:0}.sidebar-item.sidebar-heading+.sidebar-item-children{transition:height .1s ease-out;overflow:hidden;margin-bottom:.75rem}.sidebar-item.collapsible{cursor:pointer}.sidebar-item.collapsible .arrow{position:relative;top:-.12em;left:.5em}.sidebar-item:not(.sidebar-heading){font-size:1em;font-weight:400;display:inline-block;margin:0;padding:.35rem 1rem .35rem 2rem;line-height:1.4;width:100%;box-sizing:border-box}.sidebar-item:not(.sidebar-heading)+.sidebar-item-children{padding-left:1rem;font-size:.95em}.sidebar-item-children .sidebar-item-children .sidebar-item:not(.sidebar-heading){padding:.25rem 1rem .25rem 1.75rem}.sidebar-item-children .sidebar-item-children .sidebar-item:not(.sidebar-heading).active{font-weight:500;border-left-color:transparent}a.sidebar-heading+.sidebar-item-children .sidebar-item:not(.sidebar-heading).active{border-left-color:transparent}a.sidebar-item{cursor:pointer}a.sidebar-item:hover{color:var(--c-text-accent)}.table-of-contents .badge{vertical-align:middle}.dropdown-enter-from,.dropdown-leave-to{height:0!important}.fade-slide-y-enter-active{transition:all .2s ease}.fade-slide-y-leave-active{transition:all .2s cubic-bezier(1,.5,.8,1)}.fade-slide-y-enter-from,.fade-slide-y-leave-to{transform:translateY(10px);opacity:0} diff --git a/backend/go/base/index.html b/backend/go/base/index.html new file mode 100644 index 0000000..c3b247b --- /dev/null +++ b/backend/go/base/index.html @@ -0,0 +1,1528 @@ + + + + + + + + + 瓢儿白施肥记 + + + + +

Go

编译型语言

Go 是静态类型语言,一旦某个变量被声明,那么它的类型就无法再改变了

搭建环境

vscode:

  1. 插件

  2. Go install/update tools:安装/更新工具

    Go 代理

    go env -w GO111MODULE=on
    +go env -w GOPROXY=https://goproxy.cn,direct
    +

变量及其作用域

作用域的范围就是 {} 之间的部分

短声明

// 使用 var 声明变量
+var a = 1
+// 也可以使用短声明,效果同上,但可以在无法使用 var 的地方使用
+a := 1
+
+for i := 0; i < 10; i++ {
+   // i 在 for 循环中声明,作用域只在 for 循环中
+}
+
+if a := 1; a > 0 {
+   // a 在 if 语句中声明,作用域只在 if 语句中
+}
+
+switch a := 1; a {
+case 1:
+   // a 在 switch 语句中声明,作用域只在 switch 语句中
+default:
+   //...
+}
+
+

main 函数外声明的变量拥有 package 作用域,短声明不能用来声明 package 作用域的变量

实数

声明浮点型变量

只要数字含有小数部分,那么它的类型就是 float64

/* 下面三个语句的效果是一样的 */
+days := 365.2425
+var days = 365.2425
+var days float64 = 365.2425
+/* 如果使用一个整数来初始化某个变量,则必须指定它的类型为 float64,否则它就是一个整数类型 */
+var answer float32 = 42
+

Go 语言里有两种浮点数类型:

默认是 float64

  • 64 位的浮点类型
  • 占用 8 字节

float32

  • 占用 4 字节
  • 精度比 float64 低
  • 有时叫做单精度浮点数类型

想使用单精度类型,必须再声明变量的时候指定该类型:

var pi64 = math.Pi
+var pi32 float32 = math.Pi
+fmt.Println(pi64) // 3.141592653589793
+fmt.Println(pi32) // 3.1415927
+
  • 当处理大量数据时,例如 3D 游戏中的数千个顶点,使用 float32 牺牲精度来节省内存是很有意义的
  • math 包里面的函数操作都是 float64 类型,所以应该首选使用 float 64 类型,除非有足够的理由不去使用它

零值

Go 里面的每个类型都有一个默认值,它称作零值

当声明变量却不对它进行初始化的时候,它的值就是零值

var price float64
+fmt.Println(price) // 0
+

显示浮点类型

  • Print 或 Println 打印浮点类型的时候,默认的行为是尽可能地多显示几位小数
  • Printf 函数,结合 %f 格式化动词来指定显示小数的位数
third := 1.0 / 3
+fmt.Println(third) // 0.3333333333333333
+fmt.Printf("%v\n", third) // 0.3333333333333333
+fmt.Printf("%f\n", third) // 0.333333
+fmt.Printf("%.3f\n", third) // 0.333
+fmt.Printf("%4.2f\n", third) // 0.33
+fmt.Printf("%05.2f\n", third) // 00.33,默认是空格填充
+

浮点类型不适合用于金融类计算,为了尽量最小化舍入错误,建议先做乘法,再做除法

如何比较浮点类型

piggyBank := 0.1
+piggyBank += 0.2
+fmt.Println(piggyBank == 0.3) // false
+
+fmt.Println(math.Abs(piggyBank - 0.3) < 0.0001)
+

整数类型

Go 提供了 10 种整数类型(不可以存小数部分,范围有限,通常根据数值范围来选取整数类型)

  • 5 种整数类型是有符号的,能表示正数、0、负数
  • 5 种整数类型是无符号的,能表示正数、0
// 最常用的整数类型是 int
+var year int = 2018
+// 无符号整数类型是 uint
+var month uint = 12
+

下面三个语句是等价的:

year := 2018
+var year = 2018
+var year int = 2018
+

int 和 uint 是针对目标设备优化的类型

  • 在树莓派 2、比较老的移动设备上,int 和 int32 都是 32 位的
  • 在比较新的计算机上,int 和 int64 都是 64 位的

如果在比较老的 32 位设备上,使用了超过 20 亿的整数,并且代码还能运行,那么最好使用 int64 和 uint64 来代替 int 和 uint

打印数据类型

在 Printf 函数里面,可以使用 %T 格式化动词来打印变量的类型

year := 2018
+fmt.Printf("Type %T for %v\n", year, year) // Type int for 2018
+a := "text"
+fmt.Printf("Type %T for %[1]v\n", a) // Type string for text
+b := 3.14
+fmt.Printf("Type %T for %[1]v\n", b) // Type float64 for 3.14
+c := true
+fmt.Printf("Type %T from %[1]v\n", c) // Type bool from true
+

uint8

取值范围 0-255

  • unit8 可以用来表示 8 位的颜色(红绿蓝:0-255)
    var red, green, blue unit8 = 0, 141, 213
    +

十六进制表示法

Go 语言里,在数前面加上 0x 前缀,就可以用十六进制的形式来表示

var red, green, blue unit8 = 0, 141, 213
+var red, green, blue unit8 = 0x00, 0x8d, 0xd5
+

打印十六进制

打印十六进制的数,用 %x 格式化动词

fmt.Printf("%x %x %x", red, green, blue)
+// 也可以指定最小宽度和填充
+fmt.Printf("color: #%02x%02x%02x;", red, green, blue)
+

整数环绕

所有的整数都有一个取值范围,超出这个范围,就会发生“环绕”

var red uint8 = 255
+red++
+fmt.Println(red) // 0
+
+var number int8 = 127
+number++
+fmt.Println(number) // -128
+

如何避免时间发生环绕?

Unix 系统里,时间是以 1970 年 1 月 1 日至今的秒数来表示的

但是在 2038 年,这个数就会超过 20 多亿,也就是超过了 int32 的范围

应使用:int64 或 uint64

future := time.Unix(12622780800, 0)
+fmt.Println(future) // 2370-01-01 08:00:00 +0800 CST
+

打印每个 bit

使用 %b 格式化动词

var green uint8 = 3
+fmt.Printf("%08b\n", green) // 00000011
+green++
+fmt.Printf("%08b\n", green) // 00000100
+

整数类型的最大值、最小值

  • math 包里,为与架构无关的整数类型,定义了最大、最小值常量
    math.MaxInt16
    +math.MinInt64
    +
  • 而 int 和 uint,可能是 32 位 或 64 位的

比较大的数

浮点类型可以存储非常大的数值,但是精度不高

整型很精确,但是取值范围有限

使用指数表示的数,默认就是 float64 类型

var distance = 24e2
+fmt.Printf("%T", distance) // float64
+

如果需要存储非常大的整数,可以使用 math/big 包

  • 对于比较大的整数(超过 10^18),big.Int
  • 对于任意精度的浮点类型,big.Float
  • 对于分数,big.Rat
lightSpeed := big.NewInt(299792)
+secondsPerDay := big.NewInt(86400)
+
+distance := new(big.Int)
+distance.SetString("24000000000000000000000", 10)
+fmt.Println(distance) // 24000000000000000000000
+
+seconds := new(big.Int)
+seconds.Div(distance, lightSpeed) // seconds = distance / lightSpeed
+
+days := new(big.Int)
+days.Div(seconds, secondsPerDay) // days = seconds / secondsPerDay
+
+fmt.Println("That is", days, "days of travel at light speed.")
+

一旦使用了 big.Int,那么等式里其他部分也必须使用 big.Int

NewInt() 函数可以把 int64 转化为 big.Int 类型

缺点:用起来繁琐,速度较慢

较大数值的常量

// 会报错
+const distance unit64 = 24000000000000000000000
+// 但在 Go 里面,常量是可以无类型的(untyped),下面就不会报错
+const distance = 24000000000000000000000 // untyped int
+fmt.Printf("%T", distance) // 报错
+

常量使用 const 关键字来声明,程序里每个字面值都是常量,这意味着比较大的数值可以直接使用(作为字面值)

fmt.Println(24000000000000000000000/299792/86400) // 926568346646
+

针对字面值和常量的计算是在编译阶段完成的

Go 的编译器是用 Go 编写的,这种无类型的数值字面值就是由 big 包所支持的,这使得可以操作很大的数(超过 18 的 10^18)

多语言文本

声明字符串

声明

peace := "peace"
+var peace = "peace"
+var peace string = "peace"
+

字符串的零值

var empty string
+fmt.Println(empty == "") // true
+

字符串字面值/原始字符串字面值

字符串字面值(string literal)可以包含转义字符,例如 \n

但如果想得到 \n 而不是换行的话,可以使用 ` 来代替 ",这叫做原始字符串字面值(raw string literal)

fmt.Println("peace be upon you\nupon you be peace")
+fmt.Println(`strings can span multiple lines with the \n escape sequence`)
+fmt.Println(`
+peace be upon you
+upon you be peace
+`)
+

字符,code points,runes,bytes

Unicode 联盟为超过 100 万个字符分配了相应的数值,这个数叫做 code point

  • 例如:65 代表 A,128515 代表 😃

为了表示这样的 unicode code point,Go 提供了 rune 类型,它是 int32 的别名

byte 是 unit 8 类型的别名,目的是用于二进制数据

  • byte 倒是可以表示由 ASCII 定义的英语字符,它是 Unicode 的一个子集(共 128 个字符)

类型别名

类型别名就是同一个类型的另一个名字

  • 所以,rune 和 int32 可以互换使用

也可以自定义类型别名,语法如下

type byte = uint8
+type rune = int32
+

打印

如果想打印字符而不是数值,使用 c% 格式化动词

fmt.Printf("%c", 128515) // 😃
+

任何整数类型都可以使用 %c 打印,但是 rune 意味着该数值表示了一个字符

字符

字符字面值使用 '' 括起来,例如 'A'

如果没有指定字符类型的话,Go 会推断它的类型为 rune

grade := 'A'
+var grade1 = 'A'
+var grade2 rune = 'A'
+

这里的 grade 仍然包含一个数值,本例中就是 65,它是 A 的 code point

字符字面值也可以用 byte 类型

var star byte = '*'
+

string

可以给某个变量赋予不同的 string 值,但是 string 本身是不可变的

message := "shalom"
+c := message[5]
+fmt.Printf("%c\n", c) // m
+message[5] = 'd' // 报错
+

Caesar cipher 凯撒加密法

凯撒加密法是一种简单的加密方法,它是通过将每个字符移动固定数目的位置来实现的

c := 'a'
+c = c + 3
+fmt.Printf("%c", c) // d
+if c > 'z' {
+  c = c - 26
+}
+

ROT13

ROT13 (旋转 13) 是凯撒加密在 20 世纪的变体, 它会把字母替换成 +13 后对应的字母

originalMessage := "uv vagreangvbany fcnpr fgngvba"
+for i := 0; i < len(originalMessage); i++ {
+  c := originalMessage[i]
+  if c >= 'a' && c <= 'z' {
+    c = c + 13
+    if c > 'z' {
+      c = c - 26
+    }
+  }
+  fmt.Printf("%c", c)
+}
+

Go 的内置函数

len 是 Go 语言的一个内置函数

message := "uv vagreangvbany fcnpr fgngvba"
+fmt.Println(len(message)) // 32
+

本例中 len 返回 message 所占的 byte 数

UTF-8

Go 中的字符串是用 UTF-8 编码的,UTF-8 是 Unicode Code Point 的几种编码之一

UTF8 是一种有效率的可变长度的编码,每个 code point 可以是 8 位、16 位或 32 位的

通过使用可变长度编码,UTF-8 使得从 ASCII 的转换变得简单明了,因为 ASCII 字符与其 UTF-8 编码对应的字符是相同的

UTF-8 是万维网的主要字符编码,它是由 Ken Thompson(Go 语言的设计者之一) 于 1992 年发明的

question := "¿Cómo estás?"
+fmt.Println(len(question), "bytes") // 15
+fmt.Println(utf8.RuneCountInString(question), "runes") // 12
+
+c,size := utf8.DecodeRuneInString(question)
+fmt.Printf("First rune: %c %v bytes", c, size) // First rune: ¿ 2 bytes
+

range

使用 range 关键字,可以遍历各种集合

question := "¿Cómo estás?"
+for i, c := range question {
+  fmt.Printf("%v %c\n", i, c)
+}
+

类型不能混用

连接两个字符串,使用 + 运算符

countdown := "Launch in T minus " + "10 seconds."
+

如果想连接字符串和数值,是会报错的

countdown := "Launch in T minus " + 10 + " seconds."
+

整型和浮点类型也不能混着用

age := 41
+marsDays := 687
+earthDays := 365.2425
+fmt.Println("I am", age * earthDays / marsDays, "years old on Mars.") // invalid operation: age * earthDays (mismatched types int and float64)
+

数值间类型转换

整数类型转换为浮点类型

age := 41
+// 将 age 转换为浮点类型
+marsAge := float64(age)
+

浮点类型转换为整数类型,小数点后边的部分会被截断,而不是舍入

earthDays := 365.2425
+// 将 earthDays 转换为整数类型
+fmt.Println(int(earthDays)) // 365
+

无符号和有符号整数类型之间也需要转换

不同大小的整数类型之间也需要转换

类型转换时需谨慎

环绕行为

var bh float64 = 32768
+var h = int16(bh)
+fmt.Println(h) // -32768
+

可以通过 math 包提供的 max、min 常量,来判断是否超过最大最小值

var bh float64 = 32768
+if bh < math.MinInt16 || bh > math.MaxInt16 {
+   // handle out of range error
+}
+

字符串转换

把 rune、byte 转换为 string

var pi rune = 960
+var alpha rune = 940
+var omega rune = 969
+var bang byte = 33
+fmt.Printf("%v %v %v %v\n", string(pi), string(alpha), string(omega), string(bang)) // π ά ω !
+

想把数值转化为有意义的字符串,它的值必须能转化为 code point

countdown := 10
+str := "Launch in T minus " + strconv.Itoa(countdown) + " seconds."
+fmt.Println(str) // Launch in T minus 10 seconds.
+

Itoa 是 Integer to ASCII 的意思

Unicode 是 ASCII 的超集,它们前 128 个 code points 是一样的(数字、英文字母、常用标点)

另外一种把数值转化为 string 的方式是使用 Sprintf 函数,和 Printf 略类似,但是会返回一个 string

countdown := 9
+str := fmt.Sprintf("Launch in T minus %v seconds.", countdown)
+fmt.Println(str) // Launch in T minus 9 seconds.
+

strconv 包中的 Atoi 函数(ASCII to Integer),由于字符串里面可能包含任意字符,或者要转换的数字字符串太大,所以 Atoi 函数可能会发生错误

countdown, err := strconv.Atoi("10ds")
+if err != nil {
+  // handle error
+  fmt.Println(err.Error())
+}
+fmt.Println(countdown) // 10
+

布尔类型的转换

Print 家族函数,会把 bool 类型的值打印成 true/false 文本

launch := false
+launchText := fmt.Sprintf("%v", launch)
+fmt.Println("Ready for launch:", launchText) // Ready for launch: false
+
+var yesNo string
+if launch {
+   yesNo = "yes"
+} else {
+   yesNo = "no"
+}
+fmt.Println("Ready for launch:", yesNo) // Ready for launch: no
+

如果想使用 string(false),int(false);bool(1), bool("yes") 等类似的方法进行转换,那么 Go 编译器会报错

函数

使用 func 关键字

大写字母开头的函数、变量或其他标识符都会被导出,对其他包可用;小写字母开头的则不会

实参(argument)和形参(parameter)

函数声明时,如果多个形参类型相同,那么该类型只写一次即可:

// func Unix(sec int64, nsec int64) Time
+func Unix(sec, nsec int64) Time
+

Go 的函数可以返回多个值

countdown, err := strconv.Atoi("10")
+

上面的函数声明为 func Atoi(s string) (i int, err error)

函数的多个返回值需要用括号括起来,每个返回值名字在前,类型在后。声明函数时可以把名字去掉,只保留类型:

func Atoi(s string) (int, error)
+

Println 是一个特殊的函数,它可以接收一个、两个甚至多个参数,参数类型还可以不同。其声明如下:

func Println(a ...interface{}) (n int, err error)
+

... 表示函数的参数数量是可变的

参数 a 的类型为 interface{},是一个空接口

编写函数

func kelvinToCelsius(k float64) float64 {
+  k -= 273.15
+  return k
+}
+
+func main() {
+  k := 294.0
+  c := kelvinToCelsius(k)
+  fmt.Println(k, "°K is", c, "°C")
+}
+

函数按值传递

同一个包中声明的函数在调用时彼此不需要加上包名

方法

声明新类型

关键字 type 用来声明新类型

type celsius float64
+const degrees = 20
+var temperature celsius = degrees
+temperature += 10
+fmt.Println(temperature) // 30
+

为什么声明新类型?极大地提高代码可读性和可靠性

不同的类型是无法混用的

声明函数类型

type sensor func() kelvin
+

通过方法添加行为

在 Go 里,它提供了方法,但是没提供类和对象

Go 比其他语言的方法要灵活

可以将方法与同包中声明的任何类型相关联,但不可以是 int、float32 等预声明的类型

type celsius float64
+type kelvin float64
+
+func kelvinToCelsius(k kelvin) celsius {
+  return celsius(k - 273.15)
+}
+
+func (k kelvin) celsius() celsius {
+  return celsius(k - 273.15)
+}
+

celsius 方法虽然没有参数,但它前面却有一个类型参数的接收者

每个方法可以有多个参数,但只能有一个接收者

接收者的行为和其他参数是一样的

方法调用

变量.方法名(参数)

一等函数

在 Go 里,函数是头等的,它可以用在整数、字符串或其他类型能用的地方:

  • 将函数赋给变量
  • 将函数作为参数传递给函数
  • 将函数作为函数的返回类型

闭包和匿名函数

匿名函数就是没有名字的函数,在 Go 里也称作函数字面值

var f = func() {
+  fmt.Println("Dress up for the masquerade.")
+}
+
+f := func() {
+  fmt.Println("Dress up for the masquerade.")
+}
+
+func() {
+  fmt.Println("Dress up for the masquerade.")
+}()
+

因为函数字面值需要保留外部作用域的变量引用,所以函数字面值都是闭包的

闭包就是由于匿名函数封闭并包围作用域中的变量而得名

数组

数组是一种固定长度且有序的元素集合

var colors [3]string
+colors[0] = "Red"
+colors[1] = "Green"
+
+color := colors[1]
+fmt.Println(color) // Green
+fmt.Println(colors[2] == "") // true
+fmt.Println(len(colors)) // 3
+

数组的长度可以由内置函数 len 确定

在声明数组时,未被赋值元素的值是对应类型的零值

数组越界

var colors [3]string
+colors[3] = "Red"
+i := 3
+fmt.Println(colors[i]) // panic: runtime error: index out of range
+

Go 编译器在检测到对越界元素的访问时会报错

如果 Go 编译器在编译时未能发现越界错误,那么程序在运行时会出现 Panic

Panic 会导致程序崩溃

使用复合字面值初始化数组

复合字面值(composite literal)是一种用于初始化复合类型(数组、切片、字典和结构体)的紧凑语法

只用一步就完成数组声明和数组初始化

colors := [3]string{"Red", "Green", "Blue"}
+

可以在复合字面值里使用 ... 作为数组的长度,这样 Go 编译器会自动算出数组的元素数量

colors := [...]string{"Red", "Green", "Blue"}
+

无论哪种方式,数组的长度都是固定的

遍历数组

for 循环

dwarfs := [5]string{"Ceres", "Pluto", "Haumea", "Makemake", "Eris"}
+for i := 0; i < len(dwarfs); i++ {
+  fmt.Println(i, dwarfs[i])
+}
+

range 关键字

dwarfs := [5]string{"Ceres", "Pluto", "Haumea", "Makemake", "Eris"}
+for i, dwarf := range dwarfs {
+  fmt.Println(i, dwarf)
+}
+

数组的复制

无论数组赋值给新的变量还是将它传递给函数,都会产生一个完整的数组副本

planets := [...]string{"Mercury", "Venus", "Earth", "Mars", "Jupiter", "Saturn", "Uranus", "Neptune"}
+
+planetsMarkII := planets
+planets[2] = "whoops"
+fmt.Println(planets) // [Mercury Venus whoops Mars Jupiter Saturn Uranus Neptune]
+fmt.Println(planetsMarkII) // [Mercury Venus Earth Mars Jupiter Saturn Uranus Neptune]
+fmt.Println(planets == planetsMarkII) // false
+

数组也是一种值,函数通过值传递来接受参数,所以数组作为函数的参数就非常低效

数组的长度也是数组类型的一部分,将长度不符的数组作为参数传递会报错

函数一般使用 slice 而不是数组作为参数

数组的数组

二维数组

var board [8][8]string
+
+board[0][0] = "r"
+board[0][7] = "r"
+
+for column := range board[1] {
+  board[1][column] = "p"
+}
+
+fmt.Println(board)
+

切片 Slice

Slice 指向数组的窗口

假设 planets 是一个数组,那么 planets[0:4] 就是一个切片,它指向 planets 数组的前 4 个元素

切分数组不会导致数组被修改,它只是创建了指向数组的一个窗口或视图,这种视图就是 slice 类型

planets := [...]string{"Mercury", "Venus", "Earth", "Mars", "Jupiter", "Saturn", "Uranus", "Neptune"}
+
+// terrestrial := planets[0:4]
+terrestrial := planets[:4]
+gasGiants := planets[4:6]
+// iceGiants := planets[6:8]
+iceGiants := planets[6:]
+
+allPlanets := planets[:]
+
+fmt.Println(terrestrial) // [Mercury Venus Earth Mars]
+fmt.Println(gasGiants) // [Jupiter Saturn]
+fmt.Println(iceGiants) // [Uranus Neptune]
+fmt.Println(allPlanets) // [Mercury Venus Earth Mars Jupiter Saturn Uranus Neptune]
+

忽略掉 slice 的起始索引,表示从数组的起始位置进行切分

忽略掉 slice 的结束索引,相当于使用数组的长度作为结束索引

注意:slice 的索引不能是负数

切分数组的语法也可以用于切分字符串

s := "hello, world"
+c := s[0:5]
+s = "1111111"
+fmt.Println((c)) // hello
+

切分字符串时,索引代表的时字节数而非 rune 数

question := "¿Cómo estás?"
+fmt.Println(question[:6]) // ¿Cómo
+

Slice 的复合字面值

切分数组并不是创建 slice 的唯一方法,可以直接声明 slice

dwarfArray := [...]string{"Ceres", "Pluto", "Haumea", "Makemake", "Eris"}
+dwarfs := dwarfArray[:]
+
+// 直接声明 slice,不需要指定长度
+dwarfs := []string{"Ceres", "Pluto", "Haumea", "Makemake", "Eris"}
+

切片应用

func hyperspace(worlds []string) {
+  for i := range worlds {
+    worlds[i] = strings.TrimSpace(worlds[i])
+  }
+}
+
+func main() {
+  planets := []string{" Venus  ", "Earth ", " Mars"}
+  hyperspace(planets)
+  fmt.Println(strings.Join(planets, "")) // VenusEarthMars
+}
+

带有方法的切片

在 Go 里,可以将 slice 或数组作为底层类型,然后绑定其它方法

planets := []string{"Venus", "Earth", "Mars", "Jupiter", "Saturn", "Uranus", "Neptune"}
+
+sort.StringSlice(planets).Sort()
+fmt.Println(planets) // [Earth Jupiter Mars Neptune Saturn Uranus Venus]
+

更大的 slice

append 函数也是内置函数,它用于向 slice 里追加元素

dwarfs := []string{"Ceres", "Pluto", "Haumea", "Makemake", "Eris"}
+dwarfs = append(dwarfs, "Orcus")
+fmt.Println(dwarfs) // [Ceres Pluto Haumea Makemake Eris Orcus]
+

长度和容量(length & capacity)

Slice 中元素的个数决定了 slice 的长度

如果 slice 的底层数组比 slice 还大,那么就说 slice 还有容量可供增长

func dump(label string, slice []string) {
+  fmt.Printf("%v: length %v, capacity %v %v\n", label, len(slice), cap(slice), slice)
+}
+
+func main() {
+  dwarfs := []string{"Ceres", "Pluto", "Haumea", "Makemake", "Eris"}
+  dump("dwarfs", dwarfs) // dwarfs: length 5, capacity 5 [Ceres Pluto Haumea Makemake Eris]
+  dump("dwarfs[1:2]", dwarfs[1:2]) // dwarfs[1:2]: length 1, capacity 4 [Pluto]
+}
+

再结合 append 函数看一看

func dump(label string, slice []string) {
+  fmt.Printf("%v: length %v, capacity %v %v\n", label, len(slice), cap(slice), slice)
+}
+
+func main() {
+  dwarfs1 := []string{"Ceres", "Pluto", "Haumea", "Makemake", "Eris"}
+  dwarfs2 := append(dwarfs1, "Orcus")
+  dwarfs3 := append(dwarfs2, "Salacia", "Quaoar", "Sedna")
+
+  dump("dwarfs1", dwarfs1) // dwarfs1: length 5, capacity 5 [Ceres Pluto Haumea Makemake Eris]
+  dump("dwarfs2", dwarfs2) // dwarfs2: length 6, capacity 10 [Ceres Pluto Haumea Makemake Eris Orcus]
+  dump("dwarfs3", dwarfs3) // dwarfs3: length 9, capacity 10 [Ceres Pluto Haumea Makemake Eris Orcus Salacia Quaoar Sedna]
+
+  dwarfs3[1] = "Pluto!"
+  fmt.Println(dwarfs1) // [Ceres Pluto Haumea Makemake Eris]
+	/* 下面两个切片的底层数组是相同的 */
+  fmt.Println(dwarfs2) // [Ceres Pluto! Haumea Makemake Eris Orcus]
+  fmt.Println(dwarfs3) // [Ceres Pluto! Haumea Makemake Eris Orcus Salacia Quaoar Sedna]
+}
+

三个索引的切分操作

Go 1.2 中引入了能够限制新建切片容量的三索引切分操作

func dump(label string, slice []string) {
+  fmt.Printf("%v: length %v, capacity %v %v\n", label, len(slice), cap(slice), slice)
+}
+
+func main() {
+  planets := []string{"Mercury", "Venus", "Earth", "Mars", "Jupiter", "Saturn", "Uranus", "Neptune"}
+
+  terrestrials := planets[0:4:4] // 又新分配了一个数组,长度为 4,容量为 4
+  worlds := append(terrestrials, "Ceres") // 又新分配了一个数组,长度为 4,容量为 8
+  dump("terrestrials", terrestrials) // terrestrials: length 4, capacity 4 [Mercury Venus Earth Mars]
+  dump("worlds", worlds) // worlds: length 5, capacity 8 [Mercury Venus Earth Mars Ceres]
+
+  worlds2 := append(terrestrials, "Ceres", "Pluto", "Haumea", "Makemake", "Eris")
+  dump("worlds2", worlds2) // worlds2: length 9, capacity 12 [Mercury Venus Earth Mars Ceres Pluto Haumea Makemake Eris]
+}
+

使用 make 函数对 slice 进行预分配

当 slice 的容量不足以执行 append 操作时,Go 必须创建新数组并复制旧数组中的内容

但通过内置的 make 函数,可以对 slice 进行预分配策略

  • 尽量避免额外的内存分配和数组重复操作
func dump(label string, slice []string) {
+  fmt.Printf("%v: length %v, capacity %v %v\n", label, len(slice), cap(slice), slice)
+}
+
+func main() {
+  dwarfs := make([]string, 0, 10) // 预分配了一个长度为 0,容量为 10 的 slice。如果省略第三个参数,则第二个参数即规定长度也规定容量
+
+  dump("dwarfs", dwarfs) // dwarfs: length 0, capacity 10 []
+
+  dwarfs = append(dwarfs, "Ceres", "Pluto", "Haumea", "Makemake", "Eris")
+
+  dump("dwarfs", dwarfs) // dwarfs: length 5, capacity 10 [Ceres Pluto Haumea Makemake Eris]
+}
+

声明可变参数的函数

声明 Printf、append 这样的可变参数函数,需要在函数的最后一个参数前面加上 ... 符号

func terraform(prefix string, worlds ...string) []string {
+  newWorlds := make([]string, len(worlds))
+  for i := range worlds {
+    newWorlds[i] = prefix + " " + worlds[i]
+  }
+  return newWorlds
+}
+
+func main() {
+  twoWorlds := terraform("New", "Venus", "Mars")
+  fmt.Println(twoWorlds) // [New Venus New Mars]
+
+  planets := []string{"Venus", "Mars", "Jupiter"}
+  newPlanets := terraform("New", planets...)
+  fmt.Println(newPlanets) // [New Venus New Mars New Jupiter]
+}
+

map

map 是 Go 提供的另外一种集合

  • 它可以将 key 映射到 value
  • 它快速通过 key 找到对应的 value
  • 它的 key 几乎可以是任何类型

声明 map

声明 map 必须指定 key 和 value 的类型

temperature := map[string]int{
+"Earth": 15,
+"Mars": -65,
+}
+
+temp := temperature["Earth"]
+
+fmt.Println("On average the Earth is", temp, "Celsius.")
+
+temperature["Earth"] = 16
+temperature["Venus"] = 464
+
+fmt.Println(temperature) // map[Earth:16 Mars:-65 Venus:464]
+
+moon := temperature["Moon"]
+fmt.Println(moon) // 0
+

, 与 ok 写法

temperature := map[string]int{
+  "Earth": 15,
+  "Mars": -65,
+}
+
+temp, ok := temperature["Earth"]
+fmt.Println(temp, ok) // 15 true
+
+if moon, ok := temperature["Moon"]; ok {
+  fmt.Println(moon)
+} else {
+  fmt.Println("Where is the Moon?") // Where is the Moon?
+}
+

map 不会复制

数组、int、float64 等类型在赋值给新变量或传递至函数/方法时会创建相应的副本

但 map 不会

planets := map[string]string{
+  "Earth": "Sector ZZ9",
+  "Mars": "Sector ZZ9",
+}
+
+planetsMarkII := planets
+planets["Earth"] = "whoops"
+
+fmt.Println(planets) // map[Earth:whoops Mars:Sector ZZ9]
+fmt.Println(planetsMarkII) // map[Earth:whoops Mars:Sector ZZ9]
+
+delete(planets, "Earth")
+fmt.Println(planetsMarkII) // map[Mars:Sector ZZ9]
+

使用 make 函数对 map 进行预分配

除非使用复合字面值来初始化 map,否则必须使用内置的 make 函数来为 map 分配空间

创建 map 时,make 函数可以接收一个或两个参数

  • 第一个参数是 map 的类型
  • 第二个参数是可选的,用于指定 map 的初始容量(为指定数量的 key 预先分配空间)

使用 make 函数创建的 map 初始长度是 0

temperature := make(map[float64]int, 8)
+fmt.Println(len(temperature)) // 0
+

使用 map 作计数器

temperature := []float64{
+  -28.0, 32.0, -31.0, -29.0, -23.0, -29.0, -28.0, -33.0,
+}
+
+frequency := make(map[float64]int)
+
+for _, t := range temperature {
+  frequency[t]++
+}
+
+/* range 遍历 map 时是无法保证顺序的 */
+for t, num := range frequency {
+  fmt.Printf("%+.2f occurs %d times\n", t, num)
+}
+

使用 map 和 slice 实现数据分组

temperature := []float64{
+  -28.0, 32.0, -31.0, -29.0, -23.0, -29.0, -28.0, -33.0,
+}
+
+groups := make(map[float64][]float64)
+
+for _, t := range temperature {
+  g := math.Trunc(t/10) * 10
+  groups[g] = append(groups[g], t)
+}
+
+for g, temperatures := range groups {
+  fmt.Printf("%v: %v\n", g, temperatures)
+}
+

将 map 用作 set

Set 这种集合与数组类似,但元素不会重复

Go 语言里没有提供 set 集合

但可以使用 map 来实现 set 集合

var temperatures = []float64{
+  -28.0, 32.0, -31.0, -29.0, -23.0, -29.0, -28.0, -33.0,
+}
+
+/* 去重 */
+
+set := make(map[float64]bool)
+
+for _, t := range temperatures {
+  set[t] = true
+}
+
+if set[-28.0] {
+  fmt.Println("set member") // set member
+}
+
+fmt.Println(set)
+
+/* 排序 */
+
+unique := make([]float64, 0, len(set))
+
+for t := range set {
+  unique = append(unique, t)
+}
+
+sort.Float64s(unique)
+
+fmt.Println(unique) // [-33 -31 -29 -28 -23 32]
+

结构 struct

为了将分散的零件组成一个完整的结构体,Go 提供了 struct 类型

声明结构

var curiosity struct {
+  lat float64
+  long float64
+}
+
+curiosity.lat = -4.5895
+curiosity.long = 137.4417
+
+fmt.Println(curiosity.lat, curiosity.long) // -4.5895 137.4417
+fmt.Println(curiosity) // { -4.5895 137.4417}
+

通过类型复用结构体

type location struct {
+  lat float64
+  long float64
+}
+
+var spirit location
+spirit.lat = -14.5684
+spirit.long = 175.472636
+
+/* 通过成对的字段和值进行初始化 */
+opportunity := location{lat: -1.9462, long: 354.4734}
+
+/* 按照字段声明的顺序初始化 */
+insight := location{-4.5, 135.9}
+
+fmt.Printf("%v\n", insight) // {-4.5 135.9}
+fmt.Printf("%+v\n", insight) // {lat:-4.5 long:135.9}
+
+fmt.Println(spirit, opportunity) // {-14.5684 175.472636} {-1.9462 354.4734}
+

struct 的复制

type location struct {
+  lat, long float64
+}
+
+bradbury := location{-4.5895, 137.4417}
+curiosity := bradbury // 两个不同的实例
+
+curiosity.long += 0.0106
+
+fmt.Println(bradbury, curiosity) // {-4.5895 137.4417} {-4.5895 137.4523}
+

由结构体组成的 slice

type location struct {
+  lat, long float64
+  name string
+}
+
+lats := []float64{-4.5895, -14.5684, -1.9462}
+longs := []float64{137.4417, 175.472636, 354.4734}
+
+locations := []location{
+  {lat: -4.5895, long: 137.4417, name: "Bradbury Landing"},
+  {lat: -14.5684, long: 175.472636, name: "Columbia Memorial Station"},
+  {lat: -1.9462, long: 354.4734, name: "Challenger Memorial Station"},
+}
+
+fmt.Println(locations) // [{-4.5895 137.4417 Bradbury Landing} {-14.5684 175.472636 Columbia Memorial Station} {-1.9462 354.4734 Challenger Memorial Station}]
+

将 struct 编码为 JSON

JSON (JavaScript Object Notation,JavaScript 对象表示法)

常用于 Web API

json 包中的 Marshal 函数可以将 struct 编码为 JSON

type location struct {
+  Lat, Long float64
+  // lat, long float64
+}
+
+func main() {
+  curiosity := location{-4.5895, 137.4417}
+
+  bytes, err := json.Marshal(curiosity)
+  exitOnError(err)
+
+  fmt.Println(string(bytes)) // {"lat":-4.5895,"long":137.4417}
+}
+
+func exitOnError(err error) {
+  if err != nil {
+    fmt.Println(err)
+    os.Exit(1)
+  }
+}
+

Marshal 函数只会编码 struct 中被导出的字段(首字母大写)

使用 struct 标签来定义 JSON

Go 语言中 json 包要求 struct 中的字段必须以大写字母开头(类似 CamelCase 大驼峰),但如果需要 snake_case 蛇形命名规范,可以为字段注明标签,使得 json 包在进行编码的时候能够按照标签里的样式修改字段名


+type location struct {
+  Lat float64 `json:"latitude"xml:"latitude"`
+  Long float64 `json:"longitude"`
+}
+
+func main() {
+  curiosity := location{-4.5895, 137.4417}
+
+  bytes, err := json.Marshal(curiosity)
+  exitOnError(err)
+
+  fmt.Println(string(bytes)) // {"latitude":-4.5895,"longitude":137.4417}
+}
+
+func exitOnError(err error) {
+  if err != nil {
+    fmt.Println(err)
+    os.Exit(1)
+  }
+}
+

Go 语言里没有 class

Go 和其他经典语言不同,它没有 class,没有对象,也没有继承

但是 Go 提供了 struct 和方法

type coordinate struct {
+  d, m, s float64
+  h rune
+}
+
+func (c coordinate) decimal() float64 {
+  sign := 1.0
+
+  switch c.h {
+  case 'S', 'W', 's', 'w':
+    sign = -1
+  }
+
+  return sign * (c.d + c.m / 60 + c.s / 3600)
+}
+
+func main() {
+  lat := coordinate{4, 35, 22.2, 'S'}
+  long := coordinate{137, 26, 30.12, 'E'}
+  fmt.Println(lat.decimal(), long.decimal()) // -4.5895 137.4417
+}
+

构造函数

可以使用 struct 复合字面值来初始化想要的数据;但如果 struct 初始化的时候还有做很多事情,那就可以考虑写一个构造用的函数:

type coordinate struct {
+  d, m, s float64
+  h rune
+}
+
+func (c coordinate) decimal() float64 {
+  sign := 1.0
+
+  switch c.h {
+  case 'S', 'W', 's', 'w':
+    sign = -1
+  }
+
+  return sign * (c.d + c.m / 60 + c.s / 3600)
+}
+
+type location struct {
+  lat, long float64
+}
+
+/* new/New 开头,后面跟一个类型的名称,通常就代表这个类型的构造函数(约定) */
+func newLocation(lat, long coordinate) location {
+  return location{lat.decimal(), long.decimal()}
+}
+
+

Go 语言没有专门的构造函数,但以 new 或者 New 开头的函数,通常是用来构造数据的

有一些构造函数的名称就是 New(例如 errors 包里面的 New 函数),errors.New()

class 的替代方案

Go 语言中没有 class,但是可以使用 struct 和方法来实现类似的功能

type location struct {
+  lat, long float64
+}
+
+type world struct {
+  radius float64
+}
+
+func rad(deg float64) float64 {
+  return deg * math.Pi / 180
+}
+
+func (w world) distance(p1, p2 location) float64 {
+  s1, c1 := math.Sincos(rad(p1.lat))
+  s2, c2 := math.Sincos(rad(p2.lat))
+  clong := math.Cos(rad(p1.long - p2.long))
+  return w.radius * math.Acos(s1 * s2 + c1 * c2 * clong)
+}
+
+func main() {
+  mars := world{radius: 3389.5}
+  spirit := location{-14.5684, 175.472636}
+  opportunity := location{-1.9462, 354.4734}
+  fmt.Printf("%.2f km\n", mars.distance(spirit, opportunity)) // 9669.71 km
+}
+

组合与转发(composition & forwarding)

Go 通过结构体实现组合

Go 提供了“嵌入”(embedding)特性,它可以实现方法的转发(forwarding)

组合相对于继承更简单、灵活

// type report struct {
+// 	sol         int
+// 	temperature temperature
+// 	location    location
+// }
+
+type sol int
+
+type report struct {
+	sol
+	temperature
+	location
+}
+
+type temperature struct {
+	high, low celsius
+}
+
+type location struct {
+	lat, long float64
+}
+
+type celsius float64
+
+func (t temperature) average() celsius {
+	return (t.high + t.low) / 2
+}
+
+func (s sol) days(s2 sol) int {
+	days := int(s2 - s)
+	if days < 0 {
+		return -days
+	}
+	return days
+}
+
+// func (r report) average() celsius {
+// 	return r.temperature.average()
+// }
+
+func main() {
+	t := temperature{high: -1.0, low: -78.0}
+	fmt.Printf("%v\n", t.average()) // -39.5
+	loc := location{-4.5895, 137.4417}
+	rep := report{sol: 15, temperature: t, location: loc}
+
+	fmt.Println(rep.temperature.average()) // -39.5
+	fmt.Println(rep.average())             // -39.5
+	fmt.Println(rep.high)                  // -1
+
+	fmt.Println(rep.sol.days(1446)) // 1431
+	fmt.Println(rep.days(1446))     // 1431
+
+	fmt.Printf("%+v\n", rep) // {sol:15 temperature:{high:-1 low:-78} location:{lat:-4.5895 long:137.4417}}
+}
+

Go 可以通过 struct 嵌入来实现方法的转发

在 struct 中只给定字段类型,不给定字段名即可

在 struct 中可以转发任意类型

Go 语言中,如果两个字段名字相同,那么在访问的时候就必须使用完整的路径

接口

类型关注于可以做什么,而不是存储了什么

接口通过列举类型必须满足的一组方法来进行声明

在 Go 语言中,不需要显示声明接口

var t interface {
+  talk() string
+}
+
+type martian struct {}
+
+func (m martian) talk() string {
+  return "nack nack"
+}
+
+type laser int
+
+func (l laser) talk() string {
+  return strings.Repeat("pew ", int(l))
+}
+
+func main() {
+  t = martian{}
+  fmt.Println(t.talk()) // nack nack
+
+  t = laser(3)
+  fmt.Println(t.talk()) // pew pew pew
+}
+

为了复用,通常会把接口声明为类型

按约定,接口名称通常以 er 结尾

type talker interface {
+  talk() string
+}
+
+type martian struct {}
+
+func (m martian) talk() string {
+  return "nack nack"
+}
+
+type laser int
+
+func (l laser) talk() string {
+  return strings.Repeat("pew ", int(l))
+}
+
+func shout(t talker) {
+  louder := strings.ToUpper(t.talk())
+  fmt.Println(louder)
+}
+
+/* 接口配合 struct 嵌入特性一起使用 */
+type starship struct {
+  laser
+}
+
+func main() {
+   s := starship{laser(3)}
+
+  shout(martian{}) // NACK NACK
+  shout(laser(2)) // PEW PEW
+  shout(s) // PEW PEW PEW
+}
+

同时使用组合和接口将构成非常强大的设计工具

满足接口

Go 语言的接口都是隐式满足的

Go 标准库导出了很多只有单个方法的接口

例如 fmt 包声明的 Stringer 接口

type Stringer interface {
+  String() string
+}
+
type location struct {
+  lat, long float64
+}
+
+func (l location) String() string {
+  return fmt.Sprintf("%v, %v", l.lat, l.long)
+}
+
+func main() {
+  curiosity := location{-4.5895, 137.4417}
+  fmt.Println(curiosity) // -4.5895, 137.4417
+}
+

标准库中常用的接口还包括:io.Reader、io.Writer、http.Handler、json.Marshaler 等

指针

指针是指向另一个变量地址的变量

Go 语言的指针同时强调安全性,不会出现迷途指针(dangling pointers)

变量会将它们的值存储在计算机 RAM 里,存储位置就是该变量的内存地址

& 表示地址操作符,通过 & 可以获得变量的内存地址

func main() {
+  answer := 42
+  fmt.Println(&answer) // 0xc0000140a8 类似的一个地址
+}
+

& 操作符无法获得字符串/数值/布尔字面值的地址,&42,&"hello" 都会导致编译器报错

* 操作符与 & 的作用相反,它用来解引用,提供内存地址指向的值

answer := 42
+/* Go 语言不允许 address++ 这样的指针运算进行操作 */
+address := &answer
+fmt.Println(*address) // 42
+fmt.Printf("%T\n", address) // *int
+

指针存储的是内存地址

指针类型和其他普通类型一样,出现在所有需要用到类型的地方,如变量声明、函数形参、返回值类型、结构体字段等

指针类型

canada := "Canada"
+var home *string
+fmt.Printf("home is a %T\n", home) // home is a *string
+home = &canada
+fmt.Println(*home) // Canada
+

将 * 放在类型前面,表示声明一个指针类型

将 * 放在变量前面,表示解引用操作,获取指针指向的值

两个指针变量指向同一个内存地址,那么它们就是相等的

指向结构的指针

与字符串和数值不一样,复合字面量的前面可以放置 &

type person struct {
+  name, superpower string
+  age int
+}
+
+timmy := &person{
+  name: "Timothy",
+  age: 10,
+}
+
+timmy.superpower = "flying" // 等价于 (*timmy).superpower = "flying"
+
+fmt.Printf("%+v\n", timmy) // &{name:Timothy superpower:flying age:10}
+

访问字段时,对结构体进行解引用并不是必须的

指向数组的指针

和结构体一样,可以把 & 放在数组的复合字面值前面来创建指向数组的指针

superpowers := &[3]string{"flight", "invisibility", "super strength"}
+
+fmt.Println(superpowers[0]) // flight
+fmt.Println(superpowers[1:2]) // [invisibility]
+

数组在执行索引或切片操作时,会自动解引用,没有必要写 (*superpowers)[0] 这种形式

Go 里面数组和指针是两种完全独立的类型

slice 和 map 的复合字面值前面也可以放置 & 操作符,但是 Go 并没有为它们提供自动解引用的功能

实现修改

Go 语言的函数和方法都是按值传递参数的,这意味着函数总是操作于被传递参数的副本

当指针被传递到函数时,函数将接受传入的内存地址的副本。之后函数可以通过解引用内存地址来修改指针指向的值

type person struct {
+  name string
+  age int
+}
+
+func birthday(p *person) {
+  p.age++
+}
+
+func main() {
+  timmy := &person{
+    name: "Timothy",
+    age: 10,
+  }
+
+  birthday(timmy)
+  fmt.Printf("%+v\n", timmy) // &{name:Timothy superpower: age:11}
+}
+

指针接收者

方法的接收者和方法的参数在处理指针方面是很相似的

type person struct {
+  name string
+  age int
+}
+
+func (p *person) birthday() {
+  p.age++
+}
+
+func main() {
+  timmy := &person{
+    name: "Timothy",
+    age: 10,
+  }
+
+  timmy.birthday()
+  fmt.Printf("%+v\n", timmy) // &{name:Timothy superpower: age:11}
+
+  /* Go 语言在变量通过点标记法进行调用的时候,自动使用 & 取得变量的内存地址 */
+  nathan := person{"Nathan", 18}
+  nathan.birthday() // (&nathan).birthday()
+  fmt.Printf("%+v\n", nathan) // {name:Nathan superpower: age:19}
+}
+

使用指针作为接收者的策略应该始终如一:如果一种类型的某些方法需要用到指针作为接收者,这种类型的所有方法就应该都是用指针作为接收者

内部指针

Go 提供了内部指针这种特性,它用于确定结构体中指定字段的内存地址

type stats struct {
+  level int
+  endurance, health int
+}
+
+func levelUp(s *stats) {
+  s.level++
+  s.endurance = 42 + (14 * s.level)
+  s.health = 5 * s.endurance
+}
+
+type character struct {
+  name string
+  stats stats
+}
+
+func main() {
+  player := character{name: "Matthias"}
+  levelUp(&player.stats) // & 操作符不仅可以获得结构体的内存地址,还可以获得结构体中指定字段的内存地址
+  fmt.Printf("%+v\n", player) // {name:Matthias stats:{level:1 endurance:56 health:280}}
+}
+

修改数组

函数通过指针对数组的元素进行修改

func reset(board *[8][8]rune) {
+  board[0][0] = 'r'
+}
+
+func main() {
+  var board [8][8]rune
+  reset(&board)
+
+  fmt.Printf("%c", board[0][0]) // r
+}
+

隐式的指针

Go 语言里的一些内置的集合类型就在暗中使用指针

  • map 在被赋值或者作为参数传递的时候不会被复制

    • func demolish(planets *map[string]string) 这种写法就是多此一举
    • map 的键和值都可以是指针类型
    • 需要将指针指向 map 的情况不多见
  • slice 在指向数组元素的时候也使用了指针

    • 每个 slice 内部都会被表示为一个包含 3 个元素的结构,它们分别指向数组的指针、slice 的长度和 slice 的容量
    • 当 slice 被直接传递至函数或方法时,slice 的内部指针就可以对底层数据进行修改
    • 指向 slice 的显示指针的唯一作用就是修改 slice 本身,slice 的长度、容量以及起始偏移量
func reclassify(planets *[]string) {
+  *planets = (*planets)[0:8]
+}
+
+func main() {
+  planets := []string{
+    "Mercury", "Venus", "Earth", "Mars",
+    "Jupiter", "Saturn", "Uranus", "Neptune", "Pluto",
+  }
+
+  reclassify(&planets)
+  fmt.Println(planets) // [Mercury Venus Earth Mars Jupiter Saturn Uranus Neptune]
+}
+

指针和接口

type talker interface {
+  talk() string
+}
+
+func shout(t talker) {
+  fmt.Println(strings.ToUpper(t.talk()))
+}
+
+type martian struct {}
+
+func (m martian) talk() string {
+  return "nack nack"
+}
+
+func main() {
+  /* 无论是 martian 还是指向 martian 的指针,都可以满足 talker 接口 */
+  shout(martian{}) // NACK NACK
+  shout(&martian{}) // NACK NACK
+}
+

上例中无论是 martian 还是指向 martian 的指针,都可以满足 talker 接口

如果方法使用的是指针接收者,那么情况会有所不同

type talker interface {
+  talk() string
+}
+
+func shout(t talker) {
+  fmt.Println(strings.ToUpper(t.talk()))
+}
+
+type laser int
+
+func (l *laser) talk() string {
+  return strings.Repeat("pew ", int(*l))
+}
+
+func main() {
+  laser := laser(2)
+  shout(&laser) // PEW PEW
+  shout(laser) // cannot use laser (type laser) as type talker in argument to shout: laser does not implement talker (talk method has pointer receiver)
+}
+

明智的使用指针

应合理使用指针,不要过度使用

nil

nil 是一个名词,表示“无”或者“零”

在 Go 里,nil 是一个零值

如果一个指针没有明确的指向,那么它的值就是 nil

除了指针,nil 还是 slice 、map、channel、interface 和函数的零值

Go 语言的 nil 比以往语言的 null 更为友好,并且用的没那么频繁,但是仍需谨慎使用

nil 会导致 panic

如果指针没有明确的指向,那么程序将无法对其实施解引用

尝试解引用一个 nil 指针将导致程序崩溃

var nowhere *int
+fmt.Println(nowhere) // <nil>
+fmt.Println(*nowhere) // panic: runtime error: invalid memory address or nil pointer dereference
+

避免 nil 引发 panic

type person struct {
+  age int
+}
+
+func (p *person) birthday() {
+  // 避免 nil 引发 panic
+  if p == nil {
+    return
+  }
+  p.age++
+}
+
+func main() {
+  var nobody *person
+  nobody.birthday()
+}
+

因为值为 nil 的接收者和值为 nil 的参数在行为上并没有区别,所以 Go 语言即使在接收者为 nil 的情况下,也会继续调用方法

nil 函数值

当变量被声明为函数类型时,它的默认值是 nil

var fn func(a, b int) int
+fmt.Println(fn == nil) // true
+

检查函数值是否为 nil,并在有需要时提供默认行为

nil slice

如果 slice 在声明之后没有使用复合字面值或内置的 make 函数进行初始化,那么它的值就是 nil

幸运的是,range\len\append 等内置函数都可以安全地处理值为 nil 的 slice

var soup []string
+fmt.Println(soup == nil) // true
+
+for _, ingredient := range soup {
+  fmt.Println(ingredient) // 不会执行
+}
+
+fmt.Println(len(soup)) // 0
+
+soup = append(soup, "onion", "carrot", "celery")
+fmt.Println(soup) // [onion carrot celery]
+

虽然空 slice 和值为 nil 的 slice 并不相等,但它们通常可以替换使用

nil map

和 slice 一样,如果 map 在声明之后没有使用复合字面值或内置的 make 函数进行初始化,那么它的值就是 nil

var soup map[string]int
+fmt.Println(soup == nil) // true
+
+measurements, ok := soup["onion"]
+if ok {
+  fmt.Println(measurements) // 不会执行
+}
+
+for ingredient, measurement := range soup {
+  fmt.Println(ingredient, measurement) // 不会执行
+}
+

nil 接口

声明为接口类型的变量在未被赋值时,它的零值是 nil

对于一个未被赋值的接口变量来说,它的接口类型和值都是 nil,并且变量本身也等于 nil

var v interface{}
+fmt.Printf("%T %v %v\n", v, v, v == nil) // <nil> <nil> true
+

当接口类型的变量被赋值后,接口就会在内部指向该变量的类型和值

var v interface{}
+fmt.Printf("%T %v %v\n", v, v, v == nil) // <nil> <nil> true
+var p *int
+v = p
+fmt.Printf("%T %v %v\n", v, v, v == nil) // *int <nil> false
+
+// 检验接口变量的内部表示
+fmt.Printf("%#v\n", v) // (*int)(nil)
+

在 Go 中,接口类型的变量只有在类型和值都为 nil 时才等于 nil

nil 之外的另一个选择

type number struct {
+  value int
+  valid bool
+}
+
+func newNumber(v int) number {
+  return number{value: v, valid: true}
+}
+
+func (n number) String() string {
+  if !n.valid {
+    return "未知"
+  }
+  return strconv.Itoa(n.value)
+}
+
+func main() {
+  n := newNumber(42)
+  fmt.Println(n) // 42
+  n = number{}
+  fmt.Println(n) // 未知
+}
+

错误

处理错误

Go 语言允许函数和方法同时返回多个值

按照惯例,函数在返回错误时,最后边的返回值应用来表示错误

调用函数后,应立即检查是否发生错误

  • 如果没有错误发生,那么返回的错误值为 nil
files, err := ioutil.ReadDir(".")
+if err != nil {
+  fmt.Println(err)
+  os.Exit(1)
+}
+for _, file := range files {
+  fmt.Println(file.Name())
+}
+

注意:当错误发生时,函数返回的其他值通常就不再可信

优雅的错误处理

减少错误处理代码的一种策略是:将程序中不会出错的部分和包含潜在错误隐患的部分隔离开来

对于不得不返回错误的代码,应尽力简化相应的错误处理代码

文件写入

写入文件的时候可能出错:

  • 路径不正确
  • 权限不够
  • 磁盘空间不足
  • ...

文件写入完毕后,必须被关闭,确保文件被刷到磁盘上,避免资源的泄露

// 内置类型 error 用来表示错误
+func proverbs(name string) error {
+  f, err := os.Create(name)
+  if err != nil {
+    return err
+  }
+
+  _, err = fmt.Fprintln(f, "Errors are values.")
+  if err != nil {
+    f.Close()
+    return err
+  }
+
+  _, err = fmt.Fprintln(f, "Don't just check errors, handle them gracefully.")
+  f.Close()
+  return err
+}
+
+func main() {
+  err := proverbs("proverbs.txt")
+  if err != nil {
+    fmt.Println(err)
+    os.Exit(1)
+  }
+}
+

defer 关键字

使用 defer 关键字,Go 可以确保所有 deferred 的动作可以在函数返回前执行

可以 defer 任意的函数和方法

func proverbs(name string) (err error) {
+  f, err := os.Create(name)
+  if err != nil {
+    return err
+  }
+  defer f.Close()
+
+  _, err = fmt.Fprintln(f, "Errors are values.")
+  if err != nil {
+    return err
+  }
+
+  _, err = fmt.Fprintln(f, "Don't just check errors, handle them gracefully.")
+  return err
+}
+

defer 并不是专门做错误处理的

defer 可以消除必须时刻惦记执行资源释放的负担

有创意的错误处理

type safeWriter struct {
+	w   io.Writer
+	err error
+}
+
+func (sw *safeWriter) writeln(s string) {
+	if sw.err != nil {
+		return
+	}
+	_, sw.err = fmt.Fprintln(sw.w, s)
+}
+
+func proverbs(name string) error {
+	f, err := os.Create(name)
+	if err != nil {
+		return err
+	}
+	defer f.Close()
+
+	sw := safeWriter{w: f}
+	sw.writeln("Errors are values.")
+	sw.writeln("Don't just check errors, handle them gracefully.")
+	sw.writeln("Don't panic.")
+	sw.writeln("Make the zero value useful.")
+	sw.writeln("The bigger the interface, the weaker the abstraction.")
+	sw.writeln("interface{} says nothing.")
+	sw.writeln("Gofmt's style is no one's favorite, yet gofmt is everyone's favorite.")
+	sw.writeln("Documentation is for users.")
+	sw.writeln("A little copying is better than a little dependency.")
+	sw.writeln("Clear is better than clever.")
+	sw.writeln("Concurrency is not parallelism.")
+	sw.writeln("Don't communicate by sharing memory, share memory by communicating.")
+	sw.writeln("Channels orchestrate; mutexes serialize.")
+
+	return sw.err
+}
+

New error

errors 包里有一个构造用的 New 函数,它接收 string 来作为参数用来表示错误信息,该函数返回 error 类型

const rows, columns = 9, 9
+
+// Sudoku 数独
+type Grid [rows][columns]int8
+
+// Set
+func (g *Grid) Set(row, column int, digit int8) error {
+  if !inBounds(row, column) {
+    return errors.New("out of bounds")
+  }
+  g[row][column] = digit
+  return nil
+}
+
+func inBounds(row, column int) bool {
+  if row < 0 || row >= rows {
+    return false
+  }
+  if column < 0 || column >= columns {
+    return false
+  }
+  return true
+}
+
+func main() {
+  var g Grid
+  err := g.Set(10, 0, 5)
+  if err != nil {
+    fmt.Printf("An error occurred: %v.\n", err)
+    os.Exit(1)
+  }
+}
+

错误信息应具有信息性

可以把错误信息当作用户界面的一部分,无论对最终用户还是开发者

按需返回错误

按照惯例,包含错误信息的变量名应以 Err 开头

const rows, columns = 9, 9
+
+// Sudoku 数独
+type Grid [rows][columns]int8
+
+var(
+  // ErrBounds 表示数字越界
+  ErrBounds = errors.New("out of bounds")
+  // ErrDigit 表示数字无效
+  ErrDigit = errors.New("invalid digit")
+)
+
+// Set
+func (g *Grid) Set(row, column int, digit int8) error {
+  if !inBounds(row, column) {
+    return ErrBounds
+  }
+  g[row][column] = digit
+  return nil
+}
+
+func inBounds(row, column int) bool {
+  if row < 0 || row >= rows {
+    return false
+  }
+  if column < 0 || column >= columns {
+    return false
+  }
+  return true
+}
+
+func main() {
+  var g Grid
+  err := g.Set(10, 0, 5)
+  if err != nil {
+    switch err {
+    case ErrBounds, ErrDigit:
+      fmt.Println("error!!!")
+    default:
+      fmt.Println("unknown error")
+    }
+    os.Exit(1)
+  }
+}
+

errors.New 这个构造函数是使用指针实现的,所以上例 switch 语句比较的是内存地址,而不是错误包含的文字信息

自定义错误类型

error 类型是一个内置的接口:任何类型只要实现了返回 string 的 Error() 方法就满足了该接口

可以创建新的错误类型

const rows, columns = 9, 9
+
+// Sudoku 数独
+type Grid [rows][columns]int8
+
+var (
+	// ErrBounds 表示数字越界
+	ErrBounds = errors.New("out of bounds")
+	// ErrDigit 表示数字无效
+	ErrDigit = errors.New("invalid digit")
+)
+
+type SudokuError []error
+
+func (se SudokuError) Error() string {
+	var s []string
+	for _, e := range se {
+		s = append(s, e.Error())
+	}
+	return strings.Join(s, ", ")
+}
+
+// Set
+func (g *Grid) Set(row, column int, digit int8) error {
+	var errs SudokuError
+	if !inBounds(row, column) {
+		errs = append(errs, ErrBounds)
+	}
+	if !validDigit(digit) {
+		errs = append(errs, ErrDigit)
+	}
+
+	if len(errs) > 0 {
+		return errs
+	}
+
+	g[row][column] = digit
+	return nil
+}
+
+func inBounds(row, column int) bool {
+	if row < 0 || row >= rows {
+		return false
+	}
+	if column < 0 || column >= columns {
+		return false
+	}
+	return true
+}
+
+func validDigit(digit int8) bool {
+	return digit >= 1 && digit <= 9
+}
+
+func main() {
+	var g Grid
+	err := g.Set(10, 0, 10)
+	if err != nil {
+		switch err {
+		case ErrBounds, ErrDigit:
+			fmt.Println("error!!!")
+		default:
+			fmt.Println("unknown error:", err)
+		}
+		os.Exit(1)
+	}
+}
+

按照惯例,自定义错误类型的名字应以 Error 结尾 有时候名字就是 Error,例如 url.Error

类型断言

上例中,可以使用类型断言来访问每一种错误

使用类型断言,可以把接口类型转化成底层的具体类型,例如 err.(SudokuError)

const rows, columns = 9, 9
+
+// Sudoku 数独
+type Grid [rows][columns]int8
+
+var (
+	// ErrBounds 表示数字越界
+	ErrBounds = errors.New("out of bounds")
+	// ErrDigit 表示数字无效
+	ErrDigit = errors.New("invalid digit")
+)
+
+type SudokuError []error
+
+func (se SudokuError) Error() string {
+	var s []string
+	for _, e := range se {
+		s = append(s, e.Error())
+	}
+	return strings.Join(s, ", ")
+}
+
+// Set
+func (g *Grid) Set(row, column int, digit int8) error {
+	var errs SudokuError
+	if !inBounds(row, column) {
+		errs = append(errs, ErrBounds)
+	}
+	if !validDigit(digit) {
+		errs = append(errs, ErrDigit)
+	}
+
+	if len(errs) > 0 {
+		return errs
+	}
+
+	g[row][column] = digit
+	return nil
+}
+
+func inBounds(row, column int) bool {
+	if row < 0 || row >= rows {
+		return false
+	}
+	if column < 0 || column >= columns {
+		return false
+	}
+	return true
+}
+
+func validDigit(digit int8) bool {
+	return digit >= 1 && digit <= 9
+}
+
+func main() {
+	var g Grid
+	err := g.Set(10, 0, 10)
+	if err != nil {
+		/* 相较于上例,只变动此处 */
+    if errs, ok := err.(SudokuError); ok {
+      fmt.Printf("%d error(s) occurred:\n", len(errs))
+      for _, e := range errs {
+        fmt.Printf("- %v\n", e)
+      }
+    }
+		os.Exit(1)
+	}
+}
+

如果类型满足多个接口,那么类型断言可使它从一个接口类型转化为另一个接口类型

如何 panic

Go 里有一个和其他语言异常类似的机制:panic

实际上,panic 很少出现

创建 panic:调用内置的 panic 函数

panic("invalid operation") // panic 的参数可以是任意类型
+

错误值、panic、os.Exit?

通常,更推荐使用错误值,其次才是 panic

panic 比 os.Exit() 更好:panic 后会执行所有 defer 操作,而 os.Exit() 不会

有时候 Go 程序会 panic 而不是返回错误值

var zero int
+fmt.Println(1 / zero) // panic: runtime error: integer divide by zero
+

保持冷静并继续

为了防止 panic 导致程序崩溃,Go 提供了 recover 函数

defer 的动作会在函数返回前执行,即使发生了 panic

但如果 defer 的函数调用了 recover,panic 就会停止,程序将继续运行

defer func() {
+  if err := recover(); err != nil {
+    log.Printf("run time panic: %v", err) // 2023/09/10 11:23:52 run time panic: I forgot my towel
+  }
+}()
+panic("I forgot my towel")
+

goroutine 和 并发(concurrent)

goroutine

在 Go 中,独立的任务叫做 goroutine

  • goroutine 和其他语言的协程、进程、线程都有相似之处,但并不完全相同
  • goroutine 创建效率非常高
  • Go 能直截了当地协同多个并发(concurrent)操作

在 Go 里,无需修改现有顺序式的代码,就可以通过 goroutine 以并发的方式运行任意数量的任务

启动 goroutine

只需在调用前加一个 go 关键字,就可以让函数/方法以 goroutine 方式运行

func sleepyGopher() {
+  time.Sleep(3 * time.Second)
+  fmt.Println("... snore ...")
+}
+
+func main() {
+  /* 分支线路 */
+  go sleepyGopher()
+  /* 主线路 */
+  fmt.Println("i'm waiting")
+  time.Sleep(4 * time.Second)
+}
+

不止一个 goroutine

每次使用 go 关键字都会产生一个新的 goroutine

表面上看,goroutine 似乎在同时运行,但由于计算机处理单元有限,其实技术上来说,这些 goroutine 不是真的在同时运行

  • 计算机处理器会使用“分时”技术,在多个 goroutine 上轮流花费一些时间
  • 在使用 goroutine 时,各个 goroutine 的执行顺序无法确定
func sleepyGopher(id int) {
+  time.Sleep(3 * time.Second)
+  fmt.Println("... snore ...", id)
+}
+
+func main() {
+  for i := 0; i < 5; i++ {
+    go sleepyGopher(i)
+  }
+  time.Sleep(4 * time.Second)
+}
+

通道 channel

channel 可以在多个 goroutine 之间安全地传值

通道可以用作变量、函数参数、结构体字段...

创建通道用 make 函数,并指定其传输数据的类型 c := make(chan int)

通道 channel 发送、接收

使用左箭头操作符 <- 向通道发送值或从通道接收值

  • 发送值:c <- 1
  • 接收值:r := <- c

发送操作会等待直到另一个 goroutine 尝试对该通道进行接收操作为止

  • 执行发送操作的 goroutine 在等待期间将无法执行其他操作
  • 未在等待通道操作的 goroutine 可以继续自由地运行

执行接收操作的 goroutine 将等待直到另一个 goroutine 尝试向该通道进行发送操作为止

func main() {
+  c := make(chan int)
+  for i := 0; i < 5; i++ {
+    go sleepyGopher(i, c)
+  }
+  for i := 0; i < 5; i++ {
+    gopherID := <-c
+    fmt.Println("gopher", gopherID, "has finished sleeping")
+  }
+}
+
+func sleepyGopher(id int, c chan int) {
+  time.Sleep(3 * time.Second)
+  fmt.Println("... snore ...", id)
+  c <- id
+}
+

使用 select 处理多个通道

等待不同类型的值

time.After 函数,返回一个通道,该通道在指定时间后会接收到一个值(发送该值的 goroutine 是 Go 运行时的一部分)

select 和 switch 有点像

  • 该语句包含的每个 case 都持有一个通道,用来发送或接收数据
  • select 会等待直到某个 case 分支的操作就绪,然后就会执行该 case 的分支
func main() {
+  c := make(chan int)
+  for i := 0; i < 5; i++ {
+    go sleepyGopher(i, c)
+  }
+  timeout := time.After(2 * time.Second)
+  for i := 0; i < 5; i++ {
+    select {
+    case gopherID := <-c:
+      fmt.Println("gopher", gopherID, "has finished sleeping")
+    case <-timeout:
+      fmt.Println("my patience ran out")
+      return
+    }
+  }
+}
+
+func sleepyGopher(id int, c chan int) {
+  time.Sleep(time.Duration(rand.Intn(4000)) * time.Millisecond) // 0~4s
+  c <- id
+}
+

注意:

即使已经停止等待 goroutine,但只要 main 函数还没返回,仍在运行的 goroutine 将会继续占用内存

select 语句在不包含任何 case 的情况下将永远等下去

nil 通道

如果不使用 make 初始化通道,那么通道变量的值就是 nil(零值)

对 nil 通道进行发送或接收不会引起 panic,但会导致永久阻塞

对 nil 哦那个到执行 close 函数,会引发 panic

nil 通道的用处:

  • 对于包含 select 语句的循环,如果不希望每次循环都等待 select 所涉及的所有的通道,那么就可以先将某些通道设为 nil,等到发送值准备就绪之后,再将通道变成一个非 nil 值并执行发送操作

阻塞和死锁

当 goroutine 在等待通道的发送或接收时,就说它被阻塞了

除了 goroutine 本身占用少量的内存外,被阻塞的 goroutine 并不消耗任何其他资源

当一个或多个 goroutine 因为某些永远无法发生的事情被阻塞时,则称这种情况为死锁。而出现死锁的程序通常会崩溃或挂起

引发死锁的例子:

func main() {
+  c := make(chan int)
+  /* 下面这行代码可以解除死锁 */
+  // go func () {c <- 2}
+  <- c // fatal error: all goroutines are asleep - deadlock!
+}
+

地鼠装配线

图片

func sourceGopher(upstream chan string) {
+	for _, v := range []string{"hello world", "a bad apple", "goodbye all"} {
+		upstream <- v
+	}
+	close(upstream)
+}
+
+func filterGopher(downstream, upstream chan string) {
+	for {
+		item, ok := <- downstream
+		if !ok {
+			close(upstream)
+			break
+		}
+		if !strings.Contains(item, "bad") {
+			upstream <- item
+		}
+	}
+}
+
+func PrintGopher(downstream chan string) {
+	for {
+		v, ok := <- downstream
+		if !ok {
+			break
+		}
+		fmt.Println(v)
+	}
+}
+
+func main() {
+	c0 := make(chan string)
+	c1 := make(chan string)
+	go sourceGopher(c0)
+	go filterGopher(c0, c1)
+	PrintGopher(c1)
+}
+

Go 允许在没有值可供发送的情况下通过 close 函数关闭通道,例如 close(c)

通道被关闭后无法写入任何值,如果尝试写入将引发 panic

尝试读取被关闭的通道会获得与通道类型对应的零值

注意:如果循环里读取一个已关闭的通道,并没检查通道是否关闭,那么该循环可能会一直运转下去,耗费大量 CPU 时间

执行以下代码可知通道是否被关闭

  • v, ok := <-c

常用模式

从通道里面读取值,直到它关闭为止

  • 可以使用 range 关键字达到该目的
func sourceGopher(upstream chan string) {
+	for _, v := range []string{"hello world", "a bad apple", "goodbye all"} {
+		upstream <- v
+	}
+	close(upstream)
+}
+
+func filterGopher(downstream, upstream chan string) {
+  for item := range downstream {
+    if !strings.Contains(item, "bad") {
+      upstream <- item
+    }
+  }
+  close(upstream)
+}
+
+func PrintGopher(downstream chan string) {
+  for v := range downstream {
+    fmt.Println(v)
+  }
+}
+
+func main() {
+	c0 := make(chan string)
+	c1 := make(chan string)
+	go sourceGopher(c0)
+	go filterGopher(c0, c1)
+	PrintGopher(c1)
+}
+

并发状态

  • 共享值
  • 竞争条件(race condition)

Go 的互斥锁(mutex = mutual exclusive)

  • Lock(),Unlock()
var mu sync.Mutex
+
+func main() {
+  mu.Lock()
+  defer mu.Unlock()
+  // The lock is held until we return from the function
+}
+

互斥锁定义在被保护的变量之上

type Visited struct {
+  mu sync.Mutex
+  visited map[string]int
+}
+
+func (v *Visited) VisitLink(url string) int {
+  v.mu.Lock()
+  defer v.mu.Unlock()
+  count := v.visited[url]
+  count++
+  v.visited[url] = count
+  return count
+}
+

互斥锁的隐患

  • 死锁

为保证互斥锁的安全使用,须遵循以下规则:

  • 尽可能地简化互斥锁保护的代码
  • 对每一份共享状态只使用一个互斥锁

长时间运行的工作进程

  • 工作进程(worker)
    • 通常会被写成包含 select 语句的 for 循环
func worker() {
+  n := 0
+  next := time.After(time.Second)
+  for {
+    select {
+      case <-next:
+        n++
+        fmt.Println(n)
+        next = time.After(time.Second)
+    }
+  }
+}
+
+func main() {
+  go worker()
+	for {
+		time.Sleep(time.Second)
+	}
+}
+

事件循环和 goroutine

  • 事件循环(event loop)
  • 中心循环(central loop)

Go 通过提供 goroutine 作为核心概念,消除了对中心循环的需求

Last Updated:
+ + + diff --git a/backend/go/index.html b/backend/go/index.html new file mode 100644 index 0000000..d66a22a --- /dev/null +++ b/backend/go/index.html @@ -0,0 +1,33 @@ + + + + + + + + + 瓢儿白施肥记 + + + + + + + + diff --git a/backend/go/web/index.html b/backend/go/web/index.html new file mode 100644 index 0000000..e2348d2 --- /dev/null +++ b/backend/go/web/index.html @@ -0,0 +1,558 @@ + + + + + + + + + 瓢儿白施肥记 + + + + +

使用 Go 创建 Web 应用

  • 处理请求
  • 模板
  • 中间件
  • 存储数据
  • HTTPS,HTTP2
  • 测试
  • 部署
package main
+
+import "net/http"
+
+func main() {
+	http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
+		w.Write([]byte("Hell world"))
+	})
+
+	http.ListenAndServe("localhost:8080", nil) // 传入 nil,即 DefaultServeMux
+}
+

处理(Handle)请求

  • 如何处理(Handle)Web 请求
    • http.Handle 函数
    • http.HandleFunc 函数

handler

创建 Web Server

http.ListenAndServe(addr string, handler Handler) error

  • addr:监听的地址,如果为空字符串,则使用 ":http",即监听 80 端口
  • handler:处理请求的 Handler,如果为空,则使用 DefaultServeMux

DefaultServeMux 是一个 multiplexer,即多路复用器,用于将请求分发到不同的处理器(可以看作是路由器)

http.ListenAndServe("localhost:8080", nil)
+

http.Server 是一个 struct

  • Addr 字段表示网络地址
    • 如果为 "",则使用 ":http",即监听所有网络接口的 80 端口
  • Handler 字段
    • 如果为 nil,则使用 DefaultServeMux
  • ListenAndServe 方法
// serve := &http.Server{
+serve := http.Server{
+	Addr:    "localhost:8080",
+	Handler: nil,
+}
+
+serve.ListenAndServe()
+

上面两种创建 Web Server 的方式,都只能使用 http。如果要用 https,则需要使用同理的 http.ListenAndServeTLS() 和 server.ListenAndServeTLS() 方法

Handler

Handler 是一个接口

type Handler interface {
+	ServeHTTP(ResponseWriter, *Request)
+}
+

自己实现 Handler 接口

type myHandler struct{}
+
+func (m *myHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
+	w.Write([]byte("Hello world"))
+}
+
+func main() {
+	mh := myHandler{}
+	server := http.Server{
+		Addr:    "localhost:8080",
+		Handler: &mh,
+	}
+	server.ListenAndServe()
+}
+

handler

DefaultServeMux

DefaultServeMux 是一个 multiplexer,即多路复用器,用于将请求分发到不同的处理器(可以看作是路由器)

DefaultServeMux

多个 Handler - http.Handle

func Handle(pattern string, handler Handler)
+

不指定 Server struct 里面的 Handler 字段值(指定为 nil)

可以使用 http.Handle 将某个 Handler 附加到 DefaultServeMux 上

  • http 包有一个 Handle 函数
  • ServerMux struct 也有一个 Handle 方法

如果调用 http.Handle,实际上调用的是 DefaultServeMux 的 Handle 方法

  • DefaultServeMux 就是 ServerMux 的指针变量
type helloHandler struct{}
+
+func (h *helloHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
+	w.Write([]byte("Hello world"))
+}
+
+type aboutHandler struct{}
+
+func (a *aboutHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
+	w.Write([]byte("About!"))
+}
+
+func main() {
+	hello := helloHandler{}
+	about := aboutHandler{}
+	server := http.Server{
+		Addr:    "localhost:8080",
+		Handler: nil, // DefaultServeMux
+	}
+	http.Handle("/hello", &hello)
+	http.Handle("/about", &about)
+	server.ListenAndServe()
+}
+

Handler 函数 - http.HandleFunc

Handler 函数就是那些行为与 handler 类似的函数:

  • Handler 函数的签名与 ServeHTTP 方法的签名一样,接收
    • http.ResponseWriter
    • 指向 http.Request 的指针

http.HandleFunc 原理

  • Go 有一个函数类型 HandlerFunc。可以将某个具有适当签名的函数 f,适配成为一个 Handler,而这个 Handler 就是调用 f 本身
type helloHandler struct{}
+
+func (h *helloHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
+	w.Write([]byte("Hello world"))
+}
+
+type aboutHandler struct{}
+
+func (a *aboutHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
+	w.Write([]byte("About!"))
+}
+
+func welcome(w http.ResponseWriter, r *http.Request) {
+	w.Write([]byte("Welcome!"))
+}
+
+func main() {
+	hello := helloHandler{}
+	about := aboutHandler{}
+	server := http.Server{
+		Addr:    "localhost:8081",
+		Handler: nil, // DefaultServeMux
+	}
+
+	http.Handle("/hello", &hello)
+	http.Handle("/about", &about)
+
+	http.HandleFunc("/home", func(w http.ResponseWriter, r *http.Request) {
+		w.Write([]byte("Home!"))
+	})
+	// http.HandleFunc("/welcome", welcome)
+
+	http.Handle("/welcome", http.HandlerFunc(welcome))
+
+	server.ListenAndServe()
+}
+

http.HandleFunc

  • http.HandleFunc(pattern string, handler func(ResponseWriter, *Request))
  • type HandlerFunc func(ResponseWriter, *Request)

内置的 handlers

  • NotFoundHandler
  • RedirectHandler
  • StripPrefix
  • TimeoutHandler
  • FileServer

http.NotFoundHandler

  • func NotFoundHandler() Handler
  • 返回一个 handler,它给每个请求的响应都是 “404 page not found”

http.RedirectHandler

  • func RedirectHandler(url string, code int) Handler
  • 返回一个 handler,它把每个请求使用给定的状态码跳转到指定的 URL
    • url,要跳转到的 URL
    • code,跳转的状态码(3xx),常见的:StatusMovedPermanently,StatusFound,StatusSeeOther,StatusTemporaryRedirect,StatusPermanentRedirect

http.StripPrefix

  • func StripPrefix(prefix string, h Handler) Handler
  • 返回一个 handler,它从请求的 URL 中去掉指定的前缀,然后再调用另一个 handler
    • 如果请求的 URL 与提供的前缀不符,那么 404
  • 略像中间件
    • prefix,URL 将要被移除的字符串前缀
    • h,是一个 handler,在移除字符串前缀之后,这个 handler 将会收到请求
  • 修饰了另一个 handler

http.TimeoutHandler

  • func TimeoutHandler(h Handler, dt time.Duration, msg string) Handler
  • 返回一个 handler,它在指定的时间内处理请求,如果超时,就返回一个错误信息
  • 也相当于是一个修饰器
    • h,将要被修饰的 handler
    • dt,第一个 handler 允许的处理时间
    • msg,如果超时,那么就把 msg 返回给请求,表示响应时间过长

http.FileServer

  • func FileServer(root FileSystem) Handler
  • 返回一个 handler,它会在 root 中寻找文件,并将其提供给请求
type FileSystem interface {
+	Open(name string) (File, error)
+}
+
  • 使用时需要用到操作系统的文件系统,所以还需要委托给
    • type Dir string
    • func (d Dir) Open(name string) (File, error)

例子

FileServer

通过 localhost:8081/ 访问 index.html

/* 方法1 */
+// http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
+// 	http.ServeFile(w, r, "wwwroot" + r.URL.Path)
+// })
+// http.ListenAndServe(":8081", nil)
+
+/* 方法2 */
+http.ListenAndServe(":8081", http.FileServer(http.Dir("wwwroot")))
+

请求

  • HTTP 请求
  • Request
  • URL
  • Header
  • Body

HTTP 消息

  • HTTP Request 和 HTTP Response(请求和响应)
  • 它们具有相同的结构
    • 请求(响应)行
    • 0 个或多个 Header
    • 空行
    • 可选的消息体(Body)
  • net/http 包提供了用于表示 HTTP 消息的结构

请求 Request

  • Request 是个 struct,代表了客户端发送的 HTTP 请求消息
  • 重要的字段
    • URL
    • Header
    • Body
    • Form、PostForm、MultipartForm
  • 也可以通过 Request 的方法访问请求中的 Cookie、URL、User Agent 等信息
  • Request 既可以代表发送到服务器的请求,又可代表客户端发出的请求

请求的 URL

  • Request 的 URL 字段就代表了请求行(请求信息第一行)里面的部分内容
  • URL 字段是指向 url.URL 类型的一个指针,url.URL 是一个 struct:

URL 的通用格式:scheme://[userinfo@]host/path[?query][#fragment] 不以斜杠开头的 URL 被解释为:scheme:opaque[?query][#fragment]

type URL struct {
+	Scheme		string
+	Opaque		string		// 编码后的不透明数据
+	User		*Userinfo	// 用户名和密码信息
+	Host		string		// host 或 host:port
+	Path		string
+	RawPath		string		// 编码后的 path,保留了转义符
+	ForceQuery	bool		// 是否在 URL 中添加 ? 强制添加查询参数
+	RawQuery	string		// 编码后的查询字符串,没有 '?'
+	Fragment	string		// 引用的片段(文档位置),没有 '#'
+}
+

URL Query

  • RawQuery 会提供实际查询的字符串
  • 例如:http://localhost:8080/?name=abc&age=18
    • RawQuery 为:name=abc&age=18

URL Fragment

  • 如果从浏览器发出的请求,就无法提取出 Fragment 字段的值
    • 浏览器在发送请求时会把 fragment 部分去掉
  • 但不是所有的请求都是从浏览器发出的(例如从 http 客户端包)

Request Header

  • 请求和响应(Request、Response)的 headers 是通过 Header 类型来描述的,它是一个 map,用来表述 HTTP Header 里的 Key-Value 对
  • Header map 的 key 是 string 类型,value 是一个字符串切片 []string
  • 设置 key 的时候会创建一个空的 []string 作为 value,value 里面第一个元素就是新 header 的值
  • 为指定的 key 添加一个新的 header 值,执行 append 操作即可
  • r.Header,返回 map
  • r.Header["Accept-Encoding"],返回 [gzip, deflate],[]string 类型
  • r.Header.Get("Accept-Encoding"),返回 gzip, deflate,string 类型
server := http.Server{
+	Addr: "localhost:8081",
+}
+
+http.HandleFunc("/header", func(w http.ResponseWriter, r *http.Request) {
+	fmt.Fprintln(w, r.Header)
+	fmt.Fprintln(w, r.Header["Accept-Encoding"])
+	fmt.Fprintln(w, r.Header.Get("Accept-Encoding"))
+})
+
+server.ListenAndServe()
+

Request Body

  • 请求和响应的 bodies 都是使用 Body 字段来表示的
  • Body 是一个 io.ReadCloser 接口
type ReadCloser interface {
+	Reader
+	Closer
+}
+
  • Reader 接口定义了一个 Open() 方法
    • 参数:[]byte
    • 返回:byte 的数量、可选的错误
  • Closer 接口定义了一个 Close() 方法
    • 返回:可选的错误
  • 想要读取请求 Body 的内容,可以调用 Body 的 Read 方法
server := http.Server{
+	Addr: "localhost:8081",
+}
+
+http.HandleFunc("/post", func(w http.ResponseWriter, r *http.Request) {
+	length := r.ContentLength
+	body := make([]byte, length)
+	r.Body.Read(body)
+	fmt.Fprintln(w, body)
+	fmt.Fprintln(w, string(body))
+})
+
+server.ListenAndServe()
+

查询参数(Query Parameters)

URL Query

  • http://localhost:8080/?name=abc&age=18
    • r.URL.RawQuery 为:name=abc&age=18(实际查询的原始字符串)
    • r.URL.Query() 方法返回 map[string][]string
      • map 的 key 是 string 类型
      • map 的 value 是 []string 类型
// http://localhost:8081/query?id=123&name=张三&id=466&name=李四
+	server := http.Server{
+		Addr: "localhost:8081",
+	}
+
+	http.HandleFunc("/query", func(w http.ResponseWriter, r *http.Request) {
+		url := r.URL
+		query := url.Query()
+
+		id := query["id"]
+		log.Println(id)
+
+		name := query.Get("name")
+		log.Println(name)
+	})
+
+	server.ListenAndServe()
+

Form

Request 上的函数允许从 URL 或 / 和 Body 中提取数据,通过如下字段

  • Form
  • PostForm
  • MultipartForm
  • FormValue
  • PostFormValue
  • FormFile
  • MultiPartReader

Form 里面的数据是 key-value 对

通常的做法是:

  • 先调用 ParseForm 或 ParseMultipartForm 来解析 Request
  • 然后相应地访问 Form、PostForm、MultipartForm 字段
<form
+  action="http://localhost:8080/process"
+  method="post"
+  enctype="application/x-www-form-urlencoded"
+>
+  <input type="text" name="name" placeholder="Name" />
+  <input type="text" name="email" placeholder="Email" />
+  <button type="submit">Submit</button>
+</form>
+
server := http.Server{
+	Addr: "localhost:8080",
+}
+http.HandleFunc("/process", func(w http.ResponseWriter, r *http.Request) {
+	r.ParseForm()
+	fmt.Fprintln(w, r.Form) // map[email:[2439639832@qq.com] name:[客户1号]]
+})
+server.ListenAndServe()
+

PostForm 字段

  • 上例中,如果只想得到 name 这个 Key 的 Value,可以使用 r.Form["name"],它返回含有一个元素的 slice:["客户 1 号"]
  • 如果表单和 URL 里有同样的 Key,那么它们都会放在一个 slice 里:表单里的值靠前,URL 的值靠后
  • 如果只想要表单的 key-value 对,不要 URL 的,可以使用 PostForm 字段
<form
+  action="http://localhost:8080/process?name=客户2号"
+  method="post"
+  enctype="application/x-www-form-urlencoded"
+>
+  <input type="text" name="name" placeholder="Name" />
+  <input type="text" name="email" placeholder="Email" />
+  <button type="submit">Submit</button>
+</form>
+

 







server := http.Server{
+	Addr: "localhost:8080",
+}
+http.HandleFunc("/process", func(w http.ResponseWriter, r *http.Request) {
+	r.ParseForm()
+	fmt.Fprintln(w, r.Form) // map[email:[2439639832@qq.com] name:[客户1号 客户2号]]
+	fmt.Fprintln(w, r.PostForm) // map[email:[2439639832@qq.com] name:[客户1号]]
+})
+server.ListenAndServe()
+





 
 


MultipartForm 字段

  • PostForm 只支持 application/x-www-form-urlencoded 编码
  • 要想得到 multipart/form-data 对,必须使用 MultipartForm 字段
  • 要想使用 MultiPartForm 字段,必须先调用 ParseMultipartForm 方法
    • 该方法会在必要时调用 ParseForm 方法
    • 参数是需要读取数据的长度
  • MultipartForm 只包含表单的 key-value 对
  • 返回类型是一个 struct,这个 struct 里面有两个 map:
    • key 是 string,value 是 []string
    • key 是 string,value 是 文件
<form
+  action="http://localhost:8080/process?name=客户2号"
+  method="post"
+  enctype="multipart/form-data"
+>
+  <input type="text" name="name" placeholder="Name" />
+  <input type="text" name="email" placeholder="Email" />
+  <button type="submit">Submit</button>
+</form>
+



 





server := http.Server{
+	Addr: "localhost:8080",
+}
+http.HandleFunc("/process", func(w http.ResponseWriter, r *http.Request) {
+	r.ParseMultipartForm(1024)
+	fmt.Fprintln(w, r.MultipartForm) // &{map[email:[2439639832@qq.com] name:[客户1号]] map[]}
+})
+server.ListenAndServe()
+





 


FormValue 和 PostFormValue 方法

  • FormValue 会返回 Form 字段中指定 key 对应的第一个 value
    • 无需调用 ParseForm 或 ParseMultipartForm
  • PostFormValue 也一样,但只能读取 PostForm 字段
  • FormValue 和 PostFormValue 都会调用 ParseMultipartForm 方法

上传文件

multipart/form-data 最常见的应用场景就是上传文件

  • 首先调用 ParseMultiPartForm 方法
  • 从 File 字段获得 FileHeader,调用其 Open 方法来获得文件
  • 可以使用 io.ReadAll 函数把文件内容读取到 byte 切片里
<form
+  action="http://localhost:8080/process?name=客户2号"
+  method="post"
+  enctype="multipart/form-data"
+>
+  <input type="text" name="name" placeholder="Name" />
+  <input type="text" name="email" placeholder="Email" />
+  <input type="file" name="file" />
+  <button type="submit">Submit</button>
+</form>
+
func process(w http.ResponseWriter, r *http.Request) {
+	r.ParseMultipartForm(1024)
+
+	fileHeader := r.MultipartForm.File["file"][0]
+	file, err := fileHeader.Open()
+	if err == nil {
+		data, err := io.ReadAll(file)
+		if err == nil {
+			fmt.Fprintln(w, string(data))
+		}
+	}
+}
+
+func main() {
+	server := http.Server{
+		Addr: "localhost:8080",
+	}
+
+	http.HandleFunc("/process", process)
+
+	server.ListenAndServe()
+}
+
+

FormFile 方法

  • 上传文件还有一个简便方法:FormFile
    • 无需调用 ParseMultipartForm 方法
    • 返回指定 key 对应的第一个 value
    • 同时返回 File 和 FileHeader,以及错误信息
    • 如果只上传一个文件,那么这种方式会快一些
func process(w http.ResponseWriter, r *http.Request) {
+	// r.ParseMultipartForm(1024)
+
+	// fileHeader := r.MultipartForm.File["file"][0]
+	// file, err := fileHeader.Open()
+
+	file, _, err := r.FormFile("file")
+
+	if err == nil {
+		data, err := io.ReadAll(file)
+		if err == nil {
+			fmt.Fprintln(w, string(data))
+		}
+	}
+}
+

MultiPartReader()

  • func (r *Request) MultiPartReader() (*multipart.Reader, error)
  • 如果是 multipart/form-data 或 multipart 混合的 POST 请求
    • MultiPartReader 方法会返回一个 MIME multipart reader
    • 否则返回一个错误
  • 可以使用该函数代替 ParseMultipartForm,来把请求的 body 作为 stream 进行处理
    • 不是把表单作为一个对象来处理的,不是一次性获得整个 map
    • 逐个检查来自表单的值,然后每次处理一个

POST 请求 - JSON Body

  • 不是所有的 POST 请求都来自 Form
  • 客户端框架(例如 Angular 等)会议不同的方式对 POST 请求编码:
    • jQuery 通常使用 application/x-www-form-urlencoded
    • Angular 通常使用 application/json
  • ParseForm 方法无法处理 application/json

响应

ResponseWriter

  • 从服务器向客户端返回响应需要使用 ResponseWriter
  • ResponseWriter 是一个接口,handler 用它来返回响应
  • 真正支撑 ResponseWriter 的幕后 struct 是非导出的 http.response

写入到 ResponseWriter

  • Write 方法接收一个 byte 切片作为参数,然后把它写入到 HTTP 响应的 body 里
  • 如果在 Write 方法被调用时, header 里面没有设定 content-type,那么数据的前 512 字节就会用来被检测 content type
func writeExample(w http.ResponseWriter, r *http.Request) {
+	str := `<html>
+<head><title>Go Web</title></head>
+<body><h1>Hello World</h1></body>
+</html>
+`
+	w.Write([]byte(str))
+}
+
+
+func main() {
+	server := http.Server{
+		Addr: "localhost:8080",
+	}
+	http.HandleFunc("/write", writeExample)
+	server.ListenAndServe()
+}
+
curl -i localhost:8080/write
+
+# HTTP/1.1 200 OK
+# Date: Sun, 15 Oct 2023 08:38:27 GMT
+# Content-Length: 84
+# Content-Type: text/html; charset=utf-8
+
+# <html>
+# <head><title>Go Web</title></head>
+# <body><h1>Hello World</h1></body>
+# </html>
+

WriteHeader 方法

  • WriteHeader 方法接收一个整数类型(HTTP 状态码)作为参数,并把它作为 HTTP 响应的状态码返回
  • 如果该方法没有显示调用,那么在第一次调用 Write 方法前,会隐式地调用 WriteHeader(http.StatusOK)
    • 所以 WriteHeader 主要用来发送错误类的 HTTP 状态码
  • 调用完 WriteHeader 方法之后,仍然可以写入到 ResponseWriter,但无法再修改 header 了
func writeHeaderExample(w http.ResponseWriter, r *http.Request) {
+	w.WriteHeader(501)
+	fmt.Fprintln(w, "No such service, try next door")
+}
+
+
+func main() {
+	server := http.Server{
+		Addr: "localhost:8080",
+	}
+	http.HandleFunc("/writeheader", writeHeaderExample)
+	server.ListenAndServe()
+}
+
curl -i localhost:8080/writeheader
+
+# HTTP/1.1 501 Not Implemented
+# Date: Sun, 15 Oct 2023 12:44:53 GMT
+# Content-Length: 31
+# Content-Type: text/plain; charset=utf-8
+
+# No such service, try next door
+

Header 方法

  • Header 方法返回 headers 的 map,可以进行修改
  • 修改后的 headers 将会体现在返回给客户端的 HTTP 响应里
type Post struct {
+	User string
+	Threads []string
+}
+
+func headerExample(w http.ResponseWriter, r *http.Request) {
+	w.Header().Set("Location", "http://google.com")
+	w.WriteHeader(302)
+}
+
+func jsonExample(w http.ResponseWriter, r *http.Request) {
+	w.Header().Set("Content-Type", "application/json")
+	post := &Post{
+		User: "Sau Sheong",
+		Threads: []string{"first", "second", "third"},
+	}
+	json, _ := json.Marshal(post)
+	w.Write(json)
+}
+
+
+func main() {
+	server := http.Server{
+		Addr: "localhost:8080",
+	}
+	http.HandleFunc("/header", headerExample)
+	http.HandleFunc("/json", jsonExample)
+	server.ListenAndServe()
+}
+
curl -i localhost:8080/header
+
+# HTTP/1.1 302 Found
+# Location: http://google.com
+# Date: Sun, 15 Oct 2023 12:50:47 GMT
+# Content-Length: 0
+
+curl -i localhost:8080/json
+
+# HTTP/1.1 200 OK
+# Content-Type: application/json
+# Date: Sun, 15 Oct 2023 12:56:51 GMT
+# Content-Length: 58
+
+# {"User":"Sau Sheong","Threads":["first","second","third"]}
+

内置的 Response

  • NotFound 函数,包装一个 404 状态码和一个额外的信息
  • ServeFile 函数,从文件系统提供文件,返回给请求者
  • ServeContent 函数,它可以把实现了 io.ReadSeeker 接口的任何东西里面的内容返回给请求者
    • 还可以处理 Range 请求(范围请求),如果只请求了资源的一部分内容,那么 ServeContent 就可以如此响应。而 ServeFile 或 io.Copy 就不行
  • Redirect 函数,告诉客户端重定向到另一个 URL

模板

  • Web 模板就是预先设计好的 HTML 页面,它可以被模板引擎反复的使用,来产生 HTML 页面
  • Go 的标准库提供了 text/template 和 html/template 两个模板库
    • 大多数 Go 的 Web 框架都使用这些库作为默认的模板引擎

模板与模板引擎

模板引擎可以合并模板与上下文数据,产生最终的 HTML

Go 的模板引擎

  • 主要使用的是 text/template,HTML 相关的部分使用了 html/template,是个混合体
  • 模板可以完全无逻辑,但又具有足够的嵌入特性
  • 和大多数模板引擎一样,Go Web 的模板位于无逻辑和嵌入逻辑之间的某个地方

关于模板

  • 模板必须是可读的文本格式,扩展名任意。对于 Web 应用通常就是 HTML
    • 里面会内嵌一些命令(叫作 action)
  • text/template 是通用模板引擎,html/template 是 HTML 模板引擎
  • action 位于双层花括号之间:{{ . }}
    • 这里的 . 就是一个 action
    • 它可以命令模板引擎将其替换成一个值

使用模板引擎

  1. 解析模板源(可以是字符串或模板文件),从而创建一个解析好的模板的 struct
  2. 执行解析好的模板,并传入 ResponseWriter 和数据
    • 这会触发模板引擎组合解析好的模板和数据,来产生最终的 HTML,并将它传递给 ResponseWriter
<!DOCTYPE html>
+<html>
+  <head>
+    <meta charset="UTF-8" />
+    <title>Document</title>
+    <style></style>
+  </head>
+
+  <body>
+    {{ . }}
+  </body>
+</html>
+
func process(w http.ResponseWriter, r *http.Request) {
+	t, _ := template.ParseFiles("tmpl.html")
+	t.Execute(w, "Hello World")
+}
+
+
+func main() {
+	server := http.Server{
+		Addr: "localhost:8080",
+	}
+	http.HandleFunc("/process", process)
+	server.ListenAndServe()
+}
+

解析模板

  • ParseFiles
  • ParseGlob
  • Parse

ParseFiles

  • 解析模板文件,并创建一个解析好的模板 struct,后续可以被执行
  • ParseFiles 函数是 Template struct 上 ParseFiles 方法的简便调用
  • 调用 ParseFiles 后,会创建一个新的模板,模板名字是文件名
  • New 函数
  • ParseFiles 的参数数量可变,但只返回一个模板
    • 当解析多个文件时,第一个文件作为返回的模板(名,内容),其余的作为 map,供后续执行使用
// t, _ := template.ParseFiles("tmpl.html")
+t := template.New("tmpl.html")
+t, _ = t.ParseFiles("tmpl.html")
+

ParseGlob

  • 使用模式匹配来解析特定的文件
t, _ := template.ParseGlob("*.html")
+

Parse

  • 可以解析字符串模板,其他方式最终都会调用 Parse

Lookup

  • 通过模板名来寻找模板,如果没找到就返回 nil

Must

  • 可以包裹一个函数,返回到一个模板的指针和一个错误
    • 如果错误不为 nil,那么就 panic

执行模板

  • Execute
    • 参数是 ResponseWriter、数据
    • 单模板:很适用
    • 模板集:只用第一个模板
  • ExecuteTemplate
    • 参数是 ResponseWriter、模板名、数据
    • 模板集:很适用
func process(w http.ResponseWriter, r *http.Request) {
+	t, _ := template.ParseFiles("t1.html")
+	t.Execute(w, "Hello World")
+
+	ts, _ := template.ParseFiles("t2.html", "t3.html")
+	ts.ExecuteTemplate(w, "t2.html", "Hello World")
+}
+

Action

  • Action 就是 Go 模板中嵌入的命令,位于两组花括号之间
  • . 就是一个 Action,而且是最重要的一个。它代表了传入模板的数据
  • Action 主要可以分为 5 类
    • 条件类
    • 迭代/遍历类
    • 设置类
    • 包含类
    • 定义类

条件 Action

语法

{{ if arg }}
+	some content
+{{ end }}
+
+{{ if arg }}
+	some content
+{{ else }}
+	some content
+{{ end }}
+
+{{ if arg }}
+	some content
+{{ else if arg }}
+	some content
+{{ else }}
+	some content
+{{ end }}
+

demo

<body>
+  {{ if . }} Number is greater than 5 {{ else }} Number is 5 or less {{ end }}
+</body>
+
func main() {
+	http.HandleFunc("/process", process)
+	http.ListenAndServe("localhost:8080", nil)
+}
+
+var rng = rand.New(rand.NewSource(time.Now().UnixNano()))
+
+func process(w http.ResponseWriter, r *http.Request) {
+	t, _ := template.ParseFiles("tmpl.html")
+	t.Execute(w, rng.Intn(10) > 5)
+}
+

迭代/遍历 Action

语法

{{ range array }}
+	some content {{ . }}
+{{ end }}
+
  • 这类 Action 用来遍历数组、slice、map 或 channel 等数据结构
    • “.” 用来表示每次迭代循环中的元素
<ul>
+  {{range .}}
+  <li>{{.}}</li>
+  <!-- 回落机制 -->
+  {{ else }}
+  <li>Empty list</li>
+  {{end}}
+</ul>
+
func main() {
+	http.HandleFunc("/process", process)
+	http.ListenAndServe("localhost:8080", nil)
+}
+
+func process(w http.ResponseWriter, r *http.Request) {
+	t, _ := template.ParseFiles("tmpl.html")
+	daysOfWeek := []string{"Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun"}
+	t.Execute(w, daysOfWeek)
+}
+

设置 Action

语法

{{ with arg }}
+.
+{{ end }}
+
  • 它允许在指定范围内,让“.”来表示其它指定的值(arg)

demo

<body>
+  <div>The dot is set to {{ . }}</div>
+  <div>{{ with "world" }} Now the dot is set to {{ . }} {{ end }}</div>
+  <div>The dot is {{ . }} again</div>
+</body>
+
+<!-- 回落机制 -->
+
+<body>
+  <div>The dot is set to {{ . }}</div>
+  <div>
+    {{ with "" }} Now the dot is set to {{ . }} {{ else }} The dot is still {{ .
+    }} {{ end }}
+  </div>
+  <div>The dot is {{ . }} again</div>
+</body>
+
func process(w http.ResponseWriter, r *http.Request) {
+	t, _ := template.ParseFiles("tmpl.html")
+	t.Execute(w, "hello")
+}
+

包含 Action

语法

{{ template "name"}}
+
+// 给被包含的模板传递参数
+{{ template "name" arg }}
+
  • 它允许在模板中包含其他的模板
<!DOCTYPE html>
+<html>
+  <head>
+    <meta charset="UTF-8" />
+    <title>Document</title>
+    <style></style>
+  </head>
+
+  <body>
+    <div>this is t1.html</div>
+    <div>This is the value of the dot in t1.html - [{{ . }}]</div>
+    <hr />
+    {{ template "t2.html" . }}
+    <hr />
+    <div>This is t1.html after</div>
+  </body>
+</html>
+
<div style="background-color: yellow">
+  This is t2.html <br />
+  This is the value of the dot in t2.html - [{{ . }}]
+</div>
+
func process(w http.ResponseWriter, r *http.Request) {
+	t, _ := template.ParseFiles("tmpl.html", "t2.html")
+	t.Execute(w, "hello")
+}
+

定义 Action

define action

函数和管道

参数

  • 参数就是模板里面用到的值
  • 模板里面的参数可以是任何类型的值
  • 参数可以是变量、方法(返回单个值或返回一个值和一个错误)或函数
  • 参数可以是一个“.”,也就是传入到模板引擎的那个值

在 Action 中设置变量

  • 可以在 action 中设置变量,变量以 $ 开头:
    • $variable := value
  • 一个迭代 action 的例子
{{ range $key, $value := . }}
+{{ $key }}: {{ $value }}
+{{ end }}
+

管道

  • 管道是按顺序连接到一起的参数、函数和方法
    • 和 Unix 的管道类似
  • 例如:{{ p1 | p2 | p3 }}
    • p1、p2、p3 要么是参数,要么是函数
  • 管道允许把参数的输出发给下一个参数,下一个参数由管道(|)分隔开
<body>
+  <!-- 展示 12.35 -->
+  {{ 12.3456 | printf "%.2f" }}
+  <!-- 等价于 -->
+  {{ printf "%.2f" 12.3456 }}
+</body>
+

函数

  • 参数可以是一个函数
  • Go 模板引擎提供了一些基本的内置函数,功能比较有限。例如 fmt.Sprint 的各类变体
  • 开发者可以自定义函数
    • 可以接收任意数量的输入参数
    • 返回:
      • 一个值
      • 一个值 + 一个错误

内置函数

  • define\template\block
  • html\js\urlquery。对字符串进行转义,防止安全问题
    • 如果是 Web 模板,那么不会需要经常使用这些函数
  • index
  • print/printf/println
  • len
  • with

自定义函数

template.Funcs(funcMap FuncMap) *Template

type FuncMap map[string]interface{}

创建自定义函数的步骤:

  1. 创建一个 FuncMap 类型的变量
    • key 是函数名
    • value 就是函数
  2. 把 FuncMap 附加到模板
<body>
+  <!-- 自定义函数可以在管道中使用,更强大灵活 -->
+  {{ . | fdate }}
+  <!-- 自定义函数也可以作为正常函数使用 -->
+  {{ fdate . }}
+</body>
+
func process(w http.ResponseWriter, r *http.Request) {
+	// 常见用法:
+	// template.New("").Funcs(funcMap).Parse(...)
+	// 调用顺序十分重要
+	funcMap := template.FuncMap{"fdate": formatDate}
+	t:= template.New("index.html").Funcs(funcMap)
+	t.ParseFiles("index.html")
+	t.Execute(w, time.Now())
+}
+
+func formatDate(t time.Time) string {
+	layout := "2006-01-02"
+	return t.Format(layout)
+}
+
Last Updated:
+ + + diff --git a/favicon.ico b/favicon.ico new file mode 100644 index 0000000..28f0f7d Binary files /dev/null and b/favicon.ico differ diff --git a/frontend/engineering/umi/index.html b/frontend/engineering/umi/index.html new file mode 100644 index 0000000..96126dd --- /dev/null +++ b/frontend/engineering/umi/index.html @@ -0,0 +1,137 @@ + + + + + + + + + 瓢儿白施肥记 + + + + +

使用 umi 搭建一体机项目

设计稿 1080 x 1920,不确定实际屏幕的宽高比

UI 组件库

考虑到一体机触摸体验,选用 Ant Design Mobile 高清适配,项目从 antd-mobile/2x 导入组件

  1. 为了导入方便,做以下配置:
// config.ts 中,配置一个从 antd-mobile 到 antd-mobile/2x 的别名
+alias: {
+  "antd-mobile": require.resolve("antd-mobile/2x")
+}
+
  1. 设置主题色
// 正好 umi 中 a 标签的 color: var(--adm-color-primary)
+:root:root {
+  --adm-color-primary: #{$primary-color};
+}
+
export const primaryColor = "#44bb55";
+

目录结构

📁owners-meeting
+├─ 📁config
+│  ├─ 📄config.ts # umi 配置文件
+│  └─ 📄routes.ts # 从配置文件中提取出来的路由配置
+├─ 📁public # 可能扔进服务器的静态资源
+└─ 📁src
+   ├─ 📁constants
+   │  └─ 📄color.ts # 如主题色等常量
+   ├─ 📁pages # 如果是一个单独的页面组件则采用大驼峰命名文件夹,否则采用小驼峰命名文件夹
+   ├─ 📁styles # 里面所有的mixin、变量、函数都是全局的(index.scss 是被 sass-resources-loader 处理过的)
+   ├─ 📁utils
+   │  ├─ 📄format.ts # 用于格式化的一些函数
+   │  └─ 📄px2.ts # 用于将 px 转换为 vw/vh 的函数
+   ├─ 📁layouts
+   └─ 📄global.scss # 全局样式文件
+

sass-resources-loader

该 loader 处理过的 mixin、变量、函数等可以在任意 .scss 文件中不经导入使用

config.ts 中配置:

chainWebpack(config) {
+  config.module
+    .rule("scss")
+    .test(/\.scss$/)
+    .use("sass-loader")
+    .loader("sass-loader")
+    .end()
+    .use("sass-resources-loader")
+    .loader("sass-resources-loader")
+    .options({
+      resources: [path.resolve(__dirname, "../src/styles/index.scss")]
+    });
+}
+

注意:install sass-loader & sass-resources-loader

尺寸适配

基于 1080 x 1920 的设计稿,使用 vwvh 单位进行适配。

@use "sass:math";
+
+// 默认设计稿的宽度
+$designWidth: 1080;
+// 默认设计稿的高度
+$designHeight: 1920;
+
+// px 转为 vw 的函数
+@function vw($px) {
+  @return math.div($px, $designWidth) * 100vw;
+}
+
+// px 转为 vh 的函数
+@function vh($px) {
+  @return math.div($px, $designHeight) * 100vh;
+}
+
.a {
+  width: vw(100);
+  height: vh(100);
+}
+
// 定义设计稿的宽高
+const designWidth = 1080;
+const designHeight = 1920;
+
+export const px2vw = (_px: number) => {
+  return (_px * 100.0) / designWidth + "vw";
+};
+
+export const px2vh = (_px: number) => {
+  return (_px * 100.0) / designHeight + "vh";
+};
+
import { px2vw, px2vh } from "@/utils/px2.ts";
+
/**
+* 设置字体在特定屏幕中整体缩放
+*/
+html {
+  font-size: 100px !important;
+}
+
+@media (max-width: 800px) {
+  html {
+    font-size: 80px !important;
+  }
+}
+
  • 字体使用 rem 单位,方便整体缩放或者根据屏幕大小进行缩放
    • 1rem = 100px,即 20px 字体大小为 0.2rem

静态资源

  • 图片:/public/imgs/
    • scss 文件中 url(/imgs/xxx.png),ts 文件中部分使用 import xxx from "/public/imgs/xxx.png"(否则就尝试删掉 /public)

手写的工具

页面跳转

  • 项目中有一个可以获取路径的函数,getPath
    • 页面路径跟组件名称有对应关系,思路是 getPath(所有页面组件 => 所有页面组件.跳转的页面组件名称, {参数1, 参数2, ...}) 即可拿到要跳转的路径
  • umi 提供的跳转的 hook,useNavigateopen in new window
import { getPath } from "@/utils/get";
+import { useNavigate } from "umi";
+
+const navigate = useNavigate();
+const handleClick = () => {
+  navigate(
+    getPath((component) => component.Step1_2),
+    { param: "a", query1: "b", query2: "c" }
+  ); // /xxx/step1-2/a?query1=b&query2=c
+};
+

无用数据过滤

export const formatData = (
+  data: any,
+  format?: (v: any) => any,
+  options?: { sign?: number | string }
+) => {
+  const { sign } = options || {};
+  const uselessData = [undefined, null, ""];
+  const isEmptyArr = Array.isArray(data) && data.length === 0;
+  const isEmptyObj = typeof data === "object" && Object.keys(data).length === 0;
+  if (uselessData.includes(data) || isEmptyArr || isEmptyObj)
+    return sign || "-";
+  return typeof format === "function" ? format(data) : data;
+};
+
Last Updated:
+ + + diff --git a/frontend/framework/react/redux-mobx/index.html b/frontend/framework/react/redux-mobx/index.html new file mode 100644 index 0000000..8b1867b --- /dev/null +++ b/frontend/framework/react/redux-mobx/index.html @@ -0,0 +1,90 @@ + + + + + + + + + 瓢儿白施肥记 + + + + +

了解

状态

  • 不关心过程,只关心界面处于哪个状态
  • 状态是动态数据,当前造成的结果。也就是 model 形成的 view

软件究竟在做什么

  • 软件工程的核心,就是在管理数据
  • 如果有一个功能需要开发的时候,首先考虑的是,一个数据的生命周期和作用范围

性能优化的两种方式

  • 协商缓存
  • 提前拿数据,prefetch preload prerender

状态管理

状态机应该具备的能力

  • 有独立的区域存储数据,并且能被拿到
    • 闭包
    • 单例模式
  • 有修改数据的明确方法,并且,能够让使用数据的地方感知到
    • 发布订阅
    • Proxy / Object.defineProperty
  • model 的改变触发 view 的更新
    • forceUpdate
    • $forceUpdate
    • data.x = Math.random()

状态管理简易实现

export const createData = (initialData) => {
+  let data = initialData;
+  const deps = [];
+
+  const getData = () => data;
+
+  const modifyData = (newData) => {
+    data = newData;
+    deps.forEach((fn) => fn());
+  };
+
+  const subscribeData = (handler) => {
+    deps.push(handler);
+  };
+
+  return {
+    getData,
+    modifyData,
+    subscribeData
+  };
+};
+
import { useEffect, useState } from "react";
+import { createData } from "./data";
+
+const initialData = { count: 2 };
+const { getData, modifyData, subscribeData } = createData(initialData);
+
+const Demo = () => {
+  const [state, setState] = useState(initialData);
+
+  const change0 = () => {
+    modifyData({ count: 0 });
+  };
+
+  const change1 = () => {
+    modifyData({ count: 1 });
+  };
+
+  useEffect(() => {
+    // 只需要订阅一次
+    subscribeData(() => {
+      const curData = getData();
+      console.log("the cur data is", curData);
+      setState(curData);
+    });
+  }, []);
+
+  return (
+    <div>
+      {state.count}
+      <button onClick={change0}>点击切换为0</button>
+      <button onClick={change1}>点击切换为1</button>
+    </div>
+  );
+};
+
+export default Demo;
+
Last Updated:
+ + + diff --git a/frontend/framework/we-app/01/index.html b/frontend/framework/we-app/01/index.html new file mode 100644 index 0000000..5b23cf3 --- /dev/null +++ b/frontend/framework/we-app/01/index.html @@ -0,0 +1,33 @@ + + + + + + + + + 小程序 | 瓢儿白施肥记 + + + + +

微信小程序

开发准备

  1. 申请一个 APPID 申请账号open in new window
    • 登录小程序后台[https://mp.weixin.qq.com/wxamp/basicprofile/index?token=741939644&lang=zh_CN] ,在菜单 “开发”-“开发设置” 可看到小程序的 AppID 和 AppSecret
  2. 下载并安装小程序开发工具open in new window
Last Updated:
+ + + diff --git a/frontend/framework/we-app/index.html b/frontend/framework/we-app/index.html new file mode 100644 index 0000000..cb3366b --- /dev/null +++ b/frontend/framework/we-app/index.html @@ -0,0 +1,33 @@ + + + + + + + + + 瓢儿白施肥记 + + + + +

小程序

小程序的本质是网页,只能使用宿主提供的 API,不能使用浏览器提供的 API -> webview

微信小程序

逻辑层和渲染层open in new window

小程序运行机制(冷启动、热启动)open in new window

开发准备

Last Updated:
+ + + diff --git a/frontend/js/red-book/01.html b/frontend/js/red-book/01.html new file mode 100644 index 0000000..7024e5a --- /dev/null +++ b/frontend/js/red-book/01.html @@ -0,0 +1,33 @@ + + + + + + + + + 红宝书 | 瓢儿白施肥记 + + + + +

什么是 JavaScript

小结

1. JavaScript 由哪三部分组成?

  • ECMAScript:ECMAScript 是 JavaScript 的核心规范,定义了语法、类型、语句、关键字、保留字、操作符、全局对象
  • DOM:提供与网页内容交互的方法和接口,它将网页表示为一个由节点组成的树状结构
  • BOM:提供与浏览器交互的方法和接口

2. JavaScript 和 ECMAScript 有什么关系?

ECMAScript 是 JavaScript 的标准化规范,JavaScript 是 ECMAScript 的一个实现

历史

  • 1995 年问世(网景与 Sun 公司开发),目的是让客户端处理输入验证
  • 1997 年,为解决像 Netscape Navigator(JavaScript) 和 IE(JScript) 中存在多个版本 JavaScript 的问题,JavaScript1.1 作为提案被提交给欧洲计算机制造商协会 Ecma,由 TC39 打造出 ECMA-262,也就是 ECMAScript 脚本语言标准
  • 1998 年,国际标准化组织 ISO 和国际电工委员会 IEC 也将 ECMAScript 采纳为标准,自此各家浏览器均以 ECMAScript 作为 JavaScript 实现的依据,但具体实现各有不同

JavaScript 实现

JavaScript 不限于 ECMA-262 所定义的那样,它包含以下几个部分:

  • 核心(ECMAScript):由 ECMA-262 定义并提供核心功能
  • 文档对象模型(DOM):提供与网页内容交互的方法和接口
  • 浏览器对象模型(BOM):提供与浏览器交互的方法和接口

ECMAScript

ECMA-262 定义了什么?

  • 语法
  • 类型
  • 语句
  • 关键字
  • 保留字
  • 操作符
  • 全局对象

ECMAScript 是实现 ECMA-262 这个规范描述的所有方面的一门语言,JavaScript 和 Adobe ActionScript 都实现了 ECMAScript

Web 浏览器是 ECMAScript 的一种宿主环境,宿主环境提供 ECMAScript 的基准实现和与环境自身交互必需的扩展,扩展(如 DOM)使用 ECMAScript 的核心类型和语法提供特定于环境的额外功能

DOM

文档对象模型(Document Object Model)是一个应用编程接口(API),DOM 通过创建表示文档的树,让开发者可以控制网页的结构和内容,使用 DOM API 可以轻松删除、添加、替换、修改节点

BOM

浏览器对象模型 BOM,用于支持访问和操作浏览器的窗口

Last Updated:
+ + + diff --git a/frontend/js/red-book/02.html b/frontend/js/red-book/02.html new file mode 100644 index 0000000..7820ac8 --- /dev/null +++ b/frontend/js/red-book/02.html @@ -0,0 +1,47 @@ + + + + + + + + + 红宝书 | 瓢儿白施肥记 + + + + +

HTML 中的 JavaScript

小结

1. script 元素有哪些属性?

async、defer、type、src、crossorigin、integrity

2. noscript

当浏览器不支持脚本或禁用脚本时,noscript 元素会显示出来

script

<script> 元素有以下属性:

  • async:表示立即下载脚本,但不妨碍页面中的其他操作,比如下载其他资源或等待加载其他脚本,只对外部脚本文件有效
  • defer:表示脚本可以延迟到文档完全被解析和显示之后再执行,只对外部脚本文件有效
  • src:表示包含要执行代码的外部文件
  • type:表示代码块中脚本语言的内容类型(MIME 类型),如果这个值是 module,则代码会被当成 ES6 模块,而且只有这时候代码中才能出现 import 和 export 关键字
  • crossorigin:配置相关请求的 CORS(跨源资源共享)设置。默认不使用 CORS,也就是说,如果请求的资源不是同源的,那么将会忽略这个属性。如果设置了这个属性,那么请求会包含 Origin 头部信息,如果服务器认为这个请求是合法的,那么响应会包含 Access-Control-Allow-Origin 头部信息。这个属性的可用值如下:
    • anonymous:发送请求时不会提供凭据信息(比如 cookie 等),也就是说,不会把当前域的 cookie 发送给目标请求
    • use-credentials:发送请求时会提供凭据信息(比如 cookie 等),也就是说,会把当前域的 cookie 发送给目标请求
  • integrity:允许比对接收到的资源和指定的加密签名以验证子资源完整性(SRI,Subresource Integrity)。如果接受到的资源与这个属性指定的签名不匹配,则页面会报错,脚本不会执行。这个属性用于确保内容分发网络(CDN,Content Delivery Network)不会提供恶意内容

使用 <script> 的方式有两种:

  1. 直接在页面中嵌入行内 JavaScript 代码
<script>
+	function sayScript() {
+    // 出现字符串 </script> 时,需要转义
+		console.log('<\/script>')
+	}
+</script>
+



 


  1. 通过 src 属性包含外部 JavaScript 文件

注:使用了 src 属性的 <script> 元素不应该在其 <script></script> 标签之间再包含额外的 JavaScript 代码,否则会忽略这些额外的代码

标签位置

  1. 放在页面的 <head> 元素中
  2. 放在页面的 <body> 元素中的页面内容后面

推迟执行脚本

<script> 元素中设置 defer 属性(只适用于外部脚本),告诉浏览器立即下载,但延迟执行(解析到 </html> 后)

HTML5 规范要求脚本按照它们出现的先后顺序执行,因此第一个延迟脚本会先于第二个延迟脚本执行,而这两个脚本会先于 DOMContentLoaded 事件执行。但是延迟脚本不一定会按照顺序执行,也不一定会在 DOMContentLoaded 事件触发前执行,因此最好只包含一个延迟脚本

异步执行脚本

<script> 元素中设置 async 属性(只适用于外部脚本),告诉浏览器立即下载,与 defer 的区别是,异步脚本不保证按照它们的先后顺序执行

给脚本添加 async 属性的目的是告诉浏览器,不必等待其他脚本,也不必阻塞文档呈现,立即下载并执行脚本。从这个意义上讲,标记为 async 的脚本不应该在加载期间修改 DOM。异步脚本一定会在页面的 load 事件前执行,但可能会在 DOMContentLoaded 事件触发之前或之后执行

动态加载脚本

通过向 DOM 中动态添加 <script> 元素加载指定的脚本

const script = document.createElement('script')
+script.src = 'gibberish.js'
+document.head.appendChild(script)
+

默认情况下,动态添加的脚本是异步执行的(相当于 async 为 true)

因为所有浏览器都支持 createElement() 方法,但不是所有浏览器都支持 async 属性,因此如果要统一动态脚本的加载行为,可以明确将其设置为同步加载:

const script = document.createElement('script')
+script.src = 'gibberish.js'
+script.async = false
+document.head.appendChild(script)
+

以这种方式获取的资源对浏览器预加载器是不可见的,严重影响它们在资源获取队列中的优先级,可能会影响性能

要想让预加载器知道这些动态请求文件的存在,可以在文档头部显示生声明它们:

<link rel="preload" href="gibberish.js" />
+

noscript

<noscript> 元素可以包含任何可以出现在 <body> 元素中的 HTML 元素,它的作用是提供替代内容,只有以下情况下才会显示:

  • 浏览器不支持脚本
  • 浏览器对脚本的支持被关闭
Last Updated:
+ + + diff --git a/frontend/js/red-book/03.html b/frontend/js/red-book/03.html new file mode 100644 index 0000000..cd8ce46 --- /dev/null +++ b/frontend/js/red-book/03.html @@ -0,0 +1,150 @@ + + + + + + + + + 红宝书 | 瓢儿白施肥记 + + + + +

语言基础

小结

1. 语法特点?

  • 区分大小写
  • 标识符(变量、函数、属性或函数参数的名称)可以由数字、字母、下划线、美元符号组成,但是第一个字符不能是数字
  • 单行注释 //,多行注释 /* */
  • 语句以分号结尾,也可以不用,多条语句用代码块包裹 {}

2. let、var 和 const

  • 作用域:let 和 const 声明的变量的作用域是块级的,var 声明的变量的作用域是函数级的
  • windows 对象:let 和 const 在全局作用域中声明的变量不会成为 window 对象的属性
  • 变量提升:var 存在变量提升,let 和 const 存在暂时性死区
  • 重复声明:可以多次使用 var 关键字声明同一个变量,不能在同一个块级作用域中多次使用 let 和 const 关键字声明同一个变量
  • 声明常量:使用 const 声明的变量必须进行初始化,并且不能再次赋值

3. 数据类型

ECMAScript 标准定义了 8 种数据类型:

  • 7 种基本类型:Undefined、Null、Boolean、Number、String、Symbol(ECMAScript 6 新增)和 BigInt(ECMAScript 2020 新增)
  • 1 种引用类型:Object

4. null 和 undefined

  • null 表示一个空对象指针,typeof null 返回 object
  • undefined 表示一个未初始化的变量,typeof undefined 返回 undefined

5. 转布尔值为 false 的值''0NaNnullundefined

6. 转数值

以下三个函数最终得到的都是十进制数或者 NaN

Number()

  • null 转 0,undefined 转 NaN
  • 布尔值:true 转 1,false 转 0
  • 字符串:
    • '' 转为 0
    • 如果字符串中只包含数字(包括前面带正负号和带小数点的情况),则将其转换为十进制数值
    • 否则为 NaN
  • 对象:调用对象的 valueOf() 方法,然后依照前面的规则转换返回的值。如果转换的结果是 NaN,则调用对象的 toString() 方法,然后再次依照前面的规则转换返回的值

parseInt() 区别与 Number()

  • null 转 NaN
  • 字符串:
    • '' 转为 NaN
    • 从第一个非空格字符开始解析,直到遇到一个非数字字符为止
    • 如果字符串以 0x 开头,则按照十六进制整数解析
    • 如果字符串以 0 开头,则按照八进制整数解析
  • 可以传入第二个参数指定进制

parseFloat() 区别与 parseInt()

  • 只解析十进制数,不能指定底数
  • 如果字符串表示整数,则返回整数
    • console.log(parseFloat('0x6')) // 0

7. 转字符串

  • toString():
    • null 和 undefined 没有这个方法
    • 数值调用时可以传入底数,表示转换为对应进制的字符串
  • String():
    • null 和 undefined 会返回 'null' 和 'undefined'
    • 如果值有 toString() 方法,则调用该方法(不传参数)并返回结果

ECMA-262 以一个名为 ECMAScript 的伪语言(pseudo language)的形式,定义了 JavaScript 的所有这些方面

数据类型

Number 类型

Number 类型使用 IEEE754 格式来表示整数和浮点值

超出 Number.MAX_VALUE 的值会被转换为 Infinity

isFinite() 函数可以用来判断一个数值是否有限

const result = Number.MAX_VALUE + Number.MAX_VALUE
+console.log(isFinite(result)) // false
+

NaN 是一个特殊的数值,表示一个本来要返回数值的操作失败了

console.log(0 / 0) // NaN
+console.log(Infinity / Infinity) // NaN
+console.log(5 / 0) // Infinity
+
+console.log(NaN == NaN) // false
+
+// isNaN() 函数可以用来判断一个数值是否是 NaN
+console.log(isNaN(NaN)) // true
+

String 类型

String 类型表示零或多个 16 位 Unicode 字符序列

模板字面量标签函数:

const a = 6
+const b = 9
+
+const zipTag = (strings, ...expressions) => {
+	console.log(strings)
+	console.log(expressions)
+	return expressions.reduce((prev, cur, i) => {
+		return prev + cur + strings[i + 1]
+	}, strings[0])
+}
+const taggedResult = zipTag`${a} + ${b} = ${a + b}`
+
+console.log(taggedResult)
+

String.raw() 函数:获取原始字符串

console.log(String.raw`Hi\n${2 + 3}!`) // Hi\n5!
+

Symbol 类型

Symbol 的用途是确保对象属性使用唯一标识符,没有字面量语法

  • 使用 Symbol() 函数来初始化

    • 可以传入一个字符串参数作为对符号的描述,但是只是用于调试,与符号的定义和标识没有关系
    • 不能与 new 关键字一起作为构造函数使用,避免创建符号包装对象
  • 在全局符号注册表中创建并重用符号,使用 Symbol.for() 函数

    • 如果传入的字符串参数已经存在,则返回已有的符号
    • 如果不存在,则创建一个新的符号
  • Symbol.keyFor() 函数

    • 接收符号,返回该全局符号对应的字符串键名
    • 如果查询的不是全局符号,则返回 undefined
    • 如果接收到的不是符号,则抛出 TypeError 异常
    const s1 = Symbol.for('foo')
    +console.log(Symbol.keyFor(s1)) // foo
    +
  • 凡是可以使用字符串或者数值作为属性的地方,都可以使用符号

    const s1 = Symbol('foo')
    +const s2 = Symbol('bar')
    +const s3 = Symbol('baz')
    +
    +const o = { [s1]: 'foo val' }
    +Object.defineProperty(o, s2, { value: 'bar val' })
    +Object.defineProperties(o, {
    +	[s3]: { value: 'baz val' },
    +})
    +console.log(o)
    +
    • Object.getOwnPropertyNames() 返回对象实例的常规属性数组;Object.getOwnPropertySymbols() 返回对象实例的符号属性数组,两个方法的返回值彼此互斥;Object.getOwnPropertyDescriptors() 会返回同时包含常规和符号属性描述符的对象;Reflect.ownKeys() 会返回两种类型的键
    const s1 = Symbol('foo')
    +const s2 = Symbol('bar')
    +
    +const o = {
    +	[s1]: 'foo val',
    +	[s2]: 'bar val',
    +	baz: 'baz val',
    +	qux: 'qux val',
    +}
    +
    +// ['baz', 'qux']
    +console.log(Object.getOwnPropertyNames(o))
    +
    +// [Symbol(foo), Symbol(bar)]
    +console.log(Object.getOwnPropertySymbols(o))
    +
    +/**
    +{
    +  "baz": {
    +      "value": "baz val",
    +      "writable": true,
    +      "enumerable": true,
    +      "configurable": true
    +  },
    +  "qux": {...},
    +  Symbol(bar): {...},
    +  Symbol(foo): {...}
    +}
    + */
    +console.log(Object.getOwnPropertyDescriptors(o))
    +
    +// ['baz', 'qux', Symbol(foo), Symbol(bar)]
    +console.log(Reflect.ownKeys(o))
    +

Object 类型

每个 Object 实例都有如下属性和方法:

  • constructor,用于创建当前对象的函数
  • hasOwnProperty(propertyName):用于判断当前对象实例(不是原型)上是否存在给定的属性。要检查的属性名必须是字符串或者符号
  • isPrototypeOf(object):用于判断当前对象是否是另外一个对象的原型
  • propertyIsEnumerable(propertyName):用于判断给定的属性是否可以使用 for-in 语句枚举
  • toLocaleString()
  • toString()
  • valueOf(),返回对象的字符串、数值或者布尔值表示,通常与 toString() 返回值相同

操作符

一元操作符

  1. 递增 ++ 、递减 -- 操作符

    • 后缀版与前缀版的主要区别在于,后缀版递增和递减语在语句被求值后再发生

    • 作用于非数值时,会先使用 Number() 函数将其转换为数值,再进行操作

  2. 一元加和减

    • 一元加操作符会尝试将操作数转换为数值
    • 一元减操作符会尝试将操作数转换为数值,并对其取负

位操作符

ECMAScript 中的所有数值都以 IEEE754 64 位格式存储,但是位操作符先把数值转换为 32 位整数,再进行操作,最后再将结果转换回 64 位

有符号整数使用 32 位的前 31 位表示整数值,第 32 位(第一位 表示 20)表示符号,0 表示正数,1 表示负数。这一位称为 符号位,它的值决定了数值其余部分的格式

正值以真正的二进制格式存储,负值则以二进制补码形式存储

位操作应用到非数值,首先会使用 Number() 函数将该值转换为数值,然后再应用位操作

ECMAScript 中的所有整数都表示为有符号数。特殊值 NaN 和 Infinity 在位操作中都会被当成 0

  1. 按位非(~)

    • 求操作数的反码
    • 最终效果是对数值取反并减 1‘
  2. 按位与(&)

    • 两个操作数的对应位都是 1 时,结果为 1,否则为 0
  3. 按位或(|)

    • 两个操作数的对应位只要有一个是 1,结果为 1,否则为 0
  4. 按位异或(^)

    • 两个操作数的对应位不相同时,结果为 1,否则为 0
  5. 左移(<<)

    • 将数值的所有位向左移动指定的位数,空位补 0
    • 左移会保留操作数值的符号,比如 -2 左移 5 位,结果为 -64
  6. 有符号的右移(>>)

    • 将数值的所有位向右移动指定的位数,空位补 0
    • 有符号右移会保留操作数值的符号,比如 -64 右移 5 位,结果为 -2
  7. 无符号右移(>>>)

    • 将数值的所有 32 位都向右移动指定的位数,空位补 0

布尔操作符

  1. 逻辑非(!)

    • 对操作数求反
    • 对非布尔值求反,会先使用 Boolean() 函数将其转换为布尔值,再求反
  2. 逻辑与(&&)

    • 短路操作符
  3. 逻辑或(||)

    • 短路操作符

乘性操作符

如果操作数不是数值,会先使用 Number() 函数将其转换为数值,再进行操作

  1. 乘法(*)

    • 如果操作数都是数值,则执行常规的乘法运算。如果 ECMAScript 不能表示乘积,则返回 Infinity 或 -Infinity
    • 如果有任一操作数是 NaN,则返回 NaN
    • 如果是 Infinity 与 0 相乘,则返回 NaN
  2. 除法(/)

    • 如果操作数都是数值,则执行常规的除法运算。如果 ECMAScript 不能表示商,则返回 Infinity 或 -Infinity
    • 如果有任一操作数是 NaN,则返回 NaN
    • 如果是 Infinity 除以 Infinity,则返回 NaN
    • 如果是 0 除以 0,则返回 NaN
    • 如果是非 0 的有限数除以 0,则返回 Infinity 或 -Infinity
    • 如果是 Infinity 除以任何数值,则返回 Infinity 或 -Infinity
  3. 求模(%)

    • 如果操作数都是数值,则执行常规的除法运算,返回余数
    • 如果被除数是无限值,或者除数是 0,则返回 NaN
    • 如果被除数是有限值,除数是无限值,则返回被除数
    • 如果被除数是 0 ,除数不是 0,则返回 0

指数操作符

  1. 指数操作符(**)

    • ES7 新增的操作符,与 Math.pow() 方法相同
    • console.log(3 ** 2) // 9
    • squared **= 2 // 指数赋值操作符

加性操作符

  1. 加法操作符(+)

    • 如果两个操作数都是数值
      • 有任一操作数是 NaN,则返回 NaN
      • 如果 Infinity 加 -Infinity,则返回 NaN
      • 如果 -0 加 +0,则返回 +0
    • 如果有一个操作数是字符串
      • 将另一个操作数转换为字符串,然后返回连接后的字符串
    • 如果有任一操作数是对象、数值或布尔值
      • 调用它们的 toString() 方法,对于 null 和 undefined 则调用 String() 函数,然后再应用前面的规则
  2. 减法操作符(-)

    • 如果两个操作数都是数值
      • 有任一操作数是 NaN,则返回 NaN
      • 如果是同符号的无限制相减,则返回 NaN
      • 如果是同符号的 0 相减,则返回 +0
    • 如果有任一操作数是字符串、布尔值、null 或 undefined
      • 首先调用 Number() 函数将其转换为数值,然后再应用前面的规则
    • 如果有任一操作数是对象
      • 调用它们的 valueOf() 方法,如果结果是 NaN,则减法的计算结果就是 NaN。如果对象没有 valueOf() 方法,则调用 toString() 方法,然后再将得到的字符串转为数字

关系操作符

<><=>=

  • 如果有任一操作数是数值或布尔值,则执行数值比较
  • 任何关系操作符在涉及比较 NaN 时,都返回 false
  • 如果两个操作数都是字符串,则逐个比较字符串中对应字符的编码
  • 如果有任一操作符是对象,则调用 valueOf() 方法,用得到的结果按照前面的规则进行比较。如果没有 valueOf() 方法,则调用 toString() 方法,再用得到的结果按照前面的规则进行比较

相等操作符

  1. 相等和不相等(== 和 !=)

    • 如果有任一操作数是布尔值,则在比较相等性之前先将其转换为数值
    • 如果一个操作数是字符串,另一个操作数是数值,在比较相等性之前先将字符串转换为数值
    • 如果一个操作数是对象,另一个操作数不是,则调用对象的 valueOf() 方法,用得到的基本类型值按照前面的规则进行比较
    • 如果两个操作数都是对象,则比较它们是不是同一个对象
    • null 和 undefined 是相等的
    • 如果有任一操作数是 NaN,则相等操作符返回 false,不相等操作符返回 true
  2. 全等和不全等(=== 和 !==)

    • 与相等和不相等操作符的区别是,全等和不全等操作符不会在进行比较之前转换操作数

条件操作符

variable = boolean_expression ? true_value : false_value

赋值操作符

简单赋值用 = 表示

每个数学操作符以及其他一些操作符都有对应的复合赋值操作符,如 +=-=*=/=%=**=<<=>>=>>>=

逗号操作符

逗号操作符可以用来在一条语句中执行多个操作,如

let num1 = 1,
+	num2 = 2,
+	num3 = 3
+

在赋值语句中,逗号操作符会返回表达式中的最后一项,如

let num = (5, 1, 4, 8, 0) // num 的值为 0
+

语句

if 语句

if (expression) statement1 else statement2
+

do-while 语句

do {
+	statement
+} while (expression)
+

while 语句

while (expression) statement
+

for 语句

for (initialization; expression; post - loop - expression) statement
+

for-in 语句

for (property in expression) statement
+

ECMAScript 中对象的属性是无序的,因此通过 for-in 循环输出的属性名的顺序是不可预测的。换句话说,所有可枚举的属性都会返回一次,但返回的顺序可能会因浏览器而异

for-of 语句

for (variable of object) statement
+

for-of 循环会按照可迭代对象的 next() 方法产生值的顺序迭代元素。如果尝试迭代的变量不支持迭代,则会抛出错误

标签语句

用于给语句加标签

label: statement
+

在下面的例子中, start 是一个标签,可以在后面通过 break 或 continue 语句引用它

start: for (let i = 0; i < count; i++) {
+	console.log(i)
+}
+

break 和 continue 语句

break 和 continue 都可以与标签语句一起使用,返回代码中特定的位置。通常是在嵌套循环中,如:

let num = 0
+outermost: for (let i = 0; i < 10; i++) {
+	for (let j = 0; j < 10; j++) {
+		if (i === 5 && j === 5) {
+			break outermost
+		}
+		num++
+	}
+}
+console.log(num) // 55
+

with 语句

严格模式下不允许使用 with 语句

with 语句影响性能且难于调试其中的代码,因此不建议使用

with 语句的作用是将代码的作用域设置到一个特定的对象中

with (expression) statement
+

使用 with 语句的主要场景是针对一个对象反复操作,如:

let qs = location.search.substring(1)
+let hostName = location.hostname
+let url = location.href
+

上面的每一行都包含 location 对象,如果使用 with 语句,可以简化为:

with (location) {
+	let qs = search.substring(1)
+	let hostName = hostname
+	let url = href
+}
+

switch 语句

switch (expression) {
+	case value1:
+		statement
+		break
+	case value2:
+		statement
+		break
+	case value3:
+		statement
+		break
+	default:
+		statement
+}
+

函数

最佳实践是函数要么返回值,要么不返回值。只在某个条件下返回值的函数会带来麻烦,尤其是调试时

严格模式对函数有一些限制:

  • 函数不能以 eval 或 arguments 作为名称
  • 函数的参数不能叫 eval 或 arguments
  • 两个命名参数不能同名
Last Updated:
+ + + diff --git a/frontend/js/red-book/04.html b/frontend/js/red-book/04.html new file mode 100644 index 0000000..2c455a9 --- /dev/null +++ b/frontend/js/red-book/04.html @@ -0,0 +1,60 @@ + + + + + + + + + 红宝书 | 瓢儿白施肥记 + + + + +

变量、作用域与内存

原始值与引用值

  • 保存原始值的变量是按值访问的;保存引用值的变量是按引用访问的
  • 动态属性:原始值不能有属性,尽管尝试给原始值添加属性不会报错,但是这个属性会被忽略(undefined);只有引用值可以动态添加后面可以使用的属性
  • 复制值:原始值复制时会创建一个新值,两个值互不影响;引用值复制时会复制一个指针,两个变量指向同一个对象
  • 传递参数:ECMAScript 中所有函数的参数都是按值传递的
  • 确定类型:
    • typeof 最适合用来判断一个变量是否为原始类型
    • instanceof 用来判断变量是否是给定引用类型的实例 result = variable instanceof constructor

执行上下文与作用域

执行上下文(execution context,EC):JavaScript 代码被解析和执行时所在环境的抽象概念

全局上下文是最外层的上下文,根据 ECMAScript 实现的宿主环境,表示全局上下文的对象可能不一样,浏览器中是 window 对象,Node.js 中是 global 对象

上下文在其所有代码都执行完毕后会被销毁,包括定义在上下文中的所有变量和函数(全局上下文在应用程序退出前才会被销毁,比如关闭网页或退出浏览器)

上下文中的代码在执行的时候,会创建变量对象的一个作用域链,这个作用域链决定了各级上下文中的代码在访问变量和函数时的顺序

代码正在执行的上下文变量对象始终位于作用域链最前端,全局上下文的变量对象始终位于作用域链的最末端

如果是函数上下文,其活动对象(activation object,AO)用作变量对象

函数参数被认为是当前上下文中的变量

作用域链增强

虽然执行上下文主要有全局上下文和函数上下文两种(eval() 调用内部存在第三种上下文),但有其他方式来增强作用域链

某些语句会导致在作用域链前端临时添加一个变量对象,该变量对象会在代码执行后被移除

  • try-catch 语句的 catch 块:创建一个新的变量对象,其中包含被抛出的错误对象的声明
  • with 语句:在作用域链前端添加指定的对象

变量声明

严格来讲, let 在 JavaScript 运行时中也会被提升,但由于暂时性死区(temporal dead zone,TDZ)的存在,直到执行到 let 语句时,变量才会被添加到执行上下文中

由于 const 声明暗示变量的值是单一类型且不可修改,JavaScript 运行时编译器可以将其所有实例都替换成实际的值,而不会通过查询表进行变量查找。谷歌的 V8 引擎就执行这种优化

垃圾回收

JavaScript 是使用垃圾回收的语言,执行环境负责在代码执行时管理内存,这个过程是周期性的,垃圾回收程序每隔一定时间(或者说在代码执行过程中某个预定的收集时间)就会自动运行

JavaScript 通过自动内存管理实现内存分配和闲置资源回收

标记清理

JavaScript 最常用的垃圾回收策略是标记清理。当变量进入上下文,这个变量就会被加上存在于上下文中的标记,当变量离开上下文时,就会被加上离开上下文的标记

垃圾回收程序运行的时候,会标记内存中存储的所有变量。然后,它会将所有在上下文中的变量,以及被在上下文中的变量引用的变量的标记去掉。在此之后再被加上标记就是待删除的了。随后垃圾回收程序做一次内存清理,销毁带标记的所有值并回收它们所占用的内存空间

引用计数

引用计数的思路是对每个值都记录它被引用的次数。当声明了一个变量并将一个引用类型赋值给该变量时,则这个值的引用次数就是 1。如果同一个值又被赋给另一个变量,则该值的引用次数加 1。相反,如果包含对这个值引用的变量又取得了另外一个值,则这个值的引用次数减 1。当这个值的引用次数变成 0 时,则说明没有办法再访问这个值了,因而就可以将其占用的内存空间回收回来。这样,当垃圾回收程序下次再运行时,它就会释放那些引用次数为 0 的值所占用的内存

引用计数的问题在于循环引用,两个对象相互引用,导致它们的引用次数都不为 0,所以垃圾回收程序不会回收它们占用的内存,比如:

function problem() {
+	var objectA = new Object()
+	var objectB = new Object()
+	objectA.someOtherObject = objectB
+	objectB.anotherObject = objectA
+}
+

内存管理

如果数据不再必要,最好通过将其值设置为 null 来释放其引用,这个做法叫解除引用。这个建议最适合全局变量和全局对象的属性,局部变量在超出作用域后会被自动解除引用,比如

function createPerson(name) {
+	var localPerson = new Object()
+	localPerson.name = name
+	return localPerson
+}
+var globalPerson = createPerson('Nicholas')
+// 手动解除引用
+globalPerson = null
+
  1. 通过 const 和 let 声明提升性能

    const 和 let 都以块(而非函数)为作用域,所以相比于使用 var 声明,使用这个两个关键字可能会更早地让垃圾回收程序介入,尽早回收应该回收的内存

  2. 隐藏类和删除操作

    V8 会将创建的对象与隐藏类关联起来,以跟踪他们的属性特征,能够共享相同隐藏类的对象性能会更好:

    • 避免 JavaScript 的“先创建再补充(ready-fire-aim)”式的动态属性赋值,并在构造函数中一次性声明所有属性
      function Article() {
      +	this.title = 'Inauguration Ceremony Features Kazoo Band'
      +}
      +const a1 = new Article()
      +const a2 = new Article()
      +// 导致两个 Article 实例的对应两个不同的隐藏类
      +a2.author = 'Jake'
      +
      function Article(opt_author) {
      +	this.title = 'Inauguration Ceremony Features Kazoo Band'
      +	this.author = opt_author
      +}
      +const a1 = new Article()
      +const a2 = new Article('Jake')
      +
    • 使用 delete 关键字会导致生成新的隐藏类,最佳实践是把不想要的属性设置为 null
  3. 内存泄露

  • 意外的全局变量
  • 闭包
  1. 静态分配与对象池

    为了提升 JavaScript 性能,一个关键的问题就是如何减少浏览器执行垃圾回收的次数

    浏览器决定何时运行垃圾回收程序的一个标准就是对象更替的速度,越快越频繁

    为了提升性能,V8 引入了对象池,它会对一些常见的对象结构进行缓存,当需要创建这些对象时,就会从对象池中取出,而不是重新创建

Last Updated:
+ + + diff --git a/frontend/js/red-book/05.html b/frontend/js/red-book/05.html new file mode 100644 index 0000000..f686571 --- /dev/null +++ b/frontend/js/red-book/05.html @@ -0,0 +1,108 @@ + + + + + + + + + 红宝书 | 瓢儿白施肥记 + + + + +

基本引用类型

引用值(或者对象)是某个特定引用类型的实例

Date

Date 对象基于 Unix Time Stamp,即自 1970 年 1 月 1 日(UTC)起经过的毫秒数

new Date(); // 实例化时刻的日期和时间
+new Date(value); // value 表示 1970 年 1 月 1 日(UTC)起经过的毫秒数
+new Date(dateString); // dateString 表示日期字符串,该字符串应该能被 Date.parse() 正确方法识别
+new Date(year, monthIndex [, day [, hours [, minutes [, seconds [, milliseconds]]]]]);
+
  • Date.now(),返回自 1970-1-1 00:00:00 UTC(世界标准时间)至今所经过的毫秒数
  • Date.parse(),接收一个表示日期的字符串参数,然后尝试根据这个字符串返回相应日期的毫秒数
  • Date.UTC(),接收年份、基于 0 的月份(一月是 0,二月是 1,以此类推)、月中的哪一天(1 到 31)、小时数(0 到 23)、分钟、秒以及毫秒数,返回表示这个日期的毫秒数

继承的方法

  • toLocaleString(),返回与浏览器运行的本地环境相适应的日期和时间
  • toString(),返回带有时区信息的日期和时间
  • valueOf(),返回日期的毫秒表示,故操作符(如 <)可以直接使用它返回的值

日期格式化方法

Date 类型有几个专门用于格式化日期的方法,它们都会返回字符串

  • toDateString(),以特定于实现的格式显示星期几、月、日和年
  • toTimeString(),以特定于实现的格式显示时、分、秒和时区
  • toLocaleDateString(),以特定于实现和地区的格式显示星期几、月、日和年
  • toLocaleTimeString(),以特定于实现和地区的格式显示时、分、秒
  • toUTCString(),以特定于实现的格式完整的 UTC 日期

这些方法的输出与 toLocaleString() 和 toString() 一样,会因浏览器而异,因此不能用于在用户界面上一致的显示日期

日期/时间组件方法open in new window

RegExp

let expression = /pattern/flags;
+

实例属性open in new window

实例方法open in new window

静态属性open in new window

原始值包装类型

每当用到某个原始值的方法或属性时,后台都会创建一个相应原始包装类型的对象,从而暴露出操作原始值的各种方法

引用类型与原始包装类型的主要区别在于对象的生命周期。在通过 new 实例化引用类型后,得到的实例会在离开作用域时被销毁,而自动创建的原始值包装对象则只存在于访问它的那行代码执行期间。这意味着不能在运行时给原始值添加属性和方法,比如:

let name = 'Nicholas'
+name.age = 27
+console.log(name.age) // undefined
+

可以显示地使用 Boolean、Number 和 String 创建原始值包装对象,实例上调用 typeof 会返回 object

原始值和包装对象之间的区别:

  • typeof 操作符对包装对象返回 'object'
  • 包装对象在使用 instanceof 操作符时返回 true,而原始值则不是

Boolean

要创建一个 Boolean 对象,就使用 Boolean 构造函数并传入 true 或 false

let booleanObject = new Boolean(true)
+
  • valueOf(),返回原始值 true 或 false
  • toString(),返回字符串 'true' 或 'false'

Number

要创建一个 Number 对象,就使用 Number 构造函数并传入数值

let numberObject = new Number(10)
+

继承的方法:

  • valueOf(),返回原始值
  • toString() / toLocaleString(),返回字符串形式的数值
let num = 10
+console.log(num.toString()) // '10'
+console.log(num.toString(2)) // '1010'
+console.log(num.toString(8)) // '12'
+console.log(num.toString(10)) // '10'
+console.log(num.toString(16)) // 'a'
+

格式化数值:

  • toFixed(),按指定的小数位返回数值的字符串表示
  • toExponential(),按指定的小数位返回数值的字符串表示,以科学计数法表示
  • toPrecision(),按指定的有效数位返回数值的字符串表示

isInteger() 方法与安全整数:

  • Number.isInteger(),判断数值是否为整数
    console.log(Number.isInteger(1)) // true
    +console.log(Number.isInteger(1.0)) // true
    +console.log(Number.isInteger(1.1)) // false
    +
  • Number.isSafeInteger(),判断数值是否为安全整数
    console.log(Number.isSafeInteger(Number.MAX_SAFE_INTEGER)) // true
    +console.log(Number.isSafeInteger(Number.MAX_SAFE_INTEGER + 1)) // false
    +

String

要创建一个 String 对象,就使用 String 构造函数并传入字符串

let stringObject = new String('hello world')
+

继承的方法:

  • valueOf() / toLocaleString() / toString(),返回原始字符串值

1. JavaScript 字符串

JavaScript 字符串由 16 位码元(code unit)组成。对于大多数字符,每个码元对应一个字符

  • length 属性表示字符串中码元的个数
  • charAt() 方法返回指定位置的码元,如果没有指定位置,则默认为 0
  • charCodeAt() 方法返回指定位置的码元的数值,如果没有指定位置,则默认为 0
  • fromCharCode() 方法接收一或多个码元值,然后将它们转换成字符串

对于 U+0000~U+FFFF 范围内的字符,上面的属性和方法返回的结果都跟预期是一样的

16 位只能唯一表示 2^16 个字符,这对于大多数语言字符集是足够了,在 Unicode 中称为基本多语言平面(BMP)

为了表示更多的字符,Unicode 采用了一个策略,即每个字符使用另外 16 位去选择一个增补平面。这种每个字符使用两个 16 位码元的策略称为代理对(surrogate pair)

在涉及增补平面的字符时,前面讨论的字符串方法和属性就会出问题

// "smiling face with smiling eyes" 表情符号的码点是 U+1F60A
+// 0x1F60A === 128522
+let message = 'ab😊de'
+console.log(message.length) // 6
+console.log(message.charAt(1)) // 'b'
+console.log(message.charAt(2)) // '�'
+console.log(message.charAt(3)) // '�'
+console.log(message.charAt(4)) // 'd'
+
+console.log(message.charCodeAt(1)) // 98
+console.log(message.charCodeAt(2)) // 55357
+console.log(message.charCodeAt(3)) // 56842
+console.log(message.charCodeAt(4)) // 100
+
+console.log(String.fromCharCode(0x1f60a)) // '�'
+console.log(String.fromCodePoint(0x1f60a)) // '😊'
+console.log(String.fromCharCode(97, 98, 55357, 56842, 100, 101)) // 'ab😊de'
+

码点是 Unicode 中一个字符的完整标识

迭代字符串可以智能地识别代理对的码点:

console.log([...'ab😊de']) // ['a', 'b', '😊', 'd', 'e']
+
  • codePointAt() 方法返回指定位置的码点,如果传入的码元索引并非代理对的开头,就会返回错误的码点
  • fromCodePoint() 方法接收一个或多个码点,然后将它们转换成字符串

2. normalize() 方法

某些 Unicode 字符可以有多种编码方式,多种形式间使用 === 的结果为 false。为解决这个问题,ES6 提供了 normalize() 方法,用于将字符的不同表示方法统一为同样的形式,使用时需要传入表示哪种形式的字符串:"NFC"、"NFD"、"NFKC"、"NFKD"

3. 字符串操作方法

  • concat(),拼接字符串
  • 从字符串中提取子字符串,省略第二个参数即提取到末尾
    • slice(),接收两个参数,返回从第一个参数指定位置开始到第二个参数指定位置结束的子字符串。将所有负值参数都当成字符串长度加上负值参数
    • substr(),接收两个参数,返回从第一个参数指定位置开始到第二个参数指定的长度结束的子字符串。第二个负值参数会被转换为 0
    • substring(),与 slice() 类似,但所有负值参数值都会转换为 0

4. 字符串位置方法

  • 从字符串中搜索传入的字符串,并返回位置(如果没找到则返回 -1)。可以接收第二个参数,表示开始搜素的位置
    • indexOf(),从开头开始查找子字符串
    • lastIndexOf(),从末尾开始查找子字符串

5. 字符串包含方法

  • 判断字符串中是否包含另一个字符串,从字符串中搜索传入的字符串,并返回一个是否包含的布尔值
    • startsWith()
      • 判断字符串是否以另一个字符串开头,检查开始于索引 0 的匹配项
      • 接收第二个参数,表示开始搜素的位置
    • endsWith()
      • 判断字符串是否以另一个字符串结尾,检查开始于索引 length - searchStr.length 的匹配项
      • 接收第二个参数,表示应该当作字符串末尾的位置(正则 $ 匹配的位置)
    • includes()
      • 判断字符串是否包含另一个字符串,检查整个字符串
      • 接收第二个参数,表示开始搜素的位置

6. trim() 方法

去除字符串两端的空格,返回新字符串

trimLeft() / trimRight(),去除字符串左边 / 右边的空格,返回新字符串

7. repeat() 方法

接收一个整数参数,表示将原字符串重复多少次,返回新字符串

8. padStart() / padEnd() 方法

两个方法会复制字符串,如果小于指定长度,则在相应一边填充字符,直至满足长度条件

第一个参数是长度,第二个参数是可选的填充字符,默认为空格

9. 字符串迭代与解构

字符串的原型上暴露了一个 @@iterator 方法,表示可迭代字符串中的每个字符

手动使用迭代器:

let message = 'ab😊de'
+let iterator = message[Symbol.iterator]()
+console.log(iterator.next()) // { value: 'a', done: false }
+console.log(iterator.next()) // { value: 'b', done: false }
+console.log(iterator.next()) // { value: '😊', done: false }
+console.log(iterator.next()) // { value: 'd', done: false }
+console.log(iterator.next()) // { value: 'e', done: false }
+console.log(iterator.next()) // { value: undefined, done: true }
+

在 for-of 循环中可以通过这个迭代器按序访问每个字符:

let message = 'ab😊de'
+for (let c of message) {
+	console.log(c)
+}
+// 'a'
+// 'b'
+// '😊'
+// 'd'
+// 'e'
+

字符串也可以通过解构赋值的方式进行迭代:

let message = 'ab😊de'
+let [a, b, c, d, e] = message
+console.log(a, b, c, d, e) // 'a' 'b' '😊' 'd' 'e'
+

10. 字符串大小写转换

如果不知道代码涉及什么语言,则最好使用地区特定的转换方法

  • toLowerCase(),将字符串转换为小写形式
  • toUpperCase(),将字符串转换为大写形式
  • toLocaleLowerCase(),将字符串转换为小写形式,基于特定地区实现
  • toLocaleUpperCase(),将字符串转换为大写形式,基于特定地区实现

11. 字符串模式匹配方法

String 类型专门为在字符串中实现模式匹配设计了几个方法

  • match(),本质跟 RegExp 的 exec() 方法相同
  • search(),返回模式第一个匹配的位置索引,如果没有找到则返回 -1
  • replace()

12. localeCompare() 方法

因为返回的具体值可能因具体实现而异,所以最好像这样使用:

function determineOrder(value) {
+	let result = str1.localeCompare(value)
+	if (result < 0) {
+		console.log('str1 comes before ' + value)
+	} else if (result > 0) {
+		console.log('str1 comes after ' + value)
+	} else {
+		console.log('str1 is equal to ' + value)
+	}
+}
+

localeCompare() 的独特之处在于,实现所在的地区(国家和语言)决定了这个方法如何比较字符串

单例内置对象

ECMA-262 对内置对象的定义是“任何由 ECMAScript 实现提供、与宿主环境无关,并在 ECMAScript 程序开始执行时就存在的对象”,如前面接触的 Object、Array、String、Function、Date、RegExp、Error、Boolean、Number,包括接下来介绍的两个单例内置对象 Math、Global

Global

ECMA-262 规定 Global 对象为一种兜底对象,它所针对的是不属于任何对象的属性和方法。在全局作用域中定义的变量和函数都会变成 Global 对象的属性。isNaN()、isFinite()、parseInt() 和 parseFloat() 都是 Global 对象的方法,除了这些 ECMAScript 还为 Global 对象定义了其他方法

1. URL 编码方法

有效的 URI 不能包含某些字符,比如空格。使用 URI 编码方法可以让浏览器理解它们,同时又以特殊的 UTF-8 编码替换所有无效字符,比如空格会被替换成 %20

encodeURI()、encodeURIComponent()、decodeURI()、decodeURIComponent(),用于对 统一资源标识符(URI)进行编码和解码

  • encodeURI(),主要用于整个 URI,不会对本身属于 URI 的特殊字符进行编码,比如冒号、正斜杠、问号和井号
  • encodeURIComponent(),主要用于对 URI 中的某一段进行编码,会对它发现的任何非标准字符进行编码

2. eval() 方法

这个方法就是一个完整的 ECMAScript 解释器,它接收一个参数,即要执行的 ECMAScript(或 JavaScript)字符串

eval('console.log("hi")') // 'hi'
+

3. Global 对象的属性

  • undefined、NaN、Infinity 等特殊值
  • 原生引用类型构造函数,比如 Object、Array、String 等

4. window 对象

虽然 ECMA-262 没有规定直接访问 Global 对象的方式,但浏览器将 window 对象实现为 Global 对象的代理

另一种获取 Global 对象的方式:

var global = (function () {
+	return this
+})()
+

Math

ECMAScript 提供了 Math 对象作为保存数学公式、信息和计算的地方。Math 对象提供了一些辅助计算的属性和方法

1. Math 对象属性

  • Math.E,自然对数的底数,即常量 e 的值
  • Math.LN10,10 为底的自然对数
  • Math.LN2,2 为底的自然对数
  • Math.LOG2E,以 2 为底 e 的对数
  • Math.LOG10E,以 10 为底 e 的对数
  • Math.PI,π 的值
  • Math.SQRT1_2,1/2 的平方根
  • Math.SQRT2,2 的平方根

2. min() 和 max() 方法

接收任意多个参数

3. 舍入方法

  • Math.ceil(),向上取整
  • Math.floor(),向下取整
  • Math.floor(),四舍五入
  • Math.fround(),返回最接近参数的 32 位单精度浮点值表示

4. random 方法

返回大于等于 0 小于 1 的一个随机数

可以基于如下公式使用 Math.random() 从一组整数中随机选择一个整数:

Math.floor(Math.random() * 可能值的总数 + 第一个可能的值)
+

5. 其他方法

Math 的方法open in new window

Last Updated:
+ + + diff --git a/frontend/js/red-book/06.html b/frontend/js/red-book/06.html new file mode 100644 index 0000000..f554cde --- /dev/null +++ b/frontend/js/red-book/06.html @@ -0,0 +1,346 @@ + + + + + + + + + 红宝书 | 瓢儿白施肥记 + + + + +

集合引用类型

Object

显示的创建 Object 实例有两种方式:

  1. new 操作符后跟 Object 构造函数
let person = new Object()
+person.name = 'Nicholas'
+person.age = 29
+
  1. 使用对象字面量
let person = {
+	name: 'Nicholas',
+	age: 29,
+	5: true, // 数值属性会自动转换成字符串
+}
+

在这个例子中,左大括号({)表示对象字面量开始,因为它出现在一个**表达式上下文(expression context)**中。在 ECMAScript 中,表达式上下文是指期待返回值的上下文。

同样是左大括号({),如果出现在**语句上下文(statement context)**中,比如 if 语句的条件后面,则表示一个语句块的开始

注意:在使用字面量表示法定义对象时,并不会实际调用 Object 构造函数

存取属性的两种方式:

  1. 点语法

  2. 中括号

从功能上讲,这两种存取属性的方式没有区别

使用中括号的主要优势:

  • 可以通过变量来访问属性
  • 属性名(可以包含非字母数字字符)中包含会导致语法错误的字符时,必须使用中括号语法

Array

创建数组

有两种基本的方式可以创建数组:

  1. 使用 Array 构造函数
  • 传入一个数值,则 length 属性会被自动创建并设置为这个值
    • let colors = new Array(20)
  • 传入要保存的元素
    • let colors = new Array('red', 'blue', 'green')

创建数组时给构造函数传入一个值。如果是数值,则会创建一个长度为指定数值的数组;如果是其他类型,则会创建包含那个值的数组

在使用 Array 构造函数时,也可以省略 new 操作符,结果是一样的

  1. 使用数组字面量表示法

与对象一样,在使用数组字面量表示法创建数组时,不会调用 Array 构造函数

Array 构造函数还有两个 ES6 新增的用于创建数组的静态方法:

  1. from(),用于将类数组结构转换为数组实例

  2. of(),用于将一组参数转换为数组实例

数组空位

使用数组字面量初始化数组时,可以使用一串逗号来创建空位(hole)

const options = [, , , , ,]
+console.log(options.length) // 5
+console.log(options, options[0]) // [empty × 5] undefined
+

注:ES6 新增的方法普遍将这些空位当成存在的元素;而 ES6 之前的方法则会忽略这个空位,但具体的行为也会因方法而异。因此实践中要避免使用数组空位,可以显式地使用 undefined 值代替

数组索引

let colors = ['red', 'blue', 'green']
+console.log(colors[0]) // red
+colors[2] = 'black'
+colors[3] = 'brown'
+

在中括号提供索引,表示要访问的值。如果把一个值设置给超过数组最大索引的索引,则数组长度会自动扩展到该索引值 + 1

数组中元素的数量保存在 length(>= 0) 中,它不是只读的。可以通过修改 length 属性从数组末尾删除或者添加元素

let colors = ['red', 'blue', 'green']
+colors.length = 2
+console.log(colors[2]) // undefined
+
+colors.length = 4 // 新添加的值都将以 undefined 填充
+console.log(colors[3]) // undefined
+

使用 length 属性可以方便地向数组末尾添加元素

let colors = ['red', 'blue', 'green']
+colors[colors.length] = 'black'
+colors[colors.length] = 'brown'
+

检测数组

如果判断一个对象是不是数组?

在只有一个网页(因为只有一个全局作用域)的情况下,使用 instanceof 操作符足矣:

if (value instanceof Array) {
+	// do something
+}
+

如果网页里有多个框架,则可能涉及两个不同的全局上下文,因此会有两个不同版本的 Array 构造函数。如果把数组从一个框架传给另一个框架,则这个数组的构造函数将有别于在另一个框架内本地创建的数组

为解决这个问题,ECMAScript 提供了 Array.isArray() 方法

if (Array.isArray(value)) {
+	// do something
+}
+

迭代器方法

ES6 中,Array 的原型上暴露了 3 个用于检索数组内容的方法:keys()、values() 和 entries()

  • keys(),返回数组索引的迭代器
  • values(),返回数组元素的迭代器
  • entries(),返回数组索引/元素对的迭代器
const colors = ['red', 'blue', 'green']
+const keys = colors.keys()
+const values = colors.values()
+const entries = colors.entries()
+console.log(Array.isArray(keys)) // false
+
+const keysArr = Array.from(keys)
+const valuesArr = Array.from(values)
+const entriesArr = Array.from(entries)
+
+console.log(keysArr, typeof keysArr[0] === 'number') // [0, 1, 2] true
+console.log(valuesArr) // ['red', 'blue', 'green']
+console.log(entriesArr) // [[0, 'red'], [1, 'blue'], [2, 'green']]
+

复制和填充方法

ES6 新增了两个方法:批量复制方法 copyWithin() 和填充数组方法 fill()

  • fill(),向一个已有的数组中插入全部或部分相同的值

    • 开始索引用于指定开始填充的位置,默认为 0(负值索引从数组末尾开始计算,等价于数组长度加上负索引)
    • fill() 静默忽略超出数组边界、零长度及方向相反的索引范围
  • copyWithin() 按照指定范围浅复制数组中的部分内容,然后将它们插入到指定索引开始的位置(开始索引和结束索引同 fill())

转换方法

  • valueOf(),返回数组本身
  • toString(),返回由数组中每个值的等效字符串拼接而成的一个以逗号分隔的字符串(即数组的每个值都会调用 toString() 方法)
  • toLocaleString(),返回由数组中每个值的等效字符串拼接而成的一个以逗号分隔的字符串(即数组的每个值都会调用 toLocaleString() 方法)
  • join(),接收一个参数,即字符串分隔符

栈方法

  • push(),接收任意数量的参数,把它们逐个添加到数组末尾,并返回修改后数组的长度
  • pop(),从数组末尾移除最后一项,减少数组的 length 值,然后返回移除的项

队列方法

  • shift(),移除数组中的第一个项并返回该项,同时将数组长度减 1
  • unshift(),在数组前端添加任意个项并返回新数组的长度

排序方法

  • reverse(),反转数组项的顺序
  • sort()
    • 默认按升序排列数组项,即最小的值位于最前面,最大的值排在最后面
    • sort() 会在每一项上调用 String(),然后比较字符串来决定顺序(即使数组的元素都是数值)
    const values = [0, 1, 5, 10, 15]
    +values.sort()
    +console.log(values) // [0, 1, 10, 15, 5]
    +
    • sort() 方法可以接收一个比较函数,用于判断哪个值应该排在前面。比较函数接收两个参数,如果第一个参数应该位于第二个之前则返回一个负数,如果两个参数相等则返回 0,如果第一个参数应该位于第二个之后则返回一个正数

操作方法

  • concat(),基于当前数组中的所有项创建一个新数组

    • 首先创建一个当前数组的副本,然后将接收到的参数添加到这个副本的末尾,最后返回新构建的数组
    • 如果参数不是数组,则直接把它们添加到结果数组末尾;如果传入一个或多个数组,则会把这些数组的每一项都添加到结果数组中(浅)
    const colors = ['red', 'green', 'blue']
    +const colors2 = colors.concat('yellow', ['black', 'brown'])
    +console.log(colors) // ['red', 'green', 'blue']
    +console.log(colors2) // ['red', 'green', 'blue', 'yellow', 'black', 'brown']
    +
    • 打平数组参数的行为可以重写,方法是在参数数组上指定一个特殊符号 Symbol.isConcatSpreadable。这个符号能够阻止 concat() 方法打平数组参数;相反,这个值设置为 true 可以强制打平类数组对象
    const colors = ['red', 'green', 'blue']
    +const newColors = ['black', 'brown']
    +let moreNewColors = {
    +	[Symbol.isConcatSpreadable]: true,
    +	length: 2,
    +	0: 'pink',
    +	1: 'cyan',
    +}
    +newColors[Symbol.isConcatSpreadable] = false
    +
    +// 强制不打平数组
    +const colors2 = colors.concat('yellow', newColors)
    +
    +// 强制打平类数组对象
    +const colors3 = colors.concat(moreNewColors)
    +
    +console.log(colors) // ['red', 'green', 'blue']
    +console.log(colors2) // ['red', 'green', 'blue', 'yellow', Array(2)]
    +console.log(colors3) // ['red', 'green', 'blue', 'pink', 'cyan']
    +
  • slice(),用于创建一个包含原数组中部分项的新数组

    • 接收一或两个参数,即要返回项的起始和结束位置(不包含结束索引对应的元素)
    • 如果 slice() 的参数有负值,那么就用数组长度加上该值来确定相应的位置
    • slice() 方法不会影响原始数组
  • splice(),主要目的是在数组中间插入元素,但有 3 种不同的方式使用这个方法

    • 删除。传两个参数,要删除的第一项的位置和要删除的项数,比如 splice(0, 2)
    • 插入。传三个参数,起始位置、0(要删除的项数)和要插入的项,比如 splice(2, 0, 'red', 'green')
    • 替换。传三个参数,起始位置、要删除的项数和要插入的任意数量的项,比如 splice(2, 1, 'red', 'green')

搜素和位置方法

ECMAScript 提供两类搜索数组的方法:按严格相等搜索和按断言函数搜索

1. 严格相等(===)

以下三个方法接收两个参数:要查找的项和(可选的)表示查找起点位置的索引

  • indexOf()
  • lastIndexOf()
  • includes()

2. 断言函数

这两个方法也都接受第二个可选参数,用于指定断言函数内部 this 的值

  • find(),返回第一个匹配的元素
  • findIndex(),返回第一个匹配元素的索引

迭代方法

ECMAScript 为数组定义了 5 个迭代方法,每个方法都接收两个参数:要在每一项上运行的函数和(可选的)运行该函数的作用域对象——影响 this 的值

  • every()
  • filter()
  • some()
  • forEach()
  • map()

归并方法

ECMAScript 为数组提供了两个归并方法:reduce() 和 reduceRight()

定型数组

定型数组(typed array)是 ECMAScript 新增的结构,目的是提升向原生库传输数据的效率,它所指的其实是一种特殊的包含数值类型的数组

ArrayBuffer

Float32Array 实际上是一种“视图”,可以允许 JavaScript 运行时访问一块名为 ArrayBuffer 的预分配内存。ArrayBuffer 是所有定型数组及视图引用的基本单位

ArrayBuffer() 是一个普通的 JavaScript 构造函数,可用于在内存中分配特定数量的字节空间:

const buffer = new ArrayBuffer(16) // 在内存中分配 16 字节
+console.log(buffer.byteLength) // 16
+

ArrayBuffer 一经创建就不能再调整大小。不过,可以使用 slice() 复制其全部或者部分到一个新的 ArrayBuffer 实例中:

const buf1 = new ArrayBuffer(16)
+const buf2 = buf1.slice(4, 12)
+console.log(buf2.byteLength) // 8
+

不能仅通过对 ArrayBuffer 的引用就读取或者写入内容,而是需要通过视图来实现。视图有不同的类型,但引用的都是 ArrayBuffer 中存储的二进制数据

ArrayBuffer 在分配失败时会抛出错误

ArrayBuffer 分配的内存不能超过 Number.MAX_SAFE_INTEGER(2^53 - 1)个字节

声明 ArrayBuffer 会将所有二进制初始化为 0

通过声明 ArrayBuffer 分配的堆内存可以被当成垃圾回收,不用手动释放

DataView

DataView 是允许读写 ArrayBuffer 的一种视图,专为文件 I/O 和网络 I/O 设计,其 API 支持对缓冲数据的高度控制,但相比于其他类型的视图性能差一些。DataView 对缓冲内容没有任何预设,也不能迭代

必须在对已有的 ArrayBuffer 读取或者写入时才能创建 DataView 实例:

const buf = new ArrayBuffer(16)
+
+// DataView 默认使用整个 ArrayBuffer
+const fullDataView = new DataView(buf)
+console.log(fullDataView.byteOffset) // 0
+console.log(fullDataView.byteLength) // 16
+console.log(fullDataView.buffer === buf) // true
+
+// 构造函数接收一个可选的字节偏移量和一个可选的字节长度
+//   byteOffset=0 表示视图从缓冲起点开始
+//   byteLength=8 表示限制视图为前 8 个字节
+const firstHalfDataView = new DataView(buf, 0, 8)
+console.log(firstHalfDataView.byteOffset) // 0
+console.log(firstHalfDataView.byteLength) // 8
+console.log(firstHalfDataView.buffer === buf) // true
+
+// 如果不指定,则 DataView 会使用剩余的缓存
+//   byteOffset=8 表示视图从缓冲的第 8 个字节开始
+//   byteLength 未指定,默认为剩余缓冲
+const secondHalfDataView = new DataView(buf, 8)
+console.log(secondHalfDataView.byteOffset) // 8
+console.log(secondHalfDataView.byteLength) // 8
+console.log(secondHalfDataView.buffer === buf) // true
+

要通过 DataView 读取缓冲,还需要几个组件:

  • 要读或写的字节偏移量
  • 使用 ElementType 实现 JavaScript 的 Number 类型到缓冲内二进制格式的转换
  • 内存中值的字节序,默认为大端字节序(big-endian)

1. ElementType

DateView 对存储在缓冲内的数据类型没有预设。它暴露的 API 强制开发者在读、写时指定一个 ElementType,然后 DataView 会忠实地为读、写完成相应的转换

ECMAScript6 支持 8 种不同的 ElementType(见下表):

ElementType字节说明等价的 C 类型值的范围
Int818 位有符号整数signed char-128 ~ 127
Uint818 位无符号整数unsigned char0 ~ 255
Int16216 位有符号整数short-32768 ~ 32767
Uint16216 位无符号整数unsigned short0 ~ 65535
Int32432 位有符号整数int-2147483648 ~ 2147483647
Uint32432 位无符号整数unsigned int0 ~ 4294967295
Float32432 位浮点数float1.2e-38 ~ 3.4e38
Float64864 位浮点数double5.0e-324 ~ 1.8e308

DataView 为上表中的每种类型都暴露了 get 和 set 方法,这些方法使用 byteOffset 定位要读取或者写入值的位置。类型时可以互换使用的,如下例所示:

// 在内存中分配两个字节并声明一个 DataView
+const buf = new ArrayBuffer(2)
+const view = new DataView(buf)
+
+// 说明整个缓冲确实所有二进制位都是 0
+// 检查第一个和第二个字符
+console.log(view.getInt8(0)) // 0
+console.log(view.getInt8(1)) // 0
+// 检查整个缓冲
+console.log(view.getInt16(0)) // 0
+
+// 将整个缓冲都设置为 1
+// 255 的二进制表示为 11111111 (2^8 - 1)
+view.setUint8(0, 255)
+
+// DataView 会自动将数据转换为特定的 ElementType
+// 255 的十六进制表示是 0xFF
+view.setUint8(1, 0xff)
+
+// 现在缓冲里都是 1 了
+// 如果把它当作二补数的有符号整数,则应该是 -1
+console.log(view.getInt16(0)) // -1
+

2. 字节序

“字节序”指的是计算系统维护的一种字节顺序的约定,DataView 只支持两种约定:大端字节序和小端字节序

  • 大端字节序(网络字节序):最高有效位保存在第一个字节,而最低有效位保存在最后一个字节
  • 小端字节序:最低有效位保存在第一个字节,而最高有效位保存在最后一个字节

JavaScript 运行时所在系统的原生字节序决定了如何读取或写入数据,但 DataView 并不遵守这个约定。对一段内存而言,DataView 是一个中立接口,它遵守指定的字节序。DataView 的所有 API 方法都以大端字节序作为默认值,但接收一个可选的布尔值参数,设置为 true 即可启用小端字节序

// 在内存中分配两个字节并声明一个 DataView
+const buf = new ArrayBuffer(2)
+const view = new DataView(buf)
+
+// 填充缓冲,让第一位和最后一位都是 1
+view.setUint8(0, 0x80) // 设置最左边的位等于 1(1000 0000)
+view.setUint8(1, 0x01) // 设置最右边的位等于 1 (0000 0001)
+// 则缓冲的内容为 1000 0000 0000 0001
+
+// 按大端字节序读取 Unit16
+// 0x80 是高字节,0x01 是低字节
+// 0x8001 = 2^15 + 2^0 = 32769
+console.log(view.getUint16(0)) // 32769
+
+// 按小端字节序读取 Unit16
+// 0x80 是低字节,0x01 是高字节
+// 0x0180 = 2^7 + 2^8 = 384
+console.log(view.getUint16(0, true)) // 384
+
+// 按大端字节序写入 Uint16
+view.setUint16(0, 0x0004)
+// 缓冲内容:
+// 0000 0000 0000 0100
+console.log(view.getUint8(0)) // 0
+console.log(view.getUint8(1)) // 4
+
+// 按小端字节序写入 Uint16
+view.setUint16(0, 0x0002, true)
+// 缓冲内容:
+// 0000 0010 0000 0000
+console.log(view.getUint8(0)) // 2
+console.log(view.getUint8(1)) // 0
+

3. 边界情形

DataView 完成读、写操作的前提是必须有充足的缓冲区,否则会抛出 RangeError 异常

const buf = new ArrayBuffer(6)
+const view = new DataView(buf)
+
+// 尝试读取部分超出缓冲范围的值
+view.getInt32(4) // RangeError
+
+// 尝试读取超出缓冲范围的值
+view.getInt32(8) // RangeError
+view.getInt32(-1) // RangeError
+
+// 尝试写入超出缓冲范围的值
+view.setInt32(4, 123) // RangeError
+

DataView 在写入缓冲里会尽最大努力把一个值转换为适当的类型,后备为 0。如果无法转换,则抛出 TypeError 异常

const buf = new ArrayBuffer(1)
+const view = new DataView(buf)
+
+view.setInt8(0, 1.5)
+console.log(view.getInt8(0)) // 1
+
+view.setInt8(0, [4])
+console.log(view.getInt8(0)) // 4
+
+view.setInt8(0, 'f')
+console.log(view.getInt8(0)) // 0
+
+view.setInt8(0, Symbol()) // TypeError
+

定型数组

定型数组是另一种形式的 ArrayBuffer 视图。虽然概念上与 DataView 接近,但定型数组的区别在于,它特定于一种 ElementType 且遵循系统原生的字节序。相应地,定型数组提供了适用面更广的 API 和更高的性能

设计定型数组的目的是提高于 WebGL 等原生库交换二进制数据的效率

创建定型数组的方式包括:读取已有的缓冲、使用自由缓冲、填充可迭代结构,以及填充基于任意类型的定型数组。另外通过 <ElementType>.from()<ElementType>.of() 也可以创建定型数组

// 创建一个 12 字节的缓冲
+const buf = new ArrayBuffer(12)
+// 创建一个引用该缓冲的 Int32Array
+const ints = new Int32Array(buf)
+// 这个定型数组知道自己每个元素需要 4 字节
+// 因此长度为 3
+console.log(ints.length) // 3
+
+// 创建一个长度为 6 的 Int32Array
+const ints2 = new Int32Array(6)
+// 每个数值使用 4 个字节,因此 ArrayBuffer 需要 24 字节
+console.log(ints2.length) // 6
+// 类似 DataView,定型数组也有一个指向关联缓冲的引用
+console.log(ints2.buffer.byteLength) // 24
+
+// 创建一个包含 [2, 4, 6, 8] 的 Int32Array
+const ints3 = new Int32Array([2, 4, 6, 8])
+console.log(ints3.length) // 4
+console.log(ints3.buffer.byteLength) // 16
+console.log(ints3[2]) // 6
+
+// 通过复制 ints3 创建一个 Int16Array
+const ints4 = new Int16Array(ints3)
+// 这个新类型数组会分配自己的缓冲
+// 对应索引的值会相应地转换为新格式
+console.log(ints4.length) // 4
+console.log(ints4.buffer.byteLength) // 8
+console.log(ints4[2]) // 6
+
+// 基于普通数组来创建一个 Int16Array
+const ints5 = Int16Array.from([3, 5, 7, 9])
+console.log(ints5.length) // 4
+console.log(ints5.buffer.byteLength) // 8
+console.log(ints5[2]) // 7
+
+// 基于传入的参数创建一个 Float32Array
+const floats = Float32Array.of(3.14, 2.718, 1.618)
+console.log(floats.length) // 3
+console.log(floats.buffer.byteLength) // 12
+console.log(floats[2]) // 1.6180000305175781
+

定型数组的构造函数和实例都有一个 BYTES_PER_ELEMENT 属性,返回该类型数组中每个元素的大小:

console.log(Int16Array.BYTES_PER_ELEMENT) // 2
+console.log(Int32Array.BYTES_PER_ELEMENT) // 4
+
+const ints = new Int32Array(1),
+	floats = new Float64Array(1)
+
+console.log(ints.BYTES_PER_ELEMENT) // 4
+console.log(floats.BYTES_PER_ELEMENT) // 8
+

如果定型数组没有用任何值初始化,则其关联的缓冲会以 0 填充:

const ints = new Int32Array(4)
+console.log(ints[0]) // 0
+console.log(ints[1]) // 0
+console.log(ints[2]) // 0
+console.log(ints[3]) // 0
+

1. 定型数组的行为

定型数组的行为与普通数组类似,但也有一些不同之处

不能在定型数组中使用的方法:

  • 合并、复制和修改定型数组,应当使用 set() 和 subarray() 方法
    • concat()
    • pop()
    • push()
    • shift()
    • splice()
    • unshift()

2. 上溢和下溢

// 长度为 2 有符号整数数组
+// 每个索引保存一个二补数形式的有符号整数
+// 范围是 -128 到 127
+const ints = new Int8Array(2)
+
+// 长度为 2 的无符号整数数组
+// 每个索引保存一个无符号整数
+// 范围是 0 到 255
+const unsignedInts = new Uint8Array(2)
+
+// 上溢的位不会影响相邻索引
+// 索引只取最低有效位上的 8 位
+unsignedInts[1] = 256
+console.log(unsignedInts[1]) // [0, 0]
+unsignedInts[1] = 511
+console.log(unsignedInts[1]) // [0, 255]
+
+// 下溢的位会被转换为无符号的等价值
+// 0xFF 是以二补数形式表示的 -1(截取到 8 位)
+// 但 255 是一个无符号整数
+unsignedInts[1] = -1
+console.log(unsignedInts[1]) // [0, 255]
+
+// 上溢自动变成二补数形式
+// 0x80 是无符号整数的 128,是二补数形式的 -128
+ints[1] = 128
+console.log(ints[1]) // [0, -128]
+
+// 下溢自动变为二补数形式
+// 0xFF 是无符号整数的 255,是二补数形式的 -1
+ints[1] = 255
+console.log(ints[1]) // [0, -1]
+

除了 8 种元素类型,还有一种“夹板”数组类型:Uint8ClampedArray,不允许任何方向溢出(除非真的做跟 canvas 相关的开发,否则不要使用它)。超出最大值 255 的值会被向下舍入为 255,低于最小值 0 的值会被向上舍入为 0

const clampedInts = new Uint8ClampedArray([-1, 0, 255, 256])
+console.log(clampedInts[0]) // [0, 0, 255, 255]
+

Map

作为 ECMAScript6 的新增特性,Map 是一种新的集合类型,为这门语言带来了真正的键/值存储机制。Map 的大多数特性都可以通过 Object 来实现,但二者之间还是存在一些细微的差异:

  • Map 的键可以是任意值,包括函数、对象或任意基本类型
  • Map 实例会维护键值对的插入顺序
  • 从内存占用,插入性能,删除性能来看,Map 优于 Object
  • Map 和 Object 的查找速度差异较小

基本 API

创建和初始化:

// 使用嵌套数组初始化映射
+const m1 = new Map([
+	['key1', 'val1'],
+	['key2', 'val2'],
+	['key3', 'val3'],
+])
+console.log(m1.size) // 3
+
+// 使用自定义迭代器初始化映射
+const m2 = new Map({
+	[Symbol.iterator]: function* () {
+		yield ['key1', 'val1']
+		yield ['key2', 'val2']
+		yield ['key3', 'val3']
+	},
+})
+console.log(m2.size) // 3
+console.log(m2.has(undefined)) // false
+console.log(m2.get(undefined)) // undefined
+
  • set() 添加键值对
  • get() 和 has() 查询
  • delete() 和 clear() 删除值
  • size 属性返回键值对的数量

顺序与迭代

映射实例可以提供一个迭代器(Iterator),能以插入顺序生成 [key, value] 形式的数组。可以通过 entries() 方法(或者 Symbol.iterator 属性,它引用 entires())获取这个迭代器

因为 entires() 是默认迭代器,因此可以直接对映射实例使用扩展操作符,把映射转换为数组

  • forEach((value, key, map) => {}, opt_thisArg)
  • keys() 返回键的迭代器
  • values() 返回值的迭代器

WeakMap

WeakMap 是 Map 的“兄弟”类型,其 API 也是 Map 的子集,但有一些重要的区别:

  • WeakMap 的键只能是 Object 或继承自 Object 的类型,否则会抛出 TypeError

基本 API

创建和初始化:

const key1 = { id: 1 },
+	key2 = { id: 2 },
+	key3 = { id: 3 }
+
+// 使用嵌套数组初始化弱映射
+const wm1 = new WeakMap([
+	[key1, 'val1'],
+	[key2, 'val2'],
+	[key3, 'val3'],
+])
+
  • set() 添加键值对
  • get() 和 has() 进行查询
  • delete() 删除

弱键

WeakMap 的键是弱键,这意味着如果键不再被引用,它所对应的值也会被回收

不可迭代键

WeakMap 的键不可迭代,因此没有 entries()、keys() 和 values() 方法,也没有 forEach() 方法,同时也没有 clear() 方法

WeakMap 实例之所以限制只能用对象作为键,是为了保证只有通过键对象的引用才能取得值

Set

Set 是 ECMAScript6 新增的集合类型,在很多方面都像是加强的 Map,因为它们的大多数 API 和行为都是共有的

基本 API

创建和初始化:

// 使用数组初始化集合
+const s1 = new Set(['val1', 'val2', 'val3'])
+console.log(s1.size)
+
+// 使用自定义迭代器初始化集合
+const s2 = new Set({
+	[Symbol.iterator]: function* () {
+		yield 'val1'
+		yield 'val2'
+		yield 'val3'
+	},
+})
+console.log(s2.size)
+
  • add() 添加值
  • has() 查询
  • size 属性取得元素数量
  • delete()(会返回一个布尔值,表示集合种是否存在要删除的值) 和 clear() 删除元素

顺序与迭代

Set 会维护插入时的顺序,因此支持顺序迭代

集合实例可以提供一个迭代器,能以插入顺序生成集合内容的数组。可以通过 values() 方法及其别名方法 keys()(或者 Symbol.iterator 属性,它引用 values())获取这个迭代器

因为 values() 是默认迭代器,随意可以直接对集合实例使用扩展操作,把集合转换为数组

集合的 entires() 方法返回一个迭代器,可以按照插入顺序产生包含两个元素的数组,这两个元素是集合种每个值的重复出现

  • forEach((value, dupValue, set) => {}, opt_thisArg)

WeakSet

WeakSet 是 Set 的“兄弟”类型,其 API 也是 Set 的子集,但有一些重要的区别:

  • WeakSet 的值只能是 Object 或继承自 Object 的类型,否则会抛出 TypeError

基本 API

创建和初始化:

const val1 = { id: 1 },
+	val2 = { id: 2 },
+	val3 = { id: 3 }
+
+// 使用数组初始化弱集合
+const ws1 = new WeakSet([val1, val2, val3])
+
  • add() 添加值
  • has() 查询
  • delete() 删除

弱值

WeakSet 的值是弱值,这意味着如果值不再被引用,它就会被回收

const ws = new WeakSet()
+ws.add({}) // 因为没有指向这个对象的其他引用,所以当这行代码执行完成后,这个对象值就会被当作垃圾回收
+

不可迭代值

WeakSet 的值不可迭代,因此没有 entries()、keys() 和 values() 方法,也没有 forEach() 方法,同时也没有 clear() 方法

迭代与扩展操作

ECMAScript6 新增的迭代器和扩展操作符对集合引用类型特别有用,有 4 种原生集合类型定义了默认迭代器:

  • Array
  • 所有定型数组
  • Map
  • Set

意味着上述所有类型都支持顺序迭代,都可以传入 for-of 循环,兼容扩展操作符...

Last Updated:
+ + + diff --git a/frontend/js/red-book/07.html b/frontend/js/red-book/07.html new file mode 100644 index 0000000..a83ba78 --- /dev/null +++ b/frontend/js/red-book/07.html @@ -0,0 +1,278 @@ + + + + + + + + + 红宝书 | 瓢儿白施肥记 + + + + +

迭代器与生成器

小结

迭代器是一个可以由任意对象实现的接口,支持连续获取对象产出的每一个值。任何实现 Iterable 接口的对象有一个 Symbol.iterator 属性,这个属性引用默认迭代器。默认迭代器就像一个迭代器工厂,也就是一个函数,调用之后会产生一个实现 Iterator 接口的对象

生成器是一种特殊的函数,调用之后会返回一个生成器对象。生成器对象实现了 Iterable 接口,因此可用在任何消费可迭代对象的地方。生成器的独特之处在于支持 yield 关键字,这个关键字能够暂停执行生成器函数。使用 yield 关键字还可以通过 next() 方法接收输入和产出输出。在加上 * 之后,yield 可以将跟在它后面的可迭代对象序列化为一连串值

理解迭代

在 ECMAScript 较早的版本中,执行迭代必须使用循环或其他辅助结构。随着代码里增加,代码会变得越发混乱。很多语言都通过原生语言结构解决了这个问题,开发者无须事先知道如何迭代就能实现迭代操作。这个解决方案就是迭代器模式

迭代器模式

迭代器是按需创建的一次性对象。每个迭代器都会关联一个可迭代对象,而迭代器会暴露迭代其关联可迭代对象的 API。迭代器无须了解与其关联的可迭代对象的结构,只需要知道如何取得连续的值

可迭代协议

很多内置对象都实现了 Iterable 接口(可迭代协议):

  • 字符串
  • 数组
  • 映射
  • 集合
  • arguments 对象
  • NodeList 等 DOM 集合类型

检查是否存在默认迭代器属性可以暴露这个工厂函数(调用工厂函数会生成一个迭代器):

function isIterable(object) {
+	return typeof object[Symbol.iterator] === 'function'
+}
+

实际写代码过程中,不需要显示调用这个工厂函数来生成迭代器。实现可迭代协议的所有类型都会自动兼容接收可迭代对象的任何语言特性

接收可迭代对象的语言特性包括:

  • for-of 循环
  • 解构
  • 扩展操作符
  • Array.from()
  • 创建集合
  • 创建映射
  • Promise.all() 接收由期约组成的可迭代对象
  • Promise.race() 接收由期约组成的可迭代对象
  • yield* 操作符,在生成器中使用

迭代器协议

迭代器是一种一次性使用的对象,用于迭代与其关联的可迭代对象。迭代器 API 使用 next() 方法在可迭代对象中遍历数据。每次调用 next() 方法都会返回一 IteratorResult 对象,这个结果对象包含两个属性:done 和 value

每个迭代器都表示对可迭代对象的一次性有序遍历,不同迭代器实例之间没有联系,只会独立地遍历可迭代对象

迭代器并不与可迭代对象某个时刻的快照绑定,而仅仅是使用游标来记录遍历可迭代对象的历程。如果可迭代对象在迭代期间被修改,迭代器会反映出这些变化:

const arr = ['foo', 'bar']
+const iter = arr[Symbol.iterator]()
+console.log(iter.next()) // { value: 'foo', done: false }
+arr.splice(1, 0, 'baz')
+console.log(iter.next()) // { value: 'baz', done: false }
+console.log(iter.next()) // { value: 'bar', done: false }
+console.log(iter.next()) // { value: undefined, done: true }
+

注意

迭代器维护一个指向可迭代对象的引用,因此迭代器会阻止垃圾回收程序回收可迭代对象

自定义迭代器

与 Iterator 接口类似,任何实现 Iterator 接口的对象都可以作为迭代器使用

class Counter {
+	constructor(limit) {
+		this.limit = limit
+	}
+
+	[Symbol.iterator]() {
+		let count = 1
+		const limit = this.limit
+		return {
+			next() {
+				if (count <= limit) {
+					return { done: false, value: count++ }
+				} else {
+					return { done: true, value: undefined }
+				}
+			},
+		}
+	}
+}
+const counter = new Counter(3)
+
+for (const i of counter) {
+	console.log(i)
+}
+

每个以这种方式创建的迭代器也实现了 Iterable 接口,并且 Symbol.iterator 属性引用的工厂函数会返回相同的迭代器

const arr = ['foo', 'bar', 'baz']
+const iter1 = arr[Symbol.iterator]()
+console.log(iter1[Symbol.iterator]) // ƒ [Symbol.iterator]() { [native code] }
+const iter2 = iter1[Symbol.iterator]()
+console.log(iter1 === iter2)
+



 

提前终止迭代器

可选的(意味着并不是所有的迭代器都是可关闭的,可以测试这个迭代器实例的 return 属性是不是函数来判断该迭代器是否可关闭) return() 方法用于指定在迭代器提前关闭时执行的逻辑。执行迭代的结构在想让迭代器知道它不想遍历到可迭代对象耗尽时,就可以“关闭”迭代器。可能的情况包括:

  • for-of 循环通过 break / continue / return / throw 提前退出
  • 解构操作并未消费所有值
class Counter {
+	constructor(limit) {
+		this.limit = limit
+	}
+
+	[Symbol.iterator]() {
+		let count = 1
+		const limit = this.limit
+		return {
+			next() {
+				if (count <= limit) {
+					return { done: false, value: count++ }
+				} else {
+					return { done: true, value: undefined }
+				}
+			},
+			return() {
+				console.log('Exiting early')
+				return { done: true }
+			},
+		}
+	}
+}
+const counter = new Counter(5)
+
+for (const i of counter) {
+	if (i > 2) {
+		break
+	}
+
+	console.log(i)
+}
+
+const [a, b] = counter
+
















 
 
 
 














如果迭代器没有关闭,则还可以继续从上次离开的地方继续迭代。比如,数组的迭代器就是不能关闭的:

const a = [1, 2, 3, 4, 5]
+const iter = a[Symbol.iterator]()
+
+for (const i of iter) {
+	console.log(i)
+	if (i > 2) {
+		break
+	}
+}
+// 1
+// 2
+// 3
+
+for (const i of iter) {
+	console.log(i)
+}
+// 4
+// 5
+

注意,仅仅给一个不可关闭的迭代器增加 return() 方法并不能让它变成可关闭的,这个因为调用 return() 并不会强制迭代器进入关闭状态。即便如此,return() 方法还是会被调用:

const a = [1, 2, 3, 4, 5]
+const iter = a[Symbol.iterator]()
+
+iter.return = function () {
+	console.log('Exiting early')
+	return { done: true }
+}
+
+for (const i of iter) {
+	console.log(i)
+	if (i > 2) {
+		break
+	}
+}
+// 1
+// 2
+// 3
+// Exiting early
+
+for (const i of iter) {
+	console.log(i)
+}
+// 4
+// 5
+



 
 
 
 

















生成器

生成器的形式是一个函数,函数名称前面加一个 * 表示它是一个生成器,标识生成器的星号不受两侧空格的影响。只要是可以定义函数的地方就可以定义生成器

注意

箭头函数不能用来定义生成器函数

调用生成器会产生一个生成器对象。生成器对象一开始处于暂停执行(suspended)的状态。与迭代器相似,生成器也实现了 Iterator 接口,因此具有 next() 方法,调用这个方法会让生成器开始或恢复执行

function* generatorFn() {}
+
+const g = generatorFn()
+console.log(g) // generatorFn {<suspended>}
+console.log(g.next()) // {value: undefined, done: true}
+

next() 方法的返回值类似于迭代器,由一个 done 属性和一个 value 属性

value 属性是生成器函数的返回值,默认值为 undefined,可以通过生成器函数的返回值指定:

function* generatorFn() {
+	return 'foo'
+}
+
+const generatorObject = generatorFn()
+console.log(generatorObject) // generatorFn {<suspended>}
+console.log(generatorObject.next()) // {value: 'foo', done: true}
+

生成器对象实现了 Iterator 接口,它们默认的迭代器是自引用的

通过 yield 中断执行

yield 关键字可以让生成器停止和开始执行。生成器函数在遇到 yield 关键字之前会正常执行,遇到这个关键字之后,执行会停止,函数作用域的状态会被保留。停止执行的生成器函数只能通过在生成器对象上调用 next() 方法来恢复执行

yield 关键字生成的值会出现在 next() 方法返回的对象里。通过 yield 退出的生成器函数会处在 done:false 状态;通过 return 退出的生成器函数会处于 done:true 状态

生成器函数内部的执行流程会针对每个生成器对象区分作用域

yield 只能在生成器函数内部使用,用在其他地方会抛出错误

1. 生成器对象作为可迭代对象

在生成器对象上显示调用 next() 方法的用处并不大。如果把生成器当作可迭代对象:

function* generatorFn() {
+	yield 1
+	yield 2
+	yield 3
+}
+
+for (const i of generatorFn()) {
+	console.log(i)
+}
+// 1
+// 2
+// 3
+

2. 使用 yield 实现输入和输出

除了作为函数的中间返回语句使用,yield 还可以作为函数的中间参数使用。yield 语句的值可以通过 next() 方法的参数传入生成器函数

function* generatorFn(initial) {
+	console.log(initial)
+	console.log(yield)
+	console.log(yield)
+}
+
+const generatorObject = generatorFn('foo')
+generatorObject.next('bar') // foo
+generatorObject.next('baz') // baz
+generatorObject.next('qux') // qux
+

yield 可以同时用于输入和输出

function* generatorFn() {
+	return yield 'foo'
+}
+
+const generatorObject = generatorFn()
+console.log(generatorObject.next()) // {value: 'foo', done: false}
+console.log(generatorObject.next('bar')) // {value: 'bar', done: true}
+

使用生成器填充数组

function* zeros(n) {
+	for (let i = 0; i < n; i++) {
+		yield 0
+	}
+}
+console.log(Array.from(zeros(3)))
+

3. 产生可迭代对象 可以使用星号(两侧的空格不影响)增强 yield 的行为,让它那能够迭代一个可迭代对象,从而一次产出一个值:

function* generatorFn() {
+	yield* [1, 2, 3]
+}
+
+for (const i of generatorFn()) {
+	console.log(i)
+}
+// 1
+// 2
+// 3
+

yield* 的值是关联迭代器返回 done: true 时的 value 属性

4. 使用 yield* 实现递归算法

yield* 最有用的地方时实现递归操作,此时生成器可以产生自身:

function* nTimes(n) {
+	if (n > 0) {
+		yield* nTimes(n - 1)
+		yield n - 1
+	}
+}
+
+for (const i of nTimes(3)) {
+	console.log(i)
+}
+// 0
+// 1
+// 2
+

生成器作为默认迭代器

因为生成器对象实现了 Iterable 接口,而且生成器函数和默认迭代器被调用之后都产生迭代器,所以生成器格外适合作为默认迭代器

class Foo {
+	constructor() {
+		this.values = [1, 2, 3]
+	}
+
+	*[Symbol.iterator]() {
+		yield* this.values
+	}
+}
+

提前终止生成器

1. return()

return() 方法会强制生成器进入关闭状态。提供给 return() 方法的值,就是终止迭代器对象的值

所有的生成器都有 return(),只要通过它进入关闭状态,就无法恢复了

function* generatorFn() {
+	yield* [1, 2, 3]
+}
+
+const g = generatorFn()
+console.log(g) // generatorFn {<suspended>}
+console.log(g.return(4)) // {value: 4, done: true}
+console.log(g) // generatorFn {<closed>}
+

for-of 循环等内置语言结构会忽略状态为 done: true 的 IteratorObject 内部返回的值

function* generatorFn() {
+	yield* [1, 2, 3]
+}
+
+const g = generatorFn()
+
+for (const i of g) {
+	if (i > 1) {
+		g.return(4)
+	}
+
+	console.log(i)
+}
+// 1
+// 2
+

2. throw()

throw() 方法会在暂停的时候将一个提供的错误注入到生成器对象中。如果错误未被处理,生成器就会关闭:

function* generatorFn() {
+	yield* [1, 2, 3]
+}
+
+const g = generatorFn()
+
+console.log(g) // generatorFn {<suspended>}
+try {
+	g.throw(new Error('foo'))
+} catch (e) {
+	console.log(e) // Error: foo
+}
+console.log(g) // generatorFn {<closed>}
+

假如生成器内部处理了这个错误,那么生成器就不会关闭,而且还可以恢复执行。错误处理会跳过对应的 yield:

function* generatorFn() {
+	for (const x of [1, 2, 3]) {
+		try {
+			yield x
+		} catch (e) {
+			console.log(e)
+		}
+	}
+}
+
+const g = generatorFn()
+
+console.log(g.next()) // {value: 1, done: false}
+g.throw('foo')
+console.log(g.next()) // {value: 3, done: false}
+
Last Updated:
+ + + diff --git a/frontend/js/red-book/08.html b/frontend/js/red-book/08.html new file mode 100644 index 0000000..6aceec1 --- /dev/null +++ b/frontend/js/red-book/08.html @@ -0,0 +1,349 @@ + + + + + + + + + 红宝书 | 瓢儿白施肥记 + + + + +

对象、类与面向对象编程

ECMA-262 将对象定义为一组属性的无序集合

理解对象

属性的类型

ECMA-262 使用一些内部特性来描述属性的特征,属性分为两种:数据属性和访问器属性

1. 数据属性

数据属性包含一个保存数据值的位置,在这个位置可以读取和写入值。数据属性有 4 个特性描述它的行为:

  • [[Configurable]]:默认为 true。表示是否可以通过 delete 删除并重新定义,是否可以修改它的特性,以及是否可以把它改为访问器属性
  • [[Enumerable]]:默认为 true。表示属性是否可以通过 for-in 循环返回
  • [[Writable]]:默认为 true。表示属性的值是否可以被修改
  • [[value]]: 默认为 undefined。包含属性实际的值,即前面提到的那个读取和写入属性值的位置

Object.defineProperty() 修改属性的默认特性,如果不指定 configurable / enumerable / writable,则默认为 false:

let person = {}
+Object.defineProperty(person, 'name', {
+	writable: false,
+	value: 'Nicholas',
+})
+

2. 访问器属性

访问器属性不包含数据值。相反,它们包含一个获取(getter)函数和一个设置(setter)函数,不过这两个函数不是必需的。访问器属性有 4 个特性描述它的行为:

  • [[Configurable]]:默认为 true。表示是否可以通过 delete 删除并重新定义,是否可以修改它的特性,以及是否可以把它改为数据属性‘
  • [[Enumerable]]:默认为 true。表示属性是否可以通过 for-in 循环返回
  • [[Get]]:默认为 undefined。获取函数,在读取属性时调用
  • [[Set]]:默认为 undefined。设置函数,在写入属性时调用

访问器属性是不能直接定义的,必须使用 Object.definedProperty():

let book = {
+	year_: 2004, // 下划线表示该属性并不希望在对象外部被访问
+	edition: 1,
+}
+
+Object.defineProperty(book, 'year', {
+	get() {
+		return this.year_
+	},
+	set(newValue) {
+		if (newValue > 2004) {
+			this.year_ = newValue
+			this.edition += newValue - 2004
+		}
+	},
+})
+
+book.year = 2005
+console.log(book.edition) // 2
+

定义多个属性

Object.defineProperties() 可以通过多个描述符一次定义多个属性,如果数据属性不指定 configurable / enumerable / writable,则默认为 false

读取属性的特性

Object.getOwnPropertyDescriptor() 可以取得给定属性的描述符

ECMAScript 2017 新增了 Object.getOwnPropertyDescriptors() 静态方法,可以取得给定对象所有自有属性的描述符

合并对象

Object.assign() 可以把任意多个源对象可枚举的自有属性浅拷贝到目标对象,然后返回目标对象。对每个符合条件的属性,这个方法会使用源对象上的 [[Get]] 取得属性值,然后使用目标对象上的 [[Set]] 设置属性值

const dest = {
+	set a(val) {
+		console.log(`Invoked dest setter with param ${val}`)
+	},
+}
+
+const src = {
+	get a() {
+		console.log('Invoked src getter')
+		return 'foo'
+	},
+}
+
+Object.assign(dest, src)
+// Invoked src getter
+// Invoked dest setter with param foo
+console.log(dest)
+
  • 如果多个源对象有相同的属性,则使用最后一个复制的值
  • 从源对象访问器属性取得的值,比如获取函数,会作为一个静态值赋给目标对象,即不能在两个对象间转移获取函数和设置函数
  • 如果赋值期间出错,则操作会中止并退出,同时抛出错误。因此 Object.assign() 是一个尽力而为、可能只会完成部分复制的方法

对象标识及相等判定

Object.is() 用于比较两个值是否相等,与 === 的行为基本一致,但是它对 NaN 和 +0/-0 作了特殊处理

要检查超过两个值,递归地利用相等性传递即可:

function recursivelyCheckEqual(x, ...rest) {
+	return Object.is(x, rest[0]) && (rest.length < 2 || recursivelyCheckEqual(...rest))
+}
+

增强的对象语法

ECMAScript 6 为定义和操作对象新增了很多及其有用的语法糖特性:

  • 属性值简写
  • 可计算属性
  • 简写方法名

对象解构

对象解构就是使用与对象匹配的结构来实现对象属性赋值

如果是给事先声明的变量赋值,则赋值表达式必须包含在一对括号中,否则会被当成一个代码块:

let personName, personAge
+let person = {
+	name: 'Nicholas',
+	age: 29,
+}
+;({ name: personName, age: personAge } = person)
+
  • 嵌套解构
  • 部分解构:需要注意的是,涉及多个属性的解构赋值是一个输出无关的顺序化操作。如果一个解构表达式涉及多个赋值,开始的赋值成功而后面的赋值出错,则整个解构赋值只会完成一部分
  • 参数上下文匹配:在函数参数列表中也可以进行解构赋值

创建对象

使用 Object 构造函数和对象字面量可以方便的创建对象,但是这种方式有个缺点:创建具有同样接口的多个对象需要重复编写很多代码

工厂模式

工厂模式用于抽象创建特定对象的过程

下面的例子展示了一种按照特定接口创建对象的方式:

function createPerson(name, age, job) {
+	let o = new Object()
+	o.name = name
+	o.age = age
+	o.job = job
+	o.sayName = function () {
+		console.log(this.name)
+	}
+	return o
+}
+
+let person1 = createPerson('Nicholas', 29, 'Software Engineer')
+let person2 = createPerson('Greg', 27, 'Doctor')
+

这种工厂模式虽然可以解决创建多个类似对象的问题,但没有解决对象标识问题(即新创建的对象是什么类型)

构造函数模式

ECMAScript 中的构造函数就是用于创建特定类型对象的

构造函数不一定要写成函数声明的形式,赋值给变量的函数表达式也可以

使用 new 操作符调用构造函数会执行如下操作:

  • 在内存中创建一个新对象
  • 这个新对象内部的 [[Prototype]] 特性被赋值为构造函数的 prototype 属性
  • this 指向新对象
  • 执行构造函数内部的代码
  • 如果构造函数返回非空对象,则返回该对象;否则,返回刚创建的新对象

instanceof 操作符用于检测构造函数的 prototype 属性是否出现在某个实例对象的原型链上

构造函数的问题在于其定义的方法会在每个实例上都创建一遍

原型模式

每个函数都会创建一个 prototype 属性,这个属性是一个对象,包含应该由特定引用类型的实例共享的属性和方法

function Person() {}
+
+Person.prototype.name = 'Nicholas'
+Person.prototype.age = 29
+Person.prototype.job = 'Software Engineer'
+Person.prototype.sayName = function () {
+	console.log(this.name)
+}
+
+let person1 = new Person()
+person1.sayName() // Nicholas
+
+let person2 = new Person()
+person2.sayName() // Nicholas
+
+console.log(person1.sayName === person2.sayName) // true
+

与构造函数模式不同,使用这种原型模式定义的属性和方法是由所有实例共享的

1. 理解原型

构造函数.prototype 指向原型对象,原型对象.constructor 指向构造函数,实例对象.__proto__ 指向原型对象

isPrototypeOf() 方法用于检测当前对象是否是传入对象的原型

Object.getPrototypeOf() 方法返回传入对象的原型

Object.setPrototypeOf() 方法用于设置传入对象的原型,但其可能会严重影响性能,因为其涉及所有访问了那些修改过 [[Prototype]] 的对象的代码

Object.create() 方法创建一个新对象,将参数作为新创建的对象的__proto__

2. 原型层级

hasOwnProperty() 方法用于确定某个属性实在实例上还是在原型对象上,继承自 Object

Object.getOwnPropertyDescriptor() 方法也只对实例有效

3. 原型和 in 操作符

有两张方式使用 in 操作符:

  • 单独使用,会在可以通过对象访问指定属性时返回 true,无论该属性存在于实例还是原型中
  • 在 for-in 循环中使用

单独使用 in 结合 'hasOwnProperty()' 方法可以确定属性是否存在于原型上:

function hasPrototypeProperty(object, name) {
+	return !object.hasOwnProperty(name) && name in object
+}
+

在 for-in 循环中使用 in 操作符时,可以通过对象访问且可以被枚举的属性都会返回

Object.keys() 方法返回对象上所有可枚举的实例属性

Object.getOwnPropertyNames() 方法返回对象上所有实例属性,无论是否可枚举

Object.getOwnPropertySymbols() 方法同上,但是只针对符号

4. 属性枚举顺序

  • for-in 和 Object.keys() 的枚举顺序是不确定的,取决于 JavaScript 引擎,可能因浏览器而异
  • Object.getOwnPropertyNames() / Object.getOwnPropertySymbols() / Object.assign() 的枚举顺序是确定的,先以升序枚举数值键,然后以插入顺序枚举字符串和符号键,在对象字面量中定义的键以它们逗号分隔的顺序插入
const k1 = Symbol('k1')
+const k2 = Symbol('k2')
+
+let o = {
+	1: 1,
+	first: 'first',
+	[k2]: 'k2',
+	second: 'second',
+	0: 0,
+}
+
+o[k1] = 'k1'
+o[3] = 3
+o.third = 'third'
+o[2] = 2
+
+console.log(Object.getOwnPropertyNames(o)) // ['0', '1', '2', '3', 'first', 'second', 'third']
+console.log(Object.getOwnPropertySymbols(o)) // [Symbol(k2), Symbol(k1)]
+

对象迭代

Object.values() / Object.entires:非字符串属性会被转换为字符串输出,且执行对象的浅复制,符号属性会被忽略

1. 其他原型语法

function Person() {}
+
+Person.prototype = {
+	name: 'Nicholas',
+	age: 29,
+	job: 'Software Engineer',
+	sayName() {
+		console.log(this.name)
+	},
+}
+
+// 恢复 constructor 属性
+Object.defineProperty(Person.prototype, 'constructor', {
+	enumerable: false,
+	value: Person,
+})
+

2. 原生对象原型

不推荐在产品环境中修改原生对象原型,而是创建一个自定义类继承原生类型

3. 原型的问题

最主要的问题源自它的共享特性,针对包含引用值的属性,会导致实例间相互影响,故通常不单独使用原型模式

继承

实现继承是 ECMAScript 唯一支持的继承方式,且主要通过原型链实现

原型链

ECMA-262 把原型链定义为 ECMAScript 的主要继承方式,其基本思想是通过原型继承多个引用类型的属性和方法

1. 默认原型

任何函数的默认原型都是一个 Object 的实例

2. 原型与继承关系

确定原型与实例的关系:

  • instanceof 操作符,检测构造函数的 prototype 属性是否出现在某个实例对象的原型链上
  • isPrototypeOf() 方法,如果传入的对象是实例的原型,则返回 true

3. 原型链的问题

  • 主要问题在于原型中包含的引用值会在所有实例间共享
  • 子类型在实例化时不能给父类型的构造函数传参

这些问题导致原型链基本不会单独使用

盗用构造函数

为了解决原型包含引用值导致继承问题,一种叫作“盗用构造函数(constructor stealing)”的技术在开发社区流行起来,有时也称作“对象伪装”或“经典继承”

基本思路:在子类构造函数中调用父类构造函数

function SuperType() {
+	this.colors = ['red', 'blue', 'green']
+}
+
+function SubType() {
+	// 继承了 SuperType
+	SuperType.call(this)
+}
+
+let instance1 = new SubType()
+

1. 传递参数

相比于使用原型链,盗用构造函数的一个优点就是可以在子类构造函数中向父类构造函数传参

2. 盗用构造函数的问题

主要缺点:必须在构造函数中定义方法,导致函数不能重用。此外子类也不能访问父类原型上定义的方法,导致所有类型只能使用构造函数模式

故“盗用构造函数”基本上也不能单独使用

组合继承

组合继承(伪经典继承)综合了原型链和盗用构造函数,将两者的优点集中了起来

基本思路:通过原型链继承原型上的属性和方法,通过盗用构造函数继承实例属性

这样既可以把方法定义在原型上以实现重用,又可以让每个实例都有自己的属性

function SuperType(name) {
+	this.name = name
+	this.colors = ['red', 'blue', 'green']
+}
+
+SuperType.prototype.sayName = function () {
+	console.log(this.name)
+}
+
+function SubType(name, age) {
+	// 继承属性
+	SuperType.call(this, name)
+
+	this.age = age
+}
+
+// 继承方法
+SubType.prototype = new SuperType()
+
+SubType.prototype.sayAge = function () {
+	console.log(this.age)
+}
+

原型式继承

原型式继承的思路:即使不自定义类型也可以通过原型实现对象之间的信息共享

原型式继承非常适合不需要单独创建构造函数,但仍然需要在对象间共享信息的场合

ECMAScript 5 通过 Object.create() 方法将原型式继承的概念规范化了

const person = {
+	name: 'Nicholas',
+	friends: ['Shelby', 'Court', 'Van'],
+}
+
+const anotherPerson = Object.create(person, {
+	name: {
+		value: 'Greg',
+	},
+})
+

寄生式继承

寄生式继承的思路:创建一个实现继承的函数,以某种方式增强对象,然后返回这个对象

function createAnother(original) {
+	const clone = Object.create(original) // 通过调用函数创建一个新对象(任何返回新对象的函数都可以在这里使用)
+	clone.sayHi = function () {
+		// 以某种方式增强这个对象
+		console.log('hi')
+	}
+	return clone // 返回这个对象
+}
+

通过寄生式继承给对象添加函数会导致函数难以重用,与构造函数模式类似

寄生式组合继承

组合继承存在效率问题:父类构造函数始终会被调用两次(一次是创建子类原型时调用,另一次是在子类构造函数中调用),且子类的原型上会存在多余的属性(构造函数中的)

寄生式组合继承的基本思路:通过盗用构造函数来继承属性,但使用混合式原型链继承方法。基本思路是不通过调用父类构造函数给子类原型赋值,而是取得父类原型的一个副本

function inheritPrototype(subType, superType) {
+	const prototype = Object.create(superType.prototype) // 创建对象
+	prototype.constructor = subType // 增强对象
+	subType.prototype = prototype // 指定对象
+}
+
+function SuperType(name) {
+	this.name = name
+	this.colors = ['red', 'blue', 'green']
+}
+
+SuperType.prototype.sayName = function () {
+	console.log(this.name)
+}
+
+function SubType(name, age) {
+	SuperType.call(this, name)
+
+	this.age = age
+}
+
+inheritPrototype(SubType, SuperType)
+
+SubType.prototype.sayAge = function () {
+	console.log(this.age)
+}
+
















 




 




ECMAScript 6 类表面上看起来可以支持正式第面向对象编程,但实际上背后使用的仍然是原型和构造函数的概念

类定义

类定义主要有两种方式:类声明和类表达式

// 类声明
+class Person {}
+
+// 类表达式
+const Animal = class {}
+

默认情况下,类定义中的代码都在严格模式下执行

类构造函数与构造函数的主要区别:类构造函数必须使用 new 操作符, 否则会抛出错误;而普通构造函数如果不使用 new 调用,那么就会以全局的 this 作为内部对象

从各方面看,ECMAScript 类就是一种特殊函数

  • typeof 类名 === ’function‘
  • 类标识符有 prototype 属性,而这个原型也有一个 constructor 属性指向类自身
  • 可以使用 instanceof 操作符检测构造函数原型是否存在于实例的原型链中

类是 JavaScript 的一等公民,因此可以像其他对象或函数引用一样把类作为参数传递

实例、原型和类成员

1. 实例成员

通过 new 调用类标识符,都会执行构造函数。在这个函数的内部,可以为新创建的实例添加“自有”属性

2. 原型方法与访问器

为了在实例间共享方法,类定义语法把在类块中定义的方法作为原型方法

类方法等同于对象属性,因此可以使用字符串、符号或计算的值作为键

不能在类块中给原型添加原始值或者对象作为成员数据

类定义也支持获取和设置访问器,语法与行为跟普通对象一样:

class Person {
+	get name() {
+		return this.name_
+	}
+	set name(newName) {
+		this.name_ = newName
+	}
+}
+

3. 静态类方法

静态类成员在类定义中使用 static 关键字作为前缀。在静态成员中,this 引用类自身

静态类方法非常适合作为实例工厂:

class Person {
+	constructor(age) {
+		this.age_ = age
+	}
+
+	sayAge() {
+		console.log(this.age_)
+	}
+
+	static create() {
+		return new Person(Math.floor(Math.random() * 100))
+	}
+}
+

4. 迭代器与生成器方法

类定义语法支持在原型和类本身上定义生成器方法,所以可以通过一个默认的迭代器,把类实例变成可迭代对象

class Person {
+	constructor() {
+		this.nickNames = ['di', 'diqiu', 'didi']
+	}
+
+	// *[Symbol.iterator]() {
+	// 	yield* this.nickNames
+	// }
+
+	// 也可以只返回迭代器实例
+	[Symbol.iterator]() {
+		return this.nickNames.values()
+	}
+}
+
+const p = new Person()
+
+for (const nickName of p) {
+	console.log(nickName)
+}
+

继承

ECMAScript 6 新增特性中最出色的一个就是原生支持了类继承机制。虽然类继承使用的是新语法,但背后依旧使用的是原型链

1. 继承基础

ES6 类支持单继承,使用 extends 关键字,就可以继承任何拥有 [[Construct]] 和原型的对象(类和普通的构造函数)

派生类都会通过原型链访问到类和原型上定义的方法

2. 构造函数、HomeObject 和 super()

派生类的方法可以通过 super 关键字引用它们的原型

super 关键字只能在派生类中使用,而且仅限于类构造函数、实例方法和静态方法内部

在类构造函数中使用 super 可以调用父类构造函数:

class Vehicle {
+	constructor() {
+		this.hasEngine = true
+	}
+}
+
+class Bus extends Vehicle {
+	constructor() {
+		/* 不要在 super 之前引用 this,否则会抛出 ReferenceError */
+		super() // 相当于 super.constructor()
+		console.log(this instanceof Vehicle) // true
+		console.log(this) // Bus {hasEngine: true}
+	}
+}
+
+new Bus()
+

在静态方法中可以通过 super 调用继承的类上的静态方法:

class Vehicle {
+	static identify() {
+		console.log('vehicle')
+	}
+}
+
+class Bus extends Vehicle {
+	static identify() {
+		super.identify()
+	}
+}
+
+Bus.identify() // vehicle
+

ES6 给类构造函数和静态方法内部添加了内部属性 [[HomeObject]],这个特性是一个指针,指向定义该方法的对象。这个指针是自动赋值的,而且只能在 JavaScript 引擎内部访问。super 始终会定义为 [[HomeObject]] 的原型

在使用 super 时需要注意几个问题:

  • super 只能在派生类构造函数静态方法中使用
  • 不能单独使用 super 关键字。要么用它调用构造函数,要么用它引用静态方法
  • 调用 super() 会调用父类的构造函数,并将返回的实例赋值给 this
  • 如果没有定义类构造函数,在实例化派生类时会调用 super(),而且会传入所有传给派生类的参数
  • 在类构造函数中,不能在调用 super() 之前引用 this
  • 如果在派生类中显示定义了构造函数,则要么必须在其中调用 super(),要么必须在其中返回一个对象

3. 抽象基类

有时候可能需要定义这样一个类,它可供其他类继承,但本身不会被实例化

虽然 ECMAScript 没有专门支持这种类的语法,但通过 new.target(保存通过 new 关键字调用的类或函数) 也很容易实现

通过在实例化时检测 new.target 是不是抽象基类,可以阻止对抽象基类实例化:

// 抽象基类
+class Vehicle {
+	constructor() {
+		console.log(new.target)
+		if (new.target === Vehicle) {
+			throw new Error('Vehicle cannot be directly instantiated')
+		}
+	}
+}
+
+// 派生类
+class Bus extends Vehicle {}
+
+new Bus() // Bus {}
+
+new Vehicle() // Uncaught Error: Vehicle cannot be directly instantiated
+

另外,通过在抽象基类构造函数中进行检查,可以要求派生类必须定义某个方法(原型方法在嗲用类构造函数之前就已经存在了):

class Vehicle {
+	constructor() {
+		if (new.target === Vehicle) {
+			throw new Error('Vehicle cannot be directly instantiated')
+		}
+		if (!this.foo) {
+			throw new Error('Inheriting class must define foo()')
+		}
+		console.log('success')
+	}
+}
+
+// 派生类
+class Bus extends Vehicle {
+	foo() {}
+}
+
+// 派生类
+class Van extends Vehicle {}
+
+// new Bus() // success
+new Van() // Uncaught Error: Inheriting class must define foo()
+

4. 继承内置类型

ES6 类为继承内置引用类型提供了顺畅的机制,开发者可以方便地扩展内置类型:

有些内置类型的方法会返回新实例。默认情况下,返回实例的类型与原始实例的类型时一致的。如果想覆盖这个默认行为,则可以覆盖 Symbol.species 访问器,这个访问器决定在创建返回的实例时使用的类:

class SuperArray extends Array {
+	static get [Symbol.species]() {
+		return Array
+	}
+}
+
+const a1 = new SuperArray(1, 2, 3, 4)
+const a2 = a1.map(x => x * x)
+
+console.log(a1 instanceof SuperArray) // true
+console.log(a2 instanceof SuperArray) // false
+

5. 类混入

把不同类的行为集中到一个类是一种常见的 JavaScript 模式。虽然 ES6 没有显示支持多类继承,但通过现有特性可以轻松地模拟这种行为

注意

Object.assign() 方法是为了混入对象行为而设计的。只有在需要混入类的行为时才有必要自己实现混入表达式。如果只是需要混入多个对象的属性,那么使用 Object.assign() 就足够了

很多 JavaScript 框架(特别是 React)已经抛弃混入模式,转向了组合模式(把方法提取到独立的类和辅助对象中,然后把它们组合起来,但不是继承)。这反映了那个众所周知的软件设计原则:“组合胜过继承”

Last Updated:
+ + + diff --git a/frontend/js/red-book/09.html b/frontend/js/red-book/09.html new file mode 100644 index 0000000..1c7e0bc --- /dev/null +++ b/frontend/js/red-book/09.html @@ -0,0 +1,113 @@ + + + + + + + + + 红宝书 | 瓢儿白施肥记 + + + + +

代理与反射

ECMAScript 6 新增的代理和反射为开发者提供了拦截并向基本操作嵌入额外行为的能力

代理基础

代理是目标对象的抽象

创建空代理

最简单的代理是空代理,即除了作为一个抽象的目标对象,什么也不做

代理使用 Proxy 构造函数创建,接收两个必填参数:目标对象和处理程序对象

const target = {
+	id: 'target',
+}
+
+const handler = {}
+
+const proxy = new Proxy(target, handler)
+
+console.log(target === proxy) // false
+

针对上述代码中 targetproxy 两个对象,注意:

  • hasOwnProperty() 方法都会应用到目标对象
  • Proxy.prototype 是 undefined,因此不能使用 instanceof 操作符检查对象是否是代理

定义捕获器

使用代理的主要目的是可以定义捕获器(trap)

例如,定义一个 get() 捕获器,在 ECMAScript 操作以某种形式调用 get() 时触发:

const target = {
+	foo: 'bar',
+}
+
+const handler = {
+	get() {
+		return 'handler override'
+	},
+}
+
+const proxy = new Proxy(target, handler)
+

注意:

  • get() 不是 ECMAScript 对象可以调用的方法
  • proxy[property]、proxy.property 或 Object.create(proxy)[property] 等操作都可以触发基本的 get() 操作以获取属性

捕获器参数和反射 API

所有捕获器都可以访问相应的参数,基于这些参数可以重建被捕获方法的原始行为

比如,get() 捕获器接收以下参数:

  • 目标对象
  • 要查询的属性
  • 代理本身

通过这些参数可以重建被捕获方法的原始行为:

const target = {
+	foo: 'bar',
+}
+
+const handler = {
+	get(trapTarget, property, receiver) {
+		return trapTarget[property]
+	},
+}
+
+const proxy = new Proxy(target, handler)
+

但并非所有捕获器行为都像 get() 这么简单,因此 ECMAScript 6 为所有捕获器定义了一组默认行为,这些行为可以在 Reflect 对象上找到

处理程序对象所有可以捕获的方法都有对应的反射(Reflect)API 方法,使用反射 API 定义空代理对象:

const target = {
+	foo: 'bar',
+}
+
+const proxy = new Proxy(target, Reflect)
+

在反射 API 的基础上可以用最少的代码修改捕获的方法。比如,在某个属性被访问时,对返回的值进行一番修饰:

const target = {
+	foo: 'bar',
+}
+
+const handler = {
+	get(trapTarget, property, receiver) {
+		const decoration = property === 'foo' ? '!!!' : ''
+		return Reflect.get(...arguments) + decoration
+	},
+}
+
+const proxy = new Proxy(target, handler)
+
+console.log(proxy.foo) // bar!!!
+

捕获器不变式

捕获器处理程序的行为必须遵守“捕获器不变式(trap invariant)”

比如,如果目标对象有一个不可配置且不可写的数据属性,那么在捕获器返回一个与该属性不同的值时,会抛出 TypeError

可撤销代理

new Proxy() 创建的代理对象与目标对象之间的联系会在代理对象的生命周期内一直存在

Proxy.revocable() 方法创建一个可撤销的代理,返回一个对象,包含两个属性:

  • proxy:新创建的代理对象
  • revoke:一个函数,调用后会撤销代理

注意:

  • 撤销代理的操作时不可逆的
  • 撤销函数(revoke())是幂等的,调用多少次结果都一样
  • 撤销代理之后再调用代理会抛出 TypeError

实用反射 API

某些情况下应该优先使用反射 API:

1. 反射 API 与 对象 API

  • 反射 API 并不限于捕获处理程序
  • 大多数反射 API 方法在 Object 类型上有对应的方法
  • Object 上的方法适用于通用程序,而反射方法适用于细粒度的对象控制与操作

2. 状态标记

很多反射方法返回称作“状态标记”的布尔值,表示操作是否成功。

以下方法都会提供状态标记:

  • Reflect.defineProperty()
  • Reflect.preventExtensions()
  • Reflect.setPrototypeOf()
  • Reflect.set()
  • Reflect.deleteProperty()

3. 用一等函数替代操作符

  • Reflect.get(),替代对象属性访问操作符
  • Reflect.set(),替代=赋值操作符
  • Reflect.has(),替代 in 操作符或 with()
  • Reflect.deleteProperty(),替代 delete 操作符
  • Reflect.construct(),替代 new 操作符

4. 安全地应用函数

在通过 apply 方法调用函数,被调用的函数可能也定义了自己的 apply 属性,此时:

Function.prototype.apply.call(myFunc, thisVal, argumentList)
+
+// 可替换为
+Reflect.apply(myFunc, thisVal, argumentList)
+

代理另外一个代理

代理可以拦截反射 API 的操作,而这意味着完全可以创建一个代理,通过它去代理另外一个代理。这样就可以在一个目标对象上建立多层拦截网:

const target = {
+	foo: 'bar',
+}
+
+const firstProxy = new Proxy(target, {
+	get() {
+		console.log('first proxy')
+		return Reflect.get(...arguments)
+	},
+})
+
+const secondProxy = new Proxy(firstProxy, {
+	get() {
+		console.log('second proxy')
+		return Reflect.get(...arguments)
+	},
+})
+
+console.log(secondProxy.foo)
+// second proxy
+// first proxy
+// bar
+

代理的问题与不足

1. 代理中的 this

如果目标对象依赖于对象标识,那就可能遇到意料之外的问题

2. 代理与内部插槽 有些 ECMAScript 内置类型可能会依赖代理无法控制的机制,结果导致在代理上调用某些方法会出错

比如,Date 类型方法的执行依赖 this 值上的内部槽位 [[NumberDate]],代理对象上不存在这个内部槽位,而且这个内部槽位的值也不能通过普通的 get() 和 set() 访问到,于是代理拦截后本应转发给目标对象的方法会抛出 TypeError:

const target = new Date()
+const proxy = new Proxy(target, {})
+console.log(proxy instanceof Date) // true
+proxy.getDate() // TypeError
+

代理捕获器与反射方法

代理可以捕获 13 种不同的基本操作,这些操作有不同的反射 API 方法、参数、关联 ECMAScript 操作和不变式

  • get()
  • set()
  • has()
  • definedProperty()
  • getOwnPropertyDescriptor()
  • deleteProperty()
  • ownKeys()
  • getPrototypeOf()
  • setPrototypeOf()
  • isExtensible()
  • preventExtensions()
  • apply()
  • construct()

代理模式

使用代理可以在代码中实现一些有用的编程模式

  • 跟踪属性访问
  • 隐藏属性
  • 属性验证
  • 函数与构造函数参数验证
  • 数据绑定与可观察对象
Last Updated:
+ + + diff --git a/frontend/js/red-book/10.html b/frontend/js/red-book/10.html new file mode 100644 index 0000000..c1c244b --- /dev/null +++ b/frontend/js/red-book/10.html @@ -0,0 +1,33 @@ + + + + + + + + + 红宝书 | 瓢儿白施肥记 + + + + +

函数

函数实际上是对象,每个函数都是 Function 类型的实例

不推荐使用 Function 构造函数来定义函数 let sum = new Function("num1", "num2", "return num1 + num2"),因为这种方式会导致解析两次代码(第一次是解析常规 ECMAScript 代码,第二次是解析传入构造函数的字符串)

箭头函数

Last Updated:
+ + + diff --git a/frontend/js/red-book/index.html b/frontend/js/red-book/index.html new file mode 100644 index 0000000..d4259e5 --- /dev/null +++ b/frontend/js/red-book/index.html @@ -0,0 +1,33 @@ + + + + + + + + + 瓢儿白施肥记 + + + + + + + + diff --git a/frontend/other/errors/index.html b/frontend/other/errors/index.html new file mode 100644 index 0000000..1752cad --- /dev/null +++ b/frontend/other/errors/index.html @@ -0,0 +1,45 @@ + + + + + + + + + 瓢儿白施肥记 + + + + +

终端错误

1. BUILD_ENV=XXX 命令不支持

package.json  的  scripts  属性下配置命令  "BUILD_ENV=XXX,切换到  Windows  环境下报错

'BUILD_ENV' 不是内部或外部命令,也不是可运行的程序

原因: Windows  环境不支持  BUILD_ENV=XXX  命令

解决:开启 VSCodewls 命令行,使用 Linux 环境
# Get started using Visual Studio Code with Windows Subsystem for Linuxopen in new window

npm

1. Unable to authenticate, need: Basic realm="aliyun"

解决:npm login

云效账号密码open in new window

2. cb() never called!

使用 npm i 安装依赖出现此错误

使用 rimraf 可快速删除 node_modules

npm install -g rimraf
+

解决:

依次执行如下命令

rimraf node_modules
+rimraf package-lock.json
+
+npm cache verify
+npm cache clean --force
+
+npm i
+

若上述步骤不能解决,则细看究竟是哪个包导致的问题,分批拉依赖

编译

1. “xxxx”不能用作 JSX 组件

解决:https://juejin.cn/post/7089463577634930718

通过 resolutions 指定版本

"resolutions": {
+  "@types/react": "17.0.44"
+},
+

如果用的是 npm(yarn 不需要) 的话,还需要在 package.json 的 script 中添加如下 preinstall,通过使用 npm-force-resolutions 包来根据 resolutions 进行版本限定。

"preinstall": "npm install --package-lock-only --ignore-scripts && npx npm-force-resolutions"
+
Last Updated:
+ + + diff --git a/frontend/other/tools/ali-iconfont.html b/frontend/other/tools/ali-iconfont.html new file mode 100644 index 0000000..a802f56 --- /dev/null +++ b/frontend/other/tools/ali-iconfont.html @@ -0,0 +1,110 @@ + + + + + + + + + 小工具 | 瓢儿白施肥记 + + + + +

阿里图标库

Font class 用法

只需要引入 css 和字体文件,把字体编码为 base64 格式,那只需要引入一个 css 文件即可

import fetch from 'node-fetch'
+import fs from 'fs'
+import prompts from 'prompts'
+
+const main = async () => {
+const validateUrl = url => (/\/(font.*)\.css/.test(url) ? true : '地址不对哦')
+const questions = [
+  {
+    type: 'text',
+    name: 'url',
+    message: '输入下iconfont的fontClass地址?',
+    validate: validateUrl
+  }
+]
+const { url } = await prompts(questions)
+const cssUrl = `https://${url}`
+// 指定保存字体文件的目录
+const fontDirectory = './fonts'
+const cssDirectory = '.s'
+// 创建字体文件保存目录
+if (!fs.existsSync(fontDirectory)) {
+  fs.mkdirSync(fontDirectory)
+}
+// 使用node-fetch获取CSS文件内容
+fetch(cssUrl)
+    .then(response => response.text())
+    .then(cssContent => {
+      // 使用正则表达式提取字体文件的URL
+      const fontUrls = cssContent.match(/url\('\/\/([^']+)'\)/g)
+
+      if (fontUrls) {
+        // 使用Promise.all()等待所有字体文件的下载完成
+        Promise.all(
+          fontUrls.map(fontUrl => {
+            // 提取URL中的字体文件链接
+            const urlMatch = fontUrl.match(/url\('\/\/([^']+)'\)/)
+            if (urlMatch && urlMatch[1]) {
+              const fontFileUrl = `https://${urlMatch[1]}` // 添加协议
+              // 下载字体文件并转换为Base64编码
+              return fetchAndEncodeToBase64(fontFileUrl)
+            }
+            return Promise.resolve('')
+          })
+        ).then(encodedFonts => {
+          // 将所有字体的Base64编码插入CSS
+          encodedFonts.forEach((encodedFont, index) => {
+            cssContent = cssContent.replace(fontUrls[index], encodedFont)
+          })
+
+          // 生成包含所有字体的Base64编码的CSS文件
+          fs.writeFileSync('src/styles/iconfont.scss', cssContent)
+          console.log('包含所有字体的Base64编码的CSS文件已生成')
+        })
+      } else {
+        console.error('未找到字体文件URL')
+      }
+    })
+    .catch(error => {
+      console.error('下载CSS文件时出错:', error)
+    })
+}
+
+// 下载字体文件并转换为Base64编码
+async function fetchAndEncodeToBase64(fontFileUrl) {
+  try {
+    const response = await fetch(fontFileUrl)
+    const buffer = await response.arrayBuffer()
+    const base64Font = Buffer.from(buffer).toString('base64')
+    const ext = fontFileUrl.substring(fontFileUrl.lastIndexOf('.') + 1)
+    return `url('data:application/font-${ext};charset=utf-8;base64,${base64Font}')`
+  } catch (error) {
+    console.error(`下载字体文件并转换为Base64编码时出错:${fontFileUrl}`, error)
+    return '' // 返回一个空字符串以避免破坏CSS
+  }
+}
+
+main()
+
Last Updated:
+ + + diff --git a/frontend/other/tools/git.html b/frontend/other/tools/git.html new file mode 100644 index 0000000..98781a6 --- /dev/null +++ b/frontend/other/tools/git.html @@ -0,0 +1,56 @@ + + + + + + + + + 小工具 | 瓢儿白施肥记 + + + + +

git

好使的教程 --> 猴子都能懂的 git 入门open in new window

常用命令

revert

# 回退一个commit
+git revert commit号
+
+# 回退多个连续的commit (后面的,前面的]
+git revert 后面的...前面的
+

添加远程仓库并取别名

git remote add upstream xxxxxx(上游仓库地址)
+

基于远程分支新建一个分支并切换过去

git checkout upstream/dev -b xxxx(分支名)
+

stash

# 暂存现在的内容
+git stash [save '描述']
+# 查看所有的暂存
+git stash list
+# 清空所有的暂存
+git stash clear
+# 删除某一个暂存,默认删除 stash@{0}
+git stash drop [stash@{某一个序号}]
+# 恢复某一个暂存并删掉它,默认恢复 stash@{0}
+git stash pop [stash@{某一个序号}]
+# 同上恢复,但是不删掉它
+git stash apply [stash@{某一个序号}]
+

clone

git clone 加 --single-branch 是下载单个分支, --depth=1 是下载单个 commit 这俩配置项可以提高拉取代码的速度

 git clone --depth=1 --single-branch git @github.com:ant-design/ant-design.git
+

设置命令别名

# git lg
+git config --global alias.lg "log --color --graph --pretty=format:'%Cred%h%Creset -%C(yellow)%d%Creset %s %Cgreen(%cr) %C(bold blue)<%an>%Creset' --abbrev-commit"
+

报错

LF will be replaced by CRLF the next time Git touches it

git config --global core.autocrlf false
+
Last Updated:
+ + + diff --git a/frontend/other/tools/index.html b/frontend/other/tools/index.html new file mode 100644 index 0000000..6aab9c3 --- /dev/null +++ b/frontend/other/tools/index.html @@ -0,0 +1,33 @@ + + + + + + + + + 瓢儿白施肥记 + + + + + + + + diff --git a/frontend/other/tools/json-server.html b/frontend/other/tools/json-server.html new file mode 100644 index 0000000..d0f2bc5 --- /dev/null +++ b/frontend/other/tools/json-server.html @@ -0,0 +1,62 @@ + + + + + + + + + 小工具 | 瓢儿白施肥记 + + + + +

JSON Server

模拟数据接口

开始

安装

npm install -g json-server
+

创建一个 db.json 文件

{
+	"posts": [{ "id": 1, "title": "json-server", "author": "typicode" }],
+	"comments": [{ "id": 1, "body": "some comment", "postId": 1 }],
+	"profile": { "name": "typicode" }
+}
+

启动

json-server --watch db.json
+

注意:

  • POST, PUT, PATCH, DELETE 请求带来的改变会出现在 db.json 中
  • 请求体必须是对象
  • id 不可变。使用 PUT / PATCH 请求改变 id 的值将会被忽略,用 POST 创建的 id 也不能与已有的重复
  • POST / PUT / PATCH 请求必须设置 Content-Type: application/json

路由

默认路由

基于上述的 db.json,所有的默认路由如下:

路由描述
GET /posts所有的 posts
GET /posts/1单个 post
GET /profileprofile 对象
POST /posts创建一个 post
POST /profile创建一个 profile
PUT /posts/1替换一个 post
PUT /profile替换一个 profile
PATCH /posts/1更新一个 post
PATCH /profile更新一个 profile
DELETE /posts/1删除一个 post

过滤

. 访问深层属性

GET /posts?title=json-server&author=typicode
+GET /comments?author.name=typicode
+

分页

GET /posts?_page=7
+GET /posts?_page=7&_limit=20 # _limit 默认为 10
+

排序

默认升序,降序加 _order=desc

GET /posts?_sort=views
+GET /posts/1/comments?_sort=votes&_order=desc
+
+# 多个字段排序
+GET /posts?_sort=user,views&_order=desc,asc
+

切片

行为同 Array.slice,不包含 _end

GET /posts?_start=20&_end=30
+GET /posts/1/comments?_start=20&_end=30
+GET /posts/1/comments?_start=20&_limit=10
+

操作符

_gte _lte,取得范围内的值

GET /posts?views_gte=10&views_lte=20
+

_ne,排除值

GET /posts?id_ne=1
+

_like,模糊查询(可以使用 RegExp)

GET /posts?title_like=server
+

全文搜索

q 参数,搜索所有的字段

GET /posts?q=internet
+

关系

_embed,获取子资源

GET /posts?_embed=comments
+GET /posts/1?_embed=comments
+

_expand,获取父资源

GET /comments?_expand=post
+GET /comments/1?_expand=post
+

创建或者获取嵌套资源

GET /posts/1/comments
+POST /posts/1/comments
+
Last Updated:
+ + + diff --git a/general/network/index.html b/general/network/index.html new file mode 100644 index 0000000..3597fd6 --- /dev/null +++ b/general/network/index.html @@ -0,0 +1,33 @@ + + + + + + + + + 计算机网络 | 瓢儿白施肥记 + + + + +
Last Updated:
+ + + diff --git a/imgs/logo.png b/imgs/logo.png new file mode 100644 index 0000000..231a61d Binary files /dev/null and b/imgs/logo.png differ diff --git a/index.html b/index.html new file mode 100644 index 0000000..cf9e6c0 --- /dev/null +++ b/index.html @@ -0,0 +1,33 @@ + + + + + + + + + 瓢儿白施肥记 + + + + +
瓢儿白施肥记

瓢儿白施肥记

时间的最大损失是拖延、期待和依赖将来

开始 GitHub open in new window

+ + +