diff --git a/404.html b/404.html new file mode 100644 index 0000000..4605c30 --- /dev/null +++ b/404.html @@ -0,0 +1,33 @@ + + +
+ + + + + +小结
1. JavaScript 由哪三部分组成?
2. JavaScript 和 ECMAScript 有什么关系?
ECMAScript 是 JavaScript 的标准化规范,JavaScript 是 ECMAScript 的一个实现
JavaScript 不限于 ECMA-262 所定义的那样,它包含以下几个部分:
ECMA-262 定义了什么?
ECMAScript 是实现 ECMA-262 这个规范描述的所有方面的一门语言,JavaScript 和 Adobe ActionScript 都实现了 ECMAScript
Web 浏览器是 ECMAScript 的一种宿主环境,宿主环境提供 ECMAScript 的基准实现和与环境自身交互必需的扩展,扩展(如 DOM)使用 ECMAScript 的核心类型和语法提供特定于环境的额外功能
文档对象模型(Document Object Model)是一个应用编程接口(API),DOM 通过创建表示文档的树,让开发者可以控制网页的结构和内容,使用 DOM API 可以轻松删除、添加、替换、修改节点
浏览器对象模型 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(`小结
1. script 元素有哪些属性?
async、defer、type、src、crossorigin、integrity
2. noscript
当浏览器不支持脚本或禁用脚本时,noscript 元素会显示出来
<script>
元素有以下属性:
使用 <script>
的方式有两种:
<script>
+ function sayScript() {
+ // 出现字符串 </script> 时,需要转义
+ console.log('<\\/script>')
+ }
+</script>
+
注:使用了 src 属性的 <script>
元素不应该在其 <script>
和 </script>
标签之间再包含额外的 JavaScript 代码,否则会忽略这些额外的代码
<head>
元素中<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>
元素可以包含任何可以出现在 <body>
元素中的 HTML 元素,它的作用是提供替代内容,只有以下情况下才会显示:
小结
1. 语法特点?
//
,多行注释 /* */
{}
2. let、var 和 const
3. 数据类型
ECMAScript 标准定义了 8 种数据类型:
4. null 和 undefined
5. 转布尔值为 false 的值''
、0
、NaN
、null
、undefined
6. 转数值
以下三个函数最终得到的都是十进制数或者 NaN
Number()
:
''
转为 0NaN
valueOf()
方法,然后依照前面的规则转换返回的值。如果转换的结果是 NaN
,则调用对象的 toString()
方法,然后再次依照前面的规则转换返回的值parseInt()
区别与 Number()
:
''
转为 NaNparseFloat()
区别与 parseInt()
:
console.log(parseFloat('0x6')) // 0
7. 转字符串
ECMA-262 以一个名为 ECMAScript 的伪语言(pseudo language)的形式,定义了 JavaScript 的所有这些方面
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 类型表示零或多个 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.for() 函数
Symbol.keyFor() 函数
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)
+
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 实例都有如下属性和方法:
递增 ++
、递减 --
操作符
后缀版与前缀版的主要区别在于,后缀版递增和递减语在语句被求值后再发生
作用于非数值时,会先使用 Number() 函数将其转换为数值,再进行操作
一元加和减
ECMAScript 中的所有数值都以 IEEE754 64 位格式存储,但是位操作符先把数值转换为 32 位整数,再进行操作,最后再将结果转换回 64 位
`,28),r=s("strong",null,"符号位",-1),d=a(`正值以真正的二进制格式存储,负值则以二进制补码形式存储
位操作应用到非数值,首先会使用 Number() 函数将该值转换为数值,然后再应用位操作
ECMAScript 中的所有整数都表示为有符号数。特殊值 NaN 和 Infinity 在位操作中都会被当成 0
按位非(~)
按位与(&)
按位或(|)
按位异或(^)
左移(<<)
有符号的右移(>>)
无符号右移(>>>)
逻辑非(!)
逻辑与(&&)
逻辑或(||)
如果操作数不是数值,会先使用 Number() 函数将其转换为数值,再进行操作
乘法(*)
除法(/)
求模(%)
指数操作符(**)
console.log(3 ** 2) // 9
squared **= 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 (expression) statement1 else statement2
+
do {
+ statement
+} while (expression)
+
while (expression) statement
+
for (initialization; expression; post - loop - expression) statement
+
for (property in expression) statement
+
ECMAScript 中对象的属性是无序的,因此通过 for-in 循环输出的属性名的顺序是不可预测的。换句话说,所有可枚举的属性都会返回一次,但返回的顺序可能会因浏览器而异
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 都可以与标签语句一起使用,返回代码中特定的位置。通常是在嵌套循环中,如:
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 (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 (expression) {
+ case value1:
+ statement
+ break
+ case value2:
+ statement
+ break
+ case value3:
+ statement
+ break
+ default:
+ statement
+}
+
最佳实践是函数要么返回值,要么不返回值。只在某个条件下返回值的函数会带来麻烦,尤其是调试时
严格模式对函数有一些限制:
result = variable instanceof constructor
执行上下文(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
+
引用值(或者对象)是某个特定引用类型的实例
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 类型有几个专门用于格式化日期的方法,它们都会返回字符串
`,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(`这些方法的输出与 toLocaleString() 和 toString() 一样,会因浏览器而异,因此不能用于在用户界面上一致的显示日期
let expression = /pattern/flags;
+
每当用到某个原始值的方法或属性时,后台都会创建一个相应原始包装类型的对象,从而暴露出操作原始值的各种方法
引用类型与原始包装类型的主要区别在于对象的生命周期。在通过 new 实例化引用类型后,得到的实例会在离开作用域时被销毁,而自动创建的原始值包装对象则只存在于访问它的那行代码执行期间。这意味着不能在运行时给原始值添加属性和方法,比如:
let name = 'Nicholas'
+name.age = 27
+console.log(name.age) // undefined
+
可以显示地使用 Boolean、Number 和 String 创建原始值包装对象,实例上调用 typeof 会返回 object
原始值和包装对象之间的区别:
要创建一个 Boolean 对象,就使用 Boolean 构造函数并传入 true 或 false
let booleanObject = new Boolean(true)
+
要创建一个 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() 方法与安全整数:
console.log(Number.isInteger(1)) // true
+console.log(Number.isInteger(1.0)) // true
+console.log(Number.isInteger(1.1)) // false
+
console.log(Number.isSafeInteger(Number.MAX_SAFE_INTEGER)) // true
+console.log(Number.isSafeInteger(Number.MAX_SAFE_INTEGER + 1)) // false
+
要创建一个 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
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
+})()
+
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 实例有两种方式:
let person = new Object()
+person.name = 'Nicholas'
+person.age = 29
+
let person = {
+ name: 'Nicholas',
+ age: 29,
+ 5: true, // 数值属性会自动转换成字符串
+}
+
在这个例子中,左大括号({)表示对象字面量开始,因为它出现在一个**表达式上下文(expression context)**中。在 ECMAScript 中,表达式上下文是指期待返回值的上下文。
同样是左大括号({),如果出现在**语句上下文(statement context)**中,比如 if 语句的条件后面,则表示一个语句块的开始
注意:在使用字面量表示法定义对象时,并不会实际调用 Object 构造函数
存取属性的两种方式:
点语法
中括号
从功能上讲,这两种存取属性的方式没有区别
使用中括号的主要优势:
- 可以通过变量来访问属性
- 属性名(可以包含非字母数字字符)中包含会导致语法错误的字符时,必须使用中括号语法
有两种基本的方式可以创建数组:
let colors = new Array(20)
let colors = new Array('red', 'blue', 'green')
创建数组时给构造函数传入一个值。如果是数值,则会创建一个长度为指定数值的数组;如果是其他类型,则会创建包含那个值的数组
在使用 Array 构造函数时,也可以省略 new 操作符,结果是一样的
与对象一样,在使用数组字面量表示法创建数组时,不会调用 Array 构造函数
Array 构造函数还有两个 ES6 新增的用于创建数组的静态方法:
from(),用于将类数组结构转换为数组实例
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()
fill(),向一个已有的数组中插入全部或部分相同的值
copyWithin() 按照指定范围浅复制数组中的部分内容,然后将它们插入到指定索引开始的位置(开始索引和结束索引同 fill())
const values = [0, 1, 5, 10, 15]
+values.sort()
+console.log(values) // [0, 1, 10, 15, 5]
+
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']
+
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(),用于创建一个包含原数组中部分项的新数组
splice(),主要目的是在数组中间插入元素,但有 3 种不同的方式使用这个方法
splice(0, 2)
splice(2, 0, 'red', 'green')
splice(2, 1, 'red', 'green')
ECMAScript 提供两类搜索数组的方法:按严格相等搜索和按断言函数搜索
1. 严格相等(===)
以下三个方法接收两个参数:要查找的项和(可选的)表示查找起点位置的索引
2. 断言函数
这两个方法也都接受第二个可选参数,用于指定断言函数内部 this 的值
ECMAScript 为数组定义了 5 个迭代方法,每个方法都接收两个参数:要在每一项上运行的函数和(可选的)运行该函数的作用域对象——影响 this 的值
ECMAScript 为数组提供了两个归并方法:reduce() 和 reduceRight()
定型数组(typed array)是 ECMAScript 新增的结构,目的是提升向原生库传输数据的效率,它所指的其实是一种特殊的包含数值类型的数组
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 是允许读写 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 类型 | 值的范围 |
---|---|---|---|---|
Int8 | 1 | 8 位有符号整数 | signed char | -128 ~ 127 |
Uint8 | 1 | 8 位无符号整数 | unsigned char | 0 ~ 255 |
Int16 | 2 | 16 位有符号整数 | short | -32768 ~ 32767 |
Uint16 | 2 | 16 位无符号整数 | unsigned short | 0 ~ 65535 |
Int32 | 4 | 32 位有符号整数 | int | -2147483648 ~ 2147483647 |
Uint32 | 4 | 32 位无符号整数 | unsigned int | 0 ~ 4294967295 |
Float32 | 4 | 32 位浮点数 | float | 1.2e-38 ~ 3.4e38 |
Float64 | 8 | 64 位浮点数 | double | 5.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. 定型数组的行为
定型数组的行为与普通数组类似,但也有一些不同之处
不能在定型数组中使用的方法:
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]
+
作为 ECMAScript6 的新增特性,Map 是一种新的集合类型,为这门语言带来了真正的键/值存储机制。Map 的大多数特性都可以通过 Object 来实现,但二者之间还是存在一些细微的差异:
创建和初始化:
// 使用嵌套数组初始化映射
+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 是 Map 的“兄弟”类型,其 API 也是 Map 的子集,但有一些重要的区别:
创建和初始化:
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 是 ECMAScript6 新增的集合类型,在很多方面都像是加强的 Map,因为它们的大多数 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 是 Set 的“兄弟”类型,其 API 也是 Set 的子集,但有一些重要的区别:
创建和初始化:
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 关键字之前会正常执行,遇到这个关键字之后,执行会停止,函数作用域的状态会被保留。停止执行的生成器函数只能通过在生成器对象上调用 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}
+
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. 原型与继承关系
确定原型与实例的关系:
instanceof
操作符,检测构造函数的 prototype 属性是否出现在某个实例对象的原型链上isPrototypeOf()
方法,如果传入的对象是实例的原型,则返回 true3. 原型链的问题
这些问题导致原型链基本不会单独使用
为了解决原型包含引用值导致继承问题,一种叫作“盗用构造函数(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‘
类是 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)已经抛弃混入模式,转向了组合模式(把方法提取到独立的类和辅助对象中,然后把它们组合起来,但不是继承)。这反映了那个众所周知的软件设计原则:“组合胜过继承”
ECMAScript 6 新增的代理和反射为开发者提供了拦截并向基本操作嵌入额外行为的能力
代理是目标对象的抽象
最简单的代理是空代理,即除了作为一个抽象的目标对象,什么也不做
代理使用 Proxy 构造函数创建,接收两个必填参数:目标对象和处理程序对象
const target = {
+ id: 'target',
+}
+
+const handler = {}
+
+const proxy = new Proxy(target, handler)
+
+console.log(target === proxy) // false
+
针对上述代码中
target
和proxy
两个对象,注意:
- 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() 操作以获取属性
所有捕获器都可以访问相应的参数,基于这些参数可以重建被捕获方法的原始行为
比如,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() 方法创建一个可撤销的代理,返回一个对象,包含两个属性:
注意:
- 撤销代理的操作时不可逆的
- 撤销函数(revoke())是幂等的,调用多少次结果都一样
- 撤销代理之后再调用代理会抛出 TypeError
某些情况下应该优先使用反射 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 操作和不变式
使用代理可以在代码中实现一些有用的编程模式
只需要引入 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()
+
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
+
设计稿 1080 x 1920,不确定实际屏幕的宽高比
考虑到一体机触摸体验,选用 Ant Design Mobile 高清适配,项目从 antd-mobile/2x 导入组件
// config.ts 中,配置一个从 antd-mobile 到 antd-mobile/2x 的别名
+alias: {
+ "antd-mobile": require.resolve("antd-mobile/2x")
+}
+
// 正好 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 # 全局样式文件
+
该 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 的设计稿,使用 vw
和 vh
单位进行适配。
@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;
+ }
+}
+
1rem = 100px
,即 20px 字体大小为 0.2rem
。/public/imgs/
url(/imgs/xxx.png)
,ts 文件中部分使用 import xxx from "/public/imgs/xxx.png"
(否则就尝试删掉 /public)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;
+};
+
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
+}
+
http.ListenAndServe(addr string, handler Handler) error
DefaultServeMux 是一个 multiplexer,即多路复用器,用于将请求分发到不同的处理器(可以看作是路由器)
http.ListenAndServe("localhost:8080", nil)
+
http.Server 是一个 struct
// serve := &http.Server{
+serve := http.Server{
+ Addr: "localhost:8080",
+ Handler: nil,
+}
+
+serve.ListenAndServe()
+
上面两种创建 Web Server 的方式,都只能使用 http。如果要用 https,则需要使用同理的 http.ListenAndServeTLS() 和 server.ListenAndServeTLS() 方法
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()
+}
+
DefaultServeMux 是一个 multiplexer,即多路复用器,用于将请求分发到不同的处理器(可以看作是路由器)
func Handle(pattern string, handler Handler)
+
不指定 Server struct 里面的 Handler 字段值(指定为 nil)
可以使用 http.Handle 将某个 Handler 附加到 DefaultServeMux 上
如果调用 http.Handle,实际上调用的是 DefaultServeMux 的 Handle 方法
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 函数就是那些行为与 handler 类似的函数:
http.HandleFunc 原理
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.NotFoundHandler
http.RedirectHandler
http.StripPrefix
http.TimeoutHandler
http.FileServer
type FileSystem interface {
+ Open(name string) (File, error)
+}
+
例子
通过 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")))
+
请求的 URL
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
http://localhost:8080/?name=abc&age=18
name=abc&age=18
URL Fragment
Request Header
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
type ReadCloser interface {
+ Reader
+ Closer
+}
+
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()
+
URL Query
name=abc&age=18
(实际查询的原始字符串)// 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()
+
Request 上的函数允许从 URL 或 / 和 Body 中提取数据,通过如下字段
Form 里面的数据是 key-value 对
通常的做法是:
<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 字段
<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 字段
<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 方法
上传文件
multipart/form-data 最常见的应用场景就是上传文件
<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 方法
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()
写入到 ResponseWriter
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>
+
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
+
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"]}
+
模板与模板引擎
模板引擎可以合并模板与上下文数据,产生最终的 HTML
Go 的模板引擎
关于模板
{{ . }}
使用模板引擎
<!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
// t, _ := template.ParseFiles("tmpl.html")
+t := template.New("tmpl.html")
+t, _ = t.ParseFiles("tmpl.html")
+
ParseGlob
t, _ := template.ParseGlob("*.html")
+
Parse
Lookup
Must
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
语法
{{ 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 }}
+
<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 }}
+
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 中设置变量
{{ range $key, $value := . }}
+{{ $key }}: {{ $value }}
+{{ end }}
+
管道
{{ p1 | p2 | p3 }}
<body>
+ <!-- 展示 12.35 -->
+ {{ 12.3456 | printf "%.2f" }}
+ <!-- 等价于 -->
+ {{ printf "%.2f" 12.3456 }}
+</body>
+
函数
内置函数
自定义函数
template.Funcs(funcMap FuncMap) *Template
type FuncMap map[string]interface{}
创建自定义函数的步骤:
<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)
+}
+
编译型语言
Go 是静态类型语言,一旦某个变量被声明,那么它的类型就无法再改变了
vscode:
插件
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
+
Go 里面的每个类型都有一个默认值,它称作零值
当声明变量却不对它进行初始化的时候,它的值就是零值
var price float64
+fmt.Println(price) // 0
+
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 种整数类型(不可以存小数部分,范围有限,通常根据数值范围来选取整数类型)
// 最常用的整数类型是 int
+var year int = 2018
+// 无符号整数类型是 uint
+var month uint = 12
+
下面三个语句是等价的:
year := 2018
+var year = 2018
+var year int = 2018
+
int 和 uint 是针对目标设备优化的类型
如果在比较老的 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
+
取值范围 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
+
使用 %b 格式化动词
var green uint8 = 3
+fmt.Printf("%08b\\n", green) // 00000011
+green++
+fmt.Printf("%08b\\n", green) // 00000100
+
math.MaxInt16
+math.MinInt64
+
浮点类型可以存储非常大的数值,但是精度不高
整型很精确,但是取值范围有限
使用指数表示的数,默认就是 float64 类型
var distance = 24e2
+fmt.Printf("%T", distance) // float64
+
如果需要存储非常大的整数,可以使用 math/big 包
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
+\`)
+
Unicode 联盟为超过 100 万个字符分配了相应的数值,这个数叫做 code point
为了表示这样的 unicode code point,Go 提供了 rune 类型,它是 int32 的别名
byte 是 unit 8 类型的别名,目的是用于二进制数据
类型别名就是同一个类型的另一个名字
也可以自定义类型别名,语法如下
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 本身是不可变的
message := "shalom"
+c := message[5]
+fmt.Printf("%c\\n", c) // m
+message[5] = 'd' // 报错
+
凯撒加密法是一种简单的加密方法,它是通过将每个字符移动固定数目的位置来实现的
c := 'a'
+c = c + 3
+fmt.Printf("%c", c) // d
+if c > 'z' {
+ c = c - 26
+}
+
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)
+}
+
len 是 Go 语言的一个内置函数
message := "uv vagreangvbany fcnpr fgngvba"
+fmt.Println(len(message)) // 32
+
本例中 len 返回 message 所占的 byte 数
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 关键字,可以遍历各种集合
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)
+
假设 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
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]
+
append 函数也是内置函数,它用于向 slice 里追加元素
dwarfs := []string{"Ceres", "Pluto", "Haumea", "Makemake", "Eris"}
+dwarfs = append(dwarfs, "Orcus")
+fmt.Println(dwarfs) // [Ceres Pluto Haumea Makemake Eris Orcus]
+
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]
+}
+
当 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 是 Go 提供的另外一种集合
声明 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
+
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?
+}
+
数组、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]
+
除非使用复合字面值来初始化 map,否则必须使用内置的 make 函数来为 map 分配空间
创建 map 时,make 函数可以接收一个或两个参数
使用 make 函数创建的 map 初始长度是 0
temperature := make(map[float64]int, 8)
+fmt.Println(len(temperature)) // 0
+
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)
+}
+
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)
+}
+
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]
+
为了将分散的零件组成一个完整的结构体,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}
+
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}
+
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}]
+
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 中被导出的字段(首字母大写)
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 提供了 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()
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
+}
+
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)
这种写法就是多此一举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 是一个名词,表示“无”或者“零”
在 Go 里,nil 是一个零值
如果一个指针没有明确的指向,那么它的值就是 nil
除了指针,nil 还是 slice 、map、channel、interface 和函数的零值
Go 语言的 nil 比以往语言的 null 更为友好,并且用的没那么频繁,但是仍需谨慎使用
如果指针没有明确的指向,那么程序将无法对其实施解引用
尝试解引用一个 nil 指针将导致程序崩溃
var nowhere *int
+fmt.Println(nowhere) // <nil>
+fmt.Println(*nowhere) // panic: runtime error: invalid memory address or nil pointer dereference
+
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
var fn func(a, b int) int
+fmt.Println(fn == nil) // true
+
检查函数值是否为 nil,并在有需要时提供默认行为
如果 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 并不相等,但它们通常可以替换使用
和 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
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
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 语言允许函数和方法同时返回多个值
按照惯例,函数在返回错误时,最后边的返回值应用来表示错误
调用函数后,应立即检查是否发生错误
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 关键字,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
+}
+
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)
+ }
+}
+
如果类型满足多个接口,那么类型断言可使它从一个接口类型转化为另一个接口类型
Go 里有一个和其他语言异常类似的机制:panic
实际上,panic 很少出现
创建 panic:调用内置的 panic 函数
panic("invalid operation") // panic 的参数可以是任意类型
+
通常,更推荐使用错误值,其次才是 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")
+
在 Go 中,独立的任务叫做 goroutine
在 Go 里,无需修改现有顺序式的代码,就可以通过 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)
+}
+
每次使用 go 关键字都会产生一个新的 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 可以在多个 goroutine 之间安全地传值
通道可以用作变量、函数参数、结构体字段...
创建通道用 make 函数,并指定其传输数据的类型 c := make(chan int)
使用左箭头操作符 <- 向通道发送值或从通道接收值
c <- 1
r := <- c
发送操作会等待直到另一个 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
+}
+
等待不同类型的值
time.After 函数,返回一个通道,该通道在指定时间后会接收到一个值(发送该值的 goroutine 是 Go 运行时的一部分)
select 和 switch 有点像
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 的情况下将永远等下去
如果不使用 make 初始化通道,那么通道变量的值就是 nil(零值)
对 nil 通道进行发送或接收不会引起 panic,但会导致永久阻塞
对 nil 哦那个到执行 close 函数,会引发 panic
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 时间
执行以下代码可知通道是否被关闭
从通道里面读取值,直到它关闭为止
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)
+}
+
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
+}
+
为保证互斥锁的安全使用,须遵循以下规则:
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)
+ }
+}
+
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('BUILD_ENV=XXX
命令不支持package.json 的 scripts
属性下配置命令 "BUILD_ENV=XXX
,切换到 Windows 环境下报错
'BUILD_ENV' 不是内部或外部命令,也不是可运行的程序
原因: Windows 环境不支持 BUILD_ENV=XXX
命令
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
+
若上述步骤不能解决,则细看究竟是哪个包导致的问题,分批拉依赖
“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"
+
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;
+
模拟数据接口
安装
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 /profile | profile 对象 |
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
+
编译型语言
Go 是静态类型语言,一旦某个变量被声明,那么它的类型就无法再改变了
vscode:
插件
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
+
Go 里面的每个类型都有一个默认值,它称作零值
当声明变量却不对它进行初始化的时候,它的值就是零值
var price float64
+fmt.Println(price) // 0
+
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 种整数类型(不可以存小数部分,范围有限,通常根据数值范围来选取整数类型)
// 最常用的整数类型是 int
+var year int = 2018
+// 无符号整数类型是 uint
+var month uint = 12
+
下面三个语句是等价的:
year := 2018
+var year = 2018
+var year int = 2018
+
int 和 uint 是针对目标设备优化的类型
如果在比较老的 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
+
取值范围 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
+
使用 %b 格式化动词
var green uint8 = 3
+fmt.Printf("%08b\n", green) // 00000011
+green++
+fmt.Printf("%08b\n", green) // 00000100
+
math.MaxInt16
+math.MinInt64
+
浮点类型可以存储非常大的数值,但是精度不高
整型很精确,但是取值范围有限
使用指数表示的数,默认就是 float64 类型
var distance = 24e2
+fmt.Printf("%T", distance) // float64
+
如果需要存储非常大的整数,可以使用 math/big 包
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
+`)
+
Unicode 联盟为超过 100 万个字符分配了相应的数值,这个数叫做 code point
为了表示这样的 unicode code point,Go 提供了 rune 类型,它是 int32 的别名
byte 是 unit 8 类型的别名,目的是用于二进制数据
类型别名就是同一个类型的另一个名字
也可以自定义类型别名,语法如下
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 本身是不可变的
message := "shalom"
+c := message[5]
+fmt.Printf("%c\n", c) // m
+message[5] = 'd' // 报错
+
凯撒加密法是一种简单的加密方法,它是通过将每个字符移动固定数目的位置来实现的
c := 'a'
+c = c + 3
+fmt.Printf("%c", c) // d
+if c > 'z' {
+ c = c - 26
+}
+
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)
+}
+
len 是 Go 语言的一个内置函数
message := "uv vagreangvbany fcnpr fgngvba"
+fmt.Println(len(message)) // 32
+
本例中 len 返回 message 所占的 byte 数
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 关键字,可以遍历各种集合
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)
+
假设 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
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]
+
append 函数也是内置函数,它用于向 slice 里追加元素
dwarfs := []string{"Ceres", "Pluto", "Haumea", "Makemake", "Eris"}
+dwarfs = append(dwarfs, "Orcus")
+fmt.Println(dwarfs) // [Ceres Pluto Haumea Makemake Eris Orcus]
+
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]
+}
+
当 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 是 Go 提供的另外一种集合
声明 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
+
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?
+}
+
数组、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]
+
除非使用复合字面值来初始化 map,否则必须使用内置的 make 函数来为 map 分配空间
创建 map 时,make 函数可以接收一个或两个参数
使用 make 函数创建的 map 初始长度是 0
temperature := make(map[float64]int, 8)
+fmt.Println(len(temperature)) // 0
+
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)
+}
+
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)
+}
+
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]
+
为了将分散的零件组成一个完整的结构体,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}
+
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}
+
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}]
+
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 中被导出的字段(首字母大写)
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 提供了 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()
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
+}
+
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)
这种写法就是多此一举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 是一个名词,表示“无”或者“零”
在 Go 里,nil 是一个零值
如果一个指针没有明确的指向,那么它的值就是 nil
除了指针,nil 还是 slice 、map、channel、interface 和函数的零值
Go 语言的 nil 比以往语言的 null 更为友好,并且用的没那么频繁,但是仍需谨慎使用
如果指针没有明确的指向,那么程序将无法对其实施解引用
尝试解引用一个 nil 指针将导致程序崩溃
var nowhere *int
+fmt.Println(nowhere) // <nil>
+fmt.Println(*nowhere) // panic: runtime error: invalid memory address or nil pointer dereference
+
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
var fn func(a, b int) int
+fmt.Println(fn == nil) // true
+
检查函数值是否为 nil,并在有需要时提供默认行为
如果 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 并不相等,但它们通常可以替换使用
和 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
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
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 语言允许函数和方法同时返回多个值
按照惯例,函数在返回错误时,最后边的返回值应用来表示错误
调用函数后,应立即检查是否发生错误
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 关键字,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
+}
+
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)
+ }
+}
+
如果类型满足多个接口,那么类型断言可使它从一个接口类型转化为另一个接口类型
Go 里有一个和其他语言异常类似的机制:panic
实际上,panic 很少出现
创建 panic:调用内置的 panic 函数
panic("invalid operation") // panic 的参数可以是任意类型
+
通常,更推荐使用错误值,其次才是 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")
+
在 Go 中,独立的任务叫做 goroutine
在 Go 里,无需修改现有顺序式的代码,就可以通过 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)
+}
+
每次使用 go 关键字都会产生一个新的 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 可以在多个 goroutine 之间安全地传值
通道可以用作变量、函数参数、结构体字段...
创建通道用 make 函数,并指定其传输数据的类型 c := make(chan int)
使用左箭头操作符 <- 向通道发送值或从通道接收值
c <- 1
r := <- c
发送操作会等待直到另一个 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
+}
+
等待不同类型的值
time.After 函数,返回一个通道,该通道在指定时间后会接收到一个值(发送该值的 goroutine 是 Go 运行时的一部分)
select 和 switch 有点像
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 的情况下将永远等下去
如果不使用 make 初始化通道,那么通道变量的值就是 nil(零值)
对 nil 通道进行发送或接收不会引起 panic,但会导致永久阻塞
对 nil 哦那个到执行 close 函数,会引发 panic
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 时间
执行以下代码可知通道是否被关闭
从通道里面读取值,直到它关闭为止
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)
+}
+
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
+}
+
为保证互斥锁的安全使用,须遵循以下规则:
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)
+ }
+}
+
Go 通过提供 goroutine 作为核心概念,消除了对中心循环的需求
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
+}
+
http.ListenAndServe(addr string, handler Handler) error
DefaultServeMux 是一个 multiplexer,即多路复用器,用于将请求分发到不同的处理器(可以看作是路由器)
http.ListenAndServe("localhost:8080", nil)
+
http.Server 是一个 struct
// serve := &http.Server{
+serve := http.Server{
+ Addr: "localhost:8080",
+ Handler: nil,
+}
+
+serve.ListenAndServe()
+
上面两种创建 Web Server 的方式,都只能使用 http。如果要用 https,则需要使用同理的 http.ListenAndServeTLS() 和 server.ListenAndServeTLS() 方法
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()
+}
+
DefaultServeMux 是一个 multiplexer,即多路复用器,用于将请求分发到不同的处理器(可以看作是路由器)
func Handle(pattern string, handler Handler)
+
不指定 Server struct 里面的 Handler 字段值(指定为 nil)
可以使用 http.Handle 将某个 Handler 附加到 DefaultServeMux 上
如果调用 http.Handle,实际上调用的是 DefaultServeMux 的 Handle 方法
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 函数就是那些行为与 handler 类似的函数:
http.HandleFunc 原理
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.NotFoundHandler
http.RedirectHandler
http.StripPrefix
http.TimeoutHandler
http.FileServer
type FileSystem interface {
+ Open(name string) (File, error)
+}
+
例子
通过 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")))
+
请求的 URL
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
http://localhost:8080/?name=abc&age=18
name=abc&age=18
URL Fragment
Request Header
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
type ReadCloser interface {
+ Reader
+ Closer
+}
+
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()
+
URL Query
name=abc&age=18
(实际查询的原始字符串)// 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()
+
Request 上的函数允许从 URL 或 / 和 Body 中提取数据,通过如下字段
Form 里面的数据是 key-value 对
通常的做法是:
<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 字段
<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 字段
<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 方法
上传文件
multipart/form-data 最常见的应用场景就是上传文件
<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 方法
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()
写入到 ResponseWriter
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>
+
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
+
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"]}
+
模板与模板引擎
模板引擎可以合并模板与上下文数据,产生最终的 HTML
Go 的模板引擎
关于模板
{{ . }}
使用模板引擎
<!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
// t, _ := template.ParseFiles("tmpl.html")
+t := template.New("tmpl.html")
+t, _ = t.ParseFiles("tmpl.html")
+
ParseGlob
t, _ := template.ParseGlob("*.html")
+
Parse
Lookup
Must
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
语法
{{ 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 }}
+
<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 }}
+
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 中设置变量
{{ range $key, $value := . }}
+{{ $key }}: {{ $value }}
+{{ end }}
+
管道
{{ p1 | p2 | p3 }}
<body>
+ <!-- 展示 12.35 -->
+ {{ 12.3456 | printf "%.2f" }}
+ <!-- 等价于 -->
+ {{ printf "%.2f" 12.3456 }}
+</body>
+
函数
内置函数
自定义函数
template.Funcs(funcMap FuncMap) *Template
type FuncMap map[string]interface{}
创建自定义函数的步骤:
<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)
+}
+
设计稿 1080 x 1920,不确定实际屏幕的宽高比
考虑到一体机触摸体验,选用 Ant Design Mobile 高清适配,项目从 antd-mobile/2x 导入组件
// config.ts 中,配置一个从 antd-mobile 到 antd-mobile/2x 的别名
+alias: {
+ "antd-mobile": require.resolve("antd-mobile/2x")
+}
+
// 正好 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 # 全局样式文件
+
该 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 的设计稿,使用 vw
和 vh
单位进行适配。
@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;
+ }
+}
+
1rem = 100px
,即 20px 字体大小为 0.2rem
。/public/imgs/
url(/imgs/xxx.png)
,ts 文件中部分使用 import xxx from "/public/imgs/xxx.png"
(否则就尝试删掉 /public)getPath(所有页面组件 => 所有页面组件.跳转的页面组件名称, {参数1, 参数2, ...})
即可拿到要跳转的路径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;
+};
+
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;
+
小结
1. JavaScript 由哪三部分组成?
2. JavaScript 和 ECMAScript 有什么关系?
ECMAScript 是 JavaScript 的标准化规范,JavaScript 是 ECMAScript 的一个实现
JavaScript 不限于 ECMA-262 所定义的那样,它包含以下几个部分:
ECMA-262 定义了什么?
ECMAScript 是实现 ECMA-262 这个规范描述的所有方面的一门语言,JavaScript 和 Adobe ActionScript 都实现了 ECMAScript
Web 浏览器是 ECMAScript 的一种宿主环境,宿主环境提供 ECMAScript 的基准实现和与环境自身交互必需的扩展,扩展(如 DOM)使用 ECMAScript 的核心类型和语法提供特定于环境的额外功能
文档对象模型(Document Object Model)是一个应用编程接口(API),DOM 通过创建表示文档的树,让开发者可以控制网页的结构和内容,使用 DOM API 可以轻松删除、添加、替换、修改节点
浏览器对象模型 BOM,用于支持访问和操作浏览器的窗口
小结
1. script 元素有哪些属性?
async、defer、type、src、crossorigin、integrity
2. noscript
当浏览器不支持脚本或禁用脚本时,noscript 元素会显示出来
<script>
元素有以下属性:
使用 <script>
的方式有两种:
<script>
+ function sayScript() {
+ // 出现字符串 </script> 时,需要转义
+ console.log('<\/script>')
+ }
+</script>
+
注:使用了 src 属性的 <script>
元素不应该在其 <script>
和 </script>
标签之间再包含额外的 JavaScript 代码,否则会忽略这些额外的代码
<head>
元素中<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>
元素可以包含任何可以出现在 <body>
元素中的 HTML 元素,它的作用是提供替代内容,只有以下情况下才会显示:
小结
1. 语法特点?
//
,多行注释 /* */
{}
2. let、var 和 const
3. 数据类型
ECMAScript 标准定义了 8 种数据类型:
4. null 和 undefined
5. 转布尔值为 false 的值''
、0
、NaN
、null
、undefined
6. 转数值
以下三个函数最终得到的都是十进制数或者 NaN
Number()
:
''
转为 0NaN
valueOf()
方法,然后依照前面的规则转换返回的值。如果转换的结果是 NaN
,则调用对象的 toString()
方法,然后再次依照前面的规则转换返回的值parseInt()
区别与 Number()
:
''
转为 NaNparseFloat()
区别与 parseInt()
:
console.log(parseFloat('0x6')) // 0
7. 转字符串
ECMA-262 以一个名为 ECMAScript 的伪语言(pseudo language)的形式,定义了 JavaScript 的所有这些方面
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 类型表示零或多个 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.for() 函数
Symbol.keyFor() 函数
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)
+
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 实例都有如下属性和方法:
递增 ++
、递减 --
操作符
后缀版与前缀版的主要区别在于,后缀版递增和递减语在语句被求值后再发生
作用于非数值时,会先使用 Number() 函数将其转换为数值,再进行操作
一元加和减
ECMAScript 中的所有数值都以 IEEE754 64 位格式存储,但是位操作符先把数值转换为 32 位整数,再进行操作,最后再将结果转换回 64 位
有符号整数使用 32 位的前 31 位表示整数值,第 32 位(第一位 表示 20)表示符号,0 表示正数,1 表示负数。这一位称为 符号位,它的值决定了数值其余部分的格式
正值以真正的二进制格式存储,负值则以二进制补码形式存储
位操作应用到非数值,首先会使用 Number() 函数将该值转换为数值,然后再应用位操作
ECMAScript 中的所有整数都表示为有符号数。特殊值 NaN 和 Infinity 在位操作中都会被当成 0
按位非(~)
按位与(&)
按位或(|)
按位异或(^)
左移(<<)
有符号的右移(>>)
无符号右移(>>>)
逻辑非(!)
逻辑与(&&)
逻辑或(||)
如果操作数不是数值,会先使用 Number() 函数将其转换为数值,再进行操作
乘法(*)
除法(/)
求模(%)
指数操作符(**)
console.log(3 ** 2) // 9
squared **= 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 (expression) statement1 else statement2
+
do {
+ statement
+} while (expression)
+
while (expression) statement
+
for (initialization; expression; post - loop - expression) statement
+
for (property in expression) statement
+
ECMAScript 中对象的属性是无序的,因此通过 for-in 循环输出的属性名的顺序是不可预测的。换句话说,所有可枚举的属性都会返回一次,但返回的顺序可能会因浏览器而异
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 都可以与标签语句一起使用,返回代码中特定的位置。通常是在嵌套循环中,如:
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 (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 (expression) {
+ case value1:
+ statement
+ break
+ case value2:
+ statement
+ break
+ case value3:
+ statement
+ break
+ default:
+ statement
+}
+
最佳实践是函数要么返回值,要么不返回值。只在某个条件下返回值的函数会带来麻烦,尤其是调试时
严格模式对函数有一些限制:
result = variable instanceof constructor
执行上下文(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
+
通过 const 和 let 声明提升性能
const 和 let 都以块(而非函数)为作用域,所以相比于使用 var 声明,使用这个两个关键字可能会更早地让垃圾回收程序介入,尽早回收应该回收的内存
隐藏类和删除操作
V8 会将创建的对象与隐藏类关联起来,以跟踪他们的属性特征,能够共享相同隐藏类的对象性能会更好:
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')
+
内存泄露
静态分配与对象池
为了提升 JavaScript 性能,一个关键的问题就是如何减少浏览器执行垃圾回收的次数
浏览器决定何时运行垃圾回收程序的一个标准就是对象更替的速度,越快越频繁
为了提升性能,V8 引入了对象池,它会对一些常见的对象结构进行缓存,当需要创建这些对象时,就会从对象池中取出,而不是重新创建
引用值(或者对象)是某个特定引用类型的实例
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() 一样,会因浏览器而异,因此不能用于在用户界面上一致的显示日期
let expression = /pattern/flags;
+
每当用到某个原始值的方法或属性时,后台都会创建一个相应原始包装类型的对象,从而暴露出操作原始值的各种方法
引用类型与原始包装类型的主要区别在于对象的生命周期。在通过 new 实例化引用类型后,得到的实例会在离开作用域时被销毁,而自动创建的原始值包装对象则只存在于访问它的那行代码执行期间。这意味着不能在运行时给原始值添加属性和方法,比如:
let name = 'Nicholas'
+name.age = 27
+console.log(name.age) // undefined
+
可以显示地使用 Boolean、Number 和 String 创建原始值包装对象,实例上调用 typeof 会返回 object
原始值和包装对象之间的区别:
要创建一个 Boolean 对象,就使用 Boolean 构造函数并传入 true 或 false
let booleanObject = new Boolean(true)
+
要创建一个 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() 方法与安全整数:
console.log(Number.isInteger(1)) // true
+console.log(Number.isInteger(1.0)) // true
+console.log(Number.isInteger(1.1)) // false
+
console.log(Number.isSafeInteger(Number.MAX_SAFE_INTEGER)) // true
+console.log(Number.isSafeInteger(Number.MAX_SAFE_INTEGER + 1)) // false
+
要创建一个 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
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
+})()
+
ECMAScript 提供了 Math 对象作为保存数学公式、信息和计算的地方。Math 对象提供了一些辅助计算的属性和方法
1. Math 对象属性
2. min() 和 max() 方法
接收任意多个参数
3. 舍入方法
4. random 方法
返回大于等于 0 小于 1 的一个随机数
可以基于如下公式使用 Math.random() 从一组整数中随机选择一个整数:
Math.floor(Math.random() * 可能值的总数 + 第一个可能的值)
+
5. 其他方法
显示的创建 Object 实例有两种方式:
let person = new Object()
+person.name = 'Nicholas'
+person.age = 29
+
let person = {
+ name: 'Nicholas',
+ age: 29,
+ 5: true, // 数值属性会自动转换成字符串
+}
+
在这个例子中,左大括号({)表示对象字面量开始,因为它出现在一个**表达式上下文(expression context)**中。在 ECMAScript 中,表达式上下文是指期待返回值的上下文。
同样是左大括号({),如果出现在**语句上下文(statement context)**中,比如 if 语句的条件后面,则表示一个语句块的开始
注意:在使用字面量表示法定义对象时,并不会实际调用 Object 构造函数
存取属性的两种方式:
点语法
中括号
从功能上讲,这两种存取属性的方式没有区别
使用中括号的主要优势:
- 可以通过变量来访问属性
- 属性名(可以包含非字母数字字符)中包含会导致语法错误的字符时,必须使用中括号语法
有两种基本的方式可以创建数组:
let colors = new Array(20)
let colors = new Array('red', 'blue', 'green')
创建数组时给构造函数传入一个值。如果是数值,则会创建一个长度为指定数值的数组;如果是其他类型,则会创建包含那个值的数组
在使用 Array 构造函数时,也可以省略 new 操作符,结果是一样的
与对象一样,在使用数组字面量表示法创建数组时,不会调用 Array 构造函数
Array 构造函数还有两个 ES6 新增的用于创建数组的静态方法:
from(),用于将类数组结构转换为数组实例
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()
fill(),向一个已有的数组中插入全部或部分相同的值
copyWithin() 按照指定范围浅复制数组中的部分内容,然后将它们插入到指定索引开始的位置(开始索引和结束索引同 fill())
const values = [0, 1, 5, 10, 15]
+values.sort()
+console.log(values) // [0, 1, 10, 15, 5]
+
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']
+
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(),用于创建一个包含原数组中部分项的新数组
splice(),主要目的是在数组中间插入元素,但有 3 种不同的方式使用这个方法
splice(0, 2)
splice(2, 0, 'red', 'green')
splice(2, 1, 'red', 'green')
ECMAScript 提供两类搜索数组的方法:按严格相等搜索和按断言函数搜索
1. 严格相等(===)
以下三个方法接收两个参数:要查找的项和(可选的)表示查找起点位置的索引
2. 断言函数
这两个方法也都接受第二个可选参数,用于指定断言函数内部 this 的值
ECMAScript 为数组定义了 5 个迭代方法,每个方法都接收两个参数:要在每一项上运行的函数和(可选的)运行该函数的作用域对象——影响 this 的值
ECMAScript 为数组提供了两个归并方法:reduce() 和 reduceRight()
定型数组(typed array)是 ECMAScript 新增的结构,目的是提升向原生库传输数据的效率,它所指的其实是一种特殊的包含数值类型的数组
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 是允许读写 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 类型 | 值的范围 |
---|---|---|---|---|
Int8 | 1 | 8 位有符号整数 | signed char | -128 ~ 127 |
Uint8 | 1 | 8 位无符号整数 | unsigned char | 0 ~ 255 |
Int16 | 2 | 16 位有符号整数 | short | -32768 ~ 32767 |
Uint16 | 2 | 16 位无符号整数 | unsigned short | 0 ~ 65535 |
Int32 | 4 | 32 位有符号整数 | int | -2147483648 ~ 2147483647 |
Uint32 | 4 | 32 位无符号整数 | unsigned int | 0 ~ 4294967295 |
Float32 | 4 | 32 位浮点数 | float | 1.2e-38 ~ 3.4e38 |
Float64 | 8 | 64 位浮点数 | double | 5.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. 定型数组的行为
定型数组的行为与普通数组类似,但也有一些不同之处
不能在定型数组中使用的方法:
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]
+
作为 ECMAScript6 的新增特性,Map 是一种新的集合类型,为这门语言带来了真正的键/值存储机制。Map 的大多数特性都可以通过 Object 来实现,但二者之间还是存在一些细微的差异:
创建和初始化:
// 使用嵌套数组初始化映射
+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 是 Map 的“兄弟”类型,其 API 也是 Map 的子集,但有一些重要的区别:
创建和初始化:
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 是 ECMAScript6 新增的集合类型,在很多方面都像是加强的 Map,因为它们的大多数 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 是 Set 的“兄弟”类型,其 API 也是 Set 的子集,但有一些重要的区别:
创建和初始化:
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 循环,兼容扩展操作符...
小结
迭代器是一个可以由任意对象实现的接口,支持连续获取对象产出的每一个值。任何实现 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 关键字之前会正常执行,遇到这个关键字之后,执行会停止,函数作用域的状态会被保留。停止执行的生成器函数只能通过在生成器对象上调用 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}
+
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. 原型与继承关系
确定原型与实例的关系:
instanceof
操作符,检测构造函数的 prototype 属性是否出现在某个实例对象的原型链上isPrototypeOf()
方法,如果传入的对象是实例的原型,则返回 true3. 原型链的问题
这些问题导致原型链基本不会单独使用
为了解决原型包含引用值导致继承问题,一种叫作“盗用构造函数(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‘
类是 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)已经抛弃混入模式,转向了组合模式(把方法提取到独立的类和辅助对象中,然后把它们组合起来,但不是继承)。这反映了那个众所周知的软件设计原则:“组合胜过继承”
ECMAScript 6 新增的代理和反射为开发者提供了拦截并向基本操作嵌入额外行为的能力
代理是目标对象的抽象
最简单的代理是空代理,即除了作为一个抽象的目标对象,什么也不做
代理使用 Proxy 构造函数创建,接收两个必填参数:目标对象和处理程序对象
const target = {
+ id: 'target',
+}
+
+const handler = {}
+
+const proxy = new Proxy(target, handler)
+
+console.log(target === proxy) // false
+
针对上述代码中
target
和proxy
两个对象,注意:
- 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() 操作以获取属性
所有捕获器都可以访问相应的参数,基于这些参数可以重建被捕获方法的原始行为
比如,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() 方法创建一个可撤销的代理,返回一个对象,包含两个属性:
注意:
- 撤销代理的操作时不可逆的
- 撤销函数(revoke())是幂等的,调用多少次结果都一样
- 撤销代理之后再调用代理会抛出 TypeError
某些情况下应该优先使用反射 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 操作和不变式
使用代理可以在代码中实现一些有用的编程模式
函数实际上是对象,每个函数都是 Function 类型的实例
不推荐使用 Function 构造函数来定义函数 let sum = new Function("num1", "num2", "return num1 + num2")
,因为这种方式会导致解析两次代码(第一次是解析常规 ECMAScript 代码,第二次是解析传入构造函数的字符串)
BUILD_ENV=XXX
命令不支持package.json 的 scripts
属性下配置命令 "BUILD_ENV=XXX
,切换到 Windows 环境下报错
'BUILD_ENV' 不是内部或外部命令,也不是可运行的程序
原因: Windows 环境不支持 BUILD_ENV=XXX
命令
解决:开启 VSCode
的 wls
命令行,使用 Linux
环境
# Get started using Visual Studio Code with Windows Subsystem for Linux
Unable to authenticate, need: Basic realm="aliyun"
解决:npm login
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
+
若上述步骤不能解决,则细看究竟是哪个包导致的问题,分批拉依赖
“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"
+
只需要引入 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()
+
好使的教程 --> 猴子都能懂的 git 入门
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
+
模拟数据接口
安装
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 /profile | profile 对象 |
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
+