ES6相关笔记

ES6

语法糖:语法糖能够增加程序的可读性,从而减少程序代码出错的机会。(更容易表达一个操作的语法)

let和const命令

ES6 明确规定,如果区块中存在letconst命令,这个区块对这些命令声明的变量,从一开始就形成了封闭作用域。凡是在声明之前就使用这些变量,就会报错。(js有变量提升)

let和var

let:用法和var相似,但生命的变量只在了let命令所在的代码块内有效,只要let、const出现,则这个区块对于它们定义的变量形成封闭作用域。即在块级作用域内,有let、const声明语句,在let语句出现之前,该变量都是不可用的,Error。(暂时性死区)但如果一个变量从始至终未被声明,当使用时,也只是会显示undefined。(严格遵循先定义,再使用)

var:声明的变量全局有效,如果在声明之前使用,那就是undefined不会报错。

for循环的循环条件等是父作用域而循环体是子作用域

在一个代码块内,一般一个{}如果有let就算一个代码块了,不允许重复声明

块级作用域

比如for循环时用var定义循环变量i,但循环结束后i等于退出循环的值,即它没有消失,泄露成了全局变量。

块级作用域可以任意嵌套,立即执行匿名函数因为它的出现不再必要

{
   let tmp=...;
}
函数在块级作用域内声明
  • 允许在块级作用域内声明函数
  • 函数声明类似于var,即会提升到全局作用域或函数作用域的头部。
  • 函数声明还会提升到所在的块级作用域的头部。
注意
  • 避免在块级作用域内声明函数,应写成函数表达式的形式let a=function……
do表达式

块级作用域是一个语句无返回值,用do可以让她变成表达式。

const

const定义只读变量,本质是该变量指向的内存地址不得改动

如const foo={};即定义foo是个对象,可以对foo添加各种属性等,但不能让它指向其他的变量

若想将对象冻结(属性修改也不允许),应该使用Object.freeze方法。

const foo=Object.freeze({});//即foo指向一个冻结的对象{}

ES6声明变量的6种方法

var、function、let、const、import和class

顶层对象的属性

对于浏览器就是window,在node环境指的是global对象。

  1. ES6中,var命令和function命令声明的全局变量依旧是顶层对象的属性(即它们声明的可以用window.xx来使用)
  2. let、const、class声明的全局变量则不是。
解构赋值
变量的解构赋值

解构:ES6允许按照移动的模式从数组和对象中提取值,然后对变量进行赋值。

let [a,b,c]=[1,2,3];
let [ , ,third]=["g","r","g"];
let [x,y, ...z]=['a'];//这是链表结构,z是'a',y是undefined(属于解构不成功的情况),z则是空链表[]

注意:等号右边得是数组,允许指定默认值(等号后的值优先,若为undefined再考虑默认值)

let [a,b=6]=['a'];//原理上来说a='a',b=undefined但b已经赋值过了,所以是有值的
let [a=1]=[undefined];//综上,同理,a=1,[]指undefined而不是null
let [a=1]=[null];//es6内部严格用===来判断,所以不严格等于undefined,默认值是不会生效的
对象的解构赋值
let {foo,bar}={foo :"aaa",bar:"bbb"}
let {foo : baz}={foo :"aaa",bar:"bbb"};//baz:"aaa"

对象中要指定属性名,因为数组赋值是根据索引,但对象中是根据属性名同才可赋值

内部机制:先找到同名属性,再赋值给对应的变量,真正被复制是的变量。

let {fool}={bar:"bqz"};
foo//undefined,因为解构失败,但是如果想取foo的子对象会报错
//错误写法
let x;
{x}={x:1};//会被认为是代码块
//正确写法
let x;
({x}={x:1});//单该括号语句也是合法的可以执行,但毫无意义

注意:前面一定写let,语句才是对的,否则只写{},

字符串的解构赋值
const [a,b,c,d,e]='hello';//a:'h'
let {length:len}='hello';//每个字符串都有length属性,len=5;
数值和布尔值的解构赋值

解构赋值时,如果等号右边是数值和布尔值,则会先转为对象。

数值和布尔值的包装对象都有toString属性

规则:只要等号右边不是对象或者数组,就先将其转为对象,而undefined和null是不能转换对象的。

--------学了箭头函数再来看p43----------

函数参数的解构赋值

function f([x,y,z]){…}

会自动把数组内的内容解析

function foo({x, y = 5} = {}) {
  console.log(x, y);
}
foo() // undefined 5

上面代码指定,如果没有提供参数,函数foo的参数默认为一个空对象。

解构

把var [a,b,c]=[‘aa’,‘bb’,‘cc’]中的具体值拆出来

[a,b,…c]=[‘aa’,‘bb’,‘cc’,‘dd’,‘ee’]那么c是[‘cc’,‘dd’,‘ee’]

圆括号的使用问题

可以使用的情况:赋值语句的非模式部分

(let开头的是声明语句)

字符串的扩展

\uFFFF :FFFF代表字符的Unicode码点超过FFFF就不识别了,js中的字符用UTF-16存储,即格式定位2个字节

ES6优化:在码点处+{},如:hell\u{6F}===hello

codePointAt():可以测试一个字符是由2个字节还是4个字节组成
var s='吉a';
//for...of可以识别32位的UTF-16字符(编号大于0xFFFF)
for(let ch of a){
   console.log(ch.codePointAt(0).toString(16));
}
//即把s中的两个字符用16进制表示出来。
String.fromCodePoint():用于从码点返回对应字符

若该方法有多个参数,则会被合并成一个字符串返回

字符串的遍历器接口:for…of能正确识别字符串中的各个元素
at():能返回正确的字符
normalize():Unicode正规化(能识别两个符号的合成),将字符从不同表示方法统一位同样的形式

NFC、NFD、NFKC、NFKD集中分解合成发方式

includes()、startsWith()、endsWith():ES6提供的判断一个字符串是否包含在另一个中

includes():布尔值,表示是否找到了参数字符串

startsWith():布尔值,表示参数字符是否在源字符串的头部

endsWith():布尔值,表示参数字符串是否子啊源字符串的尾部

上述方法使用方式:

s.includes('hello'[,index]);
repeat():返回一个新字符串,表示将源字符串重复n次

参数:是正整数,若为正小数会被取整,若为-1到0的之间的小数,会被取整到0,参数NaN相当于0,参数是字符串是字符串会先转换成数字

padStart()、padEnd():字符串补全长度的功能

数字是指字符串长度

'x'.padStart(5,'ab');//若某个字符不够长度,补全'ababx'
'x'.padStart(4,'ab');//'abax'
'x'.padEnd(5,'ab');//'ababx'
'xxx'.padStart(2,'ab');//若原字符串长度>参数,返回原字符串'xxx'
'xxx'.padStart(10,'0123456789');//若原字符串长度<参数,返回原字符串0123456abc

用途:数值补全指定位数、提示字符串格式

模板字符串

一般js写添加带参数的html语句都是用字符串的拼接来写的,es6引入``符号,反引号标识

