[阮一峰ES6标准]学习笔记

let

  • let声明的变量只在代码块内有效
  • 不存在变量提升
  • let声明变量之前,该变量都是不可用的,称为暂时性死区
  • 相同作用域内不允许重复声明同一个变量

const

  • const声明一个只读的常量,一旦声明,常量的值不能改变.
  • const一旦声明变量,必须初始化,不赋值就会报错.
  • const保证的其实是保存变量的内存地址不得改动.对于简单数据类型,值就保存在变量指向的内存地址,等同于常量.对于复合类型的数据,变量指向的地址保存的是一个指针.
  • var和function声明的全局变量,是顶层对象的属性.而let,const,class声明的全局变量不再属于顶层对象.

变量的解构赋值

从数组和对象中提取值,对变量进行赋值,这就称为解构.

1
2
let [a,b,c] = [1,2,3];//a=1,b=2,c=3
let [a,b,c] = [1,2] //解构不成功变量值为undefined

如果等号右边不是数组(或不是可遍历的解构),会报错.

1
let [a] = false;//false不是可遍历解构,报错

解构赋值允许指定默认值

1
let [a,b=666] = [1]//a=1,b=666

对象也可以用于解构赋值

1
2
3
4
5
6
7
let {foo,bar} = {foo:'a',bar:'b'};//foo='a',bar='b'
//属性名相同才能取到值
let {foo,bar} = {foo:'a',baszz:'b'};//bar为undefined
//{foo}其实就是{foo:foo}的简写
let {foo:bar} = {foo:'a'}//bar='a',foo未定义
//对象解构赋值也可以指定默认值
let {x,y=3}={x:1}//x=1,y=3

字符串也可用于解构赋值,因此此时,字符串被转换为一个类似数组的对象.

1
let [a,b,c,d,e] = 'hello';//a='h',b='e'

函数的参数也可用于解构赋值.

1
2
3
4
function add([x,y=3]){
return x + y;
}
add([1,2]);

字符串的扩展

ES6为字符串添加了便利器接口,使得字符串可以被for…of循环遍历.

ES5有indexOf用来确定一个字符串是否在另一个字符串中.

ES6又提供了三种方法:

  • includes():返回布尔值,表示是否找到了参数字符串
  • startsWith():返回布尔值,表示是否在原字符串的头部
  • endsWith():返回布尔值,表示是否在原字符串的尾部

这三个方法都提供了第二个参数,表示搜索的位置.

1
2
3
4
let s ="hello World"
s.includes("hello")//true
s.startsWith("hel")//true
s.endsWith("rld")//true

repeat()方法返回一个新字符串,表示将元字符串重复n次.

1
'hello'.repeat(2);//'hellohello'

模板字符串

1
2
let s = 'world'
let a = `hello ${s} !`//a='hello world !'

${}里面不仅可以嵌入变量,还可以嵌入函数.

数值的扩展

  • Number.isFinite()用来检查一个数值是否为有限的(finite)
  • Number.isNaN()用来检查一个数值是否为NaN.
  • 上面两个方法与传统的全局方法isFinite()和isNaN()的区别在于,传统方法会先调用Number()将非数值转换为数值在判断,新的两个方法只对数值有效,非数组一律返回false.
  • Number.parseInt();
  • Number.parseFloat();将全局方法移植到Number对象上,使语言逐步模块化.
  • Number.isInteger();用来判断一个值是否为整数.
  • 指数运算符,例如2**3===8;

函数的扩展

函数参数的默认值

1
2
3
4
5
6
7
8
9
function log(x,y='world'){
console.log(x,y)
}
log('hello')//hello world
//参数是默认声明的,所以不能用let和const再次声明
function foo(x=5){
let x = 1;//error
const x = 2;//error
}

rest参数

ES6引入rest参数,用于获取函数的多余参数,rest参数搭配的变量是一个数组,该变量将多余的参数放入数组中.rest参数只能是最后一个参数,否则会报错.

1
2
3
4
5
6
7
8
function(...values){
let sum = 0;
for(var val of values){
sum += val;
}
return sum;
}
add(2,3,5)//10

函数的name属性返回该函数的函数名.

箭头函数

