ES6语法
阮一峰ES6入门教程学习笔记
主要是梳理知识点,具体实例分析可以看教程。
顺序可能有打乱,我是综合(教程+我认为更重要的章节)的顺序来的。
let & const命令
1. let命令
1.1 基本用法
let
声明的变量,只在let命令所在代码块内有效。
适用于for
循环计数器。for
循环的循环计数器语句部分和循环体内部是两个作用域。
1.2 不存在变量提升
var
存在变量提升,变量在声明前被访问,值为undefined
,而let
声明前访问会报错。
1.3 暂时性死区(TDZ, Temporal Dead Zone)
如果区块中存在let
和const
命令,这个区块对这些命令声明的变量,从一开始就形成了封闭作用域。凡是在声明之前就使用这些变量,包括使用typeof
,都会报错。
1.4 不允许重复声明
在相同作用域内,不能用let
重复声明同一个变量;所以不能在函数内部重新声明参数。
2. 块级作用域
2.1 为什么需要块级作用域
内层变量可能会覆盖外层变量for
循环计数器变量会泄露为全局变量
2.2 let为ES6新增块级作用域
- 块级作用域可以嵌套
- 内层作用域可以定义外层作用域的同名变量。
这和上面说的“不能在函数重新声明参数”不同,后者是因为参数是在函数体作用域的,如果再声明,就是在同一作用域内重复声明,是不允许的,而这里是不同层次的作用域,不是重复声明,而是覆盖。 - IIFE可以使用块级作用域代替
2.3 关于在块级作用域内声明函数
考虑到环境导致的行为差异太大,应该避免在块级作用域内声明函数。如果确实需要,也应该写成函数表达式,而不是函数声明语句。且块级作用域必须有大括号。2.4 do表达式
在块级作用域前加上do
关键字,可以1234let x = do {let t = f();t * t + 1;};
3. const命令
3.1 基本用法
与let
相同,除了const
用于声明常量,声明时必须初始化,不能只声明不赋值。
3.2 本质
const
实际上是保证变量指向的那个内存地址不能改动。如果声明的变量是个对象或数组,可以对该对象进行添加属性等操作,但不能将它指向另一个新的对象。
如果想让对象的属性也不能改变,可以将对象彻底冻结:
3.3 ES6声明变量有六种方法:
var, function, let, const, import, class
前两个命令声明的全局变量,依旧是顶层对象(window, global
)的属性;let, const, class
声明的变量不属于顶层对象的属性。
5. global对象
为了在浏览器,Node,Web Worker中都能获取顶层对象,引入垫片库system.global
虽然我目前还不知道为什么需要获取全局对象。。
变量的解构赋值
destructuring 模式匹配-解构失败则赋值为undefined
1. 数组解构赋值
|
|
解构赋值可以指定默认值,只有在相应位置的值===undefined时,才会使用默认值。
- 如果赋值语句以大括号开头,必须用圆括号包裹起来,否则大括号放在行首会被解析器当做代码块而不是赋值语句。
- 对象也可以嵌套解构赋值,但要注意模式不会被赋值,变量才会。
- 数组是特殊的对象,也可以使用对象解构的方法解构赋值。
3. 字符串解构赋值
字符串会被转换成类数组对象,有length
属性
4. 数值和布尔值的解构赋值
解构赋值的右边如果不是对象或数组都会先转为对象。undefined
和null
无法转成对象,解构赋值写在右边会报错。
5. 函数参数的解构赋值
注意以下两种情况的区别:
6. 圆括号问题
能不用就不用。能用的情况:赋值语句的非模式部分
7. 解构赋值的用途
- 交换变量的值
- 从函数中返回多个值
- 函数传参对应
- 提取json数据
- 函数参数的默认值
遍历Map结构:
for...of
遍历123456789101112// 获取键名for (let [key] of map) {// ...}// 获取键值for (let [,value] of map) {// ...}// 获取键值对for (let [key, value] of map) {console.log(key + " is " + value);}输入模块的指定方法
const { SourceMapConsumer, SourceNode } = require("source-map");
字符串的扩展
1. 字符串的Unicode表示法
\u{xxxx}
即使xxxx超过范围(\u0000~\uFFFF
),也可以正确解读。以前要用两个双字节(32bit)的形式表示超出范围的字符,否则会解读成两个字符。
2. codePointAt(index) 对应于charAt(index)
- 该方法定义在字符串的实例对象上
- 将字符转成码点
- js内部字符以UTF-16格式储存,每个字符固定为2字节(双字节,16bit)。对于需要两个双字节储存的字符来说,js会认为它们是两个字符:
length
为误判为2;用charAt
和charCodeAt
都无法读取整个字符,而是会分别返回前两个字节和后两个字节。 - ES6解决办法:
codePointAt()
会正确返回32bit的字符的码点,默认以十进制值表示,如果要十六进制的值,可以用toString
转换。 遍历含有32bit字符的字符串时,为避免字符串索引下标问题(如果用下标访问字符,还是要把32bit字符当做占了2个下标来看待),应该用
for...of
循环,它会正确识别32bit的UTF-16字符:123456var s = '𠮷a';for (let ch of s) {console.log(ch.codePointAt(0).toString(16));}// 20bb7// 61利用
codePointAt()
测试一个字符是16bit还是32bit123function is32Bit(c) {return c.codePointAt(0) > 0xFFFF;}
2. String.fromCodePoint(codePoint) 对应于fromCharCode(codePoint)
- 该方法定义在String上
- 将码点转化为字符串,可传入多个码点
- 可以识别大于
0xFFFF
的字符
3. at(index) 对应于charAt(index)
- 获取给定位置的字符
- 可以识别大于
0xFFFF
的字符
4. normalize()
Unicode正规化
5. includes(), startsWith(), endsWith()
- 第一个参数为源字符串,第二个参数可选,表示开始搜索的位置
- 均返回布尔值,表示是否找到/是否在源字符串的开头/末尾
6. repeat()
返回一个新字符串,表示将原字符串重复n次。参数如果是小数,会被取整;如果是字符串,则会先转换成数字。
7. padStart(), padEnd()
- 字符串长度补全,头部/尾部补全。
- 原字符串长度大于第一个参数:返回原字符串。
- 原字符串与补全串长度之和大于第一个参数,截去超出位数的补全字符串。
- 第二个参数省略则默认为空格
padStart()
常见用途:为数值补全指定位数;提示字符串格式12'123456'.padStart(10, '0') // "0000123456"'12'.padStart(10, 'YYYY-MM-DD') // "YYYY-MM-12"
8. 模板字符串
- 用反引号标识,可以包含html标签
- 可以用
${}
嵌入变量,表达式运算,访问对象属性,函数调用。 - 如果大括号内不是字符串,比如对象,会调用其
toString()
- 如果大括号内变量未定义,会报错。
- 模板字符串可嵌套
引用模板字符串本身
123456789// 写法一:用new Function('param', str)let str = 'return ' + '`Hello ${name}!`';let func = new Function('name', str);func('Jack') // "Hello Jack!"// 写法二:用eval.call(null, str)let str = '(name) => `Hello ${name}!`';let func = eval.call(null, str);func('Jack') // "Hello Jack!"模板编译实例:正则表达式匹配-将template string转换成含
echo
的js表达式字符串;将转换后的template string放到一个用模板字符串写的函数里,作为编译函数compile(template)
的返回值script
;调用该编译函数。- 标签模板:函数调用的特殊形式。标签-函数,模板-参数。
若模板字符串里有多个参数,则相当于传参:第一个参数为数组,元素是没有变量替换的部分,后面的参数依次为变量替换后的值。123456789101112131415161718192021222324252627282930313233343536373839404142//如何将各个参数按照原来的位置合并回去var total = 30;var msg = passthru `The total is ${total} (${total*1.05} with tax)`;function passthru(literals) {// console.log("~~~~literals: ~~~~", literals);// console.log("~~~~arguments:~~~~", arguments);var result = '';var i = 0;while (i < literals.length) { //3// console.log("----literals[", i, "]----", literals[i]);result += literals[i++];if (i < arguments.length) { //3// console.log("====arguments[", i, "]====", arguments[i]);result += arguments[i];}}return result;}console.log(msg); // "The total is 30 (31.5 with tax)"//采用rest参数的写法:function passthru(literals, ...values) {var output = "";for (var index = 0; index < values.length; index++) {output += literals[index] + values[index];}output += literals[index]return output;}
-(敲黑板!)标签模板的重要应用:过滤HTML字符串,将特殊字符如<``>
转义,防止用户输入恶意内容,注入不安全代码。
- React的jsx语法其实就用了模板字符串,定义jsx函数,将DOM字符串转成React对象。
- 模板处理函数的第一个参数是模板字符串,其实是模板字符串数组,该数组的第一个元素是字符串本身,第二个元素是raw属性,保存转义后的原字符串。
String.raw()
数组的扩展
1. Array.from()
- 将类数组对象(如arguments, DOM操作返回的NodeList)转成真正的数组;
ES5:[].slice.call(arraylike);
ES6:Array.from(arraylike);
扩展运算符...
也可以实现此功能:本质上还是调用Iterator接口。var arr = [...arguments];
var arr = [...document.querySelectorAll('div')];
- 将可遍历对象(部署了Iterator接口的数据结构,如ES6新增数据结构Set和Map)转成真正的数组。
- 可接收第二个参数:接收一个函数,类似于数组的
map
方法。 - 如果
map
函数用到了this
,还可以传入第三个参数用来绑定this
- 看一个灵活的用法:用第一个参数指定第二个参数的运行次数
Array.from({ length: 2 }, () => 'jack'); // ['jack', 'jack']
2.Array.of()
- 这个函数的出现是为了弥补构造函数
Array()
的不足:它的行为很统一,总是返回一个由参数作为元素的数组。而构造函数只有在参数不少于2个时才返回数组,参数只有一个时只会指定新数组的长度。 - 模拟:123function ArrayOf(){return [].slice.call(arguments);}
下面介绍的都是数组实例方法,即:用数组实例arr调用,或Array.prototype.
调用
3. 数组实例方法copyWithin()
copyWithin(target, start = 0, end = this.length);
- 在当前数组内部,将指定位置的成员复制到其他位置(会覆盖原有成员),然后返回当前数组。数组会被修改。
- 如果负数,倒数,-1对应最后一个元素。
- 这里的例子出现了
new Int32Array()
,js对象的32 位整数值的类型化数组。
4. 数组实例方法find()
和findIndex()
- 参数均为回调函数,
find()
找出第一个符合参数函数条件的数组成员,没有则返回undefined
;findIndex()
找出相应的位置,没有则返回-1。 - 回调函数的参数:当前遍历的value, 当前遍历位置index, 原数组arr。
- 均可以接受第二个参数,用来绑定回调函数的
this
对象。 - 均可以发现
NaN
,弥补了indexOf()
的不足12[NaN].indexOf(NaN); // -1[NaN].findIndex(y => Object.is(NaN, y)); // 0
5. 数组实例方法fill()
- 适用于填充空数组。非空数组原有元素会被替代。
fill(value, start, end)
,可指定始末位置。
6. 数组实例方法entries(), keys(), values()
- 用于遍历数组:键值对、键、值。
- 配合
for...of
或arr.entries().next()
遍历1234567891011for (let [index, elem] of ['a', 'b'].entries()) {console.log(index, elem);}// 0 "a"// 1 "b"let letter = ['a', 'b', 'c'];let entries = letter.entries();console.log(entries.next()); //{ value: [ 0, 'a' ], done: false }console.log(entries.next()); //{ value: [ 1, 'b' ], done: false }console.log(entries.next()); //{ value: [ 2, 'c' ], done: false }console.log(entries.next()); //{ value: undefined, done: true }
7. 数组实例方法includes()
- 是对ES5数组方法
indexOf()
的弥补。 - 检测数组中是否包含目标元素,可以检测出
NaN
- 第二个参数可选,搜索的起始位置。
- Map和Set数据结构有一个
has
方法,需要注意与includes
区分。
Map结构的has方法,是用来查找键名的,比如Map.prototype.has(key)
、WeakMap.prototype.has(key)
、Reflect.has(target, propertyKey)
。
Set结构的has
方法,是用来查找值的,比如Set.prototype.has(value)
、WeakSet.prototype.has(value)
。
8. 数组的空位
- ES5对空位的处理很不统一, ES6明确将空位转化成
undefined
Array.from()
- 扩展运算符
...
copyWithin(), fill(), for...of, entries(), keys(), values(), find(), findIndex()
- 尽量避免出现空位
函数的扩展
1. 函数的默认值
- 允许为参数赋默认值,可以是具体值,也可以是表达式-惰性求值
- 不能在函数体内对参数变量再次声明
与解构赋值结合使用。看这两种写法的区别:
12345678910111213141516171819202122232425// 写法一:函数参数的默认值是空对象,但是设置了对象解构赋值的默认值function m1({x = 0, y = 0} = {}) {return [x, y];}// 写法二:函数参数的默认值是一个有具体属性的对象,但是没有设置对象解构赋值的默认值function m2({x, y} = { x: 0, y: 0 }) {return [x, y];}// 函数没有参数的情况m1() // [0, 0]m2() // [0, 0]// x和y都有值的情况m1({x: 3, y: 8}) // [3, 8]m2({x: 3, y: 8}) // [3, 8]// x有值,y无值的情况m1({x: 3}) // [3, 0]m2({x: 3}) // [3, undefined]// x和y都无值的情况m1({}) // [0, 0];m2({}) // [undefined, undefined]定义了 默认值的参数尽量作为尾参数
- 指定了默认值后函数的
length
属性将返回没有指定默认值的参数个数length
属性是预期传入的参数个数- rest参数也不会计入
length
属性 - !!!如果设置了默认值的参数不是尾参数,那么length属性也不再计入后面的参数了
- 作用域
一旦设置了参数的默认值,函数进行声明初始化时,参数会形成一个单独的作用域。等到初始化结束,这个作用域就会消失。这种语法行为,在不设置参数默认值时,是不会出现的。 - 应用:利用参数默认值,可以指定某一个参数不得省略,如果省略就抛出一个错误。12345678910function throwIfMissing() {throw new Error('Missing parameter');}function foo(mustBeProvided = throwIfMissing()) {return mustBeProvided;}foo()// Error: Missing parameter
2. rest参数
- 代替
arguments
对象,获取函数的多余参数。rest 参数搭配的变量是一个数组,该变量将多余的参数放入数组中。 - rest参数之后不能再有其他参数。
- 函数的
length
属性不包括rest参数
3. 扩展运算符
主要用于函数传参,把数组或类数组对象展开成一系列用逗号隔开的参数序列。
12345678910111213141516var foo = function(a, b, c) {console.log(a);console.log(b);console.log(c);}var arr = [1, 2, 3];//传统写法foo(arr[0], arr[1], arr[2]);//使用扩展运算符foo(...arr);//1//2//3替代数组的
apply
方法123456789101112var arr1 = [0, 1, 2];var arr2 = [3, 4, 5];// ES5的写法Array.prototype.push.apply(arr1, arr2);// ES6的写法arr1.push(...arr2);//ES5写法中,push方法的参数不能是数组,所以只好通过apply方法变通使用push方法。有了扩展运算符,就可以直接将数组传入push方法。// ES5的写法Math.max.apply(null, arr1);// ES6的写法Math.max(...arr1);应用:
- 合并数组,替代
concat
方法。 - 与解构赋值结合,生成新数组,扩展运算符要放在最后一位。
- 函数返回值为多个,要作为另一个函数的参数时,可用扩展运算符处理返回值然后作为传参。
将字符串转为数组:能正确识别32位Unicode字符。包括长度、
reverse
反转字符串时的32位Unicode字符问题。1234567let str = 'x\uD83D\uDE80y';str.split('').reverse().join('')// 'y\uDE80\uD83Dx' 这个结果不是我们想要的,中间应该是一个字符整体。[...str].reverse().join('')// 'y\uD83D\uDE80x'任何部署了Iterator接口的对象(Map, Set, Generator函数),都可以用扩展运算符转为真正的数组。如果要将没有部署Iterator接口的类数组对象转为真正的数组:用
Array.from()
方法。
- 合并数组,替代
4. 严格模式
只要函数参数使用了默认值、解构赋值、或者扩展运算符,那么函数内部就不能显式设定为严格模式。
是为了规避ES5中函数参数部分先于函数体部分执行可能带来的问题:ES5中,若函数体中有规定严格模式而参数执行不符合严格模式,则会先执行参数部分然后才报错。
5. name属性
作为函数的属性,体现函数的名称。若将一个匿名函数赋给一个变量,则ES6中name为该变量名,ES5中name为空字符串。
6. 箭头函数
- ES6中大括号开头会被解释为代码块,所以如果直接返回一个对象,要在对象外面加上小括号。
- 函数体内的
this
对象,指向定义时所在对象,而不是运行时所在对象。
箭头函数没有自己的this
,它引用外层代码块的this
。也不能用bind
,call
,apply
改变this
的指向。除了this
,arguments
,super
,new.target
这三个变量在箭头函数中也不存在,指向外层函数的相应变量。 - 不能当作构造函数,不能使用
new
,因为箭头函数没有自己的this
- 不能使用
arguments
对象,可以用rest参数代替。 - 不能使用
yield
命令,所以不能当作Generator函数。 - 嵌套的箭头函数
部署管道机制:前一个函数的输出是后一个函数的输入
改写lamda演算。
7. 绑定this
双冒号,ES7提案,Babel已支持。左边是对象,右边是函数,将对象绑定在函数上面。
如果双冒号左边为空,右边是一个对象的方法,则等于将该方法绑定在该对象上面。
双冒号返回的是原对象,可以采用链式写法。
8. 尾调用优化 (*好难…)
- 什么是尾调用:函数的最后一步是执行另一个函数,不能有附加的调用后的其他操作。
-如何优化:只保留内层函数的调用帧。因为函数嵌套调用会形成调用帧,调用栈,会在内存中保存外层函数的信息及内层函数的位置信息等,如果是尾调用,并且不再需要外层函数的内部变量,则只需要保存内层函数就行了,可以删除外层函数的调用帧。(改写了函数的调用栈) - 尾递归:不会发生栈溢出,节省内存。
- 递归函数的改写:确保最后一步只调用自身:把所有用到的内部变量改写成函数的参数。
- 在尾递归函数之外,再提供一个正常行驶的函数。
- curry化
- 使用ES6的函数参数默认值
- 递归本质是循环。函数式编程语言没有循环操作的命令,都是用递归实现循环操作,尾递归对这些语言很重要。
- ES6 一旦使用递归,最好使用尾递归。
- 尾调用模式只在严格模式下生效。
- 自己实现尾递归优化:将递归换成循环。
- 蹦床函数 + 将递归函数改写成每一步返回另一个函数
9. 尾逗号
ES2017允许函数的最后一个参数有逗号,方便添加、修改参数。
对象的扩展
1. 属性的简洁表示法
- 在对象中直接写变量:属性名就是变量名,属性值就是变量的值;
- 方法也可以简写
- 这种写法用于函数的返回值为对象时很方便
- CommonJS输出变量,属性的赋值器和取值器 都适用这种写法
- 属性名总是会解析成字符串
2. 属性名表达式
- 用字面量定义对象时,可以用
[表达式]
作为属性名、方法名 - 属性名表达式和简洁表示法不能同时使用,一般用属性名表达式作为属性名之后,要写上属性值
- 属性名表达式如果是个对象,会自动将对象转为字符串作为属性名,所以如果有多个对象作为属性名,最后只会有最后那个对象转字符串作为属性名,发生覆盖。
3. 方法的name属性
- 返回方法名
如果对象的方法使用了取值函数(getter)和存值函数(setter),则name属性不是在该方法上面,而是该方法的属性的描述对象的get和set属性上面,返回值是方法名前加上get和set。
123456789101112const obj = {get foo() {},set foo(x) {}};obj.foo.name// TypeError: Cannot read property 'name' of undefinedconst descriptor = Object.getOwnPropertyDescriptor(obj, 'foo');descriptor.get.name // "get foo"descriptor.set.name // "set foo"bind
方法创造的函数,name属性返回bound加上原函数的名字;Function
构造函数创造的函数,name属性返回anonymous。- 如果对象的方法名是一个 Symbol 值,那么name属性返回的是这个 Symbol 值的描述。
4. Object.is()
与ES5严格相等
===
行为基本一致,但弥补了+0
应该不等于-0
和NaN
应该等于自身。12345+0 === -0 //trueNaN === NaN // falseObject.is(+0, -0) // falseObject.is(NaN, NaN) // true如何在ES5中手动部署这个方法:
12345678910111213Object.defineProperty(Object, 'is', {value: function(x, y) {if (x === y) {// 针对+0 不等于 -0的情况return x !== 0 || 1 / x === 1 / y;}// 针对NaN的情况return x !== x && y !== y;},configurable: true,enumerable: false,writable: true});
5. Object.assign(target, source1, source2)
- 对象的合并:将源对象(可多个)的所有可枚举属性赋值到目标对象(1个)
不在首参数的字符串会以字符数组的形式拷贝进目标对象。其他类型值如数值、布尔值,都没有任何效果,因为只有字符串的包装对象会产生可枚举属性。
1234Object(true) // {[[PrimitiveValue]]: true}Object(10) // {[[PrimitiveValue]]: 10}Object('abc') // {0: "a", 1: "b", 2: "c", length: 3, [[PrimitiveValue]]: "abc"}//PrimitiveValue是包装对象的内部属性,这个值不会被Object.assign拷贝不拷贝继承属性和不可枚举的属性
- 是浅拷贝
- 同名属性会被替换
会把数组视为对象
12Object.assign([1, 2, 3], [4, 5])// [4, 5, 3]用处:
- 为对象添加属性、方法
克隆对象
1234567function clone(origin) { //只能克隆origin自身的值,不能克隆其继承值return Object.assign({}, origin);}function clone(origin) { //保持了继承链,连同其继承的值一并克隆let originProto = Object.getPrototypeOf(origin);return Object.assign(Object.create(originProto), origin);}合并多个对象
- 为属性指定默认值12345678910const DEFAULTS = { //其中的属性最好不要是对象,因为是浅拷贝,可能会被options对象全部覆盖,达不到预期的只改变某几个参数的效果logLevel: 0,outputFormat: 'html'};function processContent(options) {options = Object.assign({}, DEFAULTS, options); //options是用户提供的参数,会覆盖DEFAULTS中的同名参数。console.log(options);// ...}
6. 属性的可枚举性
Object.getOwnPropertyDescriptor
12345678let obj = { foo: 123 };Object.getOwnPropertyDescriptor(obj, 'foo')// {// value: 123,// writable: true,// enumerable: true,// configurable: true// }ES5中只处理可枚举属性的方法:
for...in
继承的和自身的。toString
和length
属性都是不可枚举的所以可以不被遍历到。尽量不用这个来遍历而用下面的。Object.keys()
自身的JSON.stringify()
自身的
7. 属性的遍历
for…in 自身+继承,可枚举的,无Symbol属性
Object.keys(obj) 同上,除了继承
Object.getOwnPropertyNames(obj) 自身,可枚举+不可枚举,无Symbol属性
Object.getOwnPropertySymbols(obj) 自身的Symbol属性
Reflect.ownKeys(obj) 自身,可枚举+不可枚举,含Symbol
8. 设置/读取原型对象
Object.setPrototypeOf(obj, proto);
Object.getPrototypeOf()
9.Object.keys()遍历补充
都是遍历自身的,可枚举的,无Symbol属性Object.keys()
Object.values()
Object.entries()
10. 对象的扩展运算符
- 解构赋值:
- 等号右边得是对象
- 是浅拷贝
- 只会拷贝对象自身的属性,不会拷贝继承的属性
- 扩展运算符用于解构赋值要作为最后一个参数,否则报错。
- 扩展运算符
11. Object.getOwnPropertyDescriptor
- 配合
Object.create()
实现浅拷贝12const clone = Object.create(Object.getPrototypeOf(obj),Object.getOwnPropertyDescriptors(obj));
12. Null传导运算符
- 简化赋值运算前的层层判断
const firstName = message?.body?.user?.firstName || 'default';
- 传导运算符的四种用法:
Obj?.prop
读取对象属性Obj?.[expr]
同上func?.(...args)
函数的调用new C?.[...args]
构造函数的调用
Set 和 Map 数据结构
1. Set
- 两个
NaN
是相等,两个对象不相等 new Set()
接收参数可以为:数组或类数组对象- Set实例的方法:
Set.prototype.constructor
,Set.prototype.size
,add(value)返回Set本身,所以可以链式调用这个方法
,delete(value)
has(value)
clear()
Array.from(set)
将set转为数组- 数组去重:
[...new Set(array)]
或Array.from(new Set(array))
遍历操作:
keys()
,values()
,entries()
,可以直接用for...of
遍历12for(let x of set){}set.forEach((value, key) => {}) //注意是先写value后写key扩展运算符,
map
,filter
方法都可以用于Set集合的交集、并集、差集的实现
123let union = new Set([...a, ...b]); //并集let intersect = new Set([...a].filter(x => b.has(x))); //交集let difference = new Set([...a].filter(x => !b.has(x))); //差集遍历的同时向改变Set元素的值:
map
或Array.from
123456// 方法一let set = new Set([1, 2, 3]);set = new Set([...set].map(val => val * 2));// 方法二set = new Set(Array.from(set, val => val * 2));// set的值是2, 4, 6
2. WeakSet
(1)与Set的两点不同:
- 成员只能是对象
- 成员对象都是弱引用。如果其他对象都不再引用该对象,那么垃圾回收机制会自动回收该对象所占用的内存,不考虑该对象是否还存在于 WeakSet 之中。
WeakSet 里面的引用,都不计入垃圾回收机制,所以就不存在忘记取消引用而导致内存无法释放、内存泄漏的问题。因此,WeakSet 适合临时存放一组对象(比如储存 DOM 节点,不用担心这些节点从文档移除时,会引发内存泄漏),以及存放跟对象绑定的信息。只要这些对象在外部消失,它在 WeakMap 里面的引用就会自动消失。
WeakSet 内部有多少个成员,取决于垃圾回收机制有没有运行,运行前后很可能成员个数是不一样的,而垃圾回收机制何时运行是不可预测的,因此 ES6 规定 WeakSet 不可遍历。
(2)构造函数的对象为数组或类数组对象,且其中元素必须为对象。
3. Map
- 是对Object Hash结构的弥补:Object结构:“键-值”对应,Map结构:“值-值”对应
- 构造函数接收数组(元素为键值对形式的数组)作为参数
- 同一个键多次赋值,会覆盖
- 同一个键的定义:要为同一个对象(内存地址相同)
- 实例方法、遍历方法类似于Set结构:
set(key, value)
可链式调用
- Map与数组、对象、JSON的互相转换
4. WeakMap
(1) 只接受对象作为键名
(2) 键名所指向的对象不计入垃圾回收机制
如果你要往对象上添加数据,又不想干扰垃圾回收机制,就可以使用 WeakMap。在网页的 DOM 元素上添加数据,就可以使用WeakMap结构。当该 DOM 元素被清除,其所对应的WeakMap记录就会自动被移除。
(3)应用:都是为了避免内存泄漏
- DOM元素作为键,值可以为要绑定的监听事件handler,或该DOM元素对应的属性值。
- 部署class的私有属性
Promise对象
1. 含义
- 是个容器,保存异步操作
- 是个对象,有统一的API处理异步操作,获取异步操作的消息
- 对象有三种状态,除了异步操作的结果,没有别的方式能改变状态
- 一旦状态改变,就不会再变,再给Promise对象添加回调函数也会立即得到结果
- 缺点:无法取消Promise;Promise内部抛出的错误要反映到外部必须通过设置回调函数,麻烦;Pending状态不明确,无法表明是刚开始还是快完成状态。
- Stream模式:适用于某些事件不断地反复发生的情况
2. 用法
创造实例:构造函数参数为一个函数
123456789var promise = new Promise(function(resolve, reject) { //resolve和reject两个参数是js引擎提供的两个函数,不用自己部署// ... some codeif (/* 异步操作成功 */){resolve(value); //resolve函数作用:将Promise实例的状态变为Resolved,并将异步操作成功执行的结果作为参数value传出去} else {reject(error);}});用
then
指定两种状态的回调函数,两个参数分别为两个函数,第二个可选12345promise.then(function(value) { //resolve函数执行后,要在当前所有同步任务执行完之后才会执行这个回调函数,value是resolve函数传来的// success}, function(error) {// failure});
实例:
- 当一个Promise的resolve参数是另一个Promise时,后者的状态会决定前者的状态,即,前者的状态无效,前者的then函数相当于是后者的。123456789101112var p1 = new Promise(function (resolve, reject) {setTimeout(() => reject(new Error('fail')), 3000)})var p2 = new Promise(function (resolve, reject) {setTimeout(() => resolve(p1), 1000) //p1的状态决定了p2的状态})p2 //p2的语句都是针对p1的.then(result => console.log(result)).catch(error => console.log(error))// Error: fail
3. Promise.prototype.then()
- 定义在原型上,是Promise实例方法
- 返回的是一个新的Promise实例 => 可以链式调用另一个then方法
4. Promise.prototype.catch()
- 捕获Promise对象中的异步操作抛出的错误和then方法指定的回调函数抛出的错误
- 错误具有“冒泡”性质,会一直向后传递直到被捕获为止
- 一般来说,不要在then方法里面定义Reject状态的回调函数(即then的第二个参数),总是使用catch方法。
- 返回的还是一个新的Promise对象,可以调用then方法。若是这个then方法里报错就与前面的catch无关了。
- catch里面可以再抛出错误
5.Promise.all()
- 接收数组参数,将多个Promise实例包装成一个新的Promise实例
- 所有Promise状态都变成resolved,新的Promise状态才为resolved
- 有一个Promise状态变为rejected,新的Promise状态就为rejected
6. Promise.race()
- 接收数组参数,将多个Promise实例包装成一个新的Promise实例
- 只要有一个Promise状态改变了,新的Promise状态就跟着改变
7. Promise.resolve(), Promise.reject()
- 将现有对象转为Promise对象
- 四种参数:
- Promise实例:返回原对象
- thenable对象:(具有then方法的对象),转为Promise对象后立即执行其then方法
- 不是对象或不具有then方法的对象:返回状态为resolved/rejected的Promise对象
- 无参数:同第三种
event loop: 立即resolve的Promise对象,是在本轮“事件循环”(event loop)的结束时,而不是在下一轮“事件循环”的开始时。
12345678910111213setTimeout(function () { //在下一轮“事件循环”开始时执行console.log('three');}, 0);Promise.resolve().then(function () { //在本轮“事件循环”结束时执行console.log('two');});console.log('one'); //立即执行// one// two// three对比
1234567891011121314151617181920212223let thenable = {then: function(resolve, reject) {resolve(42);}};let p1 = Promise.resolve(thenable);p1.then(function(value) {console.log(value); // 42,这里的值是thenable对象then函数中的value});//----------------------------------const thenable = {then(resolve, reject) {reject('出错了');}};Promise.reject(thenable).catch(e => {console.log(e === thenable) // true, 这里e是thenable对象,而不是then函数中的字符串'出错了'});
8. 两个有用的附加方法
- done(): 在回调链尾端,保证抛出任何可能出现的错误
- finally(): 无论Promise对象最后状态如何,都会执行其参数回调函数。
9. 应用
加载图片:一旦加载完成,状态就变化
12345678const preloadImage = function (path) {return new Promise(function (resolve, reject) {var image = new Image();image.onload = resolve;image.onerror = reject;image.src = path;});};Generator函数与Promise的结合
10. Promise.try()
模拟try代码块,无论函数是否为异步操作,都用promise处理,用then方法指定下一步操作。
Class
1. 基本语法
1.1 概述
- ES6的语法糖,为了让构造对象的方式更像面向对象编程的语法
- class中的
constructor
函数对应ES5中的构造函数 - class的数据类型其实就是function,类本身就指向构造函数。如下Point是class定义的“类”:
Point === Point.prototype.constructor; //true
- class中的函数,不写
function
关键字,用ES6简洁写法。各函数之间不用逗号 - class中定义的方法其实都定义在类的
prototype
上。调用实例的方法也就是调用类的原型上的方法。如下,b是通过new
创建的类B”的实例:b.constructor === B.prototype.constructor; //true
- 可以用
Object.assign(Point.prototype, {//funcions...})
一次性向类添加多个方法。 - 类内部定义的所有方法都是不可枚举的
- 类的属性名、方法名,可以用方括号表达式表示
1.2 constructor方法
- 类的默认方法。如果没有显示定义会自动添加一个空的。
可以指定返回全新的对象
Object.create(null)
,这样instanceof
运算符的结果就不是那个类了12345678class Foo {constructor() {return Object.create(null);}}new Foo() instanceof Foo// false只有用
new
才能调用构造函数,不同于ES5构造函数可以直接执行不用new
1.3 不存在变量提升
- ES6不会把class的声明提升到代码头部
- 必须先定义class再使用(包括new和被继承)
- 与继承有关,必须保证子类在父类之后定义
1.4 class表达式
- 如果用类表达式,而不是类声明,要注意类的最终名称是类表达式的变量名而不是class关键字后面的类名,后者只在类内部能使用,且可省略。
name
属性总是返回紧跟class
关键字后面的名字- 立即执行类 -> 立即执行函数
1.5 私有方法的实现
- 体现在命名上:方法名前加下划线
_
,但是这样仍能被外部访问到 - 将方法移出class,在class内使用
func.call(this,args)
来调用func
- 利用
Symbol
值唯一性,且只能在class内部取到。将方法名用方括号内的Symbol
值表示。
1.6 this的指向
- class中的方法中的
this
默认指向类的实例,在class外面使用该方法可能会报错,因为this
在外面指向该方法运行时的环境,这个同ES5,解决办法:
(1)在构造函数中绑定this
(2)在构造函数中使用箭头函数
(3)Proxy(暂时没看)
1.7 严格模式
- 类和模块内部,默认是严格模式
- ES6其实把整个语言升级到了严格模式
2 class的继承
2.1 基本用法
extends
关键字- 子类必须在constructor方法中调用super方法,否则新建实例时会报错。
这是因为子类没有自己的this对象,而是继承父类的this对象,然后对其进行加工。如果不调用super方法,子类就得不到this对象。
ES5的继承,实质是先创造子类的实例对象this,然后再将父类的方法添加到this上面(Parent.apply(this)
)。ES6的继承机制完全不同,实质是先创造父类的实例对象this(所以必须先调用super方法),然后再用子类的构造函数修改this。 - 子类的
constructor
中,必须先调用super
才能使用this
- 子类的实例,用
instanceof
运算符可验证其既是子类的实例,又是父类的实例
2.2 类的prototype属性和proto属性
- 两条继承链:12345678class A {}class B extends A {}B.__proto__ === A // true 构造函数的继承B.prototype.__proto__ === A.prototype // true 方法的继承
理解:
作为一个对象:子类B的原型__proto__
是父类A;
作为一个构造函数:子类的原型prototype
是父类的实例A.prototype
- 类继承的实质:利用
Object.setPrototypeOf(child, parent)
,而该函数的实现是用__proto__
2.3 extends 后面的三种特殊情况
class A extends Object
12A.__proto__ === Object; //trueA.prototype.__proto__ === Object.prototype; //true不写继承,则A是一个普通函数
12A.__proto__ === Function.prototype; //trueA.prototype.__proto__ === Object.prototype; //trueclass A extends null
12A.__proto__ === Function.prototype; //trueA.prototype.__proto__ === undefined; //true
2.4 Object.getPrototypeOf()
从子类获取父类。可用来判断一个子类是否继承了另一个父类。Object.getPrototypeOf(ColorPoint) === Point;
2.5 super关键字
- 作为函数调用:代表父类的构造函数,子类的构造函数中必须执行一次,并且只能用于子类的构造函数中。虽然执行的是父类的构造函数,但是返回的是子类的实例(可以用
new.target.name
验证,它永远指向当前正在执行的函数)。相当于Parent.prototype.constructor.call(this);
- 作为对象使用:指向父类的原型对象
Parent.prototype
,注意,是原型对象而不是实例。所以定义在父类实例上的方法和属性就不能通过super
对象取到了。 作为对象使用,通过super调用父类的方法时,super会绑定子类的this。所以如果通过super对某个属性赋值,这时super就是this,赋值的属性会变成子类实例的属性。但是通过super去读取这个赋值的属性时,super相当于父类的原型对象。
1234567891011121314151617class A {constructor() {this.x = 1;}}class B extends A {constructor() {super();this.x = 2;super.x = 3;console.log(super.x); // undefined,相当于A.prototype.xconsole.log(this.x); // 3}}let b = new B();作为对象使用,如果用在静态方法之中,这时super将指向父类,而不是父类的原型对象。
- 作为对象使用,super在静态方法之中指向父类,在普通方法之中指向父类的原型对象
2.6 实例的proto属性
子类实例的原型是父类实例,子类实例的原型的原型是父类实例的原型。p2.__proto__.__proto__ === p1.__proto__;
3. 原生构造函数的继承
- ES5不能正确继承原生构造函数,ES6可以。=> ES6可以创建原生数据结构的子类,自定制数据结构。
- ES5是先新建子类的实例对象this,再将父类的属性添加到子类上,由于父类的内部属性无法获取,导致无法继承原生的构造函数。
- ES6先新建父类的实例对象this,然后再用子类的构造函数修饰this,使得父类的所有行为都可以继承。
4. class的getter和setter函数
|
|
5. class的static方法
- 不会被实例继承,而是直接通过类来调用。可以直接通过
类名.方法名
访问。 - 可以被子类继承
- 可以在子类中通过
super.方法名
调用
6. class的静态属性和实例属性
定义在实例上的属性:用this.prop
定义在类上的属性为静态属性:class.prop
,ES6规定要写在class外部,因为class内部只有静态方法,没有静态属性。但是ES7提案可以写在class内部,加上static
关键字即可,并且Babel也支持。
7. 类的私有属性
提案:#
属性前缀
8. new.target属性
- new命令的属性,用于构造函数中。
- 返回new命令作用于的构造函数。如果构造函数不是由new调用(比如通过
call
,apply
调用)的,则该属性为undefined
。如果是通过new调用的,返回相应的class名字或构造函数名。 - 子类构造函数中该属性指向子类。
9. Mixin模式
通过继承mix
类,将多个类合成一个类:
Module语法
1. 为什么要引入Module
- CommonJS和AMD,都是运行时确定模块之间的依赖关系。“运行时加载”:整体加载某个模块,加载其所有方法,生成一个对象,然后从该对象中读取所需的几个方法。运行时才获取该对象,无法在编译时做“静态优化”。
ES6提出的解决方案,更加静态化。 ES6编译时加载(静态加载),之加载需要的某几个方法,可以在编译时就加载完成,
1234// CommonJS模块let { stat, exists, readFile } = require('fs');// ES6模块import { stat, exists, readFile } from 'fs';ES6好处:
1)效率更高;
2)可以静态分析,拓宽js语法(宏Macro, 类型检验)
3)浏览器、服务器端都支持,不再需要UMD
4)将来浏览器新的API可通过模块格式提供,不再需要做成全局变量或navigator
对象的属性
5)不再需要对象作为命名空间(比如Math
对象),而是通过模块格式提供
2. 严格模式
ES6模块自动采用严格模式
3. export & import
var, function, class
都可以export
CommonJS 模块输出的是值的缓存,不存在动态更新。而ES6输出的值会动态更新。
export
和import
不能出现在块级作用域内,可以出现在模块顶层的任何位置。
import
的写法和export
基本相同。import { ... } from ''
后面的路径可以是相对或绝对的,可以省略.js
,如果配置文件中配置过的话。import * as circle from './circle'
模块的整体加载。加载的对象不可以修改其属性和方法,因为不允许运行时改变。import moduleA;
只执行模块,不引入任何变量。
import
有变量提升效果,在编译阶段(代码运行前)执行,静态执行,所以不能用表达式和变量。
import
语句是 Singleton 模式。如果多次加载同一个模块,对应的也是同一个对象。
4. export default命令
- 使用模块要先知道模块中输出了哪些东西,才好指定
import
具体引入哪些东西。 - 默认输出的函数,在
import
时不用大括号,可以为这个默认引入的函数指定任意名字调用。 - 该命令一个模块只能使用一次,因为一个模块只能指定一个默认输出,所以
import
才不用大括号。 - 后面不能跟变量赋值语句,可以跟变量。因为默认输出变量
default
,跟着变量则将该变量赋值给default
变量。 export
和import
结合- 具名接口和默认接口互相改变。
5. 跨模块常量
const
声明的常量只在当前模块有效,不能跨模块获取到。解决办法:
将不同模块放在同一目录下,再将这些模块引入到一个文件里,最后只需要引入这个文件即可。
6. 动态加载提案:import()
- 类似于Node的
require
,运行时加载。import()
是异步加载,require()
是同步加载。 - 返回Promise对象。
- 按需加载,条件加载,加载动态生成路径。
Module的加载实现
1. 浏览器加载
- 传统方法:
同步加载。浏览器的渲染引擎遇到<script>
标签就停下来,先执行脚本(如果是外部脚本,还要先下载),然后再继续渲染。
异步加载:defer
或async
:遇到<script>
先下载,暂不执行。前者要等到整个页面正常渲染结束,才会执行,(先渲染,后执行,多个有序);后者一旦下载完,渲染引擎就会中断渲染,执行这个脚本以后,再继续渲染(仅仅是下载不阻塞,但下载完就执行,多个不一定有序)。
- 浏览器加载ES6模块:
<script type="module" src="">
或<script type="module"></script>
,异步加载。默认渲染完再执行(同defer
),也可设置为async
2. ES6模块与CommonJS模块的差别
- CommonJS 模块输出的是一个值的拷贝(将加载执行后的值缓存了),ES6 模块输出的是值的引用(在静态分析时生成该值的只读(不可以重新赋值)引用,等脚本真正执行时才去模块里根据引用去取值)。所以ES6输出的会随模块内部相应值动态更新而CommonJS不会。
- CommonJS 模块是运行时加载,ES6 模块是编译时输出接口。
3. Node加载
- Node加载ES6模块和CommonJS模块是分开的,只要有
import
或export
就认为是ES6模块,否则就为CommonJS模块。 - ES6 模块之中,顶层的this指向undefined;CommonJS 模块的顶层this指向当前模块
加载规则:
12345678910111213141516import './foo';// 依次寻找// ./foo.js// ./foo/package.json// ./foo/index.jsimport 'baz';// 依次寻找// ./node_modules/baz.js// ./node_modules/baz/package.json// ./node_modules/baz/index.js// 寻找上一级目录// ../node_modules/baz.js// ../node_modules/baz/package.json// ../node_modules/baz/index.js// 再上一级目录交叉命令加载:
- 用
import
加载CommonJS模块:将CommonJS模块的module.exports
视作export default
。采用import...as bar from ''
的写法时,要通过bar.default
拿到模块的默认输出。加载的模块仍具有CommonJS特性(运行时加载,输出值的拷贝缓存机制),不允许大括号语法。 - 用
require
加载ES6模块:模块的所有输出值都为require
的赋值对象的属性。采用这种方式引入ES6模块也是缓存机制,不能动态更新值。
- 用
4. 循环加载
CommonJS:同步加载,按照代码顺序执行每个模块,加载时停在这里等它执行完毕才会执行后面的代码。每个模块只会加载一次,如果遇到循环加载,要看该模块有没有被执行过或正在执行中。
ES6:引入的是引用,后期可以根据引用去相应模块中取值。
5. 另外两种ES6转码器
ES6 module transpiler:npm包,es6-module-transpiler
SystemJS: 其实调用了Google的Traceur转码器。<scripts src="system.js">
标签引入该垫片库(polyfill),用System.import('')
,异步加载,返回一个Promise