它可以用作普通字符串也可以用来定义多行字符串(所有的换行、缩进都会被保留),或者在字符串中嵌入变量${name},{表达式}里面可以进行运算

若在模板字符串中要使用该符号,则需要转义,即转义符\

若要取消被保留的空格、换行等,在模式串的结尾加上.trim()

模板字符串可以嵌套

标签模板

函数调用的一种特殊形式。“标签”指的就是函数,紧跟在后面的模板字符串就是它的参数。

let a = 5;
let b = 10;
//将模板字符串先处理成多个参数,再调用函数
tag`Hello ${ a + b } world ${ a * b }`;
// 等同于
tag(['Hello ', ' world ', ''], 15, 50);
String.raw():往往用来充当模板字符串的处理函数

它会将所有变量替换,并对反斜杠进行转移,方便下一步作为字符串使用,也可以作为正常函数使用,值是一个数组。

String.raw`Hi\n${2+3}!`;
//"Hi\\n5!"
String.raw({raw:'test'},0,1,2);
//'t0e1s2t'
函数的扩展

参数可以有默认值,它不是传值的,而是每次都重新计算默认值表达式的值。也就是说,参数默认值是惰性求值的。

let x = 99;
function foo(p = x + 1) {
  console.log(p);
}
foo() // 100
x = 100;
foo() // 101

如果非尾部的参数设置默认值,实际上这个参数是没法省略的。

即在调用函数时,要么参数都没有,要么写明改参数的值

函数的 length 属性

指定了默认值以后,函数的length属性,将返回没有指定默认值的参数个数。也就是说,指定了默认值后,length属性将失真。 rest 参数也不会计入length属性。(且该默认参数得是尾参数,否则是默认参数前面的不失真)

作用域

一旦设置了参数的默认值,函数进行声明初始化时,参数会形成一个单独的作用域(context)。等到初始化结束,这个作用域就会消失。这种语法行为,在不设置参数默认值时,是不会出现的。

let foo = 'outer';
//参数有默认值即foo
function bar(func = () => foo) {
  let foo = 'inner';
  console.log(func());
}
bar(); // outer
rest参数(只能是最后一个参数)

rest 参数搭配的变量是一个数组,该变量将多余的参数放入数组中。

name返回函数名

箭头函数

var f=v=>v;

var f=function (v){
   return v;
}

const f=v=>v;

function f(v){
   return v;
}
this

(1)箭头函数没有自己的this对象。

(2)不可以当作构造函数,也就是说,不可以对箭头函数使用new命令,否则会抛出一个错误。

(3)不可以使用arguments对象,该对象在函数体内不存在。如果要用,可以用 rest 参数代替。

(4)不可以使用yield命令,因此箭头函数不能用作 Generator 函数。

上面四点中,最重要的是第一点。对于普通函数来说,内部的this指向函数运行时所在的对象,但是这一点对箭头函数不成立。它没有自己的this对象,内部的this就是定义时上层作用域中的this。也就是说,箭头函数内部的this指向是固定的,相比之下,普通函数的this指向是可变的。

function foo() {
  return () => {
    return () => {
      return () => {
        console.log('id:', this.id);
      };
    };
  };
}
//this的指向只有一个,就是函数foo的this,这是因为所有的内层函数都是箭头函数,都没有自己的this,它们的this其实都是最外层foo函数的this。

除了this,以下三个变量在箭头函数之中也是不存在的,指向外层函数的对应变量:argumentssupernew.target

函数的扩展

尾递归

只在严格模式下开启

尾调用指某个函数的最后一步是调用另一个函数。

对于尾递归来说,只存在一个调用帧,不会发生”栈溢出“的错误,eg:斐波那契数列的优化,尾递归只执行一个操作。(只保留一个调用记录,复杂度 O(1))

数组的扩展

...是扩展符,把数组转为逗号分隔的参数序列,主要用在函数调用,仅在该情况下扩展符可以在圆括号里面。扩展符后面也可以放置表达式可以是空数组。

  • 函数调用
  • 表达式
  • 字符串(分解成字符数组,若有Unicode字符必须用扩展符)
  • 实现了 Iterator 接口的对象(比如dom查找到的类似数组的NodeList对象可以被转化成数组,map也可以),普通对象不行
Array.from()

将两类对象转为真正的数组:类似数组的对象(有length属性)和可遍历(iterable)的对象(包括 ES6 新增的数据结构 Set 和 Map)。

Array.from还可以接受第二个参数,作用类似于数组的map方法,用来对每个元素进行处理,将处理后的值放入返回的数组。

如果map函数里面用到了this关键字,还可以传入Array.from的第三个参数,用来绑定this

Array.of()

Array.of()方法用于将一组值,转换为数组。

Array.of() // []
Array.of(3, 11, 8) // [3,11,8]
Array() // []
Array(3) // [, , ,]
Array(3, 11, 8) // [3, 11, 8]
copyWithin()
Array.prototype.copyWithin(target, start = 0, end = this.length)

它接受三个参数。

  • target(必需):从该位置开始替换数据。如果为负值,表示倒数。
  • start(可选):从该位置开始读取数据,默认为 0。如果为负值,表示从末尾开始计算。
  • end(可选):到该位置前停止读取数据,默认等于数组长度。如果为负值,表示从末尾开始计算。
[1, 2, 3, 4, 5].copyWithin(0, 3)
// [4, 5, 3, 4, 5]

上面代码表示将从 3 号位直到数组结束的成员(4 和 5),复制到从 0 号位开始的位置,结果覆盖了原来的 1 和 2。

find()和findIndex()

数组实例的find方法,用于找出第一个符合条件的数组成员。它的参数是一个回调函数,所有数组成员依次执行该回调函数,直到找出第一个返回值为true的成员,然后返回该成员。如果没有符合条件的成员,则返回undefined

数组实例的findIndex方法的用法与find方法非常类似,返回第一个符合条件的数组成员的位置,如果所有成员都不符合条件,则返回-1

这两个方法都可以接受第二个参数,用来绑定回调函数的this对象。

function f(v){
  return v > this.age;
}
let person = {name: 'John', age: 20};
[10, 12, 26, 15].find(f, person);    // 26
fill()
arr.fill(value,index,end)

value:用来填充数组的值

index和end用于指定填充的起始位置和结束位置(可选)。

['a', 'b', 'c'].fill(7, 1, 2)
// ['a', 7, 'c']
实例方法:entries(),keys() 和 values()

ES6 提供三个新的方法——entries()keys()values()——用于遍历数组。它们都返回一个遍历器对象,可以用for...of循环进行遍历,唯一的区别是keys()是对键名的遍历、values()是对键值的遍历,entries()是对键值对的遍历。(这几个方法也可以用于对象,entries返回两个数组,一个是keys一个是values)

对象的扩展

对于普通对象,没有部署原生的 iterator 接口,直接使用 for…of 会报错,for…of 可以通过Object.keys values entries遍历对象.

属性名表达式

之前的js中

