小黑子的ES6-13系列入土过程
- ES6-13系列教程
- 1.1 初识别ES6
- 1.2 let 声明变量
- 1.3 const 声明常量
- 1.4 变量解构赋值
- 1.5 模板字符串
- 1.6 字符串与数值拓展
- 1.7 数组扩展
- 1.8 对象扩展
- 1.9 函数的扩展
- 1.10 Symbol
- 1.11 Iterator迭代器
- 1.12 Set 结构
- 1.13 遍历操作
- 1.14 Map 结构
- 1.15 Proxy 代理
- 1.16 Reflect 获取目标对象的行为
- 1.17 Promise对象
- 1.18 Generator 函数
- 1.19 class语法
- 1.22 Module 语法
- 1.22 ES6 NodeJS中的模块化
- 2.1 ES7-新特性
- 3.1 ES8新特性
- 3.1 ES8-对象方法扩展
- 3.2 ES8- 字符串填充
- 4.1 ES9-rest与扩展运算符
- 4.2 ES9-正值扩展
- 4.3 ES9-Promise.finally()
- 4.4 ES9-异步迭代
- 5.1 ES10新特性
- 5.2 ES10-trimStart() and trimEnd()
- 5.3 ES10-Symbol 对象的 description 属性
- 5.4 ES10-可选的catch
- 6.1 ES11新特性
- 6.2 ES11-module新增
- 6.4 ES11-BigInt
- 6.5 ES11-globalThis
- 6.6 ES11-空值合并运算符
- 6.7 ES11-可选链操作符&&和?.
- 7.1 ES12新特性
- 7.2 ES12-数字分隔符
- 7.3 ES12-replaceAll
- 7.4 ES12-Promise.any
- 7.5 ES12-WeakRefs
- 7.6 ES12-FinalizationRegistry
- 8.1 ES13新特性
- 8.2 ES13-最外层的await
- 8.3 ES13-at函数来索引元素
- 8.4 ES13-正则匹配的开始和结束索引
- 8.5 ES13-findLast()和findLastIndex()函数
- 8.6 Error对象的Cause属性
ES6-13系列教程
1.1 初识别ES6
ECMAScript 6.0
(以下简称ES6)是JavaScript语言的下一代标准,已经在2015年6月正式发布了。它的目标,是使得JavaScript语言可以用来编写复杂的大型应用程序,成为企业级开发语言。
- 1997年: ECMAScript 1.0
- 1998年: ECMAScript 2.0
- 1999年: ECMAScript 3.0
- 2006年: ECMAScript 4.0未通过
- 2009年: ECMAScript 5.0
- 2015年: ECMAScript 6.0
- 至今,版本号改用年号的形式。
1.2 let 声明变量
ES6 新增了let命令,用来声明变量。它的用法类似于var,但是所声明的变量,只在let命令所在的代码块内有效。
用法:
1.2.1 块级作用域
{
let a = 10;
var b = 1;
}
a // ReferenceError: a is not defined.
b // 1
上面代码在代码块之中,分别用let和var声明了两个变量。然后在代码块之外调用这两个变量,结果let声明的变量报错,var声明的变量返回了正确的值。
for循环的计数器,就很合适使用let命令。
- 在for循环中区分var声明
案例:
用var定义
<ul>
<li>1111</li>
<li>2222</li>
<li>3333</li>
</ul>
<script>
var oli =document.querySelectorAll("ul li")
for(var i = 0; i<oli.length;i++)
{
oli[i].onclick = function(){
console.log(i)
}
}
</script>
var定义的i
是在全局范围内都有效,所以全局只有一个变量i
。每一次循环,变量i
的值都会发生改变,而循环内被赋点击事件内部的console.log(i),里面的i指向的就是全局的i
也就是说,所有节点oli成员里面的i
,指向的都是同一个i
,导致运行时输出的是最后一轮的i
数组长度的值,也就是 3,这就导致了无论点击哪个节点,都是获取的3
然而,使用let定义函数内i
的话,不会出现这样的问题
var oli =document.querySelectorAll("ul li")
for(let i = 0; i<oli.length;i++)
{
oli[i].onclick = function(){
console.log(i)
}
}
1.2.2 不允许重复声明
var a =1;
let a = 2'
console.log(a) //报错重名
1.2.3 没有变量声明提前
var命令会发生“变量提升”现象,即变量可以在声明之前使用,值为undefined。这种现象多多少少是有些奇怪的,按照一般的逻辑,变量应该在声明语句之后才可以使用。
为了纠正这种现象,let命令改变了语法行为,它所声明的变量一定要在声明后使用,否则报错。
1.2.4 暂存性死区
var tmp = 123;
if (true) {
tmp = 'abc'; // ReferenceError
let tmp;
}
上面代码中,存在全局变量tmp
,但是块级作用域内let
又声明了一个局部变量tmp
,导致后者绑定这个块级作用域,所以在let
声明变量前,对tmp赋值会报错。
ES6 明确规定,如果区块中存在let和const
命令,这个区块对这些命令声明的变量,从一开始就形成了封闭作用域。凡是在声明之前就使用这些变量,就会报错。
总之,在代码块内,使用let命令声明变量之前,该变量都是不可用的。这在语法上,称为“暂时性死区
”(temporal dead zone,简称 TDZ
)。
“暂时性死区”也意味着typeof不再是一个百分之百安全的操作。
typeof x; // ReferenceError
let x;
变量x使用let命令声明,所以在声明之前,都属于x的“死区”,只要用到该变量就会报错。因此,typeof运行时就会抛出一个ReferenceError
。
1.2.5 不与顶层对象挂钩
用var
var myage = 100
console. log(window.myage) //返回100
用let
Let myage = 100
console. log(window.myage) //返回undefined
1.3 const 声明常量
1.3.1 声明只读的常量
一旦声明,常量的值就不能改变。
const PI = 3.1415;
PI // 3.1415
PI = 3;
// TypeError: Assignment to constant variable.
上面代码表明改变常量的值会报错。
const声明的变量不得改变值,这意味着,const一旦声明变量,就必须立即初始化,不能留到以后赋值。
1.3.2 也不能重复定义
const声明的常量,也与let一样不可重复声明。
1.3.3 常量也是不提升和存在暂时性死区
只能在声明的位置后面使用。
1.3.4 同样不与顶层对象挂钩
1.3.5 面试问题:const声明的值是一定不能改的吗
并不是,const声明对象这种复杂类型时,就是想上期学习的var栈和堆的问题。
const fot = {};
// 为 foo 添加一个属性,可以成功
fot.prop = 123;
fot.prop // 123
// 将 foo 指向另一个对象,就会报错
fto = {}; // TypeError: "fot" is read-only
上面代码中,常量fot储存
的是一个地址,这个地址指向一个对象。不可变的只是这个地址,即不能把fot指向
另一个地址,但对象本身是可变的,所以依然可以为其添加新属性。
另一个例子:
const a = [];
a.push('Hello'); // 可执行
a.length = 0; // 可执行
a = ['Dave']; // 报错
常量a
是一个数组,这个数组本身是可写的,但是如果将另一个数组赋值给a,就会报错。
本质
- const实际上保证的,并不是变量的值不得改动,而是变量指向的那个内存地址所保存的数据不得改动。
- 对于简单类型的数据(数值、字符串、布尔值),值就保存在变量指向的那个内存地址,因此等同于常量。
- 但对于复合类型的数据(主要是对象和数组),变量指向的内存地址,保存的只是一个指向实际数据的指针,const只能保证这个指针是固定的(即总是指向另一个固定的地址),至于它指向的数据结构是不是可变的,就完全不能控制了。因此,将一个对象声明为常量必须非常小心
如果想要别人不能修改里面的值要怎么做呢
1.3.6 Object.freeze方法
const foo = Object.freeze({
prop = 456;
});
// 常规模式时,下面一行不起作用;
// 严格模式时,该行会报错
foo.prop = 123;
foo.a = 123
上面代码中,常量foo指向一个冻结的对象,所以修改和添加新属性都不起作用,严格模式时还会报错。
但是,如果里面嵌套的是对象,就无法冻住嵌套对象的值,可以嵌套对象值进行修改。如果想要也冻住嵌套的,那么就要采用函数递归冻住的办法
1.4 变量解构赋值
1.4.1 数组解构赋值
解构赋值,就是快速的从对象或者数组中取出成员的一个语法方式。
let arr = [1, 2, 3];
以前抽取数组某个元素时:
console.log(arr[1])
现在获取的新方法是:
let [a,b,c] = arr
在控制台访问,写a就1,写c就3
本质上,这种写法属于“模式匹配”,只要等号两边的模式相同,左边的变量就会被赋予对应的值。
下面是一些使用嵌套数组进行解构的案例:
let [foo, [[bar], baz]] = [1, [[2], 3]];
foo // 1
bar // 2
baz // 3
let [ , , third] = ["foo", "bar", "baz"];
third // "baz"
let [x, , y] = [1, 2, 3];
x // 1
y // 3
let [head, ...tail] = [1, 2, 3, 4];
head // 1
tail // [2, 3, 4]
let [x, y, ...z] = ['a'];
x // "a"
y // undefined
z // []
let [a, [b], d] = [1, [2, 3], 4];
a // 1
b // 2
d // 4
let [a, b, d] = [1, [2, 3], 4];
a // 1
b // [2,3]
d // 4
解构不成功,变量的值就等于undefined
。
1.4.2 对象解构赋值
对象的解构与数组有一个重要的不同。数组的元素是按次序排列的,变量的取值由它的位置决定;而对象的属性没有次序,变量必须与属性同名,才能取到正确的值。
let { foo: baz } = { foo: 'aaa', bar: 'bbb' };
baz // "aaa"
let obj = { first: 'hello', last: 'world' };
let { first: f, last: l } = obj;
f // 'hello'
l // 'world
也就是说,对象的解构赋值的内部机制,是先找到同名属性,然后再赋给对应的变量。真正被赋值的是后者,而不是前者。
let { foo: baz } = { foo: 'aaa', bar: 'bbb' };
baz // "aaa"
foo // error: foo is not defined
上面代码中,foo是匹配的模式,baz才是变量。真正被赋值的是变量baz,而不是模式foo。
与数组一样,解构也可以用于嵌套结构的对象。
let obj = {
p: [
'Hello',
{ y: 'World' }
]
};
let { p: [x, { y }] } = obj;
x // "Hello"
y // "World"
注意,这时p是模式,不是变量,因此不会被赋值。
如果p也要作为变量赋值,可以写成下面这样:
let obj = {
p: [
'Hello',
{ y: 'World' }
]
};
let { p, p: [x, { y }] } = obj;
x // "Hello"
y // "World"
p // ["Hello", {y: "World"}]
如果解构模式是嵌套的对象,而且子对象所在的父属性不存在,那么将会报错。
// 报错
let {foo: {bar}} = {baz: 'baz'};
上面代码中,等号左边对象的foo属性,对应一个子对象。该子对象的bar属性,解构时会报错。原因很简单,因为foo这时等于undefined,再取子属性就会报错。
嵌套赋值的例子:
let obj = {};
let arr = [];
({ foo: obj.prop, bar: arr[0] } = { foo: 123, bar: true });
obj // {prop:123}
arr // [true]
1.4.3 字符串解构
字符串也可以解构赋值。这是因为此时,字符串被转换成了一个类似数组的对象。
const [a, b, c, d, e] = 'hello';
a // "h"
b // "e"
c // "l"
d // "l"
e // "o"
类似数组的对象都有一个length属性,因此还可以对这个属性解构赋值。
let {length : len} = 'hello';
len // 5
1.5 模板字符串
ES6采用``实现换行的便利
let name = "van"
let oli = `<li>
<b>${name}</b>
</li>`
快速实现js赋予html数据值
<ul></ul>
<script>
let arr = ["van","wot","steve"]
let newlist = arr.map(function(itme) {
return `<li>
<b>${itme}</b>
</li>`
})//映射想要的内容
console.log(newlist)
let oul = document.querySelector("ul")//获取节点
oul.innerHTML = newlist.join("")//赋予节点想要的属性、元素,并清除之间的逗号
</script>
${}模板字符串还可以嵌套复杂的运算表达式
案例:
需求需要给第一个加颜色,在js实现
<style>
.active{
color: red;
}
</style>
<ul></ul>
<script>
let arr = ["van","wot","steve"]
let newlist = arr.map(function(itme,index) {
return `<li class = "${index===0?'active':''}">
<b>${itme}</b>
</li>`
})
console.log(newlist)
let oul = document.querySelector("ul")
oul.innerHTML = newlist.join("")
</script>
1.6 字符串与数值拓展
1.6.1 字符串—includes、startsWith、endsWith函数
传统上,JavaScript 只有indexOf方法,可以用来确定一个字符串是否包含在另一个字符串中。ES6 又提供了三种新方法。
- includes():返回布尔值,表示是否找到了参数字符串。
- startsWith():返回布尔值,表示参数字符串是否在原字符串的头部。
- endsWith():返回布尔值,表示参数字符串是否在原字符串的尾部。
let s = 'Hello world!';
s.startsWith('world', 6) // true
s.endsWith('Hello', 5) // true
s.includes('Hello', 6) // false
上面代码表示,使用第二个参数n时,endsWith的行为与其他两个方法有所不同。它针对前n个字符,而其他两个方法针对从第n个位置直到字符串结束。
1.6.2 字符串—repeat函数
repeat方法返回一个新字符串,表示将原字符串重复n次。
'x'.repeat(3) // "xxx"
'hello'.repeat(2) // "hellohello"
'na'.repeat(0) // " "
重复0次就是没有
参数如果是小数,会被取整。
'na'.repeat(2.9) // "nana"
如果repeat的参数是负数或者Infinity,会报错。
'na'.repeat(Infinity)
// RangeError
'na'.repeat(-1)
// RangeError
1.6.3 二进制和八进制表示法
ES6 提供了二进制和八进制数值的新的写法,分别用前缀0b(或0B)
和0o(或0O)
表示。
0b111110111 === 503 // true
0o767 === 503 // true
从 ES5 开始,在严格模式之中,八进制就不再允许使用前缀0表示,ES6 进一步明确,要使用前缀0o
表示。
// 非严格模式
(function(){
console.log(0o11 === 011);
})() // true
// 严格模式
(function(){
'use strict';
console.log(0o11 === 011);
})() // Uncaught SyntaxError: Octal literals are not allowed in strict mode.
1.6.4 Number.isFinite(), Number.isNaN()
Number.isFinite()用来检查一个数值是否为有限的(finite),即不是Infinity。
Number.isNaN()用来检查一个值是否为NaN。
Number.isFinite(15); // true
Number.isFinite(0.8); // true
Number.isFinite(NaN); // false
Number.isFinite(Infinity); // false
Number.isFinite(-Infinity); // false
Number.isFinite('foo'); // false
Number.isFinite('15'); // false
Number.isFinite(true); // false
注意,如果参数类型不是数值,Number.isFinite一律返回false。
Number.isNaN(NaN) // true
Number.isNaN(15) // false
Number.isNaN('15') // false
Number.isNaN(true) // false
Number.isNaN(9/NaN) // true
Number.isNaN('true' / 0) // true
Number.isNaN('true' / 'true') // true
如果参数类型不是NaN,Number.isNaN一律返回false。
它们与传统的全局方法isFinite()和isNaN()的区别在于,传统方法先调用Number()将非数值的值转为数值,再进行判断,而这两个新方法只对数值有效,Number.isFinite()对于非数值一律返回false, Number.isNaN()只有对于NaN才返回true,非NaN一律返回false。
1.6.5 Number.isInteger()判断一个数值是否为整数
Number.isInteger(25) // true
Number.isInteger(25.1) // false
Number.isInteger() // false
Number.isInteger(null) // false
Number.isInteger('15') // false
Number.isInteger(true) // false
JavaScript 内部,整数和浮点数采用的是同样的储存方法,比如 25 和 25.0 被视为同一个值。
1.6.6 Number.EPSILON()表示 1 与大于 1 的最小浮点数之间的差
Number.EPSILON实际上是 JavaScript 能够表示的最小精度2.220446049250313e-16。误差如果小于这个值,就可以认为已经没有意义了,即不存在误差了。
引入一个这么小的量的目的,在于为浮点数计算,设置一个误差范围。我们知道浮点数计算是不精确的。
0.1 + 0.2
// 0.30000000000000004
0.1 + 0.2 - 0.3
// 5.551115123125783e-17
5.551115123125783e-17.toFixed(20)
// '0.00000000000000005551'
上面代码解释了,为什么比较0.1 + 0.2与0.3得到的结果是false。
0.1 + 0.2 === 0.3 // false
判断极小常量
function isequal(x,y) {
return Math.abs(x-y)<Number.EPSILON
}
console.log(isequal(0.1+0.2,0.3))
函数判断两个值相差的值,必极小常量的值还要小,那么就可以说明两个数相等
1.6.7 Math.trunc()去除一个数的小数部分,返回整数部分
对于非数值,Math.trunc内部使用Number方法将其先转为数值。
Math.trunc('123.456') // 123
Math.trunc(true) //1
Math.trunc(false) // 0
Math.trunc(null) // 0
对于空值和无法截取整数的值,返回NaN。
Math.trunc(NaN); // NaN
Math.trunc('foo'); // NaN
Math.trunc(); // NaN
Math.trunc(undefined) // NaN
1.6.8 Math.sign()
它会返回五种值。
- 参数为正数,返回+1;
- 参数为负数,返回-1;
- 参数为 0,返回0;
- 参数为-0,返回-0;
- 其他值,返回NaN。
1.7 数组扩展
1.7.1 … 扩展运算符
扩展运算符是三个点(...
)
let arrl = [1,2,3]
1et arr2 = [4,5,6]
console.1og([...arr1....arr2])
// 1 2 3 4 5 6
console.log(...[1, 2, 3])
// 1 2 3
console.log(1, ...[2, 3, 4], 5)
// 1 2 3 4 5
[...document.querySelectorAll('div')]
// [<div>, <div>, <div>]
1.7.2 Array.from() 用于将两类对象转为真正的数组
下面是一个类似数组的对象,Array.from()将它转为真正的数组。
<ul>
<li>11111</li>
<li>11111</li>
<li>11111</li>
<li>11111</li>
<li>11111</li>
</ul>
<script>
function test(){
console.log(Array.from(arguments))
}
test(1,2,3,4)
let olis = document.querySelectorAll("li")
console.log(olis)
// 像olis的伪数组不能使用map等方法 ,需要转换为真数组才行
Array.from(olis).map(function(){})
</script>
Array.from()可以将各种值转为真正的数组,并且还提供map功能。这实际上意味着,只要有一个原始的数据结构,你就可以先对它的值进行处理,然后转成规范的数组结构,进而就可以使用数量众多的数组方法。
1.7.3 Array.of()用于将一组值转换为数组
Array.of(3, 11, 8) // [3,11,8]
Array.of(3) // [3]
Array.of(3).length // 1
这个方法的主要目的,是弥补数组构造函数Array()的不足。因为参数个数的不同,会导致Array()的行为有差异。
Array() // []
Array(3) // [, , ,]
Array(3, 11, 8) // [3, 11, 8]
上面代码中,Array()方法没有参数、一个参数、三个参数时,返回的结果都不一样。只有当参数个数不少于 2 个时,Array()才会返回由参数组成的新数组。参数只有一个正整数时,实际上是指定数组的长度。
Array.of()基本上可以用来替代Array()或new Array(),并且不存在由于参数不同而导致的重载。它的行为非常统一。
1.7.4 find()
数组实例的find()方法,用于找出第一个符合条件的数组成员。它的参数是一个回调函数,所有数组成员依次执行该回调函数,直到找出第一个返回值为true的成员,然后返回该成员。如果没有符合条件的成员,则返回undefined。
[1, 5, 10, 15].find(function(value) {
return value > 9;
}) // 10
1.7.5 findIndex()
数组实例的findIndex()方法的用法与find()方法非常类似,返回第一个符合条件的数组成员的位置,即索引值,如果所有成员都不符合条件,则返回-1。
[1, 5, 10, 15].findIndex(function(value) {
return value > 9;
}) // 2
1.7.6 findLast()和findLastIndex()
find()和findIndex()都是从数组的0号位,依次向后检查。ES2022 新增了两个方法findLast()和findLastIndex(),从数组的最后一个成员开始,依次向前检查,其他都保持不变。
1.7.7 fill() 使用给定值填充一个数组
['a', 'b', 'c'].fill(7)
// [7, 7, 7]
new Array(3).fill(7)
// [7, 7, 7]
上面代码表明,fill方法用于空数组的初始化非常方便。数组中已有的元素,会被全部抹去。
fill方法还可以接受第二个和第三个参数,用于指定填充的起始位置和结束位置。
['a', 'b', 'c'].fill(7, 1, 2)
// ['a', 7, 'c']
上面代码表示,fill方法从 1 号位开始,向原数组填充 7,到 2 号位之前结束。
快速填充案例:
let arr = new Array(3).fill({name: "Ben"});
console.log(arr)
// [{name: "Ben"}, {name: "Ben"}, {name: "Ben"}]
1.7.8 flat()
[1, 2, [3, 4]].flat()
// [1, 2, 3, 4]
上面代码中,原数组的成员里面有一个数组,flat()方法将子数组的成员取出来,添加在原来的位置。
flat()默认只会“拉平”一层,如果想要“拉平”多层的嵌套数组,可以将flat()方法的参数写成一个整数,表示想要拉平的层数,默认为1。
[1, 2, [3, [4, 5]]].flat()
// [1, 2, 3, [4, 5]]
[1, 2, [3, [4, 5]]].flat(2)
// [1, 2, 3, 4, 5]
如果不管有多少层嵌套,都要转成一维数组,可以用Infinity关键字作为参数。
[1, [2, [3]]].flat(Infinity)
// [1, 2, 3]
如果原数组有空位,flat()方法会跳过空位。
[1, 2, , 4, 5].flat()
// [1, 2, 4, 5]
但是flat也有个缺点,对复杂的对象无法扁平化
let arr = [
{
name: " A",
list: ["安庆", "安阳", "鞍山"]
},
{
name: "B"
,
list: ["北京", "保定", "包头"]
}
]
console.log(arr.flat())
1.7.9 flatMap()
flatMap()方法对原数组的每个成员执行一个函数,然后对返回值组成的数组执行flat()方法。该方法返回一个新数组,不改变原数组。
flatMap()方法的参数是一个遍历函数,该函数可以接受三个参数,分别是当前数组成员、当前数组成员的位置(从零开始)、原数组。
flatMap()返回的还是一个嵌套数组
let arr = [
{
name: " A",
list: ["安庆", "安阳", "鞍山"]
},
{
name: "B"
,
list: ["北京", "保定", "包头"]
}
]
let res = arr.flatMap(function(item) {
return item.list
})
console.log(res)
1.8 对象扩展
1.8.1 对象简写
ES6 允许在大括号里面,直接写入变量和函数,作为对象的属性和方法。这样的书写更加简洁。
const foo = 'bar';
const baz = {foo};
baz // {foo: "bar"}
// 等同于
const baz = {foo: foo};
上面代码中,变量foo直接写在大括号里面。这时,属性名就是变量名, 属性值就是变量值。
案例:
function f(x, y) {
return {x, y};
}
// 等同于
function f(x, y) {
return {x: x, y: y};
}
f(1, 2) // Object {x: 1, y: 2}
1.8.2 对象属性表达式
JavaScript 定义对象的属性,有两种方法。
// 方法一
obj.foo = true;
// 方法二
obj['a' + 'bc'] = 123;
上面代码的方法一是直接用标识符作为属性名,方法二是用表达式作为属性名,这时要将表达式放在方括号之内。
但是,如果使用字面量方式定义对象(使用大括号),在 ES5 中只能使用方法一(标识符)定义属性。
var obj = {
foo: true,
abc: 123
};
ES6 允许字面量定义对象时,用方法二(表达式)作为对象的属性名,即把表达式放在方括号内。
let propKey = 'foo';
let obj = {
[propKey]: true,
['a' + 'bc']: 123
};
案例:
快速给名字
1.8.3 扩展运算符 …
对象的扩展运算符(…)用于取出参数对象的所有可遍历属性,拷贝到当前对象之中。
合并的时候,有相同属性的会覆盖
由于数组是特殊的对象,所以对象的扩展运算符也可以用于数组。
let foo = { ...['a', 'b', 'c'] };
foo
// {0: "a", 1: "b", 2: "c"}
如果扩展运算符后面是一个空对象,则没有任何效果。
{...{}, a: 1}
// { a: 1 }
如果扩展运算符后面不是对象,则会自动将其转为对象。
// 等同于 {...Object(1)}
{...1} // {}
上面代码中,扩展运算符后面是整数1,会自动转为数值的包装对象Number{1}。由于该对象没有自身属性,所以返回一个空对象。
1.8.4 Object.assign() 用于对象的合并
Object.assign()方法用于对象的合并,将源对象(source)的所有可枚举属性,复制到目标对象(target)。
const target = { a: 1, b: 1 };
const source1 = { b: 2, c: 2 };
const source2 = { c: 3 };
Object.assign(target, source1, source2);
target // {a:1, b:2, c:3}
Object.assign()方法的第一个参数是目标对象,后面的参数都是源对象。
注意,如果目标对象与源对象有同名属性,或多个源对象有同名属性,则后面的属性会覆盖前面的属性。
1.8.5 Object.is()
ES5 比较两个值是否相等,只有两个运算符:相等运算符()和严格相等运算符(=)。它们都有缺点,前者会自动转换数据类型,后者的NaN不等于自身,以及+0等于-0。JavaScript 缺乏一种运算,在所有环境中,只要两个值是一样的,它们就应该相等。
ES6 提出“Same-value equality”(同值相等)算法,用来解决这个问题。Object.is就是部署这个算法的新方法。它用来比较两个值是否严格相等,与严格比较运算符(===)的行为基本一致。
Object.is('foo', 'foo')
// true
Object.is({}, {})
// false
不同之处只有两个:
一是+0不等于-0
二是NaN等于自身。
+0 === -0 //true
NaN === NaN // false
Object.is(+0, -0) // false
Object.is(NaN, NaN) // true
1.9 函数的扩展
1.9.1 参数默认值
ES6 允许为函数的参数设置默认值,即直接写在参数定义的后面。
function log(x, y = 'World') {
console.log(x, y);
}
log('Hello') // Hello World
log('Hello', 'China') // Hello China
log('Hello', '') // Hello
使用参数默认值时,函数不能有同名参数。
1.9.2 rest 剩余参数
ES6 引入 rest 参数(形式为…变量名),用于获取函数的多余参数,这样就不需要使用arguments对象了。rest 参数搭配的变量是一个数组,该变量将多余的参数放入数组中。
注意,rest 参数之后不能再有其他参数(即只能是最后一个参数),否则会报错。
函数的length属性,不包括 rest 参数。
(function(a) {}).length // 1
(function(...a) {}).length // 0
(function(a, ...b) {}).length // 1
1.9.3 name 属性
函数的name属性,返回该函数的函数名。
function foo() {}
foo.name // "foo"
这个属性早就被浏览器广泛支持,但是直到 ES6,才将其写入了标准。
1.9.4 箭头函数
ES6 允许使用“箭头”(=>)定义函数。
如果箭头函数只有一行语句,且不需要返回值,可以采用下面的写法,就不用写大括号了。
如果只有一个参数,可以省略小括号
let f = v => v;
// 等同于
let f = function (v) {
return v;
};
如果箭头函数不需要参数或需要多个参数,就使用一个圆括号代表参数部分。
var f = () => 5;
// 等同于
var f = function () { return 5 };
var sum = (num1, num2) => num1 + num2;
// 等同于
var sum = function(num1, num2) {
return num1 + num2;
};
如果箭头函数的代码块部分多于一条语句,就要使用大括号将它们括起来,并且使用return语句返回。
var sum = (num1, num2) => { return num1 + num2; }
由于大括号被解释为代码块,所以如果箭头函数直接返回一个对象,必须在对象外面加上括号,否则会报错。
// 报错
let getTempItem = id => { id: id, name: "Temp" };
// 不报错
let getTempItem = id => ({ id: id, name: "Temp" });
箭头函数有几个使用注意点。
-
(1)箭头函数没有自己的this对象。this是指向父作用域的
-
(2)不可以当作构造函数,也就是说,不可以对箭头函数使用new命令,否则会抛出一个错误。
-
(3)不可以使用arguments对象,该对象在函数体内不存在。如果要用,可以用 rest 参数代替。
function Timer() {
this.s1 = 0;
this.s2 = 0;
// 箭头函数
setInterval(() => this.s1++, 1000);
// 普通函数
setInterval(function () {
this.s2++;
}, 1000);
}
var timer = new Timer();
setTimeout(() => console.log('s1: ', timer.s1), 3100);
setTimeout(() => console.log('s2: ', timer.s2), 3100);
// s1: 3
// s2: 0
上面代码中,Timer函数内部设置了两个定时器,分别使用了箭头函数和普通函数。前者的this绑定定义时所在的作用域(即Timer函数),后者的this指向运行时所在的作用域(即全局对象)。所以,3100 毫秒之后,timer.s1被更新了 3 次,而timer.s2一次都没更新。
箭头函数实际上可以让this指向固定化,绑定this使得它不再可变,这种特性很有利于封装回调函数。
1.10 Symbol
ES6 引入了一种新的原始数据类型Symbol,表示独一无二的值。它属于 JavaScript 语言的原生数据类型之一,其他数据类型是:undefined、null、布尔值(Boolean)、字符串(String)、数值(Number)、大整数(BigInt)、对象(Object)。
Symbol 值通过Symbol()函数生成。这就是说,对象的属性名现在可以有两种类型,一种是原来就有的字符串,另一种就是新增的 Symbol 类型。凡是属性名属于 Symbol 类型,就都是独一无二的,可以保证不会与其他属性名产生冲突。
注意,Symbol()函数前不能使用new命令,否则会报错。这是因为生成的 Symbol 是一个原始类型的值,不是对象,所以不能使用new命令来调用。另外,由于 Symbol 值不是对象,所以也不能添加属性。基本上,它是一种类似于字符串的数据类型。
案例:
let s = Symbol();
typeof s
// "symbol"
// 没有参数的情况
let s1 = Symbol();
let s2 = Symbol();
s1 === s2 // false
// 有参数的情况
let s1 = Symbol('foo');
let s2 = Symbol('foo');
s1 === s2 // false
上面代码中,s1和s2都是Symbol()函数的返回值,而且参数相同,但是它们是不相等的。事实上,如果调用100次Symbol(),会得到100个互不相等的值。
1.10.1 不能进行运算
Symbol 值不能与其他类型的值进行运算,会报错。
let sym = Symbol('My symbol');
"your symbol is " + sym
// TypeError: can't convert symbol to string
`your symbol is ${sym}`
// TypeError: can't convert symbol to string
1.10.2 显示调用toString
Symbol 值可以显式转为字符串。
let sym = Symbol('My symbol');
String(sym) // 'Symbol(My symbol)'
sym.toString() // 'Symbol(My symbol)'
1.10.3 隐式转换为boolean
Symbol 值也可以转为布尔值,但是不能转为数值
let sym = Symbol();
Boolean(sym) // true
!sym // false
if (sym) {
// ...
}
Number(sym) // TypeError
sym + 2 // TypeError
1.10.4 使用Symbol作为对象属性名
由于每一个 Symbol 值都是不相等的,这意味着只要 Symbol 值作为标识符,用于对象的属性名,就能保证不会出现同名的属性。这对于一个对象由多个模块构成的情况非常有用,能防止某一个键被不小心改写或覆盖。
let mySymbol = Symbol();
// 第一种写法
let a = {};
a[mySymbol] = 'Hello!';
// 第二种写法
let a = {
[mySymbol]: 'Hello!'
};
// 第三种写法
let a = {};
Object.defineProperty(a, mySymbol, { value: 'Hello!' });
// 以上写法都得到同样结果
a[mySymbol] // "Hello!"
注意,Symbol 值作为对象属性名时,不能用点运算符。
const mySymbol = Symbol();
const a = {};
a.mySymbol = 'Hello!';
a[mySymbol] // undefined
a['mySymbol'] // "Hello!"
上面代码中,因为点运算符后面总是字符串,所以不会读取mySymbol作为标识名所指代的那个值,导致a的属性名实际上是一个字符串,而不是一个 Symbol 值。
1.10.5 Symbol 属性名的遍历
Symbol 值作为属性名,遍历对象的时候,该属性不会出现在for…in、for…of循环中,也不会被Object.keys()、Object.getOwnPropertyNames()、JSON.stringify()返回。
但是,它也不是私有属性,有一个Object.getOwnPropertySymbols()
方法,可以获取指定对象的所有 Symbol 属性名。该方法返回一个数组,成员是当前对象的所有用作属性名的 Symbol 值。
const obj = {};
let a = Symbol('a');
let b = Symbol('b');
obj[a] = 'Hello';
obj[b] = 'World';
const objectSymbols = Object.getOwnPropertySymbols(obj);
objectSymbols
// [Symbol(a), Symbol(b)]
上面代码是Object.getOwnPropertySymbols()方法的示例,可以获取所有 Symbol 属性名。
Object.getOwnPropertySymbols()方法与for…in循环、Object.getOwnPropertyNames方法进行对比的例子:
const obj = {};
const foo = Symbol('foo');
obj[foo] = 'bar';
for (let i in obj) {
console.log(i); // 无输出
}
Object.getOwnPropertyNames(obj) // []
Object.getOwnPropertySymbols(obj) // [Symbol(foo)]
1.10.6 作为常量用法
有利于统一代码的一致性,可以不用赋值那些什么1,2,3等常量,让人阅读代码时增加困难
//作为常量
const VIDEO = Symbol();
const AUDIO = Symbol();
const IMAGE= Symbol();
function play(type) {
switch (type) {
case VIDEO:
console.log("视频播放")
break;
case AUDIO:
console.log("音频播放")
break;
case IMAGE:
console.log("图片播放")
break;
}
}
play(VIDEO)
1.11 Iterator迭代器
JavaScript 原有的表示“集合”的数据结构,主要是数组(Array)和对象(Object),ES6 又添加了Map和Set。这样就有了四种数据集合,用户还可以组合使用它们,定义自己的数据结构,比如数组的成员是Map,Map的成员是对象。这样就需要一种统一的接口机制,来处理所有不同的数据结构。
- 遍历器(Iterator)就是这样一种机制。它是一种接口,为各种不同的数据结构提供统一的访问机制。任何数据结构只要部署 Iterator
接口,就可以完成遍历操作(即依次处理该数据结构的所有成员)。
Iterator的作用有三个: .
一是为各种数据结构, 提供一个统一 的、简便的访问接口;
二是使得数据结构的成员能够按某种次序排列;
三是ES6创造了一种新的遍历命令for…of循环,Iterator 接口主要供for…of循环
Iterator 的遍历过程是这样的。
- (1)创建一个指针对象,指向当前数据结构的起始位置。也就是说,遍历器对象本质上,就是一个指针对象。
- (2)第一次调用指针对象的next方法,可以将指针指向数据结构的第一个成员。
- (3)第二次调用指针对象的next方法,指针就指向数据结构的第二个成员。
- (4)不断调用指针对象的next方法,直到它指向数据结构的结束位置。
下面是一个模拟next方法返回值的例子。
var it = makeIterator(['a', 'b']);
it.next() // { value: "a", done: false }
it.next() // { value: "b", done: false }
it.next() // { value: undefined, done: true }
function makeIterator(array) {
var nextIndex = 0;
return {
next: function() {
return nextIndex < array.length ?
{value: array[nextIndex++], done: false} :
{value: undefined, done: true};
}
};
}
上面代码定义了一个makeIterator函数,它是一个遍历器生成函数,作用就是返回一个遍历器对象。对数组[‘a’, ‘b’]执行这个函数,就会返回该数组的遍历器对象(即指针对象)it。
指针对象的next方法,用来移动指针。开始时,指针指向数组的开始位置。然后,每次调用next方法,指针就会指向数组的下一个成员。第一次调用,指向a;第二次调用,指向b。
next方法返回一个对象,表示当前数据成员的信息。这个对象具有value和done两个属性,value属性返回当前位置的成员,done属性是一个布尔值,表示遍历是否结束,即是否还有必要再一次调用next方法。
总之,调用指针对象的next方法,就可以遍历事先给定的数据结构。
1.11.1 默认 Iterator 接口
Iterator 接口的目的,就是为所有数据结构,提供了一种统一的访问机制,即for…of循环(详见下文)。当使用for…of循环遍历某种数据结构时,该循环会自动去寻找 Iterator 接口。
一种数据结构只要部署了 Iterator 接口,我们就称这种数据结构是“可遍历的”(iterable)。
ES6 规定,默认的 Iterator 接口部署在数据结构的Symbol.iterator属性,或者说,一个数据结构只要具有Symbol.iterator属性,就可以认为是“可遍历的”(iterable)。Symbol.iterator属性本身是一个函数,就是当前数据结构默认的遍历器生成函数。执行这个函数,就会返回一个遍历器。至于属性名Symbol.iterator,它是一个表达式,返回Symbol对象的iterator属性,这是一个预定义好的、类型为 Symbol 的特殊值,所以要放在方括号内
下面的例子是数组的Symbol.iterator属性。
let arr = ['a', 'b', 'c'];
let iter = arr[Symbol.iterator]();
iter.next() // { value: 'a', done: false }
iter.next() // { value: 'b', done: false }
iter.next() // { value: 'c', done: false }
iter.next() // { value: undefined, done: true }
上面代码中,变量arr是一个数组,原生就具有遍历器接口,部署在arr的Symbol.iterator属性上面。所以,调用这个属性,就得到遍历器对象。
对于原生部署 Iterator 接口的数据结构,不用自己写遍历器生成函数,for…of循环会自动遍历它们。除此之外,其他数据结构(主要是对象)的 Iterator 接口,都需要自己在Symbol.iterator属性上面部署,这样才会被for…of循环遍历。
对象(Object)之所以没有默认部署 Iterator 接口,是因为对象的哪个属性先遍历,哪个属性后遍历是不确定的,需要开发者手动指定。本质上,遍历器是一种线性处理,对于任何非线性的数据结构,部署遍历器接口,就等于部署一种线性转换。不过,严格地说,对象部署遍历器接口并不是很必要,因为这时对象实际上被当作 Map 结构使用,ES5 没有 Map 结构,而 ES6 原生提供了。
一个对象如果要具备可被for…of循环调用的 Iterator 接口,就必须在Symbol.iterator的属性上部署遍历器生成方法(原型链上的对象具有该方法也可)。
1.11.2 调用 Iterator 接口的场合
(1)解构赋值
对数组和 Set 结构进行解构赋值时,会默认调用Symbol.iterator方法。
let set = new Set().add('a').add('b').add('c');
let [x,y] = set;
// x='a'; y='b'
let [first, ...rest] = set;
// first='a'; rest=['b','c'];
(2)扩展运算符
扩展运算符(…)也会调用默认的 Iterator 接口。
// 例一
var str = 'hello';
[...str] // ['h','e','l','l','o']
// 例二
let arr = ['b', 'c'];
['a', ...arr, 'd']
// ['a', 'b', 'c', 'd']
上面代码的扩展运算符内部就调用 Iterator 接口。
实际上,这提供了一种简便机制,可以将任何部署了 Iterator 接口的数据结构,转为数组。也就是说,只要某个数据结构部署了 Iterator 接口,就可以对它使用扩展运算符,将其转为数组。
let arr = [...iterable];
1.11.3 字符串的 Iterator 接口
字符串是一个类似数组的对象,也原生具有 Iterator 接口
var someString = "hi";
typeof someString[Symbol.iterator]
// "function"
var iterator = someString[Symbol.iterator]();
iterator.next() // { value: "h", done: false }
iterator.next() // { value: "i", done: false }
iterator.next() // { value: undefined, done: true }
上面代码中,调用Symbol.iterator方法返回一个遍历器对象,在这个遍历器上可以调用 next 方法,实现对于字符串的遍历。
可以覆盖原生的Symbol.iterator方法,达到修改遍历器行为的目的。
1.12 Set 结构
它类似于数组,但成员的值都是唯一的,没有重复的值。
Set本身是一个构造函数,用来生成 Set 数据结构。
const s = new Set();
[2, 3, 5, 4, 5, 2, 2].forEach(x => s.add(x));
for (let i of s) {
console.log(i);
}
// 2 3 5 4
上面代码通过add()方法向 Set 结构加入成员,结果表明 Set 结构不会添加重复的值。
Set函数可以接受一个数组(或者具有 iterable 接口的其他数据结构)作为参数,用来初始化。
// 例一
const set = new Set([1, 2, 3, 4, 4]);
[...set]
// [1, 2, 3, 4]
// 例二
const items = new Set([1, 2, 3, 4, 5, 5, 5, 5]);
items.size // 5
// 例三
const set = new Set(document.querySelectorAll('div'));
set.size // 56
// 类似于
const set = new Set();
document
.querySelectorAll('div')
.forEach(div => set.add(div));
set.size // 56
1.12 Set 实例的属性和方法
Set 结构的实例有以下属性。
- Set.prototype.constructor:构造函数,默认就是Set函数。
- size:返回Set实例的成员总数。
- Set 实例的方法分为两大类:操作方法(用于操作数据)和遍历方法(用于遍历成员)。下面先介绍四个操作方法。
- add(value):添加某个值,返回 Set 结构本身。
- delete(value):删除某个值,返回一个布尔值,表示删除是否成功。
- has(value):返回一个布尔值,表示该值是否为Set的成员。
- clear():清除所有成员,没有返回值。
s.add(1).add(2).add(2);
// 注意2被加入了两次
s.size // 2
s.has(1) // true
s.has(2) // true
s.has(3) // false
s.delete(2);
s.has(2) // false
下面是一个对比,看看在判断是否包括一个键上面,Object结构和Set结构的写法不同。
1.13 遍历操作
Set 结构的实例有四个遍历方法,可以用于遍历成员。
- keys():返回键名的遍历器
- values():返回键值的遍历器
- entries():返回键值对的遍历器
- forEach():使用回调函数遍历每个成员
需要特别指出的是,Set的遍历顺序就是插入顺序。这个特性有时非常有用,比如使用 Set 保存一个回调函数列表,调用时就能保证按照添加顺序调用。
keys方法、values方法、entries方法返回的都是遍历器对象。由于 Set 结构没有键名,只有键值(或者说键名和键值是同一个值),所以keys方法和values方法的行为完全一致。
let set = new Set(['red', 'green', 'blue']);
for (let item of set.keys()) {
console.log(item);
}
// red
// green
// blue
for (let item of set.values()) {
console.log(item);
}
// red
// green
// blue
for (let item of set.entries()) {
console.log(item);
}
// ["red", "red"]
// ["green", "green"]
// ["blue", "blue"]
上面代码中,entries方法返回的遍历器,同时包括键名和键值,所以每次输出一个数组,它的两个成员完全相等。
Set 结构的实例默认可遍历,它的默认遍历器生成函数就是它的values方法。
let set = new Set(['red', 'green', 'blue']);
for (let x of set) {
console.log(x);
}
// red
// green
// blue
扩展运算符(…)内部使用for…of循环,所以也可以用于 Set 结构。
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}
如果想在遍历操作中,同步改变原来的 Set 结构,目前没有直接的方法,但有两种变通方法。一种是利用原 Set 结构映射出一个新的结构,然后赋值给原来的 Set 结构;另一种是利用Array.from方法。
// 方法一
let set = new Set([1, 2, 3]);
set = new Set([...set].map(val => val * 2));
// set的值是2, 4, 6
// 方法二
let set = new Set([1, 2, 3]);
set = new Set(Array.from(set, val => val * 2));
// set的值是2, 4, 6
上面代码提供了两种方法,直接在遍历操作中改变原来的 Set 结构。
1.14 Map 结构
类似于对象,也是键值对的集合,但是“键"的范围不限于字符串,各种类型的值(包括对象)都可以当作键。
const m = new Map();
const o = {p: 'Hello World'};
m.set(o, 'content')
m.get(o) // "content"
m.has(o) // true
m.delete(o) // true
m.has(o) // false
上面代码使用 Map 结构的set方法,将对象o当作m的一个键,然后又使用get方法读取这个键,接着使用delete方法删除了这个键。
上面的例子展示了如何向 Map 添加成员。作为构造函数,Map 也可以接受一个数组作为参数。该数组的成员是一个个表示键值对的数组。
const map = new Map([
['name', '张三'],
['title', 'Author']
]);
map.size // 2
map.has('name') // true
map.get('name') // "张三"
map.has('title') // true
map.get('title') // "Author"
上面代码在新建 Map 实例时,就指定了两个键name和title。
Map构造函数接受数组作为参数,实际上执行的是下面的算法
1.14.1 属性和操作方法
(1)size 属性
size属性返回 Map 结构的成员总数。
const map = new Map();
map.set('foo', true);
map.set('bar', false);
map.size // 2
(2)set(key, value)
set方法设置键名key对应的键值为value,然后返回整个 Map 结构。如果key已经有值,则键值会被更新,否则就新生成该键。
const m = new Map();
m.set('edition', 6) // 键是字符串
m.set(262, 'standard') // 键是数值
m.set(undefined, 'nah') // 键是 undefined
set方法返回的是当前的Map对象,因此可以采用链式写法。
let map = new Map()
.set(1, 'a')
.set(2, 'b')
.set(3, 'c');
(3)get(key)
get方法读取key对应的键值,如果找不到key,返回undefined。
const m = new Map();
const hello = function() {console.log('hello');};
m.set(hello, 'Hello ES6!') // 键是函数
m.get(hello) // Hello ES6!
4)has(key)
has方法返回一个布尔值,表示某个键是否在当前 Map 对象之中。
const m = new Map();
m.set('edition', 6);
m.set(262, 'standard');
m.set(undefined, 'nah');
m.has('edition') // true
m.has('years') // false
m.has(262) // true
m.has(undefined) // true
(5)delete(key)
delete方法删除某个键,返回true。如果删除失败,返回false。
const m = new Map();
m.set(undefined, 'nah');
m.has(undefined) // true
m.delete(undefined)
m.has(undefined) // false
(6)clear()
clear方法清除所有成员,没有返回值。
let map = new Map();
map.set('foo', true);
map.set('bar', false);
map.size // 2
map.clear()
map.size // 0
1.14.3 遍历方法
Map 结构原生提供三个遍历器生成函数和一个遍历方法。
- keys():返回键名的遍历器。
- values():返回键值的遍历器。
- entries():返回所有成员的遍历器。
- forEach():遍历 Map 的所有成员。
需要特别注意的是,Map 的遍历顺序就是插入顺序。
const map = new Map([
['F', 'no'],
['T', 'yes'],
]);
for (let key of map.keys()) {
console.log(key);
}
// "F"
// "T"
for (let value of map.values()) {
console.log(value);
}
// "no"
// "yes"
for (let item of map.entries()) {
console.log(item[0], item[1]);
}
// "F" "no"
// "T" "yes"
// 或者
for (let [key, value] of map.entries()) {
console.log(key, value);
}
// "F" "no"
// "T" "yes"
// 等同于使用map.entries()
for (let [key, value] of map) {
console.log(key, value);
}
// "F" "no"
// "T" "yes"
上面代码最后的那个例子,表示 Map 结构的默认遍历器接口(Symbol.iterator属性),就是entries方法。
map[Symbol.iterator] === map.entries
// true
Map 结构转为数组结构,比较快速的方法是使用扩展运算符(…)。
const map = new Map([
[1, 'one'],
[2, 'two'],
[3, 'three'],
]);
[...map.keys()]
// [1, 2, 3]
[...map.values()]
// ['one', 'two', 'three']
[...map.entries()]
// [[1,'one'], [2, 'two'], [3, 'three']]
[...map]
// [[1,'one'], [2, 'two'], [3, 'three']]
此外,Map 还有一个forEach方法,与数组的forEach方法类似,也可以实现遍历。
1.15 Proxy 代理
Proxy如其名,它的作用是在对象和和对象的属性值之 间设置一个代理,获取该对象的值或者设置该对象的值,以及实例化等等多种操作,都会被拦截住,经过这一 层我们可以统一处理,我们可以认为它就是“代理器”。
Proxy 可以理解成,在目标对象之前架设一层“拦截”,外界对该对象的访问,都必须先通过这层拦截,因此提供了一种机制,可以对外界的访问进行过滤和改写。Proxy 这个词的原意是代理,用在这里表示由它来“代理”某些操作,可以译为“代理器”。
var proxy = new Proxy(target, handler);
Proxy 对象的所有用法,都是上面这种形式,不同的只是handler参数的写法。其中,new Proxy()表示生成一个Proxy实例,target参数表示所要拦截的目标对象,handler参数也是一个对象,用来定制拦截行为。
下面是另一个拦截读取属性行为的例子。
var proxy = new Proxy({}, {
get: function(target, propKey) {
return 35;
}
});
proxy.time // 35
proxy.name // 35
proxy.title // 35
上面代码中,作为构造函数,Proxy接受两个参数。第一个参数是所要代理的目标对象(上例是一个空对象),即如果没有Proxy的介入,操作原来要访问的就是这个对象;第二个参数是一个配置对象,对于每一个被代理的操作,需要提供一个对应的处理函数,该函数将拦截对应的操作。比如,上面代码中,配置对象有一个get方法,用来拦截对目标对象属性的访问请求。get方法的两个参数分别是目标对象和所要访问的属性。可以看到,由于拦截函数总是返回35,所以访问任何属性都得到35。
注意,要使得Proxy起作用,必须针对Proxy实例(上例是proxy对象)进行操作,而不是针对目标对象(上例是空对象)进行操作。
如果handler没有设置任何拦截,那就等同于直接通向原对象。
var target = {};
var handler = {};
var proxy = new Proxy(target, handler);
proxy.a = 'b';
target.a // "b"
上面代码中,handler是一个空对象,没有任何拦截效果,访问proxy就等同于访问target。
一个技巧是将 Proxy 对象,设置到object.proxy属性,从而可以在object对象上调用。
var object = { proxy: new Proxy(target, handler) };
Proxy 实例也可以作为其他对象的原型对象。
var proxy = new Proxy({}, {
get: function(target, propKey) {
return 35;
}
});
let obj = Object.create(proxy);
obj.time // 35
上面代码中,proxy对象是obj对象的原型,obj对象本身并没有time属性,所以根据原型链,会在proxy对象上读取该属性,导致被拦截。
同一个拦截器函数,可以设置拦截多个操作。
var handler = {
get: function(target, name) {
if (name === 'prototype') {
return Object.prototype;
}
return 'Hello, ' + name;
},
apply: function(target, thisBinding, args) {
return args[0];
},
construct: function(target, args) {
return {value: args[1]};
}
};
var fproxy = new Proxy(function(x, y) {
return x + y;
}, handler);
fproxy(1, 2) // 1
new fproxy(1, 2) // {value: 2}
fproxy.prototype === Object.prototype // true
fproxy.foo === "Hello, foo" // true
对于可以设置、但没有设置拦截的操作,则直接落在目标对象上,按照原先的方式产生结果。
1.15.1 get()
get方法用于拦截某个属性的读取操作,可以接受三个参数,依次为目标对象、属性名和 proxy 实例本身(严格地说,是操作行为所针对的对象),其中最后一个参数可选。
get方法的用法,上文已经有一个例子,下面是另一个拦截读取操作的例子。
var person = {
name: "张三"
};
var proxy = new Proxy(person, {
get: function(target, propKey) {
if (propKey in target) {
return target[propKey];
} else {
throw new ReferenceError("Prop name \"" + propKey + "\" does not exist.");
}
}
});
proxy.name // "张三"
proxy.age // 抛出一个错误
上面代码表示,如果访问目标对象不存在的属性,会抛出一个错误。如果没有这个拦截函数,访问不存在的属性,只会返回undefined。
get方法可以继承。
1.15.2 set()
set方法用来拦截某个属性的赋值操作,可以接受四个参数,依次为目标对象、属性名、属性值和 Proxy 实例本身,其中最后一个参数可选。
假定Person对象有一个age属性,该属性应该是一个不大于 200 的整数,那么可以使用Proxy保证age的属性值符合要求。
let validator = {
set: function(obj, prop, value) {
if (prop === 'age') {
if (!Number.isInteger(value)) {
throw new TypeError('The age is not an integer');
}
if (value > 200) {
throw new RangeError('The age seems invalid');
}
}
// 对于满足条件的 age 属性以及其他属性,直接保存
obj[prop] = value;
return true;
}
};
let person = new Proxy({}, validator);
person.age = 100;
person.age // 100
person.age = 'young' // 报错
person.age = 300 // 报错
上面代码中,由于设置了存值函数set,任何不符合要求的age属性赋值,都会抛出一个错误,这是数据验证的一种实现方法。利用set方法,还可以数据绑定,即每当对象发生变化时,会自动更新 DOM。
有时,我们会在对象上面设置内部属性,属性名的第一个字符使用下划线开头,表示这些属性不应该被外部使用。结合get和set方法,就可以做到防止这些内部属性被外部读写。
const handler = {
get (target, key) {
invariant(key, 'get');
return target[key];
},
set (target, key, value) {
invariant(key, 'set');
target[key] = value;
return true;
}
};
function invariant (key, action) {
if (key[0] === '_') {
throw new Error(`Invalid attempt to ${action} private "${key}" property`);
}
}
const target = {};
const proxy = new Proxy(target, handler);
proxy._prop
// Error: Invalid attempt to get private "_prop" property
proxy._prop = 'c'
// Error: Invalid attempt to set private "_prop" property
上面代码中,只要读写的属性名的第一个字符是下划线,一律抛错,从而达到禁止读写内部属性的目的。
1.15.3 apply()
apply方法拦截函数的调用、call和apply操作。
apply方法可以接受三个参数,分别是目标对象、目标对象的上下文对象(this)和目标对象的参数数组。
var handler = {
apply (target, ctx, args) {
return Reflect.apply(...arguments);
}
};
例子:
var target = function () { return 'I am the target'; };
var handler = {
apply: function () {
return 'I am the proxy';
}
};
var p = new Proxy(target, handler);
p()
// "I am the proxy"
上面代码中,变量p是 Proxy 的实例,当它作为函数调用时(p()),就会被apply方法拦截,返回一个字符串。
1.15.4 has()
has()方法用来拦截HasProperty操作,即判断对象是否具有某个属性时,这个方法会生效。典型的操作就是in运算符。
has()方法可以接受两个参数,分别是目标对象、需查询的属性名。
下面的例子使用has()方法隐藏某些属性,不被in运算符发现。
var handler = {
has (target, key) {
if (key[0] === '_') {
return false;
}
return key in target;
}
};
var target = { _prop: 'foo', prop: 'foo' };
var proxy = new Proxy(target, handler);
'_prop' in proxy // false
上面代码中,如果原对象的属性名的第一个字符是下划线,proxy.has()就会返回false,从而不会被in运算符发现。
如果原对象不可配置或者禁止扩展,这时has()拦截会报错。
var obj = { a: 10 };
Object.preventExtensions(obj);
var p = new Proxy(obj, {
has: function(target, prop) {
return false;
}
});
'a' in p // TypeError is thrown
上面代码中,obj对象禁止扩展,结果使用has拦截就会报错。也就是说,如果某个属性不可配置(或者目标对象不可扩展),则has()方法就不得“隐藏”(即返回false)目标对象的该属性。
值得注意的是,has()方法拦截的是HasProperty操作,而不是HasOwnProperty操作,即has()方法不判断一个属性是对象自身的属性,还是继承的属性。
另外,虽然for…in循环也用到了in运算符,但是has()拦截对for…in循环不生效。
let stu1 = {name: '张三', score: 59};
let stu2 = {name: '李四', score: 99};
let handler = {
has(target, prop) {
if (prop === 'score' && target[prop] < 60) {
console.log(`${target.name} 不及格`);
return false;
}
return prop in target;
}
}
let oproxy1 = new Proxy(stu1, handler);
let oproxy2 = new Proxy(stu2, handler);
'score' in oproxy1
// 张三 不及格
// false
'score' in oproxy2
// true
for (let a in oproxy1) {
console.log(oproxy1[a]);
}
// 张三
// 59
for (let b in oproxy2) {
console.log(oproxy2[b]);
}
// 李四
// 99
上面代码中,has()拦截只对in运算符生效,对for…in循环不生效,导致不符合要求的属性没有被for…in循环所排除。
1.16 Reflect 获取目标对象的行为
Reflect可以用于获取目标对象的行为,它与Object类似,但是更易读,为操作对象提供了一种更优雅的方式。它的方法与Proxy是对应的。
Reflect对象与Proxy对象一样,也是 ES6 为了操作对象而提供的新 API。Reflect对象的设计目的有这样几个。
(1) 将Object对象的一些明显属于语言内部的方法(比如Object.defineProperty),放到Reflect对象上。现阶段,某些方法同时在Object和Reflect对象上部署,未来的新方法将只部署在Reflect对象上。也就是说,从Reflect对象上可以拿到语言内部的方法。
(2) 修改某些Object方法的返回结果,让其变得更合理。比如,Object.defineProperty(obj, name, desc)在无法定义属性时,会抛出一个错误,而Reflect.defineProperty(obj, name, desc)则会返回false。
// 老写法
try {
Object.defineProperty(target, property, attributes);
// success
} catch (e) {
// failure
}
// 新写法
if (Reflect.defineProperty(target, property, attributes)) {
// success
} else {
// failure
}
(3) 让Object操作都变成函数行为。某些Object操作是命令式,比如name in obj和delete obj[name],而Reflect.has(obj, name)和Reflect.deleteProperty(obj, name)让它们变成了函数行为。
// 老写法
'assign' in Object // true
// 新写法
Reflect.has(Object, 'assign') // true
(4)Reflect对象的方法与Proxy对象的方法一一对应,只要是Proxy对象的方法,就能在Reflect对象上找到对应的方法。这就让Proxy对象可以方便地调用对应的Reflect方法,完成默认行为,作为修改行为的基础。也就是说,不管Proxy怎么修改默认行为,你总可以在Reflect上获取默认行为。
Proxy(target, {
set: function(target, name, value, receiver) {
var success = Reflect.set(target, name, value, receiver);
if (success) {
console.log('property ' + name + ' on ' + target + ' set to ' + value);
}
return success;
}
});
上面代码中,Proxy方法拦截target对象的属性赋值行为。它采用Reflect.set方法将值赋值给对象的属性,确保完成原有的行为,然后再部署额外的功能。
1.17 Promise对象
Promise 是异步编程的一种解决方案,比传统的解决方案回调函数, 更合理和更强大。ES6 将其写进了语言标准,统一了用法,原生提供了Promise对象
1.指定回调函数方式更灵活易懂。
2.解决异步回调地狱的问题。
1.17.1 回调地狱
- 当一个回调函数嵌套一个回调函数的时候
- 就会出现一个嵌套结构
- 当嵌套的多了就会出现回调地狱的情况
- 比如发送三个 ajax 请求
- 第一个正常发送
- 第二个请求需要第一个请求的结果中的某一个值作为参数
- 第三个请求需要第二个请求的结果中的某一个值作为参数
ajax({
url: '我是第一个请求',
success (res) {
// 现在发送第二个请求
ajax({
url: '我是第二个请求',
data: { a: res.a, b: res.b },
success (res2) {
// 进行第三个请求
ajax({
url: '我是第三个请求',
data: { a: res2.a, b: res2.b },
success (res3) {
console.log(res3)
}
})
}
})
}
})
回调地狱,其实就是回调函数嵌套过多导致的
当代码成为这个结构以后,已经没有维护的可能了
1.17.2 Promise使用
语法:
new Promise(function (resolve, reject) {
// resolve 表示成功的回调
// reject 表示失败的回调
}).then(function (res) {
// 成功的函数
}).catch(function (err) {
// 失败的函数
})
1.17.3 Promise 对象的状态
Promise 对象通过自身的状态,来控制异步操作。Promise 实例具有三种状态。
- 异步操作未完成(pending)
- 异步操作成功(fulfilled)
- 异步操作失败(rejected)
这三种的状态的变化途径只有两种。
- 从“未完成”到“成功”
- 从“未完成”到“失败
一旦状态发生变化,就凝固了,不会再有新的状态变化。这也是 Promise 这个名字的由来,它的英语意思是“承诺”,一旦承诺成效,就不得再改变了。这也意味着,Promise 实例的状态变化只可能发生一次。
因此,Promise 的最终结果只有两种。
- 异步操作成功,Promise 实例传回一个值(value),状态变为fulfilled。
- 异步操作失败,Promise 实例抛出一个错误(error),状态变为rejected。
1.17.4 Promise.all
Promise.all()方法用于将多个 Promise 实例,包装成一个新的 Promise 实例。
const p = Promise.all([p1, p2, p3]);
p的状态由p1,p2,p3 决定,分成两种情况。
- 只有p1、p2、p3的状态都变成fulfilled,p的状态才会变成fulfilled,此时p1、p2、p3的返回值组成一个数组,传递给p的回调函数。
- 只要p1、p2、p3之中有一个被rejected,p的状态就变成rejected,此时第一个被reject的实例的返回值,会传递给p的回调函数。
1.17.5 Promise.race
Promise.race()方法同样是将多个 Promise 实例,包装成一个新的 Promise 实例。
const p = Promise.race([p1, p2, p3]);
上面代码中,只要p1、p2、p3之中有一个实例率先改变状态,p的状态就跟着改变。那个率先改变的 Promise 实例的返回值,就传递给p的回调函数。
1.18 Generator 函数
Generator 函数是 ES6 提供的一种异步编程解决方案
Generator 函数是一个状态机,封装了多个内部状态。
执行 Generator 函数会返回一个遍历器对象,也就是说,Generator 函数除了状态机,还是一个遍历器对象生成函数。返回的遍历器对象,可以依次遍历 Generator 函数内部的每一个状态。
形式上,Generator 函数是一个普通函数,但是有两个特征。
- 一是,function关键字与函数名之间有一个
*
星号; - 二是,函数体内部使用
yield
表达式,定义不同的内部状态(yield
在英语里的意思就是“产出”)。
function* helloWorldGenerator() {
yield 'hello';
yield 'world';
return 'ending';
}
var hw = helloWorldGenerator();
上面代码定义了一个 Generator 函数helloWorldGenerator,它内部有两个yield表达式(hello和world),即该函数有三个状态:hello,world 和 return 语句(结束执行)。
然后,Generator 函数的调用方法与普通函数一样,也是在函数名后面加上一对圆括号。不同的是,调用 Generator 函数后,该函数并不执行,返回的也不是函数运行结果,而是一个指向内部状态的指针对象
下一步,必须调用遍历器对象的next方法,使得指针移向下一个状态。也就是说,每次调用next方法,内部指针就从函数头部或上一次停下来的地方开始执行,直到遇到下一个yield表达式(或return语句)为止。换言之,Generator 函数是分段执行的,yield表达式是暂停执行的标记,而next方法可以恢复执行。
hw.next()
// { value: 'hello', done: false }
hw.next()
// { value: 'world', done: false }
hw.next()
// { value: 'ending', done: true }
hw.next()
// { value: undefined, done: true }
- 第一次调用,Generator
函数开始执行,直到遇到第一个yield表达式为止。next方法返回一个对象,它的value属性就是当前yield表达式的值hello,done属性的值false,表示遍历还没有结束。 - 第二次调用,Generator
函数从上次yield表达式停下的地方,一直执行到下一个yield表达式。next方法返回的对象的value属性就是当前yield表达式的值world,done属性的值false,表示遍历还没有结束。 - 第三次调用,Generator
函数从上次yield表达式停下的地方,一直执行到return语句(如果没有return语句,就执行到函数结束)。next方法返回的对象的value属性,就是紧跟在return语句后面的表达式的值(如果没有return语句,则value属性的值为undefined),done属性的值true,表示遍历已经结束。 - 第四次调用,此时 Generator
函数已经运行完毕,next方法返回对象的value属性为undefined,done属性为true。以后再调用next方法,返回的都是这个值。
总结,调用 Generator 函数,返回一个遍历器对象,代表 Generator 函数的内部指针。以后,每次调用遍历器对象的next方法,就会返回一个有着value和done两个属性的对象。value属性表示当前的内部状态的值,是yield表达式后面那个表达式的值;done属性是一个布尔值,表示是否遍历结束。
1.18.1 yield 表达式
由于 Generator 函数返回的遍历器对象,只有调用next方法才会遍历下一个内部状态,所以其实提供了一种可以暂停执行的函数。yield表达式就是暂停标志。
遍历器对象的next方法的运行逻辑如下。
- (1)遇到yield表达式,就暂停执行后面的操作,并将紧跟在yield后面的那个表达式的值,作为返回的对象的value属性值。
- (2)下一次调用next方法时,再继续往下执行,直到遇到下一个yield表达式。
- (3)如果没有再遇到新的yield表达式,就一直运行到函数结束,直到return语句为止,并将return语句后面的表达式的值,作为返回的对象的value属性值。
- (4)如果该函数没有return语句,则返回的对象的value属性值为undefined。
需要注意的是,yield表达式后面的表达式,只有当调用next方法、内部指针指向该语句时才会执行,因此等于为 JavaScript 提供了手动的“惰性求值”(Lazy Evaluation)的语法功能。
另外需要注意,yield表达式只能用在 Generator 函数里面,用在其他地方都会报错。
yield表达式与return语句既有相似之处,也有区别。相似之处在于,都能返回紧跟在语句后面的那个表达式的值。区别在于每次遇到yield,函数暂停执行,下一次再从该位置继续向后执行,而return语句不具备位置记忆的功能。一个函数里面,只能执行一次(或者说一个)return语句,但是可以执行多次(或者说多个)yield表达式。正常函数只能返回一个值,因为只能执行一次return;Generator 函数可以返回一系列的值,因为可以有任意多个yield。从另一个角度看,也可以说 Generator 生成了一系列的值,这也就是它的名称的来历(英语中,generator 这个词是“生成器”的意思)。
1.18.2 next 方法的参数
yield表达式本身没有返回值,或者说总是返回undefined。next方法可以带一个参数,该参数就会被当作上一个yield表达式的返回值。
function* f() {
for(var i = 0; true; i++) {
var reset = yield i;
if(reset) { i = -1; }
}
}
var g = f();
g.next() // { value: 0, done: false }
g.next() // { value: 1, done: false }
g.next(true) // { value: 0, done: false }
上面代码先定义了一个可以无限运行的 Generator 函数f,如果next方法没有参数,每次运行到yield表达式,变量reset的值总是undefined。当next方法带一个参数true时,变量reset就被重置为这个参数(即true),因此i会等于-1,下一轮循环就会从-1开始递增。
这个功能有很重要的语法意义。Generator 函数从暂停状态到恢复运行,它的上下文状态(context)是不变的。通过next方法的参数,就有办法在 Generator 函数开始运行之后,继续向函数体内部注入值。也就是说,可以在 Generator 函数运行的不同阶段,从外部向内部注入不同的值,从而调整函数行为。
function* foo(x) {
var y = 2 * (yield (x + 1));
var z = yield (y / 3);
return (x + y + z);
}
var a = foo(5);
a.next() // Object{value:6, done:false}
a.next() // Object{value:NaN, done:false}
a.next() // Object{value:NaN, done:true}
var b = foo(5);
b.next() // { value:6, done:false }
b.next(12) // { value:8, done:false }
b.next(13) // { value:42, done:true }
上面代码中,第二次运行next方法的时候不带参数,导致 y 的值等于2 * undefined(即NaN),除以 3 以后还是NaN,因此返回对象的value属性也等于NaN。第三次运行Next方法的时候不带参数,所以z等于undefined,返回对象的value属性等于5 + NaN + undefined,即NaN。
如果向next方法提供参数,返回结果就完全不一样了。上面代码第一次调用b的next方法时,返回x+1的值6;第二次调用next方法,将上一次yield表达式的值设为12,因此y等于24,返回y / 3的值8;第三次调用next方法,将上一次yield表达式的值设为13,因此z等于13,这时x等于5,y等于24,所以return语句的值等于42。
注意,由于next方法的参数表示上一个yield表达式的返回值,所以在第一次使用next方法时,传递参数是无效的。V8 引擎直接忽略第一次使用next方法时的参数,只有从第二次使用next方法开始,参数才是有效的。从语义上讲,第一个next方法用来启动遍历器对象,所以不用带有参数。
1.18.3 基本语法
function *gen(){
console.log(1)
yield;
console.log(2)
yield;
console.log(3)
}
let g = gen()
g.next()
g.next()
g.next()
1.18.4 异步流程
手动版本
function *gen(){
let res1 = yield ajax("1.json")
console.log(res1)
let res2 = yield ajax("2.json")
console.log(res2)
}
let g = gen()
g.next().value.then(data=>{
g.next(data).value.then(data=>{
g.next(data)
})
})
自动版本
function* gen() {
let res1 = yield ajax("1.json")
console.log(res1)
let res2 = yield ajax("2.json")
console.log(res2)
}
function AutoRun(gen) {
let g = gen();
function next(data) {
let res = g.next(data);
if (res.done) return
res.value.then(function (data) {
next(data);
});
}
next();
}
AutoRun(gen);
1.19 class语法
1.19.1 类的写法
class Person {
constructor(name,age){
this.name = name;
this.age = age;
}
say(){
console.log(this.name,this.age)
}
}
let obj = new Person("kerwin",100)
console.log(obj)
1.19.2 get和set
<ul id="list"></ul>
<script>
class Person {
constructor(name,age,id) {
this.name = name
this.age = age
this.ele = document.querySelector(`#${id}`)
}
get location(){
console.log("get")
}
set location(arr){
console("set",arr)
}
get html() {
return this.ele.innerHTML
}
set html(arr) {
this.ele.innerHTML = arr.map(item => `<li>${item}</li>`).join("")
}
}
let obj = new Person("steve",100,"list")
obj.html = ["aaa", "bbb", "cccc"]
</script>
1.19.3 static 静态方法
类相当于实例的原型,所有在类中定义的方法,都会被实例继承。如果在一个方法前,加上static关键字,就表示该方法不会被实例继承,而是直接通过类来调用,这就称为“静态方法”。
class Person {
static name = "Person这个类"
constructor(name,age){
this.name = name;
this.age = age;
}
say(){
console.log(this.name,this.age)
}
static eat(){
console.log("eat")
}
}
let obj = new Person("kerwin",100)
console.log(Person.name)
Person.eat()
1.19.4 super()方法
super 关键字用于访问和调用一个对象的父对象上的函数。。
在构造函数中使用时,super关键字将单独出现,并且必须在使用 this 关键字之前使用。super 关键字也可以用来调用父对象上的函数。
(1)Class中的 super(),它在这里表示父类的构造函数,用来新建父类的 this 对象
super()相当于Parent.prototype.constructor.call(this)
class Demo{
constructor(x,y) {
this.x = x;
this.y = y;
}
customSplit(){
return [...this.y]
}
}
class Demo2 extends Demo{
constructor(x,y){
super(x,y);
}
customSplit(){
return [...this.x]
}
task1(){
return super.customSplit();
}
task2(){
return this.customSplit();
}
}
let d = new Demo2('hello','world');
d.task1() //["w", "o", "r", "l", "d"]
d.task2() //["h", "e", "l", "l", "o"]
(2)子类没有自己的this对象,而是继承父亲的this对象,然后进行加工。如果不调用super,子类就得不到this对象
class Demo2 extends Demo{
constructor(x,y){
this.x = x; //this is not defined
}
}
ES5的继承,实质上是先创造子类的实例对象this,然后再将父类的方法添加到this上(Parent.call(this)).
ES6的继承,需要先创建父类的this,子类调用super继承父类的this对象,然后再加工。
如果子类没有创建constructor,这个方法会被默认添加:
class Demo{
constructor(x) {
this.x = x;
}
}
class Demo2 extends Demo{}
let d = new Demo2('hello');
d.x //hello
(3) super 在静态方法之中指向父类,在普通方法之中指向父类的原型对象
1.20 class 继承
ES6 规定,子类必须在constructor()方法中调用super(),否则就会报错。这是因为子类自己的this对象,必须先通过父类的构造函数完成塑造,得到与父类同样的实例属性和方法,然后再对其进行加工,添加子类自己的实例属性和方法。如果不调用super()方法,子类就得不到自己的this对象。
class Person {
static name = "Person这个类"
constructor(name,age){
this.name = name;
this.age = age;
}
say(){
console.log(this.name,this.age)
}
static eat(){
console.log("eat")
}
}
class Student extends Person{
constructor(name,age,score){
super(name,age)
this.score = score
}
say(){
super.say()
console.log(this.score)
}
static eat(){
super.eat();
console.log("student eat")
}
}
let obj = new Student("zzy",100,200)
console.log(obj)
obj.say()
Student.eat()
对于同样类型的代码,减少重复
<div class="box1">
<h1></h1>
<ul></ul>
</div>
<div class="box2">
<h1></h1>
<img src="" alt="" style="width:100px">
<ul></ul>
</div>
<script>
var data1 = {
title: "体育",
list: ["体育-1", "体育-2", "体育-3"]
}
var data2 = {
title: "综艺",
url:
"https://pic.maizuo.com/usr/movie/5011ee407fb407d47e333a3935ec33d1.jpg?x-oss-process=image/ quality,Q_70",
list: ["综艺-1", "综艺-2", "综艺-3"]
}
class CreatBox {
constructor(select, data) {
this.ele = document.querySelector(select)
this.title = data.title
this.list = data.list
this.render()
}
render() {
let oh1 = this.ele.querySelector("h1")
let ou1 = this.ele.querySelector("ul")
oh1.innerHTML = this.title
ou1.innerHTML = this.list.map(
item =>
`<li>${item}</li>`
).join("")
}
}
new CreatBox(".box1", data1)
class CreatImgBox extends CreatBox {
constructor(select, data) {
super(select, data)
this.imgUrl = data.url
this.render
}
render() {
super.render()
let oimg = this.ele.querySelector("img")
oimg.src = this.imgUrl
}
}
new CreatImgBox(".box2", data2)
</script>
1.22 Module 语法
ES6 模块的设计思想是尽量的静态化,使得编译时就能确定模块的依赖关系,以及输入和输出的变量。CommonJS 和 AMD 模块,都只能在运行时确定这些东西。比如,CommonJS 模块就是对象,输入时必须查找对象属性。
// CommonJS模块
let { stat, exists, readfile } = require('fs');
// 等同于
let _fs = require('fs');
let stat = _fs.stat;
let exists = _fs.exists;
let readfile = _fs.readfile;
上面代码的实质是整体加载fs模块(即加载fs的所有方法),生成一个对象(_fs),然后再从这个对象上面读取 3 个方法。这种加载称为“运行时加载”,因为只有运行时才能得到这个对象,导致完全没办法在编译时做“静态优化”。
ES6 模块不是对象,而是通过export命令显式指定输出的代码,再通过import命令输入。
// ES6模块
import { stat, exists, readFile } from 'fs';
上面代码的实质是从fs模块加载 3 个方法,其他方法不加载。这种加载称为“编译时加载”或者静态加载,即 ES6 可以在编译时就完成模块加载,效率要比 CommonJS 模块的加载方式高。当然,这也导致了没法引用 ES6 模块本身,因为它不是对象。
由于 ES6 模块是编译时加载,使得静态分析成为可能。有了它,就能进一步拓宽 JavaScript 的语法,比如引入宏(macro)和类型检验(type system)这些只能靠静态分析实现的功能。
除了静态加载带来的各种好处,ES6 模块还有以下好处。
- 不再需要UMD模块格式了,将来服务器和浏览器都会支持 ES6 模块格式。目前,通过各种工具库,其实已经做到了这一点。
- [ ] 将来浏览器的新 API 就能用模块格式提供,不再必须做成全局变量或者navigator对象的属性。
- 不再需要对象作为命名空间(比如Math对象),未来这些功能可以通过模块提供。
1.22 ES6 NodeJS中的模块化
JavaScript 现在有两种模块。一种是 ES6 模块,简称 ESM;另一种是 CommonJS 模块,简称 CJS。
CommonJS 模块是 Node.js 专用的,与 ES6 模块不兼容。语法上面,两者最明显的差异是,CommonJS 模块使用require()和module.exports,ES6 模块使用import和export。
ES6 模块不是对象,而是通过export命令显式指定输出的代码,再通过import命令输入。
写法1:
export default A1
import a1 from "./1.js"
写法2:
export {A1,A2}
import {A1,A2} from "./1.js"
import {A1 as a1,A2 as a2} from "./1.js"
import * as obj from "./1.js"
export function A1(){
console.log("A1")
}
export function A2(){
console.log("A2")
}
import {A1,A2} from "./1.js"
import {A1 as a1,A2 as a2} from "./1.js"
import * as obj from "./1.js"
混合写法
export {A1}
export default A2
import A2,{A1} from "./1.js"
2.1 ES7-新特性
2.1.1 求幂运算符
Math.pow(3, 2) === 3 ** 2 // 9
2.1.2 数组的includes方法
[1, 2, NaN].includes(NaN) // true
[1, 2, NaN].indexOf(NaN) // -1
如果仅仅查找数据是否在数组中,建议使用includes,如果是查找数据的索引位置,建议使用indexOf更好一些
3.1 ES8新特性
3.1.1 async和await
async
async 函数,使得异步操作变得更加方便。
- 更好的语义。
async function test(){
console.log("test")
return new Promise((resolve,reject)=>{
reject("err-22222")
})
}
let res = test()
res.then(res=>{
console.log("success",res)
}).catch(err=>{
console.log("err",err)
})
- 1.如果async返回值是非promise对象, fulfilled,then指定
- 2.如果async返回的是promsie对象, fulfilled or rejected , then or catch
await
await命令后面接一个 Promise 对象,返回该对象的结果。如果不是 Promise 对象,就直接返回对应的值。
async function test(){
var res1 = await ajax("http://localhost:3000/news1")
var res2 = await ajax("http://localhost:3000/news2")
return res2
}
test().then(res=>{
console.log("返回结果",res)
}).catch(err=>{
console.log("err",err)
})
错误处理
try{
var res1 = await ajax("http://localhost:3000/news1")
var res2 = await ajax("http://localhost:3000/news2")
}catch(err){
console.log("err",err)
}
3.1 ES8-对象方法扩展
1.快速输出键值
let obj = {
name:"zzy",
age:100
}
console.log(Object.values(obj))
2.快速输出键名和键值
let obj = {
name:"zzy",
age:100
}
console.log(Object.entries(obj))
3.
let obj = {
name:"zzy",
age:100
}
console.log(Object.getOwnPropertyDescriptors(obj))
4.克隆对象
let obj1 = {
name:"zzy",
age:100,
location:{
provice:"辽宁",
city:"大连"
},
//只设置city,防止破坏province
get city(){
return this.location.city
},
set city(value){
this.location.city = value
},
set nameset(value){
this.name = value.substring(0,1).toUpperCase()+value.substring(1)
},
get nameset(){
return this.name
}
}
console.log(Object.getOwnPropertyDescriptors(obj1))
var obj2= {}
//Object.assign(obj2,obj1)//无法克隆 get set方法
Object.defineProperties(obj2,Object.getOwnPropertyDescriptors(obj1))
3.2 ES8- 字符串填充
padStart()、padEnd()方法可以使得字符串达到固定长度,前者从前面开始,后者从后面开始
有两个参数:字符串目标长度和填充内容。
let str= "zzy"
console.log(str.padStart(10,"x"))
console.log(str.padEnd(10,"x"))
console.log(str.padStart(5,"x"))
console.log(str.padEnd(5,"x"))
作用案例:
函数参数的末尾加逗号
『末尾逗号』在添加新的参数、属性、元素时是有用的,你可以直接新加一行而不必给上一行再补充一个逗号,这样使版本控制工具的修改记录也更加整洁
function test(
a,
b,
c,
){
console.log(a,b)
}
test(
1,
2,
3,
)
4.1 ES9-rest与扩展运算符
4.1.1 对象的剩余参数
let obj = {
name:"zzy",
age:100,
location:"dalian"
}
let {name,...other} = obj
console.log(name) //kerwin
console.log(other) //{age: 100, location: 'dalian'}
4.1.2 对象的扩展运算符
let obj1 = {
name:"zzy"
}
let obj2 = {
age:100
}
console.log({...obj1,...obj2})
4.2 ES9-正值扩展
JS正则表达式可以返回一个匹配的对象, 一个包含匹配字符串的类数组, 比如: 以 YYYY-MM-DD的格式解析日期,
这样的代码可读性很差, 并且在改变正则表达式的结构的时候很有可能就会改变匹配对象的索引
ES9允许使用命名捕获 , 在打开捕获括号后立即命名。
1.快速获取字符串的数值
let str = "今天是2022-10-10"
let reg = /([0-9]{4})-([0-9]{2})-([0-9]{2})/g
let res1 = reg.exec(str)
console.log(res1)
2.捕获分组
let str = "今天是2022-10-10"
let reg = /(?<year>[0-9]{4})-(?<month>[0-9]{2})-(?<day>[0-9]{2})/g
let res1 = reg.exec(str)
console.log(res1)
4.3 ES9-Promise.finally()
- 无论是成功还是失败, 都运行同样的代码, 比如隐藏对话框, 关闭数据连接
function ajax(){
return new Promise((resolve,reject)=>{
reject(1111)
})
}
//showloading
ajax().then(res=>{
}).catch(err=>{
}).finally(()=>{
//hideloading
console.log("finally")
})
4.4 ES9-异步迭代
4.4.1 同步遍历器的问题
function* fn() {
yield 1111
yield 2222
}
const syncI = fn();
console.log(syncI.next())
console.log(syncI.next())
console.log(syncI.next())
function* fn() {
yield new Promise(resolve=>resolve("1111"))
yield new Promise(resolve=>resolve("2222"))
}
const syncI = fn();
syncI.next().value.then(res=>{console.log(res)})
syncI.next().value.then(res=>{console.log(res)})
value属性的返回值是一个 Promise 对象,用来放置异步操作。但是这样写很麻烦,不太符合直觉,语义也比较绕。
4.4.2 异步遍历器生成函数
- Generator 函数返回一个同步遍历器,异步 Generator 函数的作用,是返回一个异步遍历器对象。在语法上,异步 Generator 函数就是async函数与 Generator 函数的结合。
async function* fn() {
yield new Promise(resolve=>resolve("1111"))
yield new Promise(resolve=>resolve("2222"))
}
const asyncI = fn();
asyncI.next().then(res=>{
console.log(res)
return asyncI.next()
}).then(res=>{
console.log(res)
return asyncI.next()
})
.then(res=>{
console.log(res)
})
4.4.3 for await of
for…of循环用于遍历同步的 Iterator 接口。新引入的for await…of循环,则是用于遍历异步的 Iterator 接口。
async function test() {
for await (let i of asyncI) {
console.log(i)
}
}
test()
4.4.4 案例
function timer(t) {
return new Promise(resolve => {
setTimeout(() => {
resolve(t)
}, t)
})
}
async function* fn() {
yield timer(1000)//任务1
yield timer(2000)//任务2
yield timer(3000)//任务3
}
// 使用一下 for await ...of
async function fn1() {
for await(const val of fn()) {
console.log("start",Date.now())
console.log(val);
console.log("end",Date.now())
}
}
fn1();
4.4.5 nodejs写法
// 传统写法
function main(inputFilePath) {
const readStream = fs.createReadStream(
inputFilePath,
{ encoding: 'utf8', highWaterMark: 1024 }
);
readStream.on('data', (chunk) => {
console.log('>>> '+chunk);
});
readStream.on('end', () => {
console.log('### DONE ###');
});
}
// 异步遍历器写法
async function main(inputFilePath) {
const readStream = fs.createReadStream(
inputFilePath,
{ encoding: 'utf8', highWaterMark: 1024 }
);
for await (const chunk of readStream) {
console.log('>>> '+chunk);
}
console.log('### DONE ###');
}
5.1 ES10新特性
5.1.1 Object.fromEntries
Object.fromEntries()方法允许你轻松地将键值对列表转换为对象
const arr = [["name", "zzy"], ["age", 100]];
console.log(Object.fromEntries(arr))//{name: 'kerwin', age: 100}
const m = new Map()
m.set("name","xiaohong")
m.set("age",18)
console.log(Object.fromEntries(m))
用处
let str ="name=zzy&age=100"
let searchParams = new URLSearchParams(str)
console.log(Object.fromEntries(searchParams))//{name: 'kerwin', age: '100'}
5.2 ES10-trimStart() and trimEnd()
trimStart()和trimEnd()方法在实现与trimLeft()和trimRight()相同。
let str = " zzy "
console.log("|"+str.trimStart(str)+"|")
console.log("|"+str.trimEnd(str)+"|")
console.log("|"+str.trimLeft(str)+"|")
console.log("|"+str.trimRight(str)+"|")
5.3 ES10-Symbol 对象的 description 属性
为Symbol对象添加了只读属性 description ,该对象返回包含Symbol描述的字符串。
let s = Symbol("van")
console.log(s.description) //kerwin
5.4 ES10-可选的catch
可以省略参数
let pro1 = new Promise(function (resolve, reject) {
//执行器函数
setTimeout(() => {
resolve("成功的结果")
}, 30000)
})
let pro2 = new Promise(function (resolve, reject) {
//执行器函数
setTimeout(() => {
reject()
}, 2000)
})
async function test() {
try {
await Promise.race([pro1, pro2])
} catch {
console.log("不关心错误结果,网络超时")
}
}
test()
6.1 ES11新特性
6.1.1 Promise.allSettled
Promise.allSettled() 方法返回一个在所有给定的 promise 都已经 fulfilled 或 rejected 后的 promise ,并带有一个对象数组,每个对象表示对应的 promise 结果。
const promises = [ ajax('/200接口'), ajax('/401接口') ];
Promise.allSettled(promises).then(results=>{
// 过滤出成功的请求
results.filter(item =>item.status === 'fulfilled');
过滤出失败的请求
results.filter(item=> item.status === 'rejected');
})
6.2 ES11-module新增
6.2.1 动态导入 import()
标准用法的 import 导入的模块是静态的,会使所有被导入的模块,在加载时就被编译(无法做到按需编译,降低首页加载速度)。有些场景中,你可能希望根据条件导入模块或者按需导入模块,这时你可以使用动态导入代替静态导入。
<body>
<button>login</button>
<script type="module">
let role1 = "管理员"
let role2 = "普通用户"
function login(){
return "普通用户"
}
async function render(role){
if(role===role1){
let res1 = await import("./1.js")
console.log(res1.default)
}else{
let res2 = await import("./2.js")
console.log(res2.default)
}
}
let obtn = document.querySelector("button")
obtn.onclick = function(){
let role = login()
render(role)
}
</script>
</body>
6.2.2 import.meta
import.meta 会返回一个对象,有一个 url 属性,返回当前模块的url路径,只能在模块内部使用。
<script type="module">
import obj from './1.js'
</script>
//1.js
console.log(import.meta)
export default {
}
6.2.3 export * as obj from ‘module’
//1.js
export default {
name:'111111'
}
export function test1(){
}
//2.js
export default {
name:"22222"
}
export function test2(){
}
export * as obj1 from './1.js'
export * as obj2 from './2.js'
//html
<script type="module">
import * as obj1 from './1.js'
console.log(obj1)
import * as {obj2,test2} from './2.js'
console.log(obj2,test2)
</script>
6.3 ES11-字符串的matchAll方法
matchAll() 方法返回一个包含所有匹配正则表达式的结果的迭代器。可以使用 for…of 遍历,或者使用 展开运算符(…) 或者 Array.from 转换为数组。
1.
let str = `
<ul>
<li>1111</li>
<li>2222</li>
<li>3333</li>
<li>4444</li>
</ul>
`
let reg = /<li>(.*)<\/li>/g
console.log(str.match(reg))
let str = `
<ul>
<li>1111</li>
<li>2222</li>
<li>3333</li>
<li>4444</li>
</ul>
`
let reg = /<li>(.*)<\/li>/g
let match = null;
while(match = reg.exec(str)){
console.log(match[0])
console.log(match[1])
}
let str = `
<ul>
<li>1111</li>
<li>2222</li>
<li>3333</li>
<li>4444</li>
</ul>
`
let reg = /<li>(.*)<\/li>/g
for(let i of str.matchAll(reg)){
console.log(i)
}
6.4 ES11-BigInt
JavaScript 能够准确表示的整数范围在-253到253之间(不含两个端点),超过这个范围,无法精确表示这个值,这使得 JavaScript 不适合进行科学和金融方面的精确计算。
9007199254740992 //9007199254740992
9007199254740993 //9007199254740992
Math.pow(2,53) === Math.pow(2,53)+1
为了与 Number 类型区别,BigInt 类型的数据必须添加后缀n。
1234 // 普通整数
1234n // BigInt
// BigInt 的运算
1n + 2n // 3n
6.5 ES11-globalThis
globalThis 提供了一个标准的方式来获取不同环境下的全局 this 对象(也就是全局对象自身)。不像 window 或者 self这些属性,它确保可以在有无窗口的各种环境下正常工作。所以,你可以安心的使用 globalThis,不必担心它的运行环境。为便于记忆,你只需要记住,全局作用域中的 this 就是 globalThis。
1.
//es6-shim
var getGlobal = function () {
// the only reliable means to get the global object is
// Function('return this')()
// However, this causes CSP violations in Chrome apps.
if (typeof self !== 'undefined') { return self; }
if (typeof window !== 'undefined') { return window; }
if (typeof global !== 'undefined') { return global; }
throw new Error('unable to locate global object');
};
var globals = getGlobal();
if (!globals.Reflect) {
defineProperty(globals, ‘Reflect’, {}, true);
}
//以前
var getGlobal = function () {
if (typeof self !== 'undefined') { return self; }
if (typeof window !== 'undefined') { return window; }
if (typeof global !== 'undefined') { return global; }
throw new Error('unable to locate global object');
};
let globals = getGlobal()
if (globals.document) {
console.log("进行dom操作相关")
} else {
console.log("不能进行dom操作")
}
//现在
if (globalThis.document) {
console.log("进行dom操作相关")
} else {
console.log("不能进行dom操作")
}
6.6 ES11-空值合并运算符
**空值合并运算符(??)**是一个逻辑运算符。当左侧操作数为 null 或 undefined 时,其返回右侧的操作数。否则返回左侧的操作数。
let obj = {
name:"zzy",
introduction:0
}
console.log(obj.introduction || "这个人很懒")
console.log(obj.introduction ?? "这个人很懒")
??和 || 的区别是什么呢?
他们两个最大的区别就是 ‘ '和 0,??的左侧 为 ’ '或者为 0 的时候,依然会返回左侧的值;
|| 会对左侧的数据进行boolean类型转换,所以’ '和 0 会被转换成false,返回右侧的值
6.7 ES11-可选链操作符&&和?.
可选链前面的值如果是null或undefined,则不再执行后面的,之前返回可选链前面的值
let obj = {
name:"zzy",
introduction:0,
// location:{
// city:"dalian"
// }
}
console.log(obj && obj.location && obj.location.city)
console.log(obj?.location?.city)
7.1 ES12新特性
7.1.1 逻辑赋值操作符
逻辑赋值操作符 ??=、&&=、 ||=
let a = true
let b = false
//a &&= b //false
a ||= b ; //true
console.log(a)
obj.introduction??="很懒"
console.log(obj.introduction)
// 0
7.2 ES12-数字分隔符
这个新特性是为了方便程序员看代码而出现的,如果数字比较大,那么看起来就不是那么一目了然
const num= 123456789;
分隔符不仅可以分割十进制,也可以分割二净值或者十六净值的数据,非常好用。
const number = 1_000_000_000_000;
const binary = 0b1010_0101_1111_1101;
const hex = 0xA1_B2_C3;
7.3 ES12-replaceAll
所有匹配都会被替代项替换。模式可以是字符串或正则表达式,而替换项可以是字符串或针对每次匹配执行的函数。并返回一个全新的字符串
const str =
"I wish to wish the wish you wish to wish, but if you wish the wish the witch wishes, I won't wish the wish you wish to wish. ";
const newStr = str.replaceAll("wish", "zzy");
console.log(newStr);
7.4 ES12-Promise.any
只要参数实例有一个变成fulfilled状态,包装实例就会变成fulfilled状态;如果所有参数实例都变成rejected状态,包装实例就会变成rejected状态。
Promise.any()跟Promise.race()方法很像,只有一点不同,就是
Promise.any()不会因为某个 Promise变成rejected状态而结束,必须等到所有参数 Promise 变成rejected状态才会结束。
7.5 ES12-WeakRefs
在一般情况下,对象的引用是强引用的,这意味着只要持有对象的引用,它就不会被垃圾回收。只有当该对象没有任何的强引用时,垃圾回收才会销毁该对象并且回收该对象所占的内存空间。
let target = {};
let wr = new WeakRef(target);
而 WeakRef 允许您保留对另一个对象的弱引用,而不会阻止被弱引用对象被垃圾回收。
let target = {};
let wr = new WeakRef(target);
let obj = wr.deref();
if (obj) { // target 未被垃圾回收机制清除
// ...
}
let like = new WeakRef(document.getElementById("like"))
let mymap = new WeakMap()
mymap.set(like.deref(), {
click: 0
})
like.deref().onclick = function () {
let times = mymap.get(like.deref())
times.click++
}
setTimeout(() => {
document.body.removeChild(like.deref())
}, 2000)
7.6 ES12-FinalizationRegistry
清理器注册表功能 FinalizationRegistry,用来指定目标对象被垃圾回收机制清除以后,所要执行的回调函数。
新建一个注册表实例
const registry = new FinalizationRegistry(data => {
// ....
});
监听
registry.register(obj, "some value");
registry.unregister(obj);
例子:
let like = new WeakRef(document.getElementById("like"))
let mymap = new WeakMap()
mymap.set(like.deref(), {
click: 0
})
like.deref().onclick = function () {
let times = mymap.get(like.deref())
times.click++
}
setTimeout(() => {
// registry.register(document.getElementById("like"), mymap.get(like.deref()));
registry.register(like.deref(), mymap.get(like.deref()));
document.body.removeChild(like.deref())
}, 2000)
const registry = new FinalizationRegistry(data => {
// ....
console.log("被销毁了", data)
});
8.1 ES13新特性
8.1.1 私有属性和方法
私有的无法访问
class Cache{
//_obj ={}
#obj ={}
get(key){
return this.#obj[key]
}
set(key,value){
this.#obj[key] =value
}
}
let cache = new Cache()
cache.set("name","van")
8.1.2 静态成员的私有属性和方法
我们还可以给类定义静态成员和静态私有函数。类的静态方法可以使用this关键字访问其他的私有或者公有静态成员.
class Cache{
static #count = 0;
static getCount(){
return this.#count
}
#obj ={}
get(key){
return this.#obj[key]
}
set(key,value){
this.#obj[key] =value
}
}
let cache = new Cache()
cache.set("name","van")
console.log(Cache.getCount())
8.1.3 静态代码块
ES13允许在类中通过static关键字定义一系列静态代码块,这些代码块只会在类被创造的时候执行一次。这其实有点像一些其他的如C#和Java等面向对象的编程语言的静态构造函数的用法。
一个类可以定义任意多的静态代码块,这些代码块会和穿插在它们之间的静态成员变量一起按照定义的顺序在类初始化的时候执行一次。我们还可以使用super关键字来访问父类的属性。
class Cache{
static obj = new Map()
static {
this.obj.set("name","van")
this.obj.set("age",100)
}
static{
console.log(this.obj)
}
}
console.log(Cache.obj)
8.1.4 使用in来判断某个对象是否拥有某个私有属性
class Cache {
#obj = {}
get(key) {
return this.#obj[key]
}
set(key, value) {
this.#obj[key] = value
}
hasObj(){
return #obj in this
}
}
let cache = new Cache()
console.log(cache.hasObj())
8.2 ES13-最外层的await
顶层await只能用在 ES6 模块,不能用在 CommonJS 模块。这是因为 CommonJS 模块的require()是同步加载,如果有顶层await,就没法处理加载了。
<script type="module">
function ajax() {
return new Promise((resolve) => {
setTimeout(() => {
resolve("data-1111");
}, 1000);
})
}
let res = await ajax();
console.log(res)
</script>
8.3 ES13-at函数来索引元素
let arr = ["xiaozhang","zzu=y","xiaohong","xiaoming"]
console.log(arr[1])
console.log(arr[arr.length-1]) //变丑了,取倒数第一个
console.log(arr[arr.length-2]) //变丑了,倒数第二个
console.log(arr.at(1))
console.log(arr.at(-1))//取倒数第一个
console.log(arr.at(-2))//倒数第二个
8.4 ES13-正则匹配的开始和结束索引
let str = "今天是2022-11-10"
let reg = /(?<year>[0-9]{4})-(?<month>[0-9]{2})-(?<day>[0-9]{2})/d
//exec
let res = reg.exec(str)
console.log(res)
8.5 ES13-findLast()和findLastIndex()函数
let arr = [11,12,13,14,15]
// let res = arr.find(function(value){
// return value % 2 === 0
// })
// let res = arr.findIndex(function(value){
// return value % 2 === 0
// })
// let res = arr.findLast(function(value){
// return value % 2 === 0
// })
let res = arr.findLastIndex(function(value){
return value % 2 === 0
})
console.log(res)
8.6 Error对象的Cause属性
Error对象多了一个cause属性来指明错误出现的原因。这个属性可以帮助我们为错误添加更多的上下文信息,从而帮助使用者们更好地定位错误。
function getData(){
try{
console.log(kerwin)
}
catch(e){
throw new Error('New error 1111111',{cause:"这是因为,,,,,,,,,"});
}
}
try{
getData()
}catch(e){
console.log(e.cause)
}