1
2
var f = v=> v;
var f = ()=>5;//没有参数或多个参数用()包括,多个函数语句用{}
  • 箭头函数内的this是定义时所在的对象,不是使用时的对象
  • 不可以当做构造函数
  • 不可以使用arguments对象
  • 不可以使用yield命令,不能作为Generator函数
  • 箭头函数中的this的指向是固定的,不可变的.

尾调用就是在函数的最后一步调用另外一个函数.

尾递归函数在最后一步调用自身就是尾递归.尾递归不会发生栈溢出,相对节省内存.

1
2
3
4
function factorrial(n,total){
if(n === 1) return total;
return factorial(n-1,n*total)
}

函数式编程有一个概念,叫做柯里化,意思就是将多参数的函数转换成单参数的函数形式.

数组的扩展

扩展运算符是三个点....它好比rest参数的逆运算,将一个数组转为用逗号分隔的参数序列.

1
console.log(...[1,2,3])//1 2 3

由于扩展运算符可以展开数组,所以不再需要apply方法将数组转为函数的函数了.

1
2
3
4
5
6
7
8
//ES5
function f(x,y,z){...}
var args = [1,2,3]
f.apply(null,args)
//ES6
function f(x,y,z){...}
let args = [1,2,3]
f(...args)

扩展运算符的应用

  • 复制数组
1
2
3
4
5
//ES5
const a1 = [1,2,3]
const a2 = a1.concat();
//ES6
cionst a2 = [...a1]
  • 合并数组
1
2
3
4
//ES5
arr1.concat(arr2,arr3)
//ES6
[...arr1,...arr2,...arr3]
  • 字符串转为数组
1
[..."hello"]//['h','e','l','l','o']
  • 只要是有Iterator接口的对象都可以用扩展运算符转为数组.

Array.from()将类似数组的对象和可遍历对象转换为真正的数组.

扩展运算符转换为数组调用的是遍历器接口Iterator,Array.from()不仅可以支持可遍历对象还支持类似数组的对象,既任何拥有length属性的对象都可以通过Array.from转换为数组,而扩展运算符不行.

1
2
3
4
5
let arrayLike = {'0':'a','1':'b',length:2}
//ES5
var arr1 = [].slice.call(arrayLike)
//ES6
let arr2 = Array.from(arrayLike)

Array.of()用于将一组值转换为数组.因为Array()和new Array(),由于参数不同导致行为不统一.一个参数指定数组的长度,不少于两个才能组成新数组,这样会导致行为有差异.Array.of()基本可以替代Array()和new Array().

1
Array.of(1,2,3);//[1,2,3]

find()用于找出第一个符合条件的数组成员.他的第一个参数是一个回调函数,所有成员依次执行该函数,直到找到第一个为true的成员,没有找到返回undefined.该回调函数一个接受3个参数,分别是value,index,arr.

1
2
3
4
[1,4,-5,10].find((n)=>{n<0})//-5
[1,4,-5,10].find(function(v,i,a){
return value > 9
})//10

findIndex()与find()用法类似,用于找出符合条件成员的索引,都不符合返回-1.由于数组的IndexOf方法无法识别数组的NaN成员,而find和findIndex弥补了数组indexOf的不足.

1
2
3
[1,4,-5,10].findIndex(function(v,i,a){
return value === 4;//1
})

fill()填充一个数组.接受第二和第三个参数分别为起始位置和结束位置之前.

1
['a','b','c'].fill(7,1,2)//['a',7,'c']

entries(),keys(),values()都返回一个遍历器对象,可以用for…of进行遍历.entries()是对键值对的遍历,keys()是对键名的遍历,values()是对值得遍历.

1
2
3
for(let index of ['a','b'].keys()){
console.log(index);//0 //1
}

数组的includes()返回一个布尔值表示数组是否包含给定的值,与字符串的includes()方法类似.另外Map和Set数据结构有一个has方法,需要注意与includes区分.Map的has用来查找key,Set的has用来查找value.

1
[1,2,3].includes(3)//true

对象的扩展

属性的简写形式

1
2
3
var foo = {x:x,y:y};
//等同于
let foo = {x,y}

属性名表达式

ES6允许字面量定义对象时,用表达式作为对象的属性名.