var obj={
   uname:'xx',//属性
}
//调用:obj.uname或者obj['uname']

方法一是直接用标识符作为属性名,方法二是用表达式作为属性名,这时要将表达式放在方括号之内。

// 方法一
obj.foo = true;
let lastWord = 'last word';
const a = {
  'first word': 'hello',
  [lastWord]: 'world'
};
a['first word'] // "hello"
a[lastWord] // "world"
a['last word'] // "world"
name属性

方法的name属性返回函数名(即方法名)。

如果对象的方法使用了取值函数(getter)和存值函数(setter),则name属性不是在该方法上面,而是该方法的属性的描述对象的getset属性上面,返回值是方法名前加上getset

const obj = {
  get foo() {},
  set foo(x) {}
};
obj.foo.name
// TypeError: Cannot read property 'name' of undefined

const descriptor = Object.getOwnPropertyDescriptor(obj, 'foo');
descriptor.get.name // "get foo"
descriptor.set.name // "set foo"

有两种特殊情况:bind方法创造的函数,name属性返回bound加上原函数的名字;Function构造函数创造的函数,name属性返回anonymous

(new Function()).name // "anonymous"
var doSomething = function() {
  // ...
};
doSomething.bind().name // "bound doSomething"

如果对象的方法是一个 Symbol 值,那么name属性返回的是这个 Symbol 值的描述。

symbol

新的数据类型,表示独一无二的值(其他数据类型是:undefinednull、布尔值(Boolean)、字符串(String)、数值(Number)、大整数(BigInt)、对象(Object))

注意,Symbol函数的参数只是表示对当前 Symbol 值的描述,因此相同参数的Symbol函数的返回值是不相等的。

// 没有参数的情况
let s1 = Symbol();
let s2 = Symbol();
s1 === s2 // false
// 有参数的情况
let s1 = Symbol('foo');
let s2 = Symbol('foo');
s1 === s2 // false
  • 不能与其他类型的值进行运算

  • 可以显式转化为字符串,转为布尔值,但不能转为数值

    let sym = Symbol('My symbol');
    String(sym) // 'Symbol(My symbol)'
    let sym = Symbol();
    Boolean(sym) // true
    !sym  // false
    
Symbol.prototype.description

创建的时候,可以添加一个描述

const sym = Symbol('foo');
sym.description // "foo"

Symbol 值作为对象属性名时,不能用点运算符。(点运算符后面总是字符串)

const mySymbol = Symbol();
const a = {};

a.mySymbol = 'Hello!';
a[mySymbol] // undefined
a['mySymbol'] // "Hello!"
属性名的遍历

Symbol 作为属性名,遍历对象的时候,该属性不会出现在for...infor...of循环中,也不会被Object.keys()Object.getOwnPropertyNames()JSON.stringify()返回。

let name = Symbol('name');
let product = {
    [name]:"洗衣机",    
    "price":799
  };
Reflect.ownKeys(product);

set和map

set类似数组,但里面的元素不相同

由于两个空对象不相等,所以它们被视为两个值。

属性和方法

Set 结构的实例有以下属性。

  • Set.prototype.constructor:构造函数,默认就是Set函数。
  • Set.prototype.size:返回Set实例的成员总数。

Set 实例的方法分为两大类:操作方法(用于操作数据)和遍历方法(用于遍历成员)。下面先介绍四个操作方法。

  • Set.prototype.add(value):添加某个值,返回 Set 结构本身。
  • Set.prototype.delete(value):删除某个值,返回一个布尔值,表示删除是否成功。
  • Set.prototype.has(value):返回一个布尔值,表示该值是否为Set的成员。
  • Set.prototype.clear():清除所有成员,没有返回值。
遍历操作

Set 结构的实例有四个遍历方法,可以用于遍历成员。

  • Set.prototype.keys():返回键名的遍历器
  • Set.prototype.values():返回键值的遍历器
  • Set.prototype.entries():返回键值对的遍历器
  • Set.prototype.forEach():使用回调函数遍历每个成员
WeakSet

也是不重复的值的集合。但是,它与 Set 有两个区别。

  • WeakSet 的成员只能是对象,而不能是其他类型的值。(任何具有 Iterable 接口的对象,都可以作为 WeakSet 的参数。)
  • WeakSet 中的对象都是弱引用,即垃圾回收机制不考虑 WeakSet 对该对象的引用,也就是说,如果其他对象都不再引用该对象,那么垃圾回收机制会自动回收该对象所占用的内存,不考虑该对象还存在于 WeakSet 之中。

WeakSet 结构有以下三个方法。(没有size属性无法遍历)

  • WeakSet.prototype.add(value):向 WeakSet 实例添加一个新成员。
  • WeakSet.prototype.delete(value):清除 WeakSet 实例的指定成员。
  • WeakSet.prototype.has(value):返回一个布尔值,表示某个值是否在 WeakSet 实例之中。
垃圾回收

垃圾回收机制根据对象的可达性(reachability)来判断回收,如果对象还能被访问到,垃圾回收机制就不会释放这块内存。

Map

只要两个值严格相等,Map 将其视为一个键。undefinednull也是两个不同的键。虽然NaN不严格相等于自身,但 Map 将其视为同一个键。

实例的属性和操作方法
  • size属性
  • set(key,value),由于返回的是当前的Map对象,所以可以采取链式写法
  • get(key)
  • has(key),返回布尔值
  • delete(key),返回布尔值
  • clear(),清楚所有成员,无返回值
遍历方法

同set,注意:Map 的遍历顺序就是插入顺序,用for...of遍历的时候,返回的值是数组里面的值是一组键值对。无数组的map方法、filter方法。

可通过...把他化为数组然后进行操作

Proxy

用于修改某些操作的默认行为,等同于在语言层面做出修改。proxy 可以理解成,在目标对象之前架设一层“拦截”,外界对该对象的访问,都必须先通过这层拦截,因此提供了一种机制,可以对外界的访问进行过滤和改写。

  • 拦截和监视外部对对象的访问
  • 降低函数或类的复杂度
  • 在复杂操作前对操作进行校验或对所需资源进行管理

生成proxy实例

var proxy = new Proxy(target, handler);

Proxy 对象的所有用法,都是上面这种形式,不同的只是handler参数的写法。其中,new Proxy()表示生成一个Proxy实例,target参数表示所要拦截的目标对象,handler参数也是一个对象,用来定制拦截行为。

如果handler没有设置任何拦截,那就等同于直接通向原对象。

Reflect

ES6 为了操作对象而提供的新 API。Reflect对象的设计目的有这样几个。

(1) 将Object对象的一些明显属于语言内部的方法(比如Object.defineProperty),放到Reflect对象上。现阶段,某些方法同时在ObjectReflect对象上部署,未来的新方法将只部署在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 objdelete 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上获取默认行为。

