【仅记录自己看书期间需要记忆的点,个人已掌握的知识点不会在这里出现,ps:书中知识点并没有这么简单描述(ps:哈哈哈可能排版有点问题 因为文章太长了 我只管记录 没有注重排版 才看到60% 就已有1.9w字了 等后期看完再修改排版)】
第一二章
略...
第三章 类型、值和变量
数值
JavaScript使用IEEE 754标准定义的64位浮点格式表示数值
数值范围: -2^53 ~ 2^53
整数字面量
JavaScript支持写法:
- 数字序列
1
0
10000
- 十六进制值
0xff
0xBADCAFE
- 二进制或八进制(ES6之后)
0b10101
0o377
浮点字面量
表示方式:
3.12313
6.02e23 // 6.02 * 10 ^ 23
1.473E-32 // 1.473 * 10 ^-32
在2020年后,数值字面量支持下划线作为分隔符
let a = 1_000_000 // 作为千位分隔符
let bytes = 0x89_AB_CD_EF // 字节分隔符
let bits = 0b0001_1101_0111 // 半字节分隔符
let f = 0.123_456 // 小数部分
Javascript算术
在ES6之后定义新的函数:
注意:
- 上溢:任何数加减乘除无穷数结果还是无穷数(只是符号可能相反)
- 下溢(数值操作结果比最小可表示数值更接近0的情况下,js返回0):如果下溢来自负数,js会返回‘负零’
- 被零除:被零除在js中不是错误,只会简单返回无穷或负无穷(0除以0 会返回NaN);无穷除以无穷、负数平方根或者无法转换为数值的非数值作为算术操作符,结果也是NaN
对于NaN的判断:
NaN === NaN
// false
NaN !== NaN
// true
Number.isNaN(NaN)
// true (判断是不是NaN)
Number.isNaN(3)
// false
Number.isFinite(Infinity)
// false (判断是不是无穷)
Number.isFinite(1000)
// true
Number.isInteger(10.8)
// false (判断是不是整数)
Number.isInteger(10)
// true
0和-0的区别:
也就作为除数使用时有区别
0 === -0
// true
1/0 === 1/-0
// false
1/0
// Infinity
1/-0
// -Infinity
浮点数计算和BigInt表示任意精度整数
Javascript存在误差,是由于二进制浮点数的变成语言造成的
就是常说的0.1+0.2 !== 0.3 的问题
BigInt写法: 一串数字后面跟小写字母n。默认情况下,基数是10,但可以通过前缀0b、0o、0x来表示二进制、八进制、十六进制BigInt
BigInt值的算术运算和常规JavaScript数值的运算类似,只不过出发会丢弃余数并向下舍入
1001n +2001n
// 3002n
2001n - 1n
// 2000n
2001n * 3n
// 6003n
2001n / 4n
// 500n
2001 / 4
// 500.25
注意: 算术计算时不能混用JavaScript数值类型和BigInt
但是比较操作符可以混用二者
字符串
操作字符串的API:
let s = 'hello, hahabboom!'
// undefined
s.length
// 17
s.substring(0, 5)
//"hello"
s.substring(2)
// "llo, hahabboom!"
s.slice(0, 5)
// "hello"
s.slice(-5)
// "boom!"
s.split('.')
// ["hello, hahabboom!"]
s.split(',')
// ["hello", " hahabboom!"]
s.indexOf('o')
// 4
s.lastIndexOf('o')
// 14
s.indexOf('o', 5)
// 13
s.startsWith('hell')
// true
s.endsWith('!')
// true
s.includes('oo')
// true
s.replace('ha', 'ma')
// "hello, mahabboom!"
s.toLocaleUpperCase()
// "HELLO, HAHABBOOM!"
s.normalize() // unicode NFC归一化:ES6新增
// "hello, hahabboom!"
s.normalize('NFD') // NFD归一化,还有NFKC和NFKD
// "hello, hahabboom!"
s.charAt('8')
// "a"
s.charCodeAt(0)
// 104
s.codePointAt(0)
// 104 (ES6,适用于码点大于16位的情形)
s.padStart(3)
// "hello, hahabboom!"(因为length已经大于3了,没变化)
s.padStart(20) // 左侧添加空格,让长度变成20
// " hello, hahabboom!"
s.padEnd(20) // 右侧添加空格,让长度变成20
// "hello, hahabboom! "
s.padStart(20, '$')
// "$$$hello, hahabboom!"
s.padEnd(20, '$')
// "hello, hahabboom!$$$"
// 去除空格系列
' s '.trim()
// "s"
' s '.trimStart()
// "s "
' s '.trimEnd()
// " s"
s.concat('~~~~') // 可以用+
// "hello, hahabboom!~~~~"
s.repeat(3)
// "hello, hahabboom!hello, hahabboom!hello, hahabboom!"
布尔值
以下几种会被转成false:
undefined
null
0
-0
NaN
'' // 空字符串
null 与 undefined
undefined表示一种系统级别、意料之外或类似错误的没有值
null表示程序级别、正常或意料之中的没有值
Symbol
Symbol是ES6新增的一种原始类型,用作非字符串的属性名。
- Symbol类型没有字面量语法
- 要获取一个Symbol值,需要调用Symbol函数
- Symbol()函数永远不会返回相同的值,即使每次传入的参数都一样,这意味着可以将调用Symbol()取得的符号值安全地用于为对象添加新属性,而无须担心可能重写已有的同名属性
Symbol.for() 和 Symbol()完全不同:Symbol()永远不会返回相同的值,而在以相同的字符串调用时Symbol.for()始终返回相同的值
let sym1 = Symbol('test')
// undefined
let sym2 = Symbol('test')
// undefined
sym1 === sym2
// false
Symbol.keyFor(sym1)
// undefined
let sym3 = Symbol.for('test2')
// undefined
let sym4 = Symbol.for('test2')
// undefined
sym3 === sym4
// true
sym3.toString()
// "Symbol(test2)"
Symbol.keyFor(sym3)
// "test2"
全局对象
JavaScript解释器启动之后,都会创建一个新的全局对象并为其添加一组初始属性:
undefined、Infinity、NaN 这样的全局变量
isNaN()、parseInt()、eval()这样的全局函数
Date()、RegExp()、String()、Object()、Array()这样的构造函数
Math、JSON这样的全局对象
在Node, 全局对象为global 在浏览器,全局对象为window
ES2020,定义globalThis作为在任何上下文中引用全局对象的标准方式
不可修改的原始值与可修改的对象引用
JavaScript的原始值类型:
undefined、null、Boolean、Number、String、Symbol、BigInt
引用类型:Object
(任何constructed对象实例的特殊非数据结构类型,也用做数据结构:new Object,new Array],new Map,new [Set],new WeakMap,new WeakSet,new Date,和几乎所有通过 new keyword创建的东西。)
类型转换
toFixed():把数值转换为字符串可以指定小数点后面的位数,不使用指数计数法
toExponential():使用指数计数法将数值转换为字符串,结果是小数点前一位,小数点后为指定位数(有效数字个数比你指定的值多一位)
toPrecision():按照指定的有效数字个数将数值转换为字符串,如果有效数字个数不足以显示数值的整数部分,将会使用指数计数法
Number():只能处理基数为10的整数,不允许末尾出现无关字符
parseInt():只解析整数,如果字符以0x或0X开头,会将其解析为十六进制
parseFloat():既解析整数也解析浮点数
parseInt和parseFloat都会跳过开头空格,尽量多的解析数字字符,忽略后面的无关字符;如果第一个非空格字符不是有效的数值字面量,会返回NaN
parseInt可接受第二个参数,用于解析数值的底数,合法数值是2到36
parseInt('11', 8)
// 9 1*8+1
parseInt('11', 2)
// 3 1*2+1
parseInt('ff', 16)
// 255 15*16+15
parseInt('077', 8)
// 63 7*8+7
parseInt('077', 10)
// 77 7*10+7
对象到原始值转换
Javascript规范定义了对象到原始值转换的3种算法:
- 偏字符串(该算法返回原始值,而且只要可能就返回字符串) 首先尝试toString()方法,如果这个方法有定义且返回原始值,则JavaScript使用该原始值(即使这个值不是字符串)。如果toString不存在,或者存在但返回对象,则JavaScript尝试valueOf方法。如果这个方法存在且返回原始值,则JavaScript使用该值。否则转换失败,报TypeError
- 偏数值(该算法返回原始值,而且只要可能就返回数值) 与偏字符串类似,只不过先尝试valueOf,再尝试toString
- 无偏好(该算法不倾向于任何原始值类型,而是由类定义自己的转换规则。JavaScript内置类型处理Date类都是先了偏数值算法,Date类实现了偏字符串算法) 取决于被转换对象的类。如果是一个Date对象,则JavaScript使用偏字符串算法,如果是其他类型的对象,则使用偏数值算法
对象->原始值类型:
-
对象转为布尔值: 所有对象都转换为true,直接适用于所有对象,包括空数组,甚至包括new Boolean(false)这样的包装对象
-
对象转为字符串: JavaScript会首先使用偏字符串算法将它转换为一个原始值,然后将得到的原始值再转换为字符串
-
对象转换为数值: JavaScript会首先使用偏数值算法将它转换为一个原始值,然后将得到的原始值再转换为数值
-
操作符转换特例:
- JavaScript中的+操作符执行数值加法和字符串拼接。如果一个操作数都是对象,那JavaScript会使用无偏好算法将对象转换为原始值。如果两个操作数都是原始值,则会检查他们的类型。如果有一个参数是字符串,则把另一个原始值转换为字符串并拼接两个字符串。否则,把两个参数都转换为数值并把它们相加。
- == 和!=操作符允许类型转换的宽松方式执行相等和不相等测试
- 关系操作符<、<=、>、>=的顺序,既可以比较数值,也可以比较字符串
解构赋值
在解构赋值中,等号右端的值是数组或对象,而左手端通过模拟数组或对象字面量语法指定一个或多个变量,在解构赋值发生时,会从右侧的值中提取(解构)出一个或多个值,并保存在左侧列出的变量中。
第四章
条件式属性访问:
let obj = {a: {b: 'c'}}
obj?.a?.b
// "c"
let temp = 'a'
// undefined
obj?.[temp]?.b
// "c"
条件式调用:
使用?.()方法调用语法,如果左侧表达式求值为undefined或者null,则整个调用表达式求值为undefined,不会抛出异常
defore:
if (log){
log()
}
after:
log?.()
操作符
除>>>之外的所有位操作符都可以作用于常规数据或BigInt
- 按位与 只有两个操作数对应的位都为一,结果的值才为1
0x1234 & 0x00ff = 0x0034
- 按位或 至少一个为1,才为1
0x1234 | 0x00ff = 0x12ff
- 按位异或 对应位只有一个为1,结果才为1
0xff00 ^ 0xf0f0 = 0x0ff0
- 按位非 反转操作数中所有位,由于JavaScript表示有符号整数,对一个值应用~操作符等于修改其符号并减1
~0x0f = 0xfffffff0
-
左移 左移一位 = *2
7<<2 = 28 -
有符号右移 如果操作数是正值,高位补0;否则高位补1 右移一位 = /2(丢弃余数)
7>>1 = 3-7>>1 = -4 -
零填充右移(BIgInt不能使用) 无论左侧高位是什么,都填充0
-1>>>4 = 0x0fffffff
== 和 ===
严格相等:
基于类型转换的相等:
比较操作符:
通过操作赋值:
先定义操作符/缺值合并(??):
先定义操作符求值其先定义的操作数,如果其左操作数不是null或undefined,就返回该值,否则就返回右操作数的值
a??b
等价于
(a !== null && a !== undefined)? a : b
??、||和&&类似,但是优先级并不比他们更高或更低,如果混用,必须使用括号表明谁优先级更高
(a??b)||c //??先执行,再执行||
a??(b||c) // ||先执行,再执行??
a??b||c // SyntaxError
第五章
for/of
for/of 专门用于可迭代对象(数组,字符串,集合和映射)
for/await与异步迭代
async function printSteam(steam){
for await(let c in steam){
console.log(c)
}
}
for/in
for/in后面可以是任意对象
第六章
let o = Object.create(null)
这种方式传入null可以创建一个没有原型的新对象,这样不会继承任何东西
let o = Object.create(Object.prototype)
这种方式创建一个普通的空对象(类似{}或new Object()返回的对象)
Object.create() 的一个而用途是防止对象被某个第三方库函数意外修改,这种情况下,不要直接把对象传给库函数,而是要传入一个继承它的对象。如果函数读取这个对象的值,可以读取到继承的值。而如果它设置这个对象的属性,则修改不会影响原始对象
let o = {x: '123'}
library.function(Object.create(o))
测试属性
检查对象中是否有一个给定名字的属性:
- in(自有属性或继承属性)
let o = {x: 1}
'x' in o // true
'toString' in o // true
- hasOwnProperty(测试对象是否含有戈丁名字的属性)
let o = {x: 1}
o.hasOwnProperty('toString') // false
o.hasOwnProperty('x') // true
- propertyIsEnumerable(细化了hasOwnProperty(), 如果传入的命名属性是自有属性且这个属性是enumerable特性位true,就会返回true)
let o = {x: 1}
// undefined
o.propertyIsEnumerable('x')
// true
o.propertyIsEnumerable('toString')
// false
for(let k in Object.prototype){ console.log(k)}
// undefined
Object.prototype.propertyIsEnumerable('toString')
// false
但是in操作符可以区分不存在的属性和存在但被设置位undefined的属性
let o = {x: undefined}
// undefined
o.x
// undefined
o.x !== undefined
// false, x存在但值是undefined
o.y !== undefined
// false,y不存在
'x' in o
// true, x存在
'y' in o
// false, y不存在
delete o.x
// true
'x' in o
// false, x不存在
枚举属性
- Object.keys()
返回对象可枚举自有属性名的数组。不包含不可枚举属性、继承属性或名字是符号的属性
- Object.getOwnPropertyNames()
类似上方,但是也会返回不可枚举自有属性名的数组,只要它们的名字是字符串
- Object.getOwnPropertySymbols()
返回名字是符号的自有属性,无论是否可枚举
- Reflect.ownKeys()
返回所有属性名,包括可枚举和不可枚举,以及字符串和符号属性
let temp = Symbol('x')
// undefined
let o = {y: 2, z: 3}
// undefined
o[temp]=1
// 1
o
// {y: 2, z: 3, Symbol(x): 1}
Object.keys(o)
// ["y", "z"]
Object.getOwnPropertyNames(o)
// ["y", "z"]
Object.getOwnPropertySymbols(o)
// [Symbol(x)]
Reflect.ownKeys(o)
// ["y", "z", Symbol(x)]
JSON.stringify(o)
// "{\"y\":2,\"z\":3}"
toString() 和 toLocaleString()
let temp = {
x: 200000,
toString: function(){return `${this.x}`},
toLocaleString: function(){return `${this.x.toLocaleString()}`}
}
// undefined
temp.toString()
// "200000"
temp.toLocaleString()
// "200,000"
toLocaleString()返回对象本地化字符串表示
ES6的计算属性:
let p = 'hello'
function name (){return 'world'}
let o = {
[p]: 1,
[name]:2
}
o
//{hello: 1, function name (){return 'world'}: 2}
let o1= {p, name}
o1
// {p: "hello", name: ƒ}
let o2 = {p, [name()]: 2}
o2
// {p: "hello", world: 2}
第七章
创建数组的几种方式:
- 数组字面量
- 对可迭代对象使用
...扩展操作符 Array()构造函数- 工厂方法
Array.of()和Array.from()
字符串是可迭代对象,因此可以使用扩展操作符把任意字符串转换为单个字符的数组
let a = new Array() // 创建一个没有元素的空数组,等价于数组字面量[]
let a = new Array(10) // 创建指定长度的数组
let a = new Array(5,4,3,2,1) // 参数会成为新数组的元素
Array.of()是为了解决Array()构造函数无法创建只包含一个数值元素的数组
let a = Array.of() // 返回空数组
let a = Array.of(10) // 可以创建只有一个元素10的数组
let a = Array.of(1, 2, 3) // [1,2,3]
Array.form()是ES6新增的另一个工厂方法,这个方法期待一个可迭代对象或类数组对象作为其第一个参数,并返回包含该数组元素的新数组
Array.from() 接受可选的第二个参数,如果第二个参数传入一个函数,那么再构建新数组的时候,源对象的每个元素都会传入这个函数,这个函数的返回值将代替原始值成为新数组元素,这样效率比先构建一个数组再映射为另一个数组高效
注意: 对数组元素使用delete操作符不会修改length属性
数组迭代器
forEach
forEach参数:数组元素的值、数组元素的索引和数组本身
map
返回新数组,并不修改调用它的数组
如果是稀疏数组,则缺失的元素不会调用我们函数,但返回的数组也会和原数组一样稀疏:长度相同,缺失元素也一样
filter
返回一个数组,该数组包含调用它的数组的子数组
filter()会跳过稀疏数组中缺失的元素,它返回的数组始终是稠密的
比如如果既想清理空隙,又想删除值为undefined和null的元素,如下
find 和 findIndex
find:返回匹配的元素,如果没找到返回undefined
findIndex:返回匹配的元素索引,没找到返回-1
every 和 some
every:对所有元素都返回true才返回true;第一次返回false就返回false,只有全部返回true才为true
some:只要有一个数组元素有让断言函数返回true的,就返回true;some对第一次返回true就返回true,只有全部false才false
如果在空数组上面使用,every返回true,some返回false
reduce 和 reduceRight
reduce接受两个参数,第一个参数是执行归并操作的函数,第二个参数是可选的,是传给归并函数的初始值
如果不传初始值参数,在空数组上面调用会导致TypeError。
如果调用它时只有一个值,比如用只包含一个元素的数组调用且不传初始值,或者用空数组调用但传了初始值,则reduce()直接返回这个值,不会调用归并函数
reduceRight从高索引到低索引处理数组
flat 和 flatMap
flatMap = map().flat()
其他函数方法
slice
返回一个数组的切片或者子数组,接受两个参数,分别用于指定切片的起止位置
[起始位置,结束位置)
第一个参数是起始位置
第二个参数是结束位置(不包含)
如果任何一个参数都是负值,则这个值表示相对数组长度指定数组元素;-1表示数组的最后一个元素
slice不会修改原有数组
let a = [1, 2, 3, 4, 5]
// undefined
a.slice(1,3)
// [2, 3]
a.slice(2)
// [3, 4, 5]
a.slice(-2)
// [4, 5]
a.slice(1, -1)
// [2, 3, 4]
a.slice(-3, -1)
// [3, 4]
a
(5) [1, 2, 3, 4, 5]
splice
splice可以做到增加、删除、修改
第一个参数是指定插入或删除的起点位置
第二个参数是指定要从数组中删除的个数,如果忽略第二个参数,则会删除从起点开始所有的元素
后面的任意参数,表示插入数组的元素
let a = [1,2,3,4,5]
a.splice(1,2)
// [2, 3]
a
// [1, 4, 5]
a.splice(2)
// [5]
a
// [1, 4]
a.splice(1,0,9,8,7)
// []
a
// [1, 9, 8, 7, 4]
a.splice(1,1,6)
// [9]
a
// [1, 6, 8, 7, 4]
fill
第一个参数是要把数组设置成的值
可选的第二个参数指定起始索引,如果忽略就从0开始
可选的第三个参数指定终止索引(不包含),如果忽略就一直到数组末尾
同样,可以传入负值相对于数组末尾指定索引
copyWithin()
把数组切片复制到数组中的新位置,会就地修改数组并返回修改后的数组,但不会改变数组长度
第一个参数指定要把第一个元素复制到的目的索引
第二个参数指定要复制的第一个元素的索引,如果忽略第二个参数,则默认值为0
第三个参数指定要复制的元素切片的终止索引,如果忽略,则使用数组的长度
从起始索引到终止索引(不包含)的元素会被复制
使用负值相对于数组末尾指定索引
是个高性能方法,尤其是对于定型数组特别有用
let a = [1,2,3,4,5,6,7]
a.copyWithin(2)
// [1, 2, 1, 2, 3, 4, 5]
// 起始:2 数组切片是:1,2,3,4,5,6,7
a.copyWithin(2,3,5)
// [1, 2, 2, 3, 3, 4, 5]
//起始:2 数组切片:2,3
a.copyWithin(0, -2)
// [4, 5, 2, 3, 3, 4, 5]
//起始:0 数组切片:4,5
[1, 2, 3, 4, 5].copyWithin(-2, -3, -1)
// [1, 2, 3, 3, 4]
//起始;倒数第二个数 数组切片:[length+start,length+end) 即3,4
indexOf 和 lastIndexOf
indexOf 从前往后找第一个找到的元素,没找到返回-1
lastIndexOf 从后往前找
字符串也有indexOf 和 lastIndexOf,区别是第二个参数是负数会被当成0
第八章 函数
箭头函数
写法:
const sum = (x,y) => {return x+y}
// 如果只有return,则可以省略return关键字,分号以及花括号
const sum = (x,y) => x+y
// 如果只有一个参数,可以省略参数列表的圆括号
const test = x=>x*x
// 没有参数,必须有圆括号
const test = () => 23
const test = x => { return {value: x} }
//等于
const test = x => ({value: x})
调用函数的五种方式:
- 作为函数
fn()
条件式调用:
f?.(x)
等于
(f!==null && f!== undefined)?f(x):undefined
对于非严格模式下的函数调用,this是全局对象
严格模式下,this是undefined
箭头函数的this继承自身定义所在环境的this值
- 作为方法
o是对象,m是一个函数
o.m()
作为方法和作为函数的区别就是:调用上下文
在这里,对象o会成为调用上下文
也可以使用
o['m'](x,y)
等价于
o.m(x,y)
如果方法返回对象,那么基于这个方法调用的返回值还可以继续调用其他方法
如果写的方法没有自己的返回值,可以考虑返回this
注意:
this关键字不具备变量那样的作用域机制,除了箭头函数,嵌套函数不会继承包含函数的this值。
如果嵌套函数被当作方法来调用,那么它的this值就是调用它的对象。
如果嵌套函数(不是箭头函数)被当作函数来调用,则它的this值要么是全局对象(非严格模式),要么是undefined(严格模式)
let o = {
m: function(){
let that = this
console.log(this === o)
f()
function f(){
console.log(that === o)
console.log(this === o)
}
}
}
o.m()
// VM397:4 true this对象是o
// VM397:7 true that是外部的值
// VM397:8 false this是全局对象或undefined
针对这事,里层的f函数可以使用箭头函数的方式继承外部的this值
const f = () => {console.log(this === o)}
或者使用嵌套函数的bind方法
const f = (function(){console.log(this===o)}).bind(this)
- 作为构造函数
比如
o = new Object()
构造函数调用会创建一个新的空对象,这个对象继承构造函数的prototype属性指定的对象
构造函数就是未了初始化对象而设计的,这个新创建的对象会被用作函数的调用上下文,因此可以在构造函数里面通过this关键字引用这个对象
构造函数正常情况不适用return关键字,而是初始化新对象并在到达函数体末尾时隐式返回了这个对象
function newFn(name, fn){
console.log(this)
this.name = name
this.fn = fn
}
let m = new newFn('haha', function(){
console.log(this)
})
// VM761:2 newFn {}fn: ƒ ()name: "haha"[[Prototype]]: Object
m.fn
m.fn()
// VM893:2 newFn {name: 'haha', fn: ƒ}
-
通过call()或apply()方法间接调用
-
通过JavaScript语言特性隐式调用
-
如果对象有获取方法或设置方法,则查询或设置其属性值可能会调用这些方法
-
当对象在字符串上下文中使用时,会调用对象的toString方法;类似的,当对象用于数值上下文时,会调用他的valueOf()
-
遍历可迭代对象的元素时,也会涉及一系列的方法调用
-
标签模板字面量也是一种伪装的函数调用
-
代理对象的行为完全由函数控制
当调用函数时传入的实参小于声明的形参时,额外的形参会获得默认值,通常是undefined
闭包
严格来讲,所有JavaScript函数都是闭包,但是多数函数调用与函数定义都在同一作用域中,所以闭包的存在无关紧要;闭包真正值得关注是定义函数和调用函数的作用域不同的时候
最常见的情形就是一个函数返回了在它内部定义的嵌套函数
let scope = 'global'
function check(){
let scope = 'part'
function f(){
return scope
}
return f()
}
check()
然后修改一下:
let scope = 'global'
function check(){
let scope = 'part'
function f(){
return scope
}
return f
}
let result = check()()
JavaScript函数是使用定义他们的作用域来执行的
在定义嵌套函数f()的作用域中,变量scope绑定的值是local,该绑定在f执行时仍然有效,无论它在哪里执行
所以,闭包会捕获自身定义所在外部函数的局部变量(及参数)绑定
函数属性、方法与构造函数
- length
函数有一个只读的length属性,表示函数的元数,即函数列表中声明的形参个数
这个值通常表示调用函数时应该传入的参数个数
如果函数有剩余形参,则这个剩余形参不包含在length属性内
- name
函数有个只读的name属性,表示定义函数时使用的名字(如果是用名字定义的话),如果是未命名的函数,表示在第一次创建这个函数时赋给该函数的变量名或属性名 (主要用于记录调试或排错)
- prototype属性
除了箭头函数,所有函数都有一个prototype属性,这个属性引用一个被称为原型对象的对象
每个函数都有自己的原型对象
当函数被当作构造函数使用时,新创建的对象从这个原型对象继承属性
- call(), apply()
// 把f函数作为对象o的方法进行调用
f.call(o, 1, 2)
f.call(o, [1,2])
- bind()
bind()方法的主要目的是把函数绑定到对象
如果在函数f上调用bind()方法并传入对象o,则这个方法会返回一个新函数
如果作为函数来调用这个新函数,就会像f是o的方法一样调用原始函数。
传给这个新函数的所有参数都会传给原始函数
function f(y){ return this.x+y}
let o = {x: 1}
let g = f.bind(o)
g(2)
//3 g(x)会在o上面调用f()
let s = { x:12, g}
s
// {x: 12, g: ƒ}
s.g(5)
// 6 此时仍然绑定到o上面
打印s的结构,会发现确实是绑定在o上面的:
箭头函数从定义它们的环境中继承this值,且这个值不能被bind覆盖,因此如果上方的代码f如果是箭头函数定义的,那么绑定就不会起作用
bind还可以实现部分应用(有时候被称柯里化),即在第一个参数之后传给bind()的参数也会随着this值一起被绑定
let sum = (x,y,z)=>x+y+z
let s = sum.bind(null, 1) // 把第一个参数绑定为1
s(2,3) // x绑定到1,2绑定到y
- toString
ECMAScript规范要求这个方法返回一个附和函数声明语句的字符串
实践中,这个方法的多数(不是全部)都返回函数完整的源代码,内置函数返回的字符串中通常包含'[native code]',表示函数体
- Function()构造函数
Function创建的是匿名函数,不接收任何指定新函数名字的参数
Function构造函数可以接收任意多个字符串参数,其中最后一个参数是函数体的文本
Function()函数允许在运行时动态创建和编译JavaScript函数
Function()构造函数每次被调用时都会解析函数体并创建一个新函数对象
通过这种方式创建的函数不使用词法作用域,而是始终编译为如同顶级函数一样
let scope = 'global'
function check(){
let scope = 'part'
return new Function('return scope')
}
check() // 如果支持,按理论来说应该返回global
类
构造函数调用的关键在于构造函数的prototype属性将被用作新对象的原型
原型强调几乎所有对象都有原型,但只有极少数对象有prototype;即只有函数对象才有prototype属性
在函数体内,可以通过一个特殊表达式new.target判断函数是否作为构造函数被调用了,如果该表达式的值有定义,就说明函数是作为构造函数,通过new关键字调用的。
如果new.target是undefined,那么包含函数就是作为普通函数被调用的,没有使用new关键字
function C(){
if(!new.target) return newC()
}
注意:
用箭头函数定义函数没有prototype属性,因此不能作为构造函数来使用;
而且,箭头函数中的this是从定义它们的上下文继承的,不会根据调用它们的对象来动态设置
instanceof —— 检查测试对象是否是xxx对象
r instanceof Range // r继承了Range.prototype
instanceof 不能验证使用的哪个函数,但它仍然以构造函数作为其右操作数(因为构造函数是类的公共标识)
如果不想以构造函数作为媒介,直接测试某个对象原型链中是否包含指定原型,可以使用isPrototypeOf()
比如:
let obj = Object.create({})
// undefined
Object.prototype.isPrototypeOf(obj)
// true
构造函数仅需要初始化this,甚至不需要返回新创建的对象,它调用会自动创建新对象,并将构造函数作为该对象的方法来调用,然后返回新对象
任何普通JavaScript函数(不包括箭头函数、生成器函数和异步函数)都可以用作构造函数,而构造函数调用需要一个prototype属性
第十章 模块
基于类、对象和闭包的模块
基于闭包的自动模块化
Node中的模块
在Node中,每个文件都是一个拥有私有命名空间的独立模块,在一个文件中定义的常量、变量、函数和类对该文件而言都是私有的,除非该文件会导出它们。
而被模块导出的值只有被另一个模块显示导入后才会在该模块中可见
Node模块使用require()函数导入其他模块,通过设置Exports对象的属性或完全替换module.exports对象来导出公共API
- Node的导出
Node定义了一个全局exports对象,这对象始终有定义,所以可以写一个导出多个值的Node模块,可以直接把这些值设置为exports对象的属性:
const square = x => x*x
const sum = (x, y)=> x+y
exports.mean = data => data.reduce(sum)/data.length
exports.stddev = function(d){
let m = exports.mean(d)
return Math.sqrt(d.map(x => x - m).map(square).reduce(sum)/(d.length - 1))
}
更多的时候我们只想让模块导出一个函数或类,而非一个包含很多函数或类的对象,可以直接赋值给module.exports
module.exports = class BitSet extends AbstractWritableSet{
}
module.exports的默认值与exports引用的是同一个对象,在前面的统计模块中,实际上也可以直接把mean的函数赋值给module.exports.mean,而不是exports.mean
另一种重写统计模块的方式是在模块末尾导出一个对象
module.exports = {mean, stddev}
- Node的导入
Node通过require()函数导入其他模块,这个函数的参数是要导入模块的名字,返回值是该模块导出的值(通常是一个函数,类或对象)
如果想导入Node内置的系统模块或通过包管理器安装在系统上的模块,可以使用模块的非限定名,即不带会被解析为文件系统路径的"/"字符的模块名
// 内置的文件系统模块
const fs = require("fs")
//不属于Node,但已经安装在本地
const express = require("express")
如果想导入自己代码中的模块,则模块名应该是只想包含模块代码中模块文件的路劲(相对于当前模块文件)——相对路径
const stats = require('./stats.js')
还可以使用解构赋值:
// 导入整个stats对象,包含所有整数
const stats = require('./stats.js')
// 只导入自己想要的
const {stddev} = require('./stats.js')
ES6的模块
ES6模块化于Node的模块化在概念上是相同的:每个文件本身都是模块,在文件中定义的常量、变量、函数和类对这个文件而言都是私有的,除非它们被显示导出
ES6模块的代码自动应用严格模式,意味着使用ES6模块时,永远不用再写use strict;同时也意味着模块中的代码无法使用with语句和arguments对象或未声明的变量
ES6模块甚至比严格模式更严格:在严格模式下,在作为函数调用的函数中的this时undefined。而在模块中,即便在顶级代码中this也是undefined(相对而言,浏览器和Node中的脚本都将this设置为全局对象)
Node13开始支持ES6模块,但目前大多数Node程序使用的仍然是时Node模块
ES6的导出
export const PI = Math.PI
export function degress(){return d/100}
export class Circle{
constructor(r) { this.r = r }
alea(){ return PI * this.r * this.r }
}
也可以在模块末尾只用一个export语句声明真正要导出的值
export {Circle, degress, PI}
一个模块值导出一个值的话:
export default class BitSet{}
ES6的导入
// 默认导出的形式
import BitSet from './bitset.js'
//从导出多个值的模块导入值
import {mean, stddev} from './stats.js'
// 会创建一个对象,并将其赋值给一个名为stats的变量
// 被导入模块的每个非默认导出都会变成这个stats对象的一个属性,需要通过stats.mean()方式调用
import * as stats from './stats.js'
// 导入没有任何导出的模块
import './analytics.js'
导入和导出时重命名
使用as关键字对导入值进行重命名
import { render as renderImage } from "./image.js"
导出重命名:
export {
// as 前面时一个标识符,而非表达式
layout as calculateLayout
}
再导出
export {mean} from './stats/mean.js'
// 导出另一个模块所有
export * from "./stats/mean.js"
再导出语法允许使用as进行重命名
export {default as mean} from "./stats/mean.js"
// 如果想将另一个模块的命名符号再导出为当前模块的默认导出,可以在import语句后面加一个export // default
export { mean as default } from "./stats.js"
// 要把另一个模块的默认导出再导出为当前模块的默认导出(没意义)
export {default} from "./stats/mean.js"
在网页中使用JavaScript模块
模块代码默认在严格模式下运行,this不引用全局对象,顶级声明默认不会全局共享
因为模块代码必须与传统非模块代码以不同方式运行,所以必须修改HTML和Javascript才能使用模块,如果想要在浏览器中以原生方式使用import指令,必须通过<script type="module">标签告诉浏览器你的代码是一个模块
ES6中每个模块的导入都是静态的,因此只要一个起始模块,浏览器就可以加载它导入的所有模块,然后加载第一批模块导入的所有模块,以此类推,直到加载完
带有type="module"属性的脚本会像带有defer属性的脚本一样被加载和执行
HTML解析器遇到<script>标签,会开始加载代码,不过代码执行会推迟到HTML解析完成才开始,HTML解析一完成,脚本就会按照它们在HTML文档中出现的顺序执行
添加async属性可以改变模块代码的时机,添加了async属性的模块会在代码加载完毕之后立即执行,而不管HTML解析是否完成,同事也有可能改变脚本执行的相对顺序
通过import()动态导入
静态导入模块:
import * as stats from "./stats.js"
动态导入:
import("./stats.js").then(stats => {
let x = stats.mean(data)
})
或者在一个async函数中
async analyzeData(){
let stats = await import("./stats.js")
return {
average: stats.mean(data)
stddev: stats.stddev(data)
}
}
import.meta.url
import.meta这个特殊语法引用一个对象,这个对象包含当前执行模块的元数据。其中,这个对象的url属性时加载模块时使用的URL(在Node中时file://url)
import.meta.url的主要使用场景是引用与模块位于同一(或相对)目录下的图片、数据文件或其他资源
使用URL()构造函数可以非常方便地相对于import.meta.url这样的绝对URL来解析相对URL
function localeStringUrl(locale){
return new URL(`l10n/${locale}.json`, import.meta.url)
}
第十一章 JavaScript标准库
集合与映射
Set
集合就是一组值,与数组类似。但是与数组不同的是,集合没有索引或顺序,也不允许重复
Set()构造函数的参数不一定是数组,但必须是一个可迭代对象
集合的size属性类似数组的length属性
集合不一定在创建时初始化,可以在创建之后再通过add()、delete()和clear()方法。
let s = new Set('hello HaHa')
s
// Set(7) {"h", "e", "l", "o", " ", …}
s.size
//7
s.add('p')
// Set(8) {"h", "e", "l", "o", " ", …}
s.add('h')
// Set(8) {"h", "e", "l", "o", " ", …}
s.add([1,2,3])
//Set(9) 0: "h"1: "e"2: "l"3: "o"4: " "5: "H"6: "a"7: "p"8: Array(3)
s.delete([1,2,3])
// false
s.delete('h')
// true
s
// Set(8) {"e", "l", "o", " ", "H", …}
s.clear()
// undefined
s
// Set(0) {}
-
首先add()接受一个参数,如果传入一个参数是数组,他会把数组而不是数组元素添加到集合中;add()始终返回调用它的集合,所以可以连续调用add()
-
delete()方法一次仅删除一个集合元素,delete()返回一个布尔值
-
集合成员是根据
===来判断是否重复重复,所以上方那种方式删不掉数组元素,真要删除,必须传入该数组的引用
Map类
Map允许任何值作为“索引”,映射速度很快,无论映射有多大,查询某个键关联的值都很快(虽然没通过索引访问数组那么快)
Map()构造函数的可选参数应该是一个可迭代对象,产出值为包含两个元素的数组[key, value],通常把关联的键和值写成数组的数组的形式
let m = new Map([['one', 1], ['two', 2]])
// undefined
m.size
// 2
m.set('three', 3)
// Map(3) {"one" => 1, "two" => 2, "three" => 3}
m.get('three')
// 3
m.has('one')
// true
m.delete('one')
// true
m.size
// 2
m.set(undefined, 12)
// Map(3) {"two" => 2, "three" => 3, undefined => 12}
m.set(NaN, 13)
// Map(4) {"two" => 2, "three" => 3, undefined => 12, NaN => 13}
m.set(null, 14)
// Map(5) {"two" => 2, "three" => 3, undefined => 12, NaN => 13, null => 14}
m.set([12], 14)
// Map(6) {"two" => 2, "three" => 3, undefined => 12, NaN => 13, null => 14, …}
m.set({12:7}, 14)
// Map(7) {"two" => 2, "three" => 3, undefined => 12, NaN => 13, null => 14, …}[[Entries]]0: {"two" => 2}1: {"three" => 3}2: {undefined => 12}3: {NaN => 13}4: {null => 14}5: {Array(1) => 14}6: {Object => 14}size: (...)__proto__: Map
m.clear()
// undefined
Map和Set类似,映射的set方法可以连续调用;
和Set一样,任何JavaScript值都可以在映射中作为键或值。这包括null、undefined、NaN、对象和数组等引用类型
同样Map按照全等性而非相等性比较键。
映射对象是可迭代的,迭代的每个值是一个两个元素的数组,其中一个元素是键,第二个元素是与该键关联的值。
Map是按照插入顺序迭代的
如果只想迭代映射的键或关联的值,可以使用keys()和values()方法,另外entries()返回的可迭代对象用于迭代键值对,与直接迭代映射一样
WeakMap 和 WeakSet
WeakMap是Map的一个变体,它不会阻止键值被当作垃圾收集
- WeakMap()构造函数与Map() 构造函数类似
- WeakMao的键必须是对象或数组,原始值不受垃圾收集控制,不能作为键
- WeakMap只是先了get()、set()、has()、delete();WeakMap不是可迭代对象,所以没有keys,values forEach方法
- WeakMap没有size属性,因为弱映射的大小可能随着对象被当作垃圾收集而随时改变
WeakMap的主要用途是实现值与对象的关联而不导致内存泄漏
WeakSet实现了一组对象,不会妨碍这些对象被作为垃圾收集
- WeakSet()构造函数和Set()构造函数类似
- WeakSet不允许原始值作为成员
- WeakSet只实现了add()、has()、delete(),并且不可迭代
- WeakSet没有size属性
WeakSet的使用场景不多,主要应用场景与WeakMap类似
定型数组与二进制数组
ToDo
正则表达式与模式匹配
- 字面量字符
所有字母字符和数字在正则表达式中都匹配自身的字面值,JavaScript正则表达式语法通过\开头转义序列也支持一些非字母字符
- 字符类
注意:
\d字符类只匹配ASCII数字- 如果想要从世界书写体系中匹配一个十进制数字,可以使用
/\p{Decimal_Number}/u - 如果想要匹配任意语言中的非十进制数字,可以大写P,即
/\P{Decimal_Number}/u - 如果想要匹配任意类数值字符,包括分数和罗马数字,即
/p{Number}/ \w字符类只匹配ASCII文本,但使用\p可以模拟一个这样的国际化版本/[\p{Alphabetic}\p{Decimal_Number}\p{Mark}]/u- 如果真要兼容世界上各式各样的语言,起始还需要添加“Connector_Punctuation” 和 “Join_Control”两个分类
- 重复
- 非贪婪重复
上方的重复字符会尽可能多地匹配,同时也允许正则表达式剩余的部分继续匹配,这种重复是贪婪的
在重复字符后面简单加一个?,就可以指定非贪婪地重复,如??、*?、{1,5}?
比如:
/a+/ 匹配一个或多个字符a,但遇到aaa,就会匹配到三个字母
/a+?/ 同样也匹配一个或多个字符a,但应用到aaa,它只匹配第一个字母a
- 任选、分组和引用
正则表达式的语法中也包含指定任选、分组子表达式和引用前面子表达式的特殊字符
/\d{3}|[a-z]{4}/匹配3个数字或4个小写字母
在找到匹配项之前,会从左到右依次匹配仍须按模式。如果左边的任选模式匹配,则忽略右边的模式,即使右边的模式可以得到更好的匹配
比如/a|ab/应用到字符串ab只会匹配字母a
圆括号的作用:
-
把独立的模式分组为子表达式,从而让这些模式可以被
|、*、+、?等当作整体 比如/(ab|cd)+|ef/匹配字符ef,也匹配一个或多个字符串ab、cd -
在完整的模式中定义子模式。当正则表达式成功匹配一个目标字符串后,可以从目标字符串中提取出与圆括号包含的子模式对应的部分 比如把匹配数字的模式放到一堆圆括号中
(/[a-z]+(\d+)/),那么就可以从整个模式的匹配项中提取出相 应的数字
比如 /([Jj]ava([Ss]cript)?)\sis\s(fun\w*)/, 嵌套子表达式[Ss]cript要使用\2来引用
与圆括号分组的子表达式相关的一个用途是在同一个正则表达式中回引子表达式,回引前面的子表达式要使用\字符加上数字。这里的数字是指圆括号分组的子表达式在整个正则表达式中的位置,\1回引第一个子表达式
注意:由于子表达式可能会嵌套,所以它们的位置是按照左括号来计算的
/(['"])[^'"]*\1/ 这个\1匹配第一个圆括号分组的子表达式匹配的内容,这个例子强制结尾的引号必须匹配开始的引号
如果不想让圆括号分组的子表达式产生数字引用,那么可以不用(和)分组,而是开头用(?:,结尾用)
比如 /([Jj]ava(?:[Ss]cript)?)\sis\s(fun\w*)/这样\2引用的应该是(fun\w*)
ES2018新特性:命名捕获组
即可以给正则表达式中的每个左圆括号指定一个关联的名字,以便后面使用这个名字而不是数字来引用匹配的文本
如果想在正则表达式中回引某个命名捕获组,可以使用名字
- 指定匹配位置
常用的锚点组件时^和$,分别把模式锚定字符串开头和末尾的位置
\b匹配ASCII码词边界
比如:想搜索Java这个单词,使用/\bJava\b/
比如Java(?!Script)([A-Z]\w*)匹配
Java后跟一个大写字母及任意数量的ASCII单词字符,但Java后面不能是Script
ES2018扩展新语法:支持“向后查找”断言
向后查找断言与向前查找类似,但关注的是当前匹配位置之前的文本
2020年初,Node、Chrome、Edge实现了该特性,Firefox和Safari还没有实现
肯定式向后查找断言使用(?<=...),否定式向后查找断言使用(?<!...)
比如搜索美国邮件地址,希望从中匹配5为邮政编码,但仅限于前面式两位字母的州简写的情况
/(?<=[A-Z]{2} )\d{5}/
要匹配前面不带Unicode货币符号的数字字符串,可以像下面这样使用否定式向后断言
/(?<![\p{Currency_Symbol}\d.])\d+(\.\d+)?/u
- 标志
每个正则表达式都可以带一个或多个标志,用于修改其行为。
JavaScript定义了6个标志,每个标志都用一个字母表示。
标志在正则表达式字面量中放在第二个斜杠后面,或者在使用RegExp()构造函数时要以字符串形式作为第二个参数
- g
这个标志意味着想要找到字符串中包含的所有匹配项,而不只是找到第一个匹配项。它会从主要的方面修改String的match()和RegExp的exec()方法
- i
表示模式匹配应该不区分大小写
- m
表示应该以“多行”模式进行,即这个RegExp要用于多行字符串,而且^和$应该既匹配字符串的开头和末尾,也匹配字符串中任何一行的开头和末尾
- s
s标志同样可以用在要搜索的文本包含换行符的时候。正常情况下,句点.在正则表达式中匹配除换行符之外的任何字符,但在使用s标志时,.将匹配任何字符,包括行终止符(在2020年初得到除Firefox之外,包括Node、Chrome、Edge、Safari的支持)
- u 代表Unicode,可以让正则表达式匹配完整的码点而不是匹配16位值。ES6新增的,如果没有特殊原因,应该对所有正则表达式都使用这个标志。如果不使用这个标志,那正则表达式将无法识别表情符号和其他需要16位以上表示的字符(包括很多中文字符)
没有u标志,.匹配任意一个UTF-16 16位值。而有u标志,.匹配一个Unicode码点,包括超过16位编码的值
在正则表达式上添加u标志之后,就可以使用新的\u{...}转义序列表示Unicode字符,同时也可以使用\p{...}表示Unicode字符类
- y 表示正则表达式时有粘性的,应该在字符串开头匹配或在紧跟前一个匹配的第一个字符处匹配
在应用给只想查找一个匹配项的正则表达式时,这个标志的作用类似于给正则表达式加上了^锚点,将其锚定到字符串的开头
对于用来在字符串中反复查找所有匹配项的正则表达式,这个标志比较有用;这时候它会导致String的match方法和RegExp的exec方法产生特殊行为,强制将每个后续匹配都锚定到前一个匹配(在字符串中)的结束位置
模式匹配的字符串方法
search()
这个方法接收一个正则表达式参数,返回第一个匹配项起点字符的位置,如果没有找到匹配项,就返回-1
如果search方法的参数不是正则表达式,他会把这个参数传给RegExp构造函数,把它转换为正则表达式。它不支持全局搜索,因此其正则表达式参数包含的g标志会被忽略
replace()
执行搜索替换操作,它接收一个正则表达式作为第一个参数,接收一个替换字符串作为第二个参数
如果正则表达式带g标志,replace方法会用替换字符串中的所有匹配项;否则只替换第一个匹配项
如果第一个参数是字符串而非正则表达式,这个方法不会像search那样将字符串通过RegExp转换为正则表达式,而是会按照字面量搜索
如果替换字符串中出现了$符号后跟一个数字,replace会将这两个字符替换为与指定子表达式匹配的文本
如果使用命名捕获组,可以通过名字而非数字来引用匹配的文本
除了给replace传替换字符串,还可以传一个函数,这个函数会被调用以计算替换的值
这个替换函数在调用是会接收到几个参数:
- 第一个参数是匹配的整个文本
- 如果RegExp有捕获组,则后面几个参数分别是这些捕获组匹配的子字符串
- 在字符串找到匹配项的位置
- 调用replace的整个字符串
- 如果RegExp包含命名捕获组,替换函数还会收到一个参数,这个参数是一个对象,其属性名是捕获组的名字,属性值是匹配的文本
match
只有一个正则表达式参数(或者参数不是正则表达式,会把它传给RegExp构造函数),返回一个数组;如果没有找到匹配项,就返回null。如果正则表达式有g标志,这个方法返回的数组会包含在字符串中找到所有匹配项
如果正则表达式没有g标志,match不会执行全局搜索,只会查找第一个匹配项
在没有g标志的情况下,返回数组的第一个元素是匹配的字符串,剩下的所有元素是正则表达式中括号分组的捕获组匹配的子字符串
是否设置y标志对match的行为也有一些影响。
如果同时设置了g和y,match返回包含所有匹配字符串的数组,就跟只设置了g而没有设置y一样,但第一个匹配项必须始于字符串开头,每个后续的匹配项必须从前一个匹配项的后一个字符开始
如果只设置了y而没有设置g标志,match会尝试找到第一个匹配项,且默认情况下,这个匹配项被限制在字符串开头
matchAll
接收一个带g标志的正则表达式,但它不像match那样返回所有匹配项的数组,而是返回一个迭代器,每次迭代都产生一个与使用match时传入非全局RegExp得到的匹配对象相同的对象
matchAll成为循环遍历字符串中所有匹配项最简单和最通用的方式
matchALl不会修改传入RegExp的lastIndex属性
split
RegExp类
RegExp构造函数接收一个或两个字符串参数,创建一个新RegExp对象
这个构造函数的第一个参数是包含正则表达式主体的表达式,即在正则表达式字面量中出现斜杠中间的部分
第二个参数是可选的,如果提供了这个参数,则代表指定正则表达式的标志
RegExp构造函数主要用于动态创建正则表达式,即创建哪些无法用正则表达式字面量语法表示的正则表达式
RegExp属性:
- source:只读,包含正则表达式的源文本
- global:只读布尔属性,设置了g标志则为true
- ignoreCase:只读布尔属性,如果设置了i标志则为true
- multiline:只读布尔属性,如果设置了m标志则为true
- dotAll:只读布尔属性,如果设置了s则为true
- unicode:只读布尔属性,如果设置了u标志则为true
- sticky:只读布尔属性,如果设置了y标志则为true
- lastIndex:可读写的整数属性,对于带有g或y标志的模式,这个属性用于指定下一次匹配的起始字符位置
- test():接收一个字符串参数,如果字符串与模式匹配就返回true,否则false
- exec():接收一个字符串参数,并从这个字符串寻找匹配,如果没有找到匹配项,就返回null,如果找到了就返回数组
-----(可加深)
日期与时间
Date()这个构造函数可以解析toString() toUTCString() toISOString()产生的格式
年:getFullYear()、getUTCFullYear、setFullYear、setUTCFullYear
同理,Month、Date、Hours、Minutes、Seconds、Milliseconds
setFullYear和setUTCFullYear可选地允许同时设置月和日
setHours和setUTCHours处理支持小时字段,还允许我们指定分钟、秒和毫秒
getDay和getUTCDay返回的是代表周几的数值(0-周日,6-周六)——周几这个字段只读,不能set
Date.now()返回的时间戳是以毫秒为单位的
performance.now()可以返回更高精度,虽然单位也是毫秒,但是它包含毫秒后面的小数部分;返回的时间戳不是像Date.now()返回的值一样的绝对时间戳,而是相对于网页加载完成后或Node进程启动后经过的时间
performance对象是performance API的一部分,已经被浏览器和Node实现,在Node中使用,需要先引入
const {performance} = require("perf_hooks")
但是高精度计时可能会让一些没有底线的网站用于采集访客指纹,因此浏览器(特别是Firefox)可能会降低performance.now()的精度,所以我们可以采用更高精度的计时(Firefox中 privacy。reduceTimePrecision为false)
格式化和解析日期字符串
let d = new Date(2021,12,25,13,20,30)
// undefined
d
// Tue Jan 25 2022 13:20:30 GMT+0800 (中国标准时间)
// 使用本地时区,但不按照当地惯例
d.toString()
// "Tue Jan 25 2022 13:20:30 GMT+0800 (中国标准时间)"
//UTC时区,但不按照当地惯例
d.toUTCString()
// "Tue, 25 Jan 2022 05:20:30 GMT"
// ISO-8601格式打印
d.toISOString()
// "2022-01-25T05:20:30.000Z"
// 使用本地时区及用户当地惯例一致的格式
d.toLocaleString()
// "2022/1/25下午1:20:30"
// 只格式化Date日期部分,忽略时间,使用本地时区,但不与当地惯例适配
d.toDateString()
// "Tue Jan 25 2022"
// 只格式化Date日期部分,使用本地时区,也当地惯例适配
d.toLocaleDateString()
// "2022/1/25"
// 只格式化时间部分,使用本地时区,但不与当地惯例适配
d.toTimeString()
// "13:20:30 GMT+0800 (中国标准时间)"
// 只格式化时间部分,使用本地时区,适配本地惯例
d.toLocaleTimeString()
// "下午1:20:30"
d.toGMTString()
// "Tue, 25 Jan 2022 05:20:30 GMT"
Error
使用Error对象的一个主要原因是在创建Error对象时,该对象能够捕获Javascript的栈状态,如果异常未被捕获,则会显示包含错误消息的栈跟踪信息,而这对排查错误很有帮助
Error对象有两个属性:
message: 值是我们传给Error()构造函数的值,必须时会被转为字符串
name:对使用Error()创建的错误对象,name的值总是Error
toString()方法:返回一个字符串,由name属性的值后跟一个冒号和一个空格,再后跟一个message属性值构成
除了Error类,JavaScript还有子类,以便触发ECMAScript定义的一些特殊类型的错误
子类包括:EvalError,RangeError,ReferenceError,SyntaxError,TypeError,URIError
可以自己定义Error的子类,以便控制自己程序的错误信息
比如:
JSON序列化与解析
如果希望JSON格式字符串对人类友好,那可以给JSON.stringify()传第二个参数null,第三个参数传一个数值或字符串,第三个参数告诉它应该把数据格式化为多行缩进格式,如果第三个参数是个数值,则该数值表示每级缩进的空格数;如果第三个参数是空白符(如'\t')字符串,则每级缩进就使用该字符串
JSON.parse()忽略空白符,因此给JSON.stringify()传第三个参数不会影响将输出的字符串再转换为原型的数据结构
如下图,JSON.parse()传如了复活函数,用于过滤某些属性并重新创建Date对象
JSON.stringify()也支持传入一个数组或函数作为其第二个参数来自定义其输出字符串
国际化API
Javascript国际化API包括3个类:Intl.NumberFormat Intl.DateTimeFormat Intl.Collator
这些类是ECMA402标准定义的
格式化数值
Intl.NumberFormat类定义了一个format方法,这个构造函数接收两个参数,第一个参数指定作为数值格式化依据的地区,第二个参数是用于指定格式化细节的对象。
如果第一个参数被省略,或者传入undefined,则使用系统设置中的地区
如果第一个参数是字符串,那它指定就是期望地区,如en-US,第一个参数也可以是一个地区字符串数组,此时会选择支持最好的一个
如果指定Intl.NumberFormat构造函数的第二个参数,则该函数应该是一个对象,且包含一个或多个下列属性
- style
指定必须的数值格式类型。默认decimal,如果指定percent则按百分比格式化数值,指定currency则表示数值为货币数量
- currency
如果style的值为currency,则这个属性是必需的,用于指定3个字母的ISO货币代码(如USD-美元,GBP-英镑)
- currencyDisplay
如果style的值是currency,则这个属性指定如何显示货币值。默认值为symbol,即如果货币有符号则使用货币符号,值code表示使用3个字母的ISO代码,值name-表示以完整形式拼出货币的名字
- useGrouping
如果不想让数值有千分位分隔符将这个属性设为false
- minimumIntegerDigits
数值中最少显示几位整数。如果数值的位数小于这个值,则在左侧填补0,默认值是1,最高可设置为21
- minimumFractionDigits、maximumFractionDigits
这两个属性控制数值的小数部分的格式,如果数值的小数部分位数小于最小值,则在右侧填补0。如果大于最小值,则小数部分会被舍入
这两个属性的范围是0-20;默认最小0,最大3;但格式化货币数量时是例外
- minimumSignificantDigits、maximumSignificantDigits
这两个属性控制数值中有效位的数量,比如让它们适合格式化科学数据
如果指定,这两个属性会覆盖前面列出的整数和小数属性,合法取值范围是1到21
let num = 1000000.23
Intl.NumberFormat('es',{style: 'currency',currency: 'EUR'}).format(num)
// "1.000.000,23 €"
Intl.NumberFormat('zh-CN',{style: 'currency',currency: 'RMB', currencyDisplay: 'symbol'}).format(num)
// "RMB 1,000,000.23"
其他语言,如印地语使用自己数值符号的文字,但倾向于默认使用ASCII数字0~9,如果想覆盖这种用于数字的默认文字,可以在地区中加上-u-nu-,后面跟上简写形式的文字名
-u-在地区中表示后面是Unicode扩展
nu是计数制扩展的名字
deva是梵文Devanagari的简写
格式化日期和时间
Intl.DateTimeFormat跟上面那种很相似
这个提供了细粒度的控制,通过传给构造函数的第二个选项对象的属性来实现
但是,Intl.DateTimeFormat并不能始终严格按照我们的要求来输出,比如我们指定了格式化时和秒的选项,但省略了分钟,格式化的结果仍然会包含分钟
属性如下:
- year
年,使用numeric表示完整的4位数年份,或使用“2-digit”表示两位数简写
- month
月,使用numeric表示可能比较短的数字,如‘1’,或使用“2-digit”始终表示两位数字,如“01”。使用long表示全名,如“January”,使用“short”表示简称,如“Jan”,而是用“narrow”表示高度简写的名字,如“J”,但不保证唯一
- day
日,使用numeric表示1位或2位数字,或使用“2-digit”始终表示两位数字
- weekday
周,使用long表示全名,如“Monday”,或使用“short”表示简称,如“Mon”,或使用“narrow”表示高度简写,如“M”,但不保证唯一
- era
这个属性指定日期在格式化的时候是否考虑纪元,例如CE或BCE。合法值为long short narrow
- hour、minute、second
这属性指定如何显示时间。使用numeric表示1位或2位数字,使用“2-digit”表示强制1位数值在左侧填补0
- timeZone
这个属性指定格式化日期使用的时区。如果省略,则使用本地时区。实现可能始终以UTC时区为准,也可能以IANA的时区为准
- timeZoneName
这个属性指定在格式化的日期和时间中如何显示时区。使用long表示时区全称,而short表示简写或数值形式的时区
- hour12
布尔值属性,指定是否使用12小时制,默认值取决于地区设置,但可以使用这个属性来覆盖
第十二章 迭代器与生成器
迭代器原理
可迭代对象指的是任何具有专用迭代器方法,且该方法返回迭代器对象的对象
迭代器对象指的是任何具有next()方法,且该方法返回迭代结果对象的对象
迭代结果对象是具有属性value和done的对象
要迭代一个可迭代对象,首先要调用其迭代器方法获得一个迭代器对象,然后重复调用这个迭代器对象的next()方法,直至返回done属性值为true的迭代结果对象
可迭代对象使用符号Symbol.iterator作为名字
实现可迭代对象
异步JavaScript
使用回调的异步编程
定时器
setTimeout只会执行一次,setInterval可以重复调用(clearInterval)
事件
客户端JavaScript编程几乎全是事件驱动的。事件驱动的JavaScript程序在特定上下文中为特定类型的事件注册回调函数,而浏览器在指定的事件发生时调用这些函数
applyUpdate()是一个假想的回调函数
网络事件
客户端JavaScript代码可以使用XMLHttpRequest类及回调函数来发送HTTP请求并异步处理服务器返回的响应
Node中的回调和事件
Node.js服务器端JavaScript环境底层就是异步的,定义了很多使用回调和事件的API
比如读取文件fs.readFile函数以接收两个参数的回调作为最后一个参数,它会异步读取指定文件,然后调用回调
期约
客户端和服务器端JavaScript环境中回调和基于事件异步编程的示例
期约是个对象,表示异步操作的结果
最简单情况下,期约就是一种处理回调的不同方式,处理多层嵌套以一种更线性的期约链形式表达出来,因此更加容易阅读和推断
回调的另一个问题就是难以处理错误。如果一个异步函数抛出异常,则该异常没有办法传播到异步操作的发起者。异步编程的一个基本事实就是它破坏了异常处理。对此一个补救方式是使用回调参数严密跟踪和传播错误并返回值。期约标准化了异步错误处理,通过期约链提供了一种让错误正确传播的途径
期约表示的是一次异步计算的未来结果。不过,不能使用它们表示重复的异步计算
使用期约
期约对象有个实例方法叫then,如果多次调用一个期约对象的then方法,则指定的每个函数都会在预期计算完成后被调用
期约表示的是一次计算,每个通过then方法注册的函数都只会被调用一次
then的第二个参数是错误的情况,但是很少用,一般都是catch方法
resolve状态的含义: 一个期约与另一个期约发生了关联,此时我们并不知道p会fulfill还是reject
错误处理
.finally 会在期约落定时被调用,无论这个期约是兑现还是被拒绝,这个回调都会被调用,而且调用时不会给它传任何参数
.finally也返回一个新期约对象,但finally的返回值通常被会忽略
并行期约
Promise.all接受一个期约对象的数组作为输入,返回一个期约。如果输入期约中的任意一个拒绝,返回的期约也被拒绝;否则,返回的期约会以每个输入期约兑现值的数组兑现
Promise.all输入数组包含期约对象和非期约值,如果这个数组的某个元素不是期约,那么它就会被当成一个已兑现期约的值,被原封不动复制到输出数组中。promise.all返回的期约会在任何一个输入期约被拒绝时拒绝,这会在第一个拒绝发生时立即发生,后面的期约状态还是待定
在ES2020中,Promise.allSettled也接受一个输入期约的数组,和Promise.all一样,但是它永远不拒绝返回的期约,而是会等所有输入期约全部落定后兑现。
Promise.allSettled返回的期约解决为一个对象数组,其中每个对象都对应一个输入期约,且都有一个status属性,值为fulfilled或rejected,如果status属性值为fulfilled,那么该对象还会有一个value属性,包含兑现的值;如果status属性值为rejected,那么该对象还会有一个reason属性,包含对应期约的错误或拒绝理由
如果想要躺尸运行多个期约,但只关心第一个兑现的值,此时可以使用Promise.race()。Promise.race()返回一个期约,这个期约会在输入数组中的期约有一个兑现或拒绝时马上兑现或拒绝(或者,如果输入数组中有非期约值,则直接返回第一个非期约值)
有时候,可能需要实现一个已有的基于期约的API,并从一个函数返回期约,尽管要执行的计算实际上并不涉及异步操作,可以使用Promise.resolve和Promise.reject
Promise.resolve接受一个值作为参数,并返回一个会立即(但异步)以该值兑现的期约
Promise.reject接收一个参数,并返回一个以该参数作为理由拒绝的期约
这两个静态方法返回的期约在被返回时并未兑现或拒绝,但它们会在当前同步代码块运行结束后立即兑现或拒绝
串行期约
如何按顺序运行任意数量的期约?
function fetchAll(urls){
// 保存响应体
const bodies = []
// 函数返回一个promise,只抓取一个响应体
function getOne(url){
return fetch(url).then().then(body =>{ bodies.push(body)})
}
// 以一个立即兑现期约开始
let p = Promise.resolve(undefined)
for(url of urls){
p = p.then(()=>{getOne(url)})
}
// 最后一个期约兑现之后,bodies也完成
return p.then(()=>{bodies})
}
// 使用
fetchAll(urls).then(bodies => { // 处理bodies})
.catch(e => console.log(e))
fetchAll会首先创建以一个返回后立即兑现的期约。然后基于这个初始期约构建一个线性的长期期约链并返回链中的最后一个期约
这是事先创建期约
另一种实现方式:让每个期约的回调创建并返回下一个期约(创建解决为其他期约的期约)
function fetchAll(inputs, promiseMaker){
inputs = [...inputs]
//伪递归,用作期约回调的函数
function handleNext(outputs){
if(inputs.length === 0){
return outputs
} else {
let next = inputs.shift()
return promiseMaker(next).then(output => outputs.concat(output))
.then(handleNext)
}
}
return Promise.resolve([]).then(handleNext)
}
//使用
function fetchBody(url){ return fetch(url).then(r => r.text())}
fetchAll(url, fetchBody).then(bodies => /* 数据处理 */).catch()
async 和 await
await关键字接收一个期约并将其转换为一个返回值或一个抛出的异常;给定一个期约p,表达式await p会一直等到p落定
await并不会导致程序阻塞或者在指定的期约落定前什么都不做,意味着任何使用await的代码本身都是异步的
async function getData(url){
let res = await fetch(url)
let body = await res.json()
return body
}
->
let value1 = await getData(url1)
let value2 = await getData(url2)
这样写意味着必须等到抓取第一个url结果之后才开始抓第二个,如果第二个不依赖第一个结果
let [value1, value2] = await Promise.all([getData(url1), getData(url2)])
实现细节
async function f(x) { /*函数*/}
等同于
function f(x){
return new Promise(function(resolve, reject){
try{
resolve((function (x){/*函数*/})(x))
}
catch{
reject(e)
}
})
}
异步迭代
由于一个期约无法用于连续的异步事件
- for/await 循环 Node 12的可读流实现了异步可迭代
const urls = [url1, url2, url3]
const promises = urls.map(url => fetch(url))
for(const promise of promises){
res = await promise
handle(res)
}
// for/await写法
for await(const res of promises){
handle(res)
}
- 异步迭代器
异步迭代器以符号名字Symbol.asyncIterator实现了一个方法,for/await与常规浏览器兼容,但它更适合异步可迭代对象,因此会在尝试iterator之前尝试asyncIterator;异步迭代器的next返回一个期约,解决为一个迭代器结果对象,而不是直接返回一个迭代器结果对象
-
异步生成器
-
实现异步迭代器