1
2
3
4
5
6
let name = 'foo'
let obj = {
[name]: 'foo',
['a'+'bc']: 123
}
//{foo:'foo','abc':123}

Object.is()用来比较两个值是否严格相等,与===行为基本一致.不同之处在于+0不等于-0,NaN等于NaN

1
2
3
4
+0 === -0;//true
Object.is(+0,-0)//false
NaN === NaN;//false
Object.is(NaN,NaN);//true

Object.assign()用于对象合并,将源对象所有可枚举属性复制到目标对象.

  • 如果只有一个参数,直接返回该对象.
  • 同名属性,后者会覆盖前者
  • Object.assign方法实行的是浅拷贝.如果某个属性的值是对象,那么目标对象拷贝的是这个对象的引用.

Object.assign的用途.

  • 为对象添加属性
  • 为对象添加方法(将函数放在空对象中)
  • 克隆对象(和一个空对象合并)

Object.setPrototypeOf()设置一个对象的prototype对象,返回参数对象本身,它是ES6整数推荐的设置原型对象的方法.

1
2
3
4
let a = {a:1}
let proto = {b: 2}
Object.setPrototypeOf(a,proto);//a.b === 2
//上面代码将proto对象设置为a对象的原型

Object.getPrototypeOf()用于读取一个对象的原型对象.

1
Object.getPrototypeOf(a);//{b:2}

super关键字指向当前对象的原型对象.

Object.keys()返回一个数组,成员是对象所有可遍历属性的键名.

Object.values()返回一个数组,成员是对象所有可遍历属性的值.

Object.entries()返回一个数组,成员是对象所有可遍历属性的键值对数组.

对象也可用于解构赋值以及扩展运算符

1
2
3
4
let {x,...y} = {x:1,y:2,z:3}
x // 1
y// {y:2,z:3}
let z = {...y}//{y:2,z:3}

Null传导运算符?.

1
2
3
const first = (msg && msg.body&&msg.body.user || 'default')
//使用null传导运算符
const first = (msg?.body?.user || 'default')

Symbol

ES6引入了一种原始数据类型Symbol,表示独一无二的值.它是js第七种数据类型,分别是undefined,null,Boolean,String,Number,Object,Symbol

由于symbol不是对象,而是数据类型,所以不能使用new,他是一种类似于字符串的的数据类型.

1
2
3
4
5
let s = Symbol();
let s1 = Symbol();
s === s1;//false,Symbol不能参与运算.
let obj = {};
obj[s] = 'hello'//Symbol作为属性名不能用.运算形式,且不能被遍历到

Set和Map

ES6提供了新的数据结构Set,它类似于数组,但是成员都是唯一的,没有重复的值.

1
2
//去除数组的重复成员
[...new Set(2,3,2,1,2,3)]
  • Set,prototype.constructor: 构造函数,默认就是Set函数
  • Set.prototype.size: 返回set实例的成员总数

Set的4个操作方法

  • add(value): 添加某个值,返回set结构本身
  • delete(value):删除某个值,返回一个布尔值,表示是否成功
  • has(value): 返回一个布尔值,表示是否为Set成员.
  • clear():清除所有成员.

Set的4个遍历方法

  • keys():返回键名的遍历器
  • values():返回值得遍历器(默认遍历生成函数)
  • entries():返回键值对的遍历器
  • forEach():使用回调函数遍历每个成员

ES6提供了Map数据类型,类似于对象,但他的”键”的范围不限于字符串,各种类型的值都可以作为键.

Map的实例属性与操作方法:

  • size: Map结构的成员总数
  • set(key,value):设置键值,返回整个结构
  • get(key): 读取某个键的值,找不到key返回undefined
  • has(key):返回布尔值,表示某个键是否在当前Map对象中
  • delete(key):删除某个键,返回布尔值表示是否成功.
  • clear():清除所有.

Map结构提供的遍历方法与Set相同.

promise对象

promise是异步编程的一种解决方案.它接受一个函数作为参数,函数有两个参数resolve和reject有js引擎提供,resolve函数将promise有未完成变为成功,reject由未完成变为失败.