静态方法
  • Reflect.apply(target, thisArg, args)

  • Reflect.construct(target, args)

    提供了不适用new调用构造函数的方法

    function Greeting(name) {
      this.name = name;
    }
    
    // new 的写法
    const instance = new Greeting('张三');
    
    // Reflect.construct 的写法
    const instance = Reflect.construct(Greeting, ['张三']);
    
  • Reflect.get(target, name, receiver)

    查找并返回target对象的name属性,如果没有该属性,则返回undefined

  • Reflect.set(target, name, value, receiver)

    Reflect.set方法设置target对象的name属性等于value

    如果name属性设置了赋值函数,则赋值函数的this绑定receiver

    var myObject = {
      foo: 4,
      set bar(value) {
        return this.foo = value;
      },
    };
    
    var myReceiverObject = {
      foo: 0,
    };
    
    Reflect.set(myObject, 'bar', 1, myReceiverObject);
    myObject.foo // 4
    myReceiverObject.foo // 1
    
  • Reflect.defineProperty(target, name, desc)

  • Reflect.deleteProperty(target, name)

  • Reflect.has(target, name)

  • Reflect.ownKeys(target)

    用于返回对象的所有属性,可以用于symbol

  • Reflect.isExtensible(target)

  • Reflect.preventExtensions(target)

    如果参数不是对象,两个都报错

  • Reflect.getOwnPropertyDescriptor(target, name)

  • Reflect.getPrototypeOf(target)

    用于读取对象的__proto__属性

    Reflect.getPrototypeOfObject.getPrototypeOf的一个区别是,如果参数不是对象,Object.getPrototypeOf会将这个参数转为对象,然后再运行,而Reflect.getPrototypeOf会报错。

  • Reflect.setPrototypeOf(target, prototype)

    如果第一个参数是undefinednullObject.setPrototypeOfReflect.setPrototypeOf都会报错。

Promise

异步编程的一种解决方案,比传统的解决方案——回调函数和事件——更合理和更强大。

Promise,简单说就是一个容器,里面保存着某个未来才会结束的事件(通常是一个异步操作)的结果。从语法上说,Promise 是一个对象,从它可以获取异步操作的消息。Promise 提供统一的 API,各种异步操作都可以用同样的方法进行处理。

有两个特点

(1)对象的状态不受外界影响。Promise对象代表一个异步操作,有三种状态:pending(进行中)、fulfilled(已成功)和rejected(已失败)。只有异步操作的结果,可以决定当前是哪一种状态,任何其他操作都无法改变这个状态。

(2)一旦状态改变,就不会再变,任何时候都可以得到这个结果。Promise对象的状态改变,只有两种可能:从pending变为fulfilled和从pending变为rejected。只要这两种情况发生,状态就凝固了,不会再变了,会一直保持这个结果,这时就称为 resolved(已定型)。如果改变已经发生了,你再对Promise对象添加回调函数,也会立即得到这个结果。这与事件(Event)完全不同,事件的特点是,如果你错过了它,再去监听,是得不到结果的。

const promise = new Promise(function(resolve, reject) {
  // ... some code
  if (/* 异步操作成功 */){
    resolve(value);
  } else {
    reject(error);
  }
});

Promise实例生成以后,可以用then方法分别指定resolved状态和rejected状态的回调函数。

promise.then(function(value) {
  // success
}, function(error) {
  // failure
});

then方法指定的回调函数,将在当前脚本所有同步任务执行完才会执行(即使是promise的函数内容那个也得执行完)

resolve函数的参数除了正常的值意外还可以是另一个Promise实例,当p2resolve方法将p1作为参数,即一个异步操作的结果是返回另一个异步操作。这时p1的状态就会传递给p2,也就是说,p1的状态决定了p2的状态。如果p1的状态是pending,那么p2的回调函数就会等待p1的状态改变;如果p1的状态已经是resolved或者rejected,那么p2的回调函数将会立刻执行。

Promise.prototype.then()

then方法是定义在原型对象Promise.prototype上的。它的作用是为 Promise 实例添加状态改变时的回调函数。第一个参数是resolved状态的回调函数,第二个参数是rejected状态的回调函数,它们都是可选的。

该方法返回的是一个新的实例(不是原来的那个Promise实例),所以可以有链式写法,因为是新的实例那么就有value值的传递,即返回的参数就是

Promise.prototype.catch()

讲到这里了,那么接收reject参数的不要用then了用catch

Promise 对象的错误具有“冒泡”性质,会一直向后传递,直到被捕获为止,即被catch方法处理。也就是说,错误总是会被下一个catch语句捕获。

Promise.prototype.catch()方法是.then(null, rejection).then(undefined, rejection)的别名,用于指定发生错误时的回调函数。

getJSON('/posts.json').then(function(posts) {
  // ...
}).catch(function(error) {
  // 处理 getJSON 和 前一个回调函数运行时发生的错误
  console.log('发生错误!', error);
});

上面代码中,getJSON()方法返回一个 Promise 对象,如果该对象状态变为resolved,则会调用then()方法指定的回调函数;如果异步操作抛出错误,状态就会变为rejected,就会调用catch()方法指定的回调函数,处理这个错误。另外,then()方法指定的回调函数,如果运行中抛出错误,也会被catch()方法捕获。

reject()方法的作用,等同于抛出错误。(如果 Promise 状态已经变成resolved,再抛出错误是无效的。)

注意

跟传统的try/catch代码块不同的是,如果没有使用catch()方法指定错误处理的回调函数,Promise 对象抛出的错误不会传递到外层代码,即不会有任何反应。

const someAsyncThing = function() {
  return new Promise(function(resolve, reject) {
    // 下面一行会报错,因为x没有声明
    resolve(x + 2);
  });
};

someAsyncThing().then(function() {
  console.log('everything is great');
});

setTimeout(() => { console.log(123) }, 2000);
// Uncaught (in promise) ReferenceError: x is not defined
// 123

Promise 内部的错误不会影响到 Promise 外部的代码,通俗的说法就是“Promise 会吃掉错误”。即使是在catch函数内部,若有错误也会报错

Promise.prototype.finally()

finally()方法用于指定不管 Promise 对象最后状态如何,在执行完thencatch指定的回调函数以后(或这一步跳过),都会执行finally方法指定的回调函数。

他不接受参数,不会知道前面的状态是成功还是失败,即不依赖于 Promise 的执行结果。

Promise.prototype.finally = function (callback) {
  let P = this.constructor;
  return this.then(
    value  => P.resolve(callback()).then(() => value),
    reason => P.resolve(callback()).then(() => { throw reason })
  );
};
Promise.all()

Promise.all()方法用于将多个 Promise 实例,包装成一个新的 Promise 实例。

全部都是resolved状态p才是,但凡有一个rejected那么p就是

const p = Promise.all([p1, p2, p3]);

如果作为参数的 Promise 实例,自己定义了catch方法,那么它一旦被rejected,并不会触发Promise.all()catch方法。(因为catch的结果也是一个实例对象,而处理好之后相当于resolved状态)

Promise.race()

只要p1p2p3之中有一个实例率先改变状态,p的状态就跟着改变。那个率先改变的 Promise 实例的返回值,就传递给p的回调函数。

