数值扩展
- 二进制(0b)和八进制(0o)表示法:
let num = 100
let b = num.toString(2) // 二进制的100:1100100
let o = num.toString(8) // 八进制的100:144
0b1100100 === 100 // true
0o144 === 100 // true
Number.isFinite()
判断一个数是否是有限的数,入参如果不是数值一律返回false
:
Number.isFinite(-2.9) // true
Number.isFinite(NaN) // false
Number.isFinite(‘’) // false
Number.isFinite(false) // true
Number.isFinite(Infinity) // false
Number.isNaN()
判断一个数值是否为NaN
,如果入参不是NaN
那结果都是false
:
Number.isNaN(NaN) // true
Number.isFinite(‘a’/0) // true
Number.isFinite(‘NaN’) // false
- 数值转化:
Number.parseInt()
和Number.parseFloat()
,非严格转化,从左到右解析字符串,遇到非数字就停止解析,并且把解析的数字返回:
parseInt(‘12a’) // 12
parseInt(‘a12’) // NaN
parseInt(‘’) // NaN
parseInt(‘0xA’) // 10,0x开头的将会被当成十六进制数
parseInt()
默认是用十进制去解析字符串的,其实他是支持传入第二个参数的,表示要以多少进制的 基数去解析第一个参数:
parseInt(‘1010’, 2) // 10
parseInt(‘ff’, 16) // 255
参考:parseInt
Number.isInteger()
判断一个数值是否为整数,入参为非数值则一定返回false
:
Number.isInteger(25) // true
Number.isInteger(25.0) // true
Number.isInteger() // false
Number.isInteger(null) // false
Number.isInteger(‘15’) // false
Number.isInteger(true) // false
Number.isInteger(3.0000000000000002) // true
❝
如果对数据精度的要求较高,不建议使用 Number.isInteger() 判断一个数值是否为整数。
❞
Number.EPSILON
表示一个可接受的最小误差范围,通常用于浮点数运算:
0.1 + 0.2 === 0.3 // false
Math.abs(0.1 + 0.2 - 0.3) < Number.EPSILON // true
Number.isSafeInteger()
用来判断一个数是否在最大安全整数(Number.MAX_SAFE_INTEGER
)和最小安全整数(Number.MIN_SAFE_INTEGER
)之间:
Number.MAX_SAFE_INTEGER === 2 ** 53 -1 // true
Number.MAX_SAFE_INTEGER === 9007199254740991 // true
Number.MIN_SAFE_INTEGER === -Number.MAX_SAFE_INTEGER // true
Number.isSafeInteger(2) // true
Number.isSafeInteger(‘2’) // false
Number.isSafeInteger(Infinity) // false
-
Math.trunc()
:返回数值整数部分 -
Math.sign()
:返回数值类型(正数 1、负数 -1、零 0) -
Math.cbrt()
:返回数值立方根 -
Math.clz32()
:返回数值的 32 位无符号整数形式 -
Math.imul()
:返回两个数值相乘 -
Math.fround()
:返回数值的 32 位单精度浮点数形式 -
Math.hypot()
:返回所有数值平方和的平方根 -
Math.expm1()
:返回e^n - 1
-
Math.log1p()
:返回 1 + n 的自然对数(Math.log(1 + n)
) -
Math.log10()
:返回以 10 为底的 n 的对数 -
Math.log2()
:返回以 2 为底的n的对数 -
Math.sinh()
:返回 n 的双曲正弦 -
Math.cosh()
:返回 n 的双曲余弦 -
Math.tanh()
:返回 n 的双曲正切 -
Math.asinh()
:返回 n 的反双曲正弦 -
Math.acosh()
:返回 n 的反双曲余弦 -
Math.atanh()
:返回 n 的反双曲正切
数组扩展
- 数组扩展运算符(…)将数组展开成用逗号分隔的参数序列,只能展开一层数组:
// 应用一:函数传参
Math.max(…[1, 2, 3]) // 3
// 应用二:数组合并
let merge = […[1, 2], …[3, 4], 5, 6] // 1, 2, 3, 4, 5, 6
// 应用三:浅克隆
let a = [1, 2, 3]
let clone = […a]
a === clone // false
// 应用四:数组解构
const [x, …y] = [1, 2, 3]
x // 1
y // [2, 3]
Array.from()
可以将类数组对象(NodeList
,arguments
)和可迭代对象转成数组:
// 应用一:字符串转数组
Array.from(‘foo’) // [‘f’, ‘o’, ‘o’]
// 应用二:数组合并去重
let merge = […[1, 2], …[2, 3]]
Array.from(new Set(merge)) // [‘1’, ‘2’, ‘3’]
// 应用三:arguments 转数组
function f() {
return Array.from(arguments)
}
f(1, 2, 3) // [1, 2, 3]
如果 Array.from()
带第二个参数 mapFn
,将对生成的新数组执行一次 map
操作:
Array.from([1, 2, 3], (x) => x * x ) // [1, 4, 9]
Array.from({length: 3}, (v, i) => ++i) // [1, 2, 3]
Array.of()
将一组参数转成数组:
Array.of(1, 2, 3) // [1, 2, 3]
// 类似于
function arrayOf(…params){
return [].slice.call(params)
}
arrayOf(1, 2, 3) // [1, 2, 3]
Array.copyWithin()
在当前数组内部,将制定位置的成员复制到其他位置(会覆盖原来位置的成员),最后返回一个新数组。接收 3 个参数,参数为负数表示右边开始计算:
[1, 2, 3, 4, 5].copyWithin(-1) // [1, 2, 3, 4, 1]
[1, 2, 3, 4, 5].copyWithin(1) // [1, 1, 2, 3, 4]
[1, 2, 3, 4, 5].copyWithin(0, 3, 4) // [4, 2, 3, 4, 5]
[1, 2, 3, 4, 5].copyWithin(0, -3, -1) // [3, 4, 3, 4, 5]
- 查找第一个出现的子成员:
find()
和findIndex()
:
// 找出第一个偶数
[1, 6, 9].find((val, index, arr) => val % 2 === 0) // 6
// 找出第一个偶数的索引位置
[1, 6, 9].findIndex((val, index, arr) => val % 2 === 0) // 1
fill()
使用给定的值来填充数组,有 3 个参数:
// 初始化空数组
Array(3).fill(1) // [1, 1, 1]
[1, 2, 3, 4].fill(‘a’, 2, 4) // [1, 2, ‘a’, ‘a’]
- 通过
keys()
(键名)、entries()
(键值)和values()
(键值对) 获取数组迭代器对象,可以被for...of
迭代,
let arr = [‘a’, ‘b’, ‘c’]
for (let x of arr.keys()) {
console.log(x) // 1, 2, 3
}
for (let v of arr.values()) {
console.log(v) // ‘a’ ‘b’ ‘c’
}
for (let e of arr.entries()) {
console.log(e) // [0, ‘a’] [0, ‘b’] [0, ‘c’]
}
- 数组空位,是指数组没有值,比如:
[,,]
,而像这种[undefined]
是不包含空位的。由于ES6
之前的一些API
对空位的处理规则很不一致,所以实际操作的时候应该尽量避免空位的出现,而为了改变这个现状,ES6
的API
会默认将空位处理成undefined
:
[…[1, , 3].values()] // [1, undefined, 3]
[1, , 3].findIndex(x => x === undefined) // 1
对象扩展
- 对象属性简写:
let name = ‘布兰’
let person = {
name,
getName() {
return this.name
}
}
// 等同于
let person1 = {
name: ‘布兰’,
getName: function() {
return this.name
}
}
- 属性名表达式:在用对象字面量定义对象的时候,允许通过属性名表达式来定义对象属性:
let name = ‘name’,
let person = {
return this.name
}
}
- 方法的
name
属性,存在好几种情况,这里仅列出常见的几种:
情况一:普通对象方法的 name
属性直接返回方法名,函数声明亦是如此,函数表达式返回变量名:
let person = {
hi(){}
}
person.hi.name // ‘hi’
情况二:构造函数的 name
为 anonymous
:
(new Function).name // ‘anonymous’
情况三:绑定函数的 name
将会在函数名前加上 bound
:
function foo() {}
foo.bind({}).name // ‘bound foo’
情况四:如果对象的方法使用了取值函数(getter
)和存值函数(setter
),则 name
属性不是在该方法上面,而是该方法的属性的描述对象的 get
和 set
属性上面:
let o = {
get foo(){},
set foo(x){}
}
o.foo.name // TypeError: Cannot read property ‘name’ of undefined
let descriptor = Object.getOwnPropertyDescriptor(o, “foo”)
descriptor.get.name // “get foo”
descriptor.set.name // “set foo”
参考:function_name
- 属性的可枚举性
对象的每个属性都有一个描述对象(Descriptor
),用来控制该属性的行为。可以通过 Object.getOwnPropertyDescriptor()
来获取对象某个属性的描述:
let person = { name: ‘布兰’, age: 12 }
Object.getOwnPropertyDescriptor(person, ‘name’)
// {
// configurable: true,
// enumerable: true,
// value: “布兰”,
// writable: true,
// }
这里的 enumerable
就是对象某个属性的可枚举属性,如果某个属性的 enumerable
值为 false
则表示该属性不能被枚举,所以该属性会被如下 4 种操作忽略:
let person = { name: ‘布兰’ }
Object.defineProperty(person, ‘age’, {
configurable: true,
enumerable: false,
value: 12,
writable: true
})
person // { name: ‘布兰’, age: 12 }
// 以下操作都将忽略 person 对象的 age 属性
for (let x in person) {
console.log(x) // ‘name’
}
Object.keys(person) // [‘name’]
JSON.stringify(person) // ‘{“name”: “布兰”}’
Object.assign({}, person) // { name: ‘布兰’ }
Reflect.ownKeys(obj)
:返回一个数组,包含对象自身的(不含继承的)所有键名,不管键名是 Symbol
或字符串,也不管是否可枚举:
// 基于上面的代码
Reflect.ownKeys(person) // [‘name’, ‘age’]
super
关键字,指向对象的原型对象,只能用于对象的方法中,其他地方将报错:
let person = {
name: ‘布兰’,
getName() {
return super.name
}
}
Object.setPrototypeOf(person, {name: ‘hello’})
person.getName() // ‘hello’
// 以下几种 super 的使用将报错
const obj1 = {
foo: super.foo
}
const obj2 = {
foo: () => super.foo
}
const obj3 = {
foo: function () {
return super.foo
}
}
Object.is()
用来判断两个值是否相等,表现基本和===
一样,除了以下两种情况:
+0 === -0 //true
NaN === NaN // false
Object.is(+0, -0) // false
Object.is(NaN, NaN) // true
Object.assign()
用于对象的合并,将源对象(source
)的所有可枚举属性,复制到目标对象(target
),如果有同名属性,则后面的会直接替换前面的:
let target = { a: 1 }
let source1 = { a: 2, b: 3, d: {e: 1, f: 2} }
let source2 = { a: 3, c: 4, d: {g: 3} }
Object.assign(target, source1, source2)
target // { a: 3, b: 3, c: 4, d: {g: 3} }
Object.assign()
实行的是浅拷贝,如果源对象某个属性是对象,那么拷贝的是这个对象的引用:
let target = {a: {b: 1}}
let source = {a: {b: 2}}
Object.assign(target, source)
target.a.b = 3
source.a.b // 3
-
__proto__
属性是用来读取和设置当前对象的原型,而由于其下划线更多的是表面其是一个内部属性,所以建议不在正式场合使用它,而是用下面的Object.setPrototypeOf()
(写操作)、Object.getPrototypeOf()
(读操作)、Object.create()
(生成操作)代替。 -
Object.setPrototypeOf()
用于设置对象原型,Object.getPrototypeOf()
用于读取对象原型:
let person = {name: ‘布兰’}
Object.setPrototypeOf(person, {name: ‘动物’})
Object.getPrototypeOf(person) // {name: ‘动物’}
正则扩展
RegExp
构造函数,允许首参为正则表达式,第二个参数为修饰符,如果有第二个参数,则修饰符以第二个为准:
let reg = new RegExp(/xYz\d+/gi, i)
reg.flags // ‘i’
-
正则方法调用变更:字符串对象的
match()
、replace()
、search()
、split()
内部调用转为调用RegExp
实例对应的RegExp.prototype[Symbol.方法]
; -
u
修饰符:含义为Unicode
模式,用来正确处理大于\uFFFF
的Unicode
字符。也就是说,如果待匹配的字符串中可能包含有大于\uFFFF
的字符,就必须加上u
修饰符,才能正确处理。
// 加上 u 修饰符才能让 . 字符正确识别大于 \uFFFF 的字符
/^.$/.test(‘???’) // false
/^.$/u.test(‘???’) // true
// 大括号 Unicode 字符表示法必须加上 u 修饰符
/\u{61}/.test(‘a’) // false
/\u{61}/u.test(‘a’) // true
// 有 u 修饰符,量词才能正确匹配大于 \uFFFF 的字符
/???{2}/.test(‘???’) // false
/???{2}/u.test(‘???’) // true
RegExp.prototype.unicode
属性表示正则是否设置了 u
修饰符:
/???/.unicode // false
/???/u.unicode // true
y
修饰符,与g
修饰符类似也是全局匹配;不同的是g
是剩余字符中匹配即可,而y
则是必须在剩余的第一个字符开始匹配才行,所以y
修饰符也叫黏连修饰符:
let s = ‘aaa_aa_a’
let r1 = /a+/g
let r2 = /a+/y
r1.exec(s) // [“aaa”]
r2.exec(s) // [“aaa”]
r1.exec(s) // [“aa”]
r2.exec(s) // null
RegExp.prototype.sticky
属性表示是否设置了 y
修饰符:
/abc/y.sticky // true
RegExp.prototype.flags
属性会返回当前正则的所有修饰符:
/abc???/iuy.flags // ‘iuy’
函数扩展
- 函数参数默认值。参数不能有同名的,函数体内不能用
let
和const
声明同参数名的变量:
function printInfo(name = ‘布兰’, age = 12) {}
使用参数默认值的时候,参数不能有同名的:
function f(x, x, y) {} // 不报错
function f(x, x, y = 1) {} // 报错
函数体内不能用 let
和 const
声明同参数名的变量:
// 报错
function f(x, y) {
let x = 0
}
函数的 length
属性会返回没有指定默认值的参数个数,且如果设置默认值的参数不是尾参数,则 length
不再计入后面的参数:
(function f(x, y){}).length // 2
(function f(x, y = 1){}).length // 1
(function f(x = 1, y){}).length // 0
- 剩余(
rest
) 参数(…变量名)的形式,用于获取函数的剩余参数,注意rest
参数必须放在最后一个位置,可以很好的代替arguments
对象:
function f(x, …y) {
console.log(x) // 1
for (let val of y) {
coonsole.log(val) // 2 3
}
}
f(1, 2, 3)
- 严格模式:只要函数参数使用了默认值、解构赋值或者扩展运算符,那么函数体内就不能显示的设定为严格模式,因为严格模式的作用范围包含了函数参数,而函数执行的顺序是先执行参数,然后再执行函数体,执行到函数体里的
use strict
的时候,那么此时因为函数参数已经执行完成了,那函数参数还要不要受到严格模式的限制呢?这就出现矛盾了。规避限制的办法有两个:设置全局的严格模式或者在函数体外在包一个立即执行函数并且声明严格模式:
// 解法一
‘use strict’
function f(x, y = 2) {
}
// 解法二
let f = (function(){
‘use strict’
return function(x, y = 2) {}
})()
- 箭头函数语法比函数表达式更简洁,并且没有自己的
this
、arguments
,不能用作构造函数和用作生成器。
几种箭头函数写法:
let f1 = () => {} // 没有参数
let f2 = (x) => {} // 1个参数
let f3 = x => {} // 1个参数可以省略圆括号
let f4 = (x, y) => {} // 2个参数以上必须加上圆括号
let f5 = (x = 1, y = 2) => {} // 支持参数默认值
let f6 = (x, …y) => {} // 支持 rest 参数
let f7 = ({x = 1, y = 2} = {}) // 支持参数解构
箭头函数没有自己的 this
:
function Person(){
this.age = 0
setInterval(() => {
this.age++
}, 1000)
}
var p = new Person() // 1 秒后 Person {age: 1}
通过 call/apply
调用箭头函数的时候将不会绑定第一个参数的作用域:
let adder = {
base: 1,
add: function(a) {
let f = v => v + this.base
return f(a)
},
addThruCall: function(a) {
let f = v => v + this.base
let b = {
base: 2
}
return f.call(b, a)
}
}
adder.add(1) // 输出 2
adder.addThruCall(1) // 仍然输出 2
箭头函数没有自己的 arguments
对象,不过可以使用 rest
参数代替:
let log = () => {
console.log(arguments) // ReferenceError
}
log(2, 3)
// 剩余参数代替写法
let restLog = (…arr) => {
console.log(arr) // [2, 3]
}
restLog(2, 3)
箭头函数不能用作构造器,和 new
一起用会抛出错误:
let Foo = () => {}
let foo = new Foo()
// TypeError: Foo is not a constructor
箭头函数返回对象字面量,需要用圆括号包起来:
let func2 = () => ({foo: 1})
参考:Arrow_functions
- 尾调用和尾递归
首先得知道什么是尾调用:函数的最后一步调用另外一个函数:
// 是尾调用
function f(x) {
return g(x)
}
// 以下都不是尾调用
function f(x) {
let y = g(x)
return y
}
function f(x) {
let y = g(x)
return g(x) + 1
}
function f(x) {
g(x)
// 因为最后一步是 return: undefined
}
尾调用有啥用?我们知道函数的相互调用是会生成“调用帧”的,而“调用帧”里存了各种信息比如函数的内部变量和调用函数的位置等,所有的“调用帧”组成了一个“调用栈”。如果在函数的最后一步操作调用了另外一个函数,因为外层函数里调用位置、内部变量等信息都不会再用到了,所有就无需保留外层函数的“调用帧”了,只要直接用内层函数的“调用帧”取代外层函数的“调用帧”即可:
function f() {
let m = 1
let n = 2
return g(m + n)
}
f()
// 等同于
function f() {
return g(3)
}
f()
// 等同于
g(3)
这样一来就很明显的减少了调用栈中的帧数,内存占用就少了,所以这就是尾调用的优化作用。尾递归也是如此,递归如果次数多那就需要保留非常多的“调用帧”,所以经常会出现栈溢出错误,而使用了尾递归优化后就不会发生栈溢出的错误了:
// 常规递归的斐波那契函数
function Fibonacci(n) {
if ( n <= 1 ) {return 1}
return Fibonacci(n - 1) + Fibonacci(n - 2)
}
Fibonacci(100) // 超时
// 尾递归优化后的斐波那契函数
function Fibonacci2(n, ac1 = 1, ac2 = 1) {
if( n <= 1 ) {return ac2}
return Fibonacci2(n - 1, ac2, ac1 + ac2)
}s
Fibonacci2(100) // 573147844013817200000
Symbol
Symbol
是一个新的原始类型,用来表示一个独一无二的值,可以通过Symbol()
函数来创建一个Symbol
类型的值,为了加以区分,可以传入一个字符串作为其描述:
let s1 = Symbol(‘foo’)
let s2 = Symbol(‘foo’)
s1 === s2 // false
Symbol
类型无法通过数学运算符进行隐式类型转换,但是可以通过String()
显示转成字符串或者通过Boolean()
显示转成布尔值:
let s = Symbol(‘foo’)
String(s) // “Symbol(‘foo’)”
s.toString() // “Symbol(‘foo’)”
Boolean(s) // true
- 引入
Symbol
最大的初衷其实就是为了让它作为对象的属性名而使用,这样就可以有效避免属性名的冲突了:
let foo = Symbol(‘foo’)
let obj = {
foo: ‘foo2’
}
objfoo // ‘foo1’
obj.foo // ‘foo2’
Symbol
属性的不可枚举性,不会被for...in
、for...of
、Object.keys()
、Object.getOwnPropertyNames()
、JSON.stringify()
等枚举:
let person = {
name: ‘布兰’,
}
for (let x in person) {
console.log(x) // ‘name’
}
Object.keys(person) // [‘name’]
Object.getOwnPropertyNames(person) // [‘name’]
JSON.stringify(person) // ‘{“name”:“布兰”}’
但是可以通过 Object.getOwnPropertySymbols()
获取到对象的所有 Symbol
属性名,返回一个数组:
// 基于上面的代码
Object.getOwnPropertySymbols(person) // [Symbol(age)]
静态方法:
Symbol.for()
按照描述去全局查找Symbol
,找不到则在全局登记一个:
let s1 = Symbol.for(‘foo’)
let s2 = Symbol.for(‘foo’)
s1 === s2 // true
Symbol.for()
的这个全局登记特性,可以用在不同的 iframe
或 service worker
中取到同一个值。
Symbol.keyFor()
根据已经在全局登记的Symbol
查找其描述:
let s = Symbol.for(‘foo’)
Symbol.keyFor(s) // ‘foo’
Symbol
的内置值:
-
Symbol.hasInstance
:指向一个内部方法,当其他对象使用instanceof
运算符判断是否为此对象的实例时会调用此方法; -
Symbol.isConcatSpreadable
:指向一个布尔,定义对象用于Array.prototype.concat()
时是否可展开; -
Symbol.species
:指向一个构造函数,当实例对象使用自身构造函数时会调用指定的构造函数; -
Symbol.match
:指向一个函数,当实例对象被String.prototype.match()
调用时会重新定义match()的行为; -
Symbol.replace
:指向一个函数,当实例对象被String.prototype.replace()
调用时会重新定义replace()
的行为; -
Symbol.search
:指向一个函数,当实例对象被String.prototype.search()
调用时会重新定义search()
的行为;s -
Symbol.split
:指向一个函数,当实例对象被String.prototype.split()
调用时会重新定义split()
的行为; -
Symbol.iterator
:指向一个默认遍历器方法,当实例对象执行for...of
时会调用指定的默认遍历器; -
Symbol.toPrimitive
:指向一个函数,当实例对象被转为原始类型的值时会返回此对象对应的原始类型值; -
Symbol.toStringTag
:指向一个函数,当实例对象被Object.prototype.toString()
调用时其返回值会出现在toString()
返回的字符串之中表示对象的类型; -
Symbol.unscopables
:指向一个对象,指定使用with
时哪些属性会被with
环境排除;
Set
Set
是一种新的数据结构,类似数组,但是它没有键只有值,且值都是唯一的。可以通过构造函数生成一个新实例,接收一个数组或者可迭代数据结构作为参数:
new Set([1, 2, 3]) // Set {1, 2, 3}
new Set(‘abc’) // Set {‘a’, ‘b’, ‘c’}
Set
判断两个值是不是相等用的是 sameValueZero 算法,类似于===
,唯一的区别是,在Set
里NaN
之间被认为是相等的:
let set = new Set()
let a = NaN
let b = NaN
set.add(a)
set.add(b)
set.size // 1
- 相同对象的不同实例也被
Set
认为是不相等的:
let set = new Set()
let a = {a: 1}
let b = {a: 1}
set.add(a)
set.add(b)
set.size // 2
Set
是有顺序的,将按照插入的顺序进行迭代,可以使用for...of
迭代:
let set = new Set([1, 3])
set.add(5)
set.add(7)
for(let x of set) {
console.log(x)
}
Set
实例属性和方法:
-
Set.prototype.constructor
:构造函数,默认就是Set
函数; -
Set.prototype.size
:返回Set
实例的成员总数; -
Set.prototype.add(value)
:添加某个值,返回Set
结构本身; -
Set.prototype.delete(value)
:删除某个值,返回一个布尔值,表示删除是否成功; -
Set.prototype.has(value)
:返回一个布尔值,表示该值是否为Set的成员; -
Set.prototype.clear()
:清除所有成员,没有返回值; -
Set.prototype.keys()
:返回键名的遍历器; -
Set.prototype.values()
:返回键值的遍历器; -
Set.prototype.entries()
:返回键值对的遍历器; -
Set.prototype.forEach()
:使用回调函数遍历每个成员;
let set = new Set([1, 3])
set.add(5) // Set {1, 3, 5}
set.size // 3
set.delete(1) // true,1 已被删除
set.has(1) // false
set.keys() // SetIterator {3, 5}
set.clear()
set.size // 0
Set
应用场景:
- 数组去重:
[…new Set([1, 3, 6, 3, 1])] // [1, 3, 6]
Array.from(new Set([1, 3, 6, 3, 1])) // [1, 3, 6]
- 字符串去重:
[…new Set(‘abcbacd’)].join(‘’) // ‘abcd’
- 求两个集合的交集/并集/差集:
let a = new Set([1, 2, 3])
let b = new Set([4, 3, 2])
// 并集
let union = new Set([…a, …b]) // Set {1, 2, 3, 4}
// 交集
let intersect = new Set([…a].filter(x => b.has(x))) // set {2, 3}
// (a 相对于 b 的)差集
let difference = new Set([…a].filter(x => !b.has(x))) // Set {1}
- 遍历修改集合成员的值:
let set = new Set([1, 2, 3])
// 方法一
let set1 = new Set([…set].map(val => val * 2)) // Set {2, 3, 6}
// 方法二
let set2 = new Set(Array.from(set, val => val * 2)) // Set {2, 4, 6}
WeakSet
WeakSet
对象允许将弱保持对象存储在一个集合中:
let ws = new WeakSet()
let foo = {}
ws.add(foo) // WeakSet {{}}
ws.has(foo) // true
ws.delete(foo) // WeakSet {}
和 Set
的区别:
-
WeakSet
只能是对象的集合,而不能是任何类型的任意值; -
WeakSet
持弱引用:集合中对象的引用为弱引用。如果没有其他的对WeakSet
中对象的引用,那么这些对象会被当成垃圾回收掉。这也意味着WeakSet
中没有存储当前对象的列表。正因为这样,WeakSet
是不可枚举的,也就没有size
属性,没有clear
和遍历的方法。
实例方法:
-
WeakSet.prototype.add(value)
:添加一个新元素value
; -
WeakSet.prototype.delete(value)
:从该WeakSet
对象中删除value
这个元素; -
WeakSet.prototype.has(value)
:返回一个布尔值, 表示给定的值value
是否存在于这个WeakSet
中;
Map
Map
是一种类似于Object
的这种键值对的数据结构,区别是对象的键只能是字符串或者Symbol
,而Map
的键可以是任何类型(原始类型、对象或者函数),可以通过Map
构造函数创建一个实例,入参是具有Iterator
接口且每个成员都是一个双元素数组[key, value]
的数据结构:
let map1 = new Map()
map1.set({}, ‘foo’)
let arr = [[‘name’, ‘布兰’], [‘age’, 12]]
let map2 = new Map(arr)
Map
中的键和Set
里的值一样也必须是唯一的,遵循 sameValueZero 算法,对于同一个键后面插入的会覆盖前面的,
let map = new Map()
let foo = {foo: ‘foo’}
map.set(foo, ‘foo1’)
map.set(foo, ‘foo2’)
map.get(foo) // ‘foo2’
- 对于键名同为
NaN
以及相同对象而不同实例的处理同Set
的值一样:
let a = NaN
let b = NaN
let map = new Map()
map.set(a, ‘a’)
map.set(b, ‘b’)
map.size // 1
map.get(a) // ‘b’
let c = {c: ‘c’}
let d = {c: ‘c’}
map.set(c, ‘c’)
map.set(d, ‘d’)
map.size // 3
map.get© // ‘c’
实例属性和方法:
-
Map.prototype.size
:返回Map
对象的键值对数量; -
Map.prototype.set(key, value)
:设置Map
对象中键的值。返回该Map
对象; -
Map.prototype.get(key)
:返回键对应的值,如果不存在,则返回undefined
; -
Map.prototype.has(key)
:返回一个布尔值,表示Map
实例是否包含键对应的值; -
Map.prototype.delete(key)
:如果Map
对象中存在该元素,则移除它并返回true
; -
Map.prototype.clear()
:移除Map
对象的所有键/值对; -
Map.prototype.keys()
:返回一个新的Iterator
对象, 它按插入顺序包含了Map
对象中每个元素的键; -
Map.prototype.values()
:返回一个新的Iterator
对象,它按插入顺序包含了Map
对象中每个元素的值; -
Map.prototype.entries()
:返回一个新的Iterator
对象,它按插入顺序包含了Map
对象中每个元素的[key, value]
数组; -
Map.prototype.forEach(callbackFn[, thisArg])
:按插入顺序遍历Map
;
let map = new Map()
map.set({a: 1}, ‘a’)
map.set({a: 2}, ‘b’)
for (let [key, value] of map) {
console.log(key, value)
}
// {a: 1} ‘a’
// {a: 2} ‘b’
for (let key of map.keys()) {
console.log(key)
}
// {a: 1}
// {a: 2}
WeakMap
- 类似于
Map
的结构,但是键必须是对象的弱引用,注意弱引用的是键名而不是键值,因而WeakMap
是不能被迭代的;
let wm = new WeakMap()
let foo = {name: ‘foo’}
wm.set(foo, ‘a’) // Weak
wm.get(foo) // ‘a’
wm.has(foo) // true
虽然 wm
的键对 foo
对象有引用,但是丝毫不会阻止 foo
对象被 GC
回收。当引用对象 foo
被垃圾回收之后,wm
的 foo
键值对也会自动移除,所以不用手动删除引用。
实例方法:
-
WeakMap.prototype.delete(key)
:移除key
的关联对象; -
WeakMap.prototype.get(key)
:返回key关联对象, 或者 undefined(没有key关联对象时); -
WeakMap.prototype.has(key)
:根据是否有key
关联对象返回一个Boolean
值; -
WeakMap.prototype.set(key, value)
:在WeakMap
中设置一组key
关联对象,返回这个WeakMap
对象;
Proxy
Proxy
用来定义基本操作的的自定义行为,可以理解为当对目标对象target
进行某个操作之前会先进行拦截(执行handler
里定义的方法),必须要对Proxy
实例进行操作才能触发拦截,对目标对象操作是不会拦截的,可以通过如下方式定义一个代理实例
let proxy = new Proxy(target, handler)
let instance = new Proxy({name: ‘布兰’}, {
get(target, propKey, receiver) {
return hello, ${target.name}
},
})
instance.name // ‘hello, 布兰’
- 如果
handle
没有设置任何拦截,那么对实例的操作就会转发到目标对象身上:
let target = {}
let proxy = new Proxy(target, {})
proxy.name = ‘布兰’
target.name // ‘布兰’
- 目标对象被
Proxy
代理的时候,内部的this
会指向代理的实例:
const target = {
m: function () {
console.log(this === proxy)
}
}
const handler = {}
const proxy = new Proxy(target, handler)
target.m() // false
proxy.m() // true
静态方法:
Proxy.revocable()
用以定义一个可撤销的Proxy
:
let target = {}
let handler = {}
let {proxy, revoke} = Proxy.revocable(target, handler)
proxy.foo = 123
proxy.foo // 123
revoke()
proxy.foo // TypeError
handle
对象的方法:
-
get(target, propKey, receiver)
:拦截对象属性的读取,比如proxy.foo和proxy[‘foo’]。 -
set(target, propKey, value, receiver)
:拦截对象属性的设置,比如proxy.foo = v或proxy['foo'] = v
,返回一个布尔值。 -
has(target, propKey)
:拦截propKey in proxy
的操作,返回一个布尔值。deleteProperty(target, propKey)
:拦截delete proxy[propKey]
的操作,返回一个布尔值。 -
ownKeys(target)
:拦截Object.getOwnPropertyNames(proxy)
、Object.getOwnPropertySymbols(proxy)
、Object.keys(proxy)
、for...in
循环,返回一个数组。该方法返回目标对象所有自身的属性的属性名,而Object.keys()
的返回结果仅包括目标对象自身的可遍历属性。 -
getOwnPropertyDescriptor(target, propKey)
:拦截Object.getOwnPropertyDescriptor(proxy, propKey)
,返回属性的描述对象。 -
defineProperty(target, propKey, propDesc)
:拦截Object.defineProperty(proxy, propKey, propDesc)
、Object.defineProperties(proxy, propDescs)
,返回一个布尔值。 -
preventExtensions(target)
:拦截Object.preventExtensions(proxy)
,返回一个布尔值。 -
getPrototypeOf(target)
:拦截Object.getPrototypeOf(proxy)
,返回一个对象。 -
isExtensible(target)
:拦截Object.isExtensible(proxy)
,返回一个布尔值。 -
setPrototypeOf(target, proto)
:拦截Object.setPrototypeOf(proxy, proto)
,返回一个布尔值。如果目标对象是函数,那么还有两种额外操作可以拦截。 -
apply(target, object, args)
:拦截Proxy
实例作为函数调用的操作,比如proxy(...args)
、proxy.call(object, ...args)
、proxy.apply(...)
。 -
construct(target, args)
:拦截Proxy
实例作为构造函数调用的操作,比如new proxy(...args)
。
Reflect
-
Reflect
是一个内置的对象,它提供拦截JavaScript
操作的方法。这些方法与proxy handlers
的方法相同。Reflect
不是一个函数对象,因此它是不可构造的。 -
设计的目的:
静态方法:
-
Reflect.apply(target, thisArgument, argumentsList)
对一个函数进行调用操作,同时可以传入一个数组作为调用参数。和Function.prototype.apply()
功能类似; -
Reflect.construct(target, argumentsList[, newTarget])
对构造函数进行new
操作,相当于执行new target(...args)
; -
Reflect.defineProperty(target, propertyKey, attributes)
和Object.defineProperty()
类似。如果设置成功就会返回true
; -
Reflect.deleteProperty(target, propertyKey)
作为函数的delete
操作符,相当于执行delete target[name]
; -
Reflect.get(target, propertyKey[, receiver])
获取对象身上某个属性的值,类似于target[name]
; -
Reflect.getOwnPropertyDescriptor(target, propertyKey)
类似于Object.getOwnPropertyDescriptor()
。如果对象中存在该属性,则返回对应的属性描述符, 否则返回undefined
; -
Reflect.getPrototypeOf(target)
类似于Object.getPrototypeOf()
; -
Reflect.has(target, propertyKey)
判断一个对象是否存在某个属性,和in
运算符 的功能完全相同; -
Reflect.isExtensible(target)
类似于Object.isExtensible()
; -
Reflect.ownKeys(target)
返回一个包含所有自身属性(不包含继承属性)的数组。(类似于Object.keys()
, 但不会受enumerable
影响); -
Reflect.preventExtensions(target)
类似于Object.preventExtensions()
。返回一个Boolean
; -
Reflect.set(target, propertyKey, value[, receiver])
将值分配给属性的函数。返回一个Boolean
,如果更新成功,则返回true
; -
Reflect.setPrototypeOf(target, prototype)
设置对象原型的函数. 返回一个Boolean
, 如果更新成功,则返回true
;
Class
- 可以用
class
关键字来定义一个类,类是对一类具有共同特征的事物的抽象,就比如可以把小狗定义为一个类,小狗有名字会叫也会跳;类是特殊的函数,就像函数定义的时候有函数声明和函数表达式一样,类的定义也有类声明和类表达式,不过类声明不同于函数声明,它是无法提升的;类也有name
属性
// 类声明
class Dog {
constructor(name) {
this.name = name
}
bark() {}
jump() {}
}
Dog.name // ‘Dog’
// 类表达式:可以命名(类的 name 属性取类名),也可以不命名(类的 name 属性取变量名)
let Animal2 = class {
// xxx
}
Animal2.name // ‘Animal2’
JS
中的类建立在原型的基础上(通过函数来模拟类,其实类就是构造函数的语法糖),和ES5
中构造函数类似,但是也有区别,比如类的内部方法是不可被迭代的:
class Dog {
constructor() {}
bark() {}
jump() {}
}
Object.keys(Dog.prototype) // []
// 类似于
function Dog2(){
}
Dog2.prototype = {
constructor() {},
bark() {},
jump() {},
}
Object.keys(Dog2.prototype) // [‘constructor’, ‘bark’, ‘jump’]
- 基于原型给类添加新方法:
Object.assign(Dog.prototype, {
wag() {} // 摇尾巴
})
-
类声明和类表达式的主体都执行在严格模式下。比如,构造函数,静态方法,原型方法,
getter
和setter
都在严格模式下执行。 -
类内部的
this
默认指向类实例,所以如果直接调用原型方法或者静态方法会导致this
指向运行时的环境,而类内部是严格模式,所以此时的this
会是undefined
:
class Dog {
constructor(name) {
this.name = name
}
bark() {
console.log( ${this.name} is bark.
)
}
static jump() {
console.log( ${this.name} is jump.
)
}
}
let dog = new Dog(‘大黄’)
let { bark } = dog
let { jump } = Dog
bark() // TypeError: Cannot read property ‘name’ of undefined
jump() // TypeError: Cannot read property ‘name’ of undefined
方法和关键字:
constructor
方法是类的默认方法,通过new
关键字生成实例的时候,会自动调用;一个类必须有constructor
方法,如果没有显示定义,则会自动添加一个空的;constructor
默认会返回实例对象:
class Point {}
// 等同于
class Point {
constructor() {}
}
- 通过
get
和set
关键字拦截某个属性的读写操作:
class Dog {
get age(){
return 1
}
set age(val){
this.age = val
}
}
- 用
static
关键字给类定义静态方法,静态方法不会存在类的原型上,所以不能通过类实例调用,只能通过类名来调用,静态方法和原型方法可以同名:
class Dog {
bark() {}
jump() {
console.log(‘原型方法’)
}
static jump() {
console.log(‘静态方法’)
}
}
Object.getOwnPropertyNames(Dog.prototype) // [‘constructor’, ‘bark’, ‘jump’]
Dog.jump() // ‘静态方法’
let dog = new Dog()
dog.jump() // ‘原型方法’
- 公有字段和私有字段:
静态公有字段和静态方法一样只能通过类名调用;私有属性和私有方法只能在类的内部调用,外部调用将报错:
class Dog {
age = 12 // 公有字段
static sex = ‘male’ // 静态公有字段
#secret = ‘我是人类的好朋友’ // 私有字段
#getSecret() { // 私有方法
return this.#secret
}
}
Dog.sex // ‘male’
let dog = new Dog()
dog.#getSecret() // SyntaxError
❝
公共和私有字段声明是 JavaScript 标准委员会 TC39 提出的实验性功能(第 3 阶段)。浏览器中的支持是有限的,但是可以通过 Babel 等系统构建后使用此功能。
❞
new.target
属性允许你检测函数、构造方法或者类是否是通过new
运算符被调用的。在通过new
运算符被初始化的函数或构造方法中,new.target
返回一个指向构造方法或函数的引用。在普通的函数调用中,new.target
的值是undefined
,子类继承父类的时候会返回子类:
class Dog {
constructor() {
console.log(new.target.name)
}
}
function fn(){
if (!new.target) return ‘new target is undefined’
console.log(‘fn is called by new’)
}
let dog = new Dog() // ‘Dog’
fn() // ‘new target is undefined’
new fn() // ‘fn is called by new’
类的继承:
- 类可以通过
extends
关键字实现继承,如果子类显示的定义了constructor
则必须在内部调用super()
方法,内部的this
指向当前子类:
class Animal {
constructor(name) {
this.name = name
}
run() {
console.log(${this.name} is running.
)
}
}
class Dog extends Animal{
constructor(name){
super(name) // 必须调用
this.name = name
}
bark() {
console.log(${this.name} is barking.
)
}
}
let dog = new Dog(‘大黄’)
dog.run() // ‘大黄 is running.’
- 通过
super()
调用父类的构造函数或者通过super
调用父类的原型方法;另外也可以在子类的静态方法里通过super
调用父类的静态方法:
// 基于上面的代码改造
class Dog extends Animal{
constructor(name){
super(name) // 调用父类构造函数
this.name = name
}
bark() {
super.run() // 调用父类原型方法
console.log(${this.name} is barking.
)
}
}
let dog = new Dog()
dog.bark()s
// ‘大黄 is running.’
// ‘大黄 is barking.’
- 子类的
__proto__
属性,表示构造函数的继承,总是指向父类;子类prototype
属性的__proto__
属性,表示方法的继承,总是指向父类的prototype属性:
class Animal {}
class Dog extends Animal {}
Dog.proto === Animal // true
Dog.prototype.proto === Animal.prototype // true
子类原型的原型指向父类的原型:
// 基于上面的代码
let animal = new Animal()
let dog = new Dog()
dog.proto.proto === animal.proto // true
- 使用
extends
还可以实现继承原生的构造函数,如下这些构造函数都可以被继承:
class MyString extends String {
constructor(name){
super(name)
this.name = name
}
welcome() {
return hello ${this.name}
}
}
let ms = new MyString(‘布兰’)
ms.welcome() // ‘hello 布兰’
ms.length // 2
ms.indexOf(‘兰’) // 1
Module
- 浏览器传统加载模块方式:
// 同步加载
// defer异步加载:顺序执行,文档解析完成后执行;
// async异步加载:乱序加载,下载完就执行。
- 浏览器现在可以按照模块(加上
type="module"
)来加载脚本,默认将按照defer
的方式异步加载;ES6
的模块加载依赖于import
和export
这 2 个命令;模块内部自动采用严格模式:
// 模块加载
export
用于输出模块的对外接口,一个模块内只能允许一个export default
存在,以下是几种输出模块接口的写法:
// person.js
// 写法一:单独导出
export const name = ‘布兰’
export const age = 12
// 写法二:按需导出
const name = ‘布兰’, age = 12
export { name, age }
// 写法三:重命名后导出
const name = ‘布兰’, age = 12
export { name as name1, age as age1 }
// 写法四:默认导出
const name = ‘布兰’
export default name
import
用于输入其他模块的接口:
// 按需导入
import { name, age } ‘./person.js’
// 导入后重命名
import { name1 as name, age1 as age } from ‘./person.js’
// 默认导入
import person from ‘./person.js’
// 整体导入
import * as person from ‘./person.js’
// 混合导入
import _, { each } from ‘lodash’
import
导入的细节:
import
和export
的复合写法:export
和import
语句可以结合在一起写成一行,相当于是在当前模块直接转发外部模块的接口,复合写法也支持用as
重命名。以下例子中需要在hub.js
模块中转发person.js
的接口:
// person.js
const name = ‘布兰’, age = 12
export { name, age }
// 按需转发接口(中转模块:hub.js)
export { name, age } from ‘./person.js’
// 相当于
import { name, age } from ‘./person.js’
export { name, age }
// person.js
const name = ‘布兰’, age = 12
export default { name, age }
// 转发默认接口(中转模块:hub.js)
export { default } from ‘./person.js’
// 相当于
import person from ‘./person.js’
export default person
ES6
模块和CommonJS
模块的差异:
Iterator和for…of
Iterator
迭代器协议,为各种数据结构提供了一种统一按照某种顺序进行访问的机制。通常部署在一个可迭代数据结构内部或其原型上。一个对象要能够成为迭代器,它必须有一个next()
方法,每次执行next()
方法会返回一个对象,这个对象包含了一个done
属性(是个布尔值,true
表示可以继续下次迭代)和一个value
属性(每次迭代的值):
// 生成一个迭代器
let makeIterator = (arr) => {
let index = 0
return {
next(){
return index < arr.length ? {
value: arr[index++],
done: false
} : { done: true }
}
}
}
iterable
可迭代数据结构:内部或者原型上必须有一个Symbol.iterator
属性(如果是异步的则是Symbol.asyncIterator
),这个属性是一个函数,执行后会生成一个迭代器:
let obj = {
return {
index: 0,
next() {
if (this.index < 3) {
return {value: this.index++, done: false}
} else {
return {done: true}
}
}
}
}
}
for (let x of obj) {