promise实例生成后可以用then指定resolved和rejected状态的回调函数,并且then返回一个新的promise实例,所以可以链式调用.

1
2
3
4
5
6
7
8
9
10
11
12
13
const promise = new Promise(function(resolve,reject){
if(/*成功*/){
resolve(value)
}else{
reject(error)
}
})
promise.then(function(value){
//success
},function(error){
//error
})
//一般来说,不要再then中定义rejected状态的回调函数,既then的第二个参数,而应该总是使用catch方法.

promise.catch是.then(null,rejection)的别名,指定发生错误是的回调函数.

promise.all将多个promise实例包装成一个promise实例.全部成功才成功,有一个失败就是失败.

promise.race将多个promise包装为一个,只要其中有一个先改变状态,整个状态就会改变.

promise.finally方法用于指定不管promise的最后状态无论怎样都会执行,它接受一个普通的回调函数作为参数,不管怎样都必须执行.

Iterator和for…of循环

当使用for…of循环某种数据结构时,该循环会自动寻找Iterator接口.

一个对象只要具备Symbol.iterator属性就代表该对象可遍历.

默认调用Iterator接口的场合:

  • 解构赋值let [x,y] = [1,2]
  • 扩展运算符[...arr]
  • yield后面如果跟的是可遍历结构就会调用遍历器接口`yield\ [2,3,4]`
  • for…of,Array.from()…

遍历器对象除了具有next()函数,还要return()和throw()

for..of与其他遍历语法比较.

  • for循环
  • 数组的forEach(无法跳出循环,break,return都不行)
  • for …in(主要是循环对象而设计,不适用于遍历数组)
  • for…of,与for..in一样简洁,可以跳出循环…

Generator函数

Generator是ES6题提供的异步编程解决方案.可以把它理解为一个状态机,封装了多个状态.还是一个遍历器对象生成函数.

Generator函数的特征:function与函数名之间有一个*号,函数体内部使用yield表达式定义不同的内部状态.

1
2
3
4
5
6
7
8
9
10
function* helloWorld(){
yield 'hello';
yield 'world';
return 'ending';
}
var hw = helloWorld();
//generator函数调用后该函数并不执行,返回的也不是函数运行结果,而是遍历器对象Iterator.然后调用遍历器对象的next方法使得指针移动到下一个状态.
hw.next();//{value:'hello',done:fasle}
hw.next();//{value:'world',done:false}
hw.next();//{value:'ending',done:true}

由于Generator函数返回一个遍历器对象,调用next才会遍历下一个内部状态,所以其实他是一个可以暂停执行的函数,yield就是暂停标志.next()遇到yield就会暂停后面的操作,并把yield后面表达式的值作为返回的value值,下一次调用next,再继续执行,知道遇到yield或return为止.另外yield表达式在其他地方使用都会报错.

next()可以带一个参数,作为上一次yield表达式的返回值.

1
2
3
4
5
6
7
8
9
10
11
12
13
function* foo(x){
var y = 2*(yield (x+1));
var z = yield (y/3);
return (x+y+z);
}
var a = foo(5);
a.next();//{value:6,done:fasle}
a.next();//{value:NaN,done:fasle}
a.next();//{value:NaN,done:fasle}
var b = foo(5);
b.next();//{value:6,done:fasle}
b.next(12);//{value:8,done:fasle}
b.next(13);//{value:42,done:fasle}

for…of可以自动遍历Generator函数生成的遍历器对象,并且不再需要调用next方法.但函数return 的值不会再循环中.

在Generator函数中调用Generator函数是没有效果的,这就需要用到yield*表达式,用来达成以上目的.并且任何数据结构只要有Iterator接口,就可以使用yield*遍历

1
2
3
4
5
6
function* bar(){
yield 'a';
yield* foo();
yield 'b';
yield* [1,2,3]
}

async函数

ES6引入了async函数,使异步操作更加方便.async函数就是Generator函数的语法糖.它将*替换成async,将yield替换成await,仅此而已.

1
2
3
4
5
6
7
8
9
10
11
12
13
const gen = function* (){
cosnt f1 = yield readFile('/etc/a.txt')
const f2 = yield readFile('/etc/b.txt')
console.log(f1.toString());
console.log(f2.toString());
}