Promise.allSettled()

Promise.allSettled()方法接受一个数组作为参数,数组的每个成员都是一个 Promise 对象,并返回一个新的 Promise 对象。只有等到参数数组的所有 Promise 对象都发生状态变更(不管是fulfilled还是rejected),返回的 Promise 对象才会发生状态变更。

const resolved = Promise.resolve(42);
const rejected = Promise.reject(-1);
const allSettledPromise = Promise.allSettled([resolved, rejected]);
allSettledPromise.then(function (results) {
  console.log(results);
});
//返回值是一个对象数组
// 异步操作成功时
{status: 'fulfilled', value: value}

// 异步操作失败时
{status: 'rejected', reason: reason}
Promise.any()

只要参数实例有一个变成fulfilled状态,包装实例就会变成fulfilled状态;如果所有参数实例都变成rejected状态,包装实例就会变成rejected状态

Promise.any()抛出的错误,不是一个一般的 Error 错误对象,而是一个 AggregateError 实例。它相当于一个数组,每个成员对应一个被rejected的操作所抛出的错误。

Promise.resolve()

有时需要将现有对象转为 Promise 对象,Promise.resolve()方法就起到这个作用。

Promise.resolve('foo')
// 等价于
new Promise(resolve => resolve('foo'))

Promise.resolve()方法的参数分成四种情况。

(1)参数是一个 Promise 实例

如果参数是 Promise 实例,那么Promise.resolve将不做任何修改、原封不动地返回这个实例。

(2)参数是一个thenable对象

thenable对象指的是具有then方法的对象,比如下面这个对象。Promise.resolve()方法会将这个对象转为 Promise 对象,然后就立即执行thenable对象的then()方法。

let thenable = {
  then: function(resolve, reject) {
    resolve(42);
  }
};

let p1 = Promise.resolve(thenable);
p1.then(function (value) {
  console.log(value);  // 42
});

(3)参数不是具有then()方法的对象,或根本就不是对象

如果参数是一个原始值,或者是一个不具有then()方法的对象,则Promise.resolve()方法返回一个新的 Promise 对象,状态为resolved

(4)不带有任何参数(除它之外都是立即执行)

Promise.resolve()方法允许调用时不带参数,直接返回一个resolved状态的 Promise 对象。立即resolve()的 Promise 对象,是在本轮“事件循环”(event loop)的结束时执行,而不是在下一轮“事件循环”的开始时。setTimeout在下一轮“事件循环”开始时执行

const p = Promise.resolve();

p.then(function () {
  // ...
});
Promise.reject()

Promise.reject(reason)方法也会返回一个新的 Promise 实例,该实例的状态为rejected,会立即执行。

Promise.try()

解决的问题:想要用Promise但是不区分f函数是同步函数还是异步函数。

  1. 用async函数来写立即执行的匿名函数,它会吃掉抛出的错误,要用catch来处理
  2. new Promise组成的匿名函数
Promise.try(() => database.users.get({id: userId}))
  .then(...)
  .catch(...)

database.users.get({id: userId})是一个Promise对象,

Iterator

遍历器(Iterator)就是这样一种机制。它是一种接口,为各种不同的数据结构提供统一的访问机制,即for...of。任何数据结构只要部署 Iterator 接口,就可以完成遍历操作(即依次处理该数据结构的所有成员)。

默认的iterator接口

默认的 Iterator 接口部署在数据结构的Symbol.iterator属性,或者说,一个数据结构只要具有Symbol.iterator属性,就可以认为是“可遍历的”

Symbol.iterator

这个属性本身是一个函数,就是当前数据结构默认的遍历器生成函数。执行这个函数,就会返回一个遍历器,至于属性名Symbol.iterator,它是一个表达式,返回Symbol对象的iterator属性。

for…of

一个数据结构只要部署了Symbol.iterator属性,就被视为具有 iterator 接口,就可以用for...of循环遍历它的成员。也就是说,for...of循环内部调用的是数据结构的Symbol.iterator方法。

for...of循环可以使用的范围包括数组、Set 和 Map 结构、某些类似数组的对象(比如arguments对象、DOM NodeList 对象)、后文的 Generator 对象,以及字符串。

JavaScript 原有的for...in循环,只能获得对象的键名,不能直接获取键值。ES6 提供for...of循环,允许遍历获得键值。

let arr = [3, 5, 7];
arr.foo = 'hello';

for (let i in arr) {
  console.log(i); // "0", "1", "2", "foo"
}

for (let i of arr) {
  console.log(i); //  "3", "5", "7"
}

for...of循环调用遍历器接口,数组的遍历器接口只返回具有数字索引的属性。

类似数组的对象

并不是所有类似数组的对象都有Iterator接口,用Array.from方法转为数组,就可以用for…of了。

对象
  1. Object.keys方法得到的键名生成一个数组,

    for (var key of Object.keys(someObject)) {
      console.log(key + ': ' + someObject[key]);
    }
    
  2. 另一个方法是使用 Generator 函数将对象重新包装一下。

for…of和foreach和for…in比较

foreach无法中途跳出forEach循环,break命令或return命令都不能奏效。

for...in循环主要是为遍历对象而设计的,不适用于遍历数组

Generator函数

ES6 提供的一种异步编程解决方案, Generator 函数就是遍历器生成函数,返回iterator对象。形式上,Generator 函数是一个普通函数,但是有两个特征。一是,function关键字与函数名之间有一个星号;二是,函数体内部使用yield表达式,定义不同的内部状态。

function* helloWorldGenerator() {
  yield 'hello';
  yield 'world';
  return 'ending';
}
//调用之后不会有任何操作,因为他是generator,第一次暂停是因为这个
//后面的暂停都因为yield
var hw = helloWorldGenerator();
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 函数,返回一个遍历器对象,代表 Generator 函数的内部指针。以后,每次调用遍历器对象的next方法,就会返回一个有着valuedone两个属性的对象。

yield表达式

yield表达式就是暂停标志。(只能用在generator函数中)

遍历器对象的next方法的运行逻辑:

  1. 遇到yield表达式,就暂停执行后面的操作,并将紧跟在yield后面的那个表达式的值,作为返回的对象的value属性值。
  2. 若没有遇到yield,继续执行知道遇到该表达式或者return否则直到结束。
  3. return语句后面的表达式的值,作为返回的对象的value属性值。如果该函数没有return语句,则返回的对象的value属性值为undefined

注意:yield表达式后面的表达式,只有当调用next方法、内部指针指向该语句时才会执行。

表达式的位置

如果用在另一个表达式之中,必须放在圆括号里面。

console.log('Hello' + (yield));
console.log('Hello' + (yield 123));
与 Iterator 接口的关系

由于 Generator 函数就是遍历器生成函数,因此可以把 Generator 赋值给对象的Symbol.iterator属性,从而使得该对象具有 Iterator 接口。

Generator 函数执行后,返回一个遍历器对象。该对象本身也具有Symbol.iterator属性,执行后返回自身。

