ES6
语法糖:语法糖能够增加程序的可读性,从而减少程序代码出错的机会。(更容易表达一个操作的语法)
let和const命令
ES6 明确规定,如果区块中存在let
和const
命令,这个区块对这些命令声明的变量,从一开始就形成了封闭作用域。凡是在声明之前就使用这些变量,就会报错。(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对象。
- ES6中,var命令和function命令声明的全局变量依旧是顶层对象的属性(即它们声明的可以用window.xx来使用)
- 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
,以下三个变量在箭头函数之中也是不存在的,指向外层函数的对应变量:arguments
、super
、new.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
属性不是在该方法上面,而是该方法的属性的描述对象的get
和set
属性上面,返回值是方法名前加上get
和set
。
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
新的数据类型,表示独一无二的值(其他数据类型是:undefined
、null
、布尔值(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...in
、for...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 将其视为一个键。undefined
和null
也是两个不同的键。虽然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
对象上。现阶段,某些方法同时在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
上获取默认行为。
静态方法
-
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.getPrototypeOf
和Object.getPrototypeOf
的一个区别是,如果参数不是对象,Object.getPrototypeOf
会将这个参数转为对象,然后再运行,而Reflect.getPrototypeOf
会报错。 -
Reflect.setPrototypeOf(target, prototype)
如果第一个参数是
undefined
或null
,Object.setPrototypeOf
和Reflect.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实例,当p2
的resolve
方法将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 对象最后状态如何,在执行完then
或catch
指定的回调函数以后(或这一步跳过),都会执行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()
只要p1
、p2
、p3
之中有一个实例率先改变状态,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函数是同步函数还是异步函数。
- 用async函数来写立即执行的匿名函数,它会吃掉抛出的错误,要用catch来处理
- 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了。
对象
-
Object.keys方法得到的键名生成一个数组,
for (var key of Object.keys(someObject)) { console.log(key + ': ' + someObject[key]); }
-
另一个方法是使用 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
方法,就会返回一个有着value
和done
两个属性的对象。
yield表达式
yield
表达式就是暂停标志。(只能用在generator函数中)
遍历器对象的next
方法的运行逻辑:
- 遇到
yield
表达式,就暂停执行后面的操作,并将紧跟在yield
后面的那个表达式的值,作为返回的对象的value
属性值。 - 若没有遇到
yield
,继续执行知道遇到该表达式或者return
否则直到结束。 - 将
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
属性为true
,for...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
属性等于undefined
、done
属性等于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
表达式。
- 将
yield
表达式替换成一个值 - 将
yield
表达式替换成一个throw
语句 - 将
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)更好的语义。
async
和await
,比起星号和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
- 从宏任务的头部取出一个任务执行;(第一次的宏任务就是script!)
- 执行过程中若遇到微任务则将其添加到微任务的队列中;
- 宏任务执行完毕后,微任务的队列中是否存在任务,若存在,则全部执行,直到执行完毕;
- GUI 渲染;
- 回到步骤 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
在类的内部可以使用get
和set
关键字,对某个属性设置存值函数和取值函数,拦截该属性的存取行为。
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
-
默认就是严格模式
-
不存在提升(必须先定义后使用)
-
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
不会在它的外层作用域引入变量eval
和arguments
不能被重新赋值arguments
不会自动反映函数参数的变化- 不能使用
arguments.callee
- 不能使用
arguments.caller
- 禁止
this
指向全局对象 - 不能使用
fn.caller
和fn.arguments
获取函数调用的堆栈 - 增加了保留字(比如
protected
、static
和interface
)
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 的复合写法
但需要注意的是,写成一行以后,foo
和bar
实际上并没有被导入当前模块,只是相当于对外转发了这两个接口,导致当前模块不能直接使用foo
和bar
。
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
方法,区别主要是前者是异步加载,后者是同步加载。