const gen = async function(){
const f1 = await readFile('/etc/a.txt')
const f2 = await readFile('/etc/b.txt')
console.log(f1.toString());
console.log(f2.toString());
}

async对generator的改进

  • 内置执行器.async函数的执行与普通函数一样gen()
  • 更好的语义.比起*与yield,语义更清楚.
  • 更广的适用性.async函数的await命令后面可以是promise对象和原始类型的值(但此时等同于同步操作).
  • 返回值是promise对象,而generator返回的是一个Iterator遍历器.而async可以看做多个异步操作包装的promise对象,而await命令就是内部then的语法糖.

async函数内部return语句返回的值会成为then方法回调函数的参数

1
2
3
4
async function f(){
return 'helloWorld'
}
f().then(v=>console,log(v))//'helloWorld'

async函数内部抛出错误会导致promise对象变为reject状态.错误对象会被catch方法回调函数接收.

正常情况下await后面是一个peomise对象,如果不是,会转成一个立即resolve的promise

for await of 用来遍历异步的iterator接口.

class

ES6引入class的概念,作为对象的模板,通过class关键字来定义类.

class其实只是一个语法糖,他的大部分功能ES5都可以做到,只是让对象原型的写法更像面向对象编程语法而已.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
//ES5
function Point (x,y){
this.x = x;
this.y = y;
}
Point.prototype.toString = function(){
return this.x+','+this.y
}
var p = new Point(1,2)
//ES6
class Point{
constructor(x,y){
this.x = x;
this.y = y;
}
toString(){
return this.x+','+this.y
}
}
Point === Point.prototype.constructor//true
var p = new Point();

toString是Point类内部定义的方法,它是不可枚举的,这与ES5不一致.

类不存在变量提升

class的静态方法,类相当于实例的原型,所有类中定义的方法都会被实例继承,如果在一个方法前加上static关键字就表示该方法不会被实例继承,而是直接通过类来调用,这就成为静态方法.静态方法中的this指的是类而不是实例.

1
2
3
4
5
6
7
8
class Foo(){
static hello(){
return 'helloWorld'
}
}
Foo.hello()//'helloWorld'
var h = new Foo();
h.hello()//报错typeError...

父的静态方法可以被子类继承

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
class Foo(){
constructor(x,y){

}
static hello(){
return 'helloWorld'
}
toString(){
...
}
}
class Bar extends Foo(){
constructor(x,y,z){
super(x,y)//调用父类的constructor(x,y)
this.z = z;
}
toString(){
return this.z +','+super.toString
}
}
Bar.hello()//'helloWorld'

super关键字表示父类的构造函数,用来新建父类的this对象.

子类必须在constructor方法中调用super方法,否则新建实例会报错,这是因为子类没有自己的this对象而是继承父类的this对象,然后对其加工,不调用super()子类就得不到this对象.

如果子类没有定义constructor,这个方法会被默认添加,super也会默认添加.只要调用super才能使用this关键字.

修饰器

修饰器函数用来修改类的行为,是对一个类进行处理的函数,修饰器函数的第一个参数就是说要修身的目标类.如果觉得一个参数不够用,可以再修饰器外再封装一层函数.

1
2
3
4
5
6
7
8
9
@testable
class MyTest(){
...
}
function testable(target){
target.isTest = true;//静态属性
target.prototype.isOk = false;//实例属性
}
MyTest.isTest // true

修饰器实在代码编译时发生的,这意味着修饰器本质就是编译时执行的函数.

Module

export 用于规定模块的对外接口.

1
2
3
4
5
6
7
export var a = 'a'
export var b = 'b'
export {a,b}
export default{
a,
b
}

import 用于输入其他模块提供的功能.

1
2
3
import {a,b} from './xxx'
import a as A from './xxx'
import * as num from './xxx'

如果import要取代Node的require方法就形成了障碍,因为require是运行时加载模块,而import无法取代require的动态加载功能.CommonJS输出的是一个值得拷贝,而ES6模块输出的是值得引用.

浏览器加载ES6模块,也使用<script>,但要加入type=’module’属性告诉浏览器这是一个ES6模块.

1
<script type="module" src="./foo.js"></script>