function* gen(){
  // some code
}
var g = gen();
g[Symbol.iterator]() === g
// true

上面代码中,gen是一个 Generator 函数,调用它会生成一个遍历器对象g。它的Symbol.iterator属性,也是一个遍历器对象生成函数,执行后返回它自己。

next方法的参数
var reset = yield i;

对于这个式子如果只执行next();返回i的值,但是reset接收不到值,赋值操作被yield阻止了,下一次调用next函数时,设置的参数就代表着前一次yield表达式的值,在此例中就是rest的值了,也正因如此,在第一次使用next方法时,传递参数是无效的

若想要第一次使用的时候久传参Generator 函数如果不用wrapper先包一层

for…of循环

它可以自动遍历 Generator 函数运行时生成的Iterator对象,且此时不再需要调用next方法。需要注意,一旦next方法的返回对象的done属性为truefor...of循环就会中止,即return后面的返回值for…of不会遍历到。

用它遍历的时候,返回的value就是yield表达式的值。

原生的 JavaScript 对象没有遍历接口,无法使用for...of循环,通过 Generator 函数为它加上这个接口,就可以用了。

      function* dataConsumer(obj) {
            let keyArray = Reflect.ownKeys(obj);
            for (let key of keyArray) {
                yield [key, obj[key]]
            }
        }
        let student = {
            name: 'hae',
            age: 18
        };
        for (let [key, value] of dataConsumer(student)) {
            console.log(`名字是${key}年龄是${value}`);
        }
Generator.prototype.throw()

Generator 函数返回的遍历器对象,都有一个throw方法,可以在函数体外抛出错误,然后在 Generator 函数体内捕获。

注意,不要混淆遍历器对象的throw方法和全局的throw命令。用遍历器对象的throw方法抛出的,函数内部又部署了try...catch代码块,就会被函数内的catch捕获,若有第二个i.throw()则交给函数体外的,若是用throw命令抛出的,只能被函数体外的catch语句捕获。

//有try...catch代码块
var g = function* () {
  try {
    yield;
  } catch (e) {
    console.log(e);
  }
};
var i = g();
i.next();

i.throw('a');//内部输出a

try {
  i.throw('a');
  i.throw('b');
} catch (e) {
  console.log('外部捕获', e);
}//内部输出a,外部输出b

try {
  throw new Error('a');
  throw new Error('b');
} catch (e) {
  console.log('外部捕获', e);
}//外部输出a,正常的throw语句处理一个就不再处理了。

//没有tey...catch代码块
i.throw();//没有任何try...catch代码块可以捕获这个错误,导致程序报错,中断执行。

throw方法抛出的错误要被内部捕获,前提是必须至少执行过一次next方法。因为在这之前函数还没开始执行。只要 Generator 函数内部部署了try...catch代码块,那么遍历器的throw方法抛出的错误,不影响下一次遍历,还会自动执行一次next()。

**一旦 Generator 执行过程中抛出错误,且没有被内部捕获,就不会再执行下去了。**如果此后还调用next方法,将返回一个value属性等于undefineddone属性等于true的对象,即 JavaScript 引擎认为这个 Generator 已经运行结束了。

外面写的可以被函数内部捕获,函数内部的也可以被外面捕获没有返回值只有错误信息,是catch的参数。

Generator.prototype.return()

可以返回给定的值,并且终结遍历 Generator 函数。

g.return('aaaa');//{ value: 'aaaa', done: true}
g.return() // { value: undefined, done: true }
try...finally

如果有try...finally代码块且正在执行try代码块,那么return()方法会导致立刻进入finally代码块,不会修改done值为true,执行完以后,整个函数才会结束。

next()、throw()、return()

它们的作用都是让 Generator 函数恢复执行,并且使用不同的语句替换yield表达式。

  1. yield表达式替换成一个值
  2. yield表达式替换成一个throw语句
  3. yield表达式替换成一个return语句
yield*表达式

在 Generator 函数内部,调用另一个 Generator 函数时使用。

若不用*那就是返回一个遍历器对象,yield*后面的 Generator 函数(没有return语句时),等同于在 Generator 函数内部,部署一个for...of循环。

任何数据结构只要有 Iterator 接口,就可以被yield*遍历

let read = (function* () {
  yield 'hello';
  yield* 'hello';
})();

read.next().value // "hello"
read.next().value // "h"

遍历完的结果就是return语句的返回值

...
    return "foo"
...
var v = yield* foo();//v:foo
作为对象属性的 Generator 函数
let obj = {
  * myGeneratorMethod() {
    ···
  }
};

上面代码中,myGeneratorMethod属性前面有一个星号,表示这个属性是一个 Generator 函数。

Generator 函数的this

generator函数不是构造函数!

没有this,不能用new

如需让他有

        function* gen() {
            this.a = 1;
            yield this.b = 2;
            yield this.c = 3;
        }

        function F() {
            //用call方法换绑this,
            return gen.call(gen.prototype);
        }
        var f = new F();
        console.log([...f, f.a]);

Generator的异步应用

异步:回调函数、事件监听、发布/订阅、promise

Thunk

传名调用:将参数放到一个临时函数之中,再将这个临时函数传入函数体,而不是先把表达式的值求出来再放入函数中

Generator 函数的流程管理

Thunk 函数现在可以用于 Generator 函数的自动流程管理

co模块

co 模块可以让你不用编写 Generator 函数的执行器。

var co = require('co');
co(gen);

上面代码中,Generator 函数只要传入co函数,就会自动执行。

co函数返回一个Promise对象,因此可以用then方法添加回调函数。

co(gen).then(function (){
  console.log('Generator 函数执行完成');
});

async函数

Generator 函数的语法糖

const asyncReadFile = async function () {
  const f1 = await readFile('/etc/fstab');
  const f2 = await readFile('/etc/shells');
  console.log(f1.toString());
  console.log(f2.toString());
};

一比较就会发现,async函数就是将 Generator 函数的星号(*)替换成async,将yield替换成await

(1)内置执行器。

Generator 函数的执行必须靠执行器,所以才有了co模块,而async函数自带执行器。也就是说,async函数的执行,与普通函数一模一样,只要一行。它一旦遇到await就会先返回,等到异步操作完成,再接着执行函数体内后面的语句

asyncReadFile();

上面的代码调用了asyncReadFile函数,然后它就会自动执行,输出最后结果。这完全不像 Generator 函数,需要调用next方法,或者用co模块,才能真正执行,得到最后结果。

(2)更好的语义。

asyncawait,比起星号和yield,语义更清楚了。async表示函数里有异步操作,await表示紧跟在后面的表达式需要等待结果。

(3)更广的适用性。

co模块约定,yield命令后面只能是 Thunk 函数或 Promise 对象,而async函数的await命令后面,可以是 Promise 对象和原始类型的值(数值、字符串和布尔值,但这时会自动转成立即 resolved 的 Promise 对象)。

(4)返回值是 Promise。

async函数的返回值是 Promise 对象,这比 Generator 函数的返回值是 Iterator 对象方便多了。你可以用then方法指定下一步的操作。

进一步说,async函数完全可以看作多个异步操作,包装成的一个 Promise 对象,而await命令就是内部then命令的语法糖

语法

async函数返回一个 Promise 对象。

函数内部的return语句返回的值则作为then方法回调函数的参数

Promise 对象的状态变化

async函数返回的 Promise 对象,必须等到内部所有await命令后面的 Promise 对象执行完,才会发生状态改变,除非遇到return语句或者抛出错误。也就是说,只有async函数内部的异步操作执行完,才会执行then方法指定的回调函数

await命令

正常情况下,await命令后面是一个 Promise 对象,返回该对象的结果。如果不是 Promise 对象,就直接返回对应的值。

下面给出了一个简化的sleep实现。

function sleep(interval) {
  return new Promise(resolve => {
    setTimeout(resolve, interval);
  })
}

// 用法
async function one2FiveInAsync() {
  for(let i = 1; i <= 5; i++) {
    console.log(i);
    await sleep(1000);
  }
}

one2FiveInAsync();

任何一个await语句后面的 Promise 对象变为reject状态,那么整个async函数都会中断执行。

多个await命令后面的异步操作,如果不存在继发关系,最好让它们同时触发。

// 写法一
let [foo, bar] = await Promise.all([getFoo(), getBar()]);

// 写法二
let fooPromise = getFoo();
let barPromise = getBar();
let foo = await fooPromise;
let bar = await barPromise;

事件循环eventloop

js是单线程的,唯一的事件循环,多个任务队列

宏任务和微任务

在 js 中,任务分为宏任务(macrotask)和微任务(microtask),这两个任务分别维护一个队列,均采用先进先出的策略进行执行!同步执行的任务都在宏任务上执行。

宏任务主要有:script(整体代码)、setTimeout、setInterval、I/O、UI 交互事件、postMessage、MessageChannel、setImmediate(Node.js 环境)。

微任务主要有:Promise.then、 MutationObserver、 process.nextTick(Node.js 环境)。

setTimeout/Promise等称之为任务源。而进入任务队列的是他们指定的具体执行任务,不同任务源的任务会进入不同的任务队列(setTimeout与setInterval是同源的。)

setTimeout作为一个任务分发器,这个函数会立即执行,而它所要分发的任务,也就是它的第一个参数,才是延迟执行

eventloop
  1. 从宏任务的头部取出一个任务执行;(第一次的宏任务就是script!)
  2. 执行过程中若遇到微任务则将其添加到微任务的队列中;
  3. 宏任务执行完毕后,微任务的队列中是否存在任务,若存在,则全部执行,直到执行完毕;
  4. GUI 渲染;
  5. 回到步骤 1,寻找另一个宏任务队列,直到宏任务执行完毕;

Class

class Point {
  // ...
}

typeof Point // "function"
Point === Point.prototype.constructor // true

事实上,类的所有方法都定义在类的prototype属性上面。

Object.assign()方法可以很方便地一次向类添加多个方法。

class Point {
  constructor(){
    // ...
  }
}
Object.assign(Point.prototype, {
  toString(){},
  toValue(){}
});

但是类的内部所有定义的方法,都是不可枚举的(ES6)

Object.keys(Point.prototype)
// []
Object.getOwnPropertyNames(Point.prototype)
// ["constructor","toString"]
constructor 方法

constructor()方法是类的默认方法,通过new命令生成对象实例时,自动调用该方法。一个类必须有constructor()方法,如果没有显式定义,一个空的constructor()方法会被默认添加。

constructor()方法默认返回实例对象(即this),也可以指定返回另外一个对象,那这个对象就不是该类的实例了。

getter和setter

在类的内部可以使用getset关键字,对某个属性设置存值函数和取值函数,拦截该属性的存取行为。

class MyClass {
  constructor() {
    // ...
  }
  get prop() {
    return 'getter';
  }
  set prop(value) {
    console.log('setter: '+value);
  }
}

let inst = new MyClass();
inst.prop = 123;
// setter: 123

inst.prop
// 'getter'

存值函数和取值函数是设置在属性的 Descriptor 对象上的。

Class表达式

与函数一样,它也可以使用表达式的形式定义。

const MyClass = class Me {
  getClassName() {
    return Me.name;
  }
};

但是Me这个类只能在Class内部用,在外部只能用MyClass来代替。而像上述代码类中没有用到Me那么可以省略,它可以携程表达式的形式,就可以写出立即执行的Class

  1. 默认就是严格模式

  2. 不存在提升(必须先定义后使用)

  3. name属性:

    class Point {}
    Point.name // "Point"
    
静态方法

加上static关键字,就表示该方法不会被实例继承,而是直接通过类来调用,这就称为“静态方法”。

class Foo {
  static classMethod() {
    return 'hello';
  }
}

Foo.classMethod() // 'hello'

var foo = new Foo();
foo.classMethod()
// TypeError: foo.classMethod is not a function

如果静态方法包含this关键字,这个this指的是类,而不是实例,静态方法可以与非静态方法重名,实例不能用类的静态方法,但是子类可以有。

//子类
class Bar extends Foo {
}
//子类也可以通过super对象调用
class Bar extends Foo {
  static classMethod() {
    return super.classMethod() + ', too';
  }
}
私有方法和私有属性

私有属性只能在定义它的 class 里面使用。

new.target属性

该属性一般用在构造函数之中,返回new命令作用于的那个构造函数。如果构造函数不是通过new命令或Reflect.construct()调用的,new.target会返回undefined,因此这个属性可以用来确定构造函数是怎么调用的。

var person = new Person('张三'); // 正确
var notAPerson = Person.call(person, '张三');  // 报错

Class 内部调用new.target,返回当前 Class。

子类继承父类时,new.target会返回子类。利用这个特点,可以写出不能独立使用、必须继承后才能使用的类。

**注意:**在函数外部,使用new.target会报错。

Class的继承

Class 可以通过extends关键字实现继承,让子类继承父类的属性和方法。

子类必须在constructor()方法中调用super(),否则就会报错。这是因为子类自己的this对象,必须先通过父类的构造函数完成塑造,得到与父类同样的实例属性和方法,然后再对其进行加工,添加子类自己的实例属性和方法。如果不调用super()方法,子类就得不到自己的this对象。

也意味着新建子类实例时,父类的构造函数必定会先运行一次。也只有调用super()之后,才可以使用this关键字

如果子类没有定义constructor()方法,这个方法会默认添加,并且里面会调用super()。也就是说,不管有没有显式定义,任何一个子类都有constructor()方法。

class ColorPoint extends Point {
}

// 等同于
class ColorPoint extends Point {
  constructor(...args) {
    super(...args);
  }
}

上一节提过:父类的静态方法会被子类继承,但私有方法和私有属性不可以。除非父类定义了私有属性的读写方法。

Object.getPrototypeOf()

Object.getPrototypeOf()方法可以用来从子类上获取父类

Object.getPrototypeOf(ColorPoint) === Point
// true

因此,可以使用这个方法判断,一个类是否继承了另一个类。

super关键字

super这个关键字,既可以当作函数使用,也可以当作对象使用。

作为函数调用时

代表父类的构造函数,子类的构造函数必须执行一次super函数。uper虽然代表了父类A的构造函数,但是返回的是子类B的实例,即super内部的this指的是B的实例。super()在这里相当于A.prototype.constructor.call(this)

作为函数时,只能用在子类的构造函数之中,用在其他地方就会报错。

作为对象时

在普通方法中,指向父类的原型对象(A.prototype);在静态方法中,指向父类。

class A {
  p() {
    return 2;
  }
}

class B extends A {
  constructor() {
    super();
    console.log(super.p()); // 2
  }
}

let b = new B();

由于super指向父类的原型对象,所以定义在父类实例上的方法或属性,是无法通过super调用的。

class A {
  constructor() {
    this.p = 2;
  }
}
A.prototype.x = 2;

class B extends A {
  constructor() {
    super();
    console.log(super.x) // 2
  }
}

let b = new B();
b.m // undefined
class A {
  constructor() {
    this.x = 1;
  }
}

class B extends A {
  constructor() {
    super();
    this.x = 2;
    super.x = 3;
    console.log(super.x); // undefined
    console.log(this.x); // 3
  }
}

let b = new B();

上面代码中,super.x赋值为3,这时等同于对this.x赋值为3。而当读取super.x的时候,读的是A.prototype.x,所以返回undefined

如果super作为对象,用在静态方法之中,这时super将指向父类,而不是父类的原型对象。使用super的时候,必须显式指定是作为函数、还是作为对象使用,否则会报错。

console.log(super); // 报错
类的 prototype 属性和__proto__属性

大多数浏览器的 ES5 实现之中,每一个对象都有__proto__属性,指向对应的构造函数的prototype属性。Class 作为构造函数的语法糖,同时有prototype属性和__proto__属性,因此同时存在两条继承链。

A.prototype(显式原型)=a._proto_(隐式原型)

(1)子类的__proto__属性,表示构造函数的继承,总是指向父类。

B.__proto__ = A;

(2)子类prototype属性的__proto__属性,表示方法的继承,总是指向父类的prototype属性。

p2.__proto__.__proto__ === p1.__proto__ // true

前者原型的原型是后者的原型。

原生构造函数的继承

比如Array等,而继承Object类,无法通过super方法向父类Object传参。这是因为 ES6 改变了Object构造函数的行为,一旦发现Object方法不是通过new Object()这种形式调用,ES6 规定Object构造函数会忽略参数。

模块化

严格模式

ES6 的模块自动采用严格模式,不管你有没有在模块头部加上"use strict";

严格模式主要有以下限制。

  • 变量必须声明后再使用
  • 函数的参数不能有同名属性,否则报错
  • 不能使用with语句
  • 不能对只读属性赋值,否则报错
  • 不能使用前缀 0 表示八进制数,否则报错
  • 不能删除不可删除的属性,否则报错
  • 不能删除变量delete prop,会报错,只能删除属性delete global[prop]
  • eval不会在它的外层作用域引入变量
  • evalarguments不能被重新赋值
  • arguments不会自动反映函数参数的变化
  • 不能使用arguments.callee
  • 不能使用arguments.caller
  • 禁止this指向全局对象
  • 不能使用fn.callerfn.arguments获取函数调用的堆栈
  • 增加了保留字(比如protectedstaticinterface
export

输出变量、函数、类

//要么直接写在定义某个变量的前面
export var firstName = 'Michael';
export function multiply(x, y) {
  return x * y;
};
// 定义之后暴露,记得`{}`
var m = 1;
export {m};
var n = 1;
export {n as m};
//对变量n重命名为m
function f() {}
export {f};

export命令可以出现在模块的任何位置,只要处于模块顶层就可以。如果处于块级作用域内,就会报错

import
// main.js
import { firstName, lastName, year } from './profile.js';

function setName(element) {
  element.textContent = firstName + ' ' + lastName;
}

大括号里面的变量名,必须与被导入模块(profile.js)对外接口的名称相同。

如果想为输入的变量重新取一个名字,import命令要使用as关键字,将输入的变量重命名。

脚本加载了变量a,对其重新赋值就会报错,因为a是一个只读的接口。但是,如果a是一个对象,改写a的属性是允许的。

import后面的from指定模块文件的位置,可以是相对路径,也可以是绝对路径。如果不带有路径,只是一个模块名,那么必须有配置文件,告诉 JavaScript 引擎该模块的位置。

import命令具有提升效果,会提升到整个模块的头部,首先执行。

foo();
import { foo } from 'my_module';

由于import是静态执行,所以不能使用表达式和变量,这些只有在运行时才能得到结果的语法结构。

export default

export default命令用于指定模块的默认输出。显然,一个模块只能有一个默认输出,因此export default命令只能使用一次。所以,import命令后面才不用加大括号,因为只可能唯一对应export default命令。

// 第一组
export default function crc32() { // 输出
  // ...
}
import crc32 from 'crc32'; // 输入

而且可以自己取名字,export default命令其实只是输出一个叫做default的变量,所以它后面不能跟变量声明语句,如export default a的含义是将变量a的值赋给变量default

如果想在一条import语句中,同时输入默认方法和其他接口,可以写成下面这样。

import _, { each, forEach } from 'lodash';
export 与 import 的复合写法

但需要注意的是,写成一行以后,foobar实际上并没有被导入当前模块,只是相当于对外转发了这两个接口,导致当前模块不能直接使用foobar

export { foo, bar } from 'my_module';

// 可以简单理解为
import { foo, bar } from 'my_module';
export { foo, bar };

默认接口的写法如下。

export { default } from 'foo';

具名接口改为默认接口的写法如下。

export { es6 as default } from './someModule';

// 等同于
import { es6 } from './someModule';
export default es6;

介绍const命令的时候说过,const声明的常量只在当前代码块有效。如果想设置跨模块的常量(即跨多个文件),或者说一个值要被多个模块共享,可以采用下面的写法。

// constants.js 模块
export const A = 1;
export const B = 3;
export const C = 4;

// test1.js 模块
import * as constants from './constants';
console.log(constants.A); // 1
console.log(constants.B); // 3

// test2.js 模块
import {A, B} from './constants';
console.log(A); // 1
console.log(B); // 3
import()

import命令会被 JavaScript 引擎静态分析,先于模块内的其他语句执行,命令只能在模块的顶层,不能在代码块之中,但也导致无法在运行时加载模块,require是运行时加载模块,import命令无法取代require的动态加载功能。

引入import()函数,支持动态加载模块。

import(specifier)

import()返回一个 Promise 对象。可以用在任何地方,不仅仅是模块,非模块的脚本也可以使用。import()函数与所加载的模块没有静态连接关系,这点也是与import语句不相同。import()类似于 Node 的require方法,区别主要是前者是异步加载,后者是同步加载。

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值