ES6 泛指 ES5.1 以后的 JavaScript 的下一代标准,涵盖了 ES2015、ES2016、ES2017 等等…
当前的整理基于阮一峰的ES 6教程,即主要是 ES2015 的新技术。
ES2015 的主要更新:
变量
let
新增块级作用域,丰富 JavaScript 的变量定义方法。解决 var 的一些历史遗留问题。
简单的说块级作用域,即大括号内有效的变量。
for (let i = 0 ; i < 10; i++ {
console.log(i) ;//0,1,2
}
console.log(i);//undefined ,如果是 var 此时打印的是 9
相对于 var ,let 主要有一下特性。
let 不存在变量提升
console.log(foo);//输出undefined
var foo = 2
console.log(foo);//报错 ReferenceError
let bar = 2;
使用 var 时,存在变量提升,即脚本运行时,foo 便已经存在,但是没有报错,是 undefined。
暂时性死区
块级作用域的变量不受外部变量影响。而且只要块级作用域内,存在 let 变量,那么在当前区域内在 let 声明之前就无法使用改变量。
typeof name ;//undefined
var name = 'mx';
if(true){
console.log(mx);// ReferenceError
typeof name ;// ReferenceError
let name = 'mm';
}
暂时性死区,即使指 let 之前使用变量将会报错。
不允许重复声明
再一个块级作用域内重复 let 声明变量会报错。
块级作用域
本质上 ES5 的 let 相当于为 JavaScript 新增了块级作用于的功能,区别于函数作用于,块级作用于更有利于编程规范。
块级作用于本质上是一个语句,将多个操作封装在一起,没有返回值。
const
const 声明一个只读常量,作用域为块级作用域,具有 let 的块级作用域特性。
const 实际上保证的并不是变量的值不得改动,而是变量指向的那个内存地址不得改动。
ES6 声明变量的6种方法
ES5 只有两种方法: var 和 function。ES6 除了增加了 let 和 const,还增加了 import 和 class 命令。所以 ES6 一共有6中声明变量的方法。
解构赋值
ES6 中允许按照一定的模式从数组和对象中提取值,然后对变量进行赋值,这被称为解构赋值。
数组的解构赋值
let [a,b,c] = [1,2,3];
let [a,[b,c]] = [1,[2,3]];
let [a,b,c] = new Set([1,2,3]);
let [a,b = 2] = [1];//默认值
let [a,b = 2] = [1,undefined]; // b = 2;
let [a,b = 2] = [1,null]; // b = null;
事实上,只要某种数据解构具有 Iterator 接口,都可以采用数组形式的解构赋值。
ES6 内部使用严格相等运算符(=== undefined)来判断一个位置是否有值,即是否启用默认值。
对象的结构赋值
let {name,sex} = {name:'mx',sex:'man'};
let {name,age = 18,loc = {address:'china'}} = {name:'mx',sex:'man'};//默认值
let {name,sex:sexStr} = {name:'mx',sex:'man'} // 重命名 sex 为 sexStr
let {name:{first,last},sex} = {name:{first:'g',last:'mx',sex:'man'}; //嵌套赋值,这时 name 是模式,不是变量,如果要对 name 也赋值,则需要如下写法
let {name,name:{first,last},sex} = {name:{first:'g',last:'mx',sex:'man'};
字符串的解构赋值
字符串也可以解构赋值。这是因为此时字符串被转换成了一个类似数组的对象。
类数组对象都有一个 length 属性,因此还可使用属性进行解构赋值。
let [first,last] = 'mx';
first // 'm';
last // 'x';
let {length:len} = 'mx';
lenght // 2;
数值和布尔值的解构赋值
数值或者布尔值结构赋值时,都会先转为对象。(似乎没啥用)
let {toString : s} = 123 ;
s === Number . prototype.toString II true
函数参数的解构赋值
基于数组和对象的结构赋值,可以对函数参数进行同样规则的解构赋值。
function add([x,y]){
return x + y;
}
add([1,2]);//3
结构赋值用途
- 便捷的解析JSON、对象、数组的值
- 交换变量的值。
- 函数中返回多个值。
- 函数参数的定义。
- 函数参数的默认值。
- 遍历。
字符串扩展
字符串的 Unicode 表示法
主要是增加对 UTF-16 的编码支持。
- ES6 改进了字符串中的 Unicode 表示法。
- 新增 codePointAt ,扩展对Unicode 字符的获取。
- 新增 String.fromCodePoint 获取码点对应的字符。
- 新增 at 方法,用于获取指定 UTF-16 位置字符。
新增字符串的遍历器接口
for (let codePoint of 'foo'){
console.log(codePoint);
}
//'f'
//'o'
//'o'
新增 API
- includes()
- startsWith()
- endsWith()
- repeat()
- padStart()
- padEnd()
新增模板字符串
let name = 'mx';
let str = `hello ${name}`;
新增标签模板
标签模板,是函数调用的一种特殊形式,以标签的形式指定函数名称,以模板的形式指定函数参数。
标签模板的函数参数分为两部分:
- 模板中的字符串部分,作为函数的第一个参数,以数组的形式表示
- 模板中的变量部分,为后续参数。
alert`1`; //相当于 alert(1)
function tag(){
console.log(arguments);
}
let name = 'mx';
let age = '18';
let ageType = '岁';
//等价于 tag(stringArr, ... values)
tag`${name}才${age+ageType}?`;// 0: ["", "才", "?", raw: Array(3)] 1: "mx" 2: "18岁"
这个参数中海油一个 raw 属性,这里存储了转移后的原字符串。
模板标签可以理解为对字符串模板在函数参数中的扩展应用,可以更灵活的处理字符串模板。主要用途有:
- 过滤HTML标签,防止用户输入恶意内容。
- i18n 的转换处理。
- 更灵活的自定义模板库 例如 jsx。
i18n 标签模板示例
const langMap = {
en:{msg1:'beijing',msg2:'mx'},
zh:{msg1:'北京',msg2:'星辰'},
}
const lang = 'zh';
function i18n(strs,...vals){
let res = strs[0];
for(let i = 1 ; i < strs.length; i ++){
res = res + langMap[lang][vals[i-1]] + strs[i];
}
return res;
}
i18n`hello ${'msg2'}, ${'msg1'} 欢迎你!`;// "hello 星辰, 北京 欢迎你!
数值的扩展
- 新增了二进制和八进制的新写法,分别用 0b(0B) 和 0o(0O)表示。
- Number 和 Math API 的扩展
- 指数运算符
- 新增 Integer 整数数据类型。
函数的扩展
- 函数的默认值,以解构赋值的方式实现。
- rest 参数,即剩余参数(function tag(name,…args))。
- ES5 规定函数内可以设置严格模式,ES6中规定在函数参数中使用了解构赋值后就不能在函数中使用严格模式了。
- 规范函数名 name 属性。
新增箭头函数
let foo = ()=>'foo';
foo();//'foo'
箭头函数注意事项
- 函数体内this的指向就是定义是所在的对象。
- 不可以作为构造函数
- 不可以使用 arguments,可以使用 rest 参数代替。
- 不可以使用 yield 命令。
尾调用优化
尾调用的优化意义在于调用栈的优化,即内存的节省。
ES6 的尾调用优化只在严格模式下开启,正常模式下是无效的。
一个尾调用的例子:
function f() {
let m = 1;
let n = 2;
return g(m + n);
}
f();
// 等同于
function f() {
return g(3);
}
f();
// 等同于
g(3);
尾递归
这是一个递归案例,这个案例并不能优化调用栈。
function f(n){
if(n === 1) return 1;
return n * f(n-1);
}
优化后的尾递归:
function f(n,total){
if(n === 1) return total;
return f(n-1,n*total);
}
对于递归的尾优化可以解决堆栈溢出的问题。
数组的扩展
扩展运算符
扩展运算符:将一个数组转为用逗号分割的参数序列,其本质是调用对象的 iterator 接口进行遍历。
用于设置函数参数
console.log(...[1,2,3]);
function push(array,...items){
array.push(...items);
}
用于数组扩展
let arr = [1,...[2,3]];
console.log(arr);//[1,2,3];
//等价于
[1].concat([2,3]);
与解构赋值结合
let [first,...rest] = [1,2,3];
字符串
[...'mx']
// ["m", "x"]
转化实现了 Iterator 接口的非数组为数组
例如 NodeList、Set、Map
const nodeList = document.querySelectorAll('div');
const array = [...nodeList];
Array.from
Array.from 用于将类数组对象和可比遍历(iterable)对象转为真正的数组,相比于扩展运算符更强大。
Array.from 也可以将字符串转为数组,结果与扩展运算符一致。
比如以下类数组对象:
let arrayLink = {
'0':'a',
'1':'b',
'2':'c',
length:3
}
这个对象可以用 Array.from转化为数组,但是不能用扩展运算符。
其他新增API
- Array.of
- copyWithin()
- find()和 findIndex()
- fill()//填充数组
- entries()、keys()、values()
- includes()
数组的空位问题
对象的扩展
属性的简介表示:
let mane = 'mx';
let age = 18;
let pat = {name,age};
属性名表达式
let pat = {
['name']:'mx',
age:18
}
新增 Object.is
Object.is 用于判断两个值是否相等,解决 == 和 === 存在的问题。
Object.is 表现基本与 === 基本一致。区别在于 对 +0 -0 和 NaN的判断;
+0 === -1 // true
NaN === NaN //false
Object.is(+0,-0)//false
Object.is(NaN,NaN)//true
新增 Object.assign
Object assign 方法用于将源对象( ource )的所有可枚举属性复制到目标对象( target )。
Object.assign({a:1},{b:2},{c:3});//{a:1,b:2,c:3}
可枚举属性
enumerable 表示属性的可枚举型。es5 有三个操作会忽略 enumerable 为 false 的属性。
- for…in
- Object.keys()
- JSON.stringify()
ES6新增的 Object.assign 操作也会忽略 enumerable 为 false 的属性。
Object.getOwnPropertyDescriptor(obj ,’foo ’)
//{
// value: 123 ,
// writable: true,
// enumerable : true,
// configurable: true
//}
属性的遍历
ES6 下共有5中遍历对象的属性的方法:
- for…in
- Object.keys(obj)
- Object.getOwnPropertyNames(obj)
- Object.getOwnPropertySymbols(obj)
- Reflect.ownKeys(obk)
对象原型
设置对象原型:
__proto__
:非ES6推荐,非JS标准。Object.setPrototypeOf(object, prototype)
:ES6 推荐标准设置对象原型的方法。
读取对象原型:
Object.getPrototypeOf()
:
对象的扩展运算
let {x,y,...z} = {x:1,y:2,a:3,b:4};
x//1
y//2
z//{a:3,b:4}
获取对象的属性
ES5 Object.getOwnPropertyDescriptor 方法用来返回某个对象属性的描述对象
descriptor )。
ES2017 引入了 Object getOwnPropertyDescriptors 方法,返回指定对象所有自身
属性(非继承属性)的描述对象。
Null 传导运算符
?. 判断依据是 null 或者 undefined。
const firstName = message?.body? . user? . firstName I I ’ default ’;
Set、WeakSet 、 Map、WeakMap
Set 集合,用来表示一组不重复的值,其判断依据类似 ===
运算法,区别在于 Set 中 NaN 不会重复出现,而 ===
运算符中 NaN 不等于自身。
const setVal = new Set([1,2,3,3,'3',NaN,NaN]);// 1,2,3,'3',NaN
Set 属性&方法
Set.prototype.size
Set.prototype.add(value)
Set.prototype.delete(value)
Set.prototype.has(value)
Set.prototype.clear()
Set 遍历
keys()/values()
Set 的keys 和 values 结果一致,都是值的遍历entries
遍历器forEach
WeakSet结构与Set类似,也是不重复的值的集合。但是,它与Set有两个区别。首先,WeakSet的成员只能是对象,而不能是其他类型的值。其次,WeakSet中的对象都是弱引用。
Map
JavaScript的对象(Object),本质上是键值对的集合(Hash结构),但是传统上只能用字符串当作键。这给它的使用带来了很大的限制。
为了解决这个问题,ES6提供了Map数据结构。它类似于对象,也是键值对的集合,但是“键”的范围不限于字符串,各种类型的值(包括对象)都可以当作键。也就是说,Object结构提供了“字符串—值”的对应,Map结构提供了“值—值”的对应,是一种更完善的Hash结构实现。如果你需要“键值对”的数据结构,Map比Object更合适。
//仅支持这两种初始化方法
const map = new Map({name:'gmx',age:18});//{name:'gmx',age:18}
const map1 = new Map().set('name','gmx').set('age'm18);;//{name:'gmx',age:18}
Map 的扩展属性
size
get(key)
set(key,value)
has(key)
delete(key)
clear()
Map 遍历
keys()
values()
entries
遍历器forEach
Map 和 Set 的转换
//二维数组转map
const map = new Map([['name','gmx'],['age',18]]);//{name:'gmx',age:18}
//map 转 二维数组
[...map];//[['name','gmx'],['age',18]]
Map 和 对象的装换
// map 转对象
function strMapToObj(strMap) {
let obj = Object.create(null);
for (let [k,v] of strMap) {
obj[k] = v;
}
return obj;
}
// 对象转 map
function objToStrMap(obj) {
let strMap = new Map();
for (let k of Object.keys(obj)) {
strMap.set(k, obj[k]);
}
return strMap;
}
WeakMap
结构与 Map
结构基本类似,唯一的区别是它只接受对象作为键名(null
除外),不接受其他类型的值作为键名,而且键名所指向的对象,不计入垃圾回收机制。
Symbol
Symbol ,ES6 引入的 JavaScript 的第七种原始数据类型,表示独一无二的值。
Symbol 值通过Symbol 函数生成。
let s = Symbol();
console.log(typeof s);// "symbol"
//接受一个字符串作为参数,其主要作用是对当前值的描述(在控制台区显示),并不影像独一无二的特性
let a = Symbol('mx');
let b = Symbol('mx');
console.log(a);// "Symbol('mx')"
console.log(b);// "Symbol('mx')"
a === b;// false;
主要用途
- 作为对象的属性名称,来避免属性名称的冲突。
- 用作静态变量,比如标示枚举。
Symbol.for、Symbol.keyFor
Symbol.for 获取某被标示的 symbol 值,如果没有则创建一个新的。
let s1 = Symbol.for('mx');
let s2 = Symbol.for('mx');
s1 === s2 ;//true
Symbol.keyFor 获取 symbol 值的标示。
let s1 = Symbol.for('mx');
console.log(Symbol.keyFor(s1));//"mx"
由于 Symbol ()写法没有登记机制,所以通过 Symbol() 创建的值,无法用 Symbol.for 再次获取。也就是说 Symbol.for 只能获取通过 Symbol.for 创建的值。Symbol.keyFor 也只能获取通过 Symbol.for 创建的值。
let s1 = Symbol('s1');
let s2 = Symbol.for('s1');
let s3 = Symbol('s3');
s1 === s2 ;// false;
Symbol.keyFor('s1');// "s1"
Symbol.keyFor('s3');// undefined
ES6 还提供了 11 个内置的 Symbol 值,指向语 内部使用的方法。
Proxy
Proxy 用于修改操作的默认行为,属于“元编程”,即对编程语言进行编程。
使用 Proxy 本质上是在使用 Proxy 创建的代理对象,此时已不是 target 本身了。
具体实现是在目标对象前架设一层“拦截”,基本用法如下:
var obj = new Proxy({},{
get:function(target,key,receiver){
console.log(`getting ${key}`);
return Reflect.get(target,key,receiver);
}
})
可以代理的操作
get、set、apply、has、construct、deleteProperty、defineProperty、getOwnPropertyDescription、getPrototypeOf、isExtensible、ownKeys、preventExtensions、setPrototypeOf
Proxy.revocable
Proxy.revocable 方法返回一个可销毁的代理。
let target= {};
let handler= {} ;
let {proxy, revoke} = Proxy . revocable(target , handler);
proxy.foo = 123;
proxy.foo //123
revoke() ;
proxy.too II TypeError: Revoked
this 问题
由于使用 Proxy 本质上是在使用 Proxy 创建的代理对象,此时已不是 target 本身了,所以要注意此时的 this 指向性问题。
const target = {
m:function(){
console.log(this === proxy);
}
}
const handler = {};
var proxy = new Proxy(target,handler);
target.m();//false
proxy.m();//true
Reflect
ES6 新增 Reflect 对象,其主要功能是为操作对象提供的心的API,而其目的则主要在于规范。
设计目的:
- 将 Object 对象的一些些明显属于语言内部的方法(比如 Object defineProperty) 放到 Reflect 对象上。
- 修改某些 Object 方法的返回结果,让其变得更合理。
- 让 Object 操作都变成函数行为。
- Reflect 对象的方法与 Proxy 对象的方法一一对应,只要是 Proxy 对象的方法,就
能在 Reflect 对象上找到对应的方法。这就使 Proxy 对象可以方便地调用对应的 Reflect
方法来完成默认行为,作为修改行为的基础。
Reflect 的13 个静态方法
• Reflect.apply(target thisArg,args)
• Reflect.construct( target,args)
• Reflect.get(target,name,receiver)
• Reflect.set(target,name, value,receiver)
• Reflect.defineProperty( target,name,desc)
• Reflect.deleteProperty( target, name)
• Reflect.has( target,name)
• Reflect.ownKeys(target)
• Reflect.isExtensible(target)
• Reflect. preventExtensions( target)
• Reflect.getOwnPropertyDescriptor(target, name)
• Reflect.getPrototypeOf(target)
• Reflect.setPrototypeOf(target, prototype)
Promise
Promise 是异步编程的一种解决方案。所谓 Promise
,简单说就是一个容器,里面保存着某个未来才会结束的事件(通常是一个异步操作)的结果。
基本用法:
//创建一个 promise
var promise = new Promise(function(resolve, reject) {
// ... some code
if (/* 异步操作成功 */){
resolve(value);
} else {
reject(error);
}
});
//指定回调函数
promise.then(function(value) {
// success
}, function(error) {
// failure
});
Iterator 遍历器
目的:为各种数据结构提供统一的遍历访问机制。
实现原理(以 array 为例)
function makeIterator(array) {
var nextIndex = 0;
return {
next: function() {
return nextIndex < array.length ?
{value: array[nextIndex++]} :
{done: true};
}
};
}
Generator 函数
Generator 函数是 ES6 提供的一种异步编程方案。
从语法上来说 Generator 实质上是提供了一种对函数体内部进行分段执行的编程方案。其本质上是将一个 Generator 函数体分割为一个被 Iterator 函数包装的可执行函数组。
从代码层面可以理解为将一段代码分割为一个多段代码的数组,将 原本一次性执行的代码段分割为可以分多次执行的代码段,并在上下文上进行重新梳理。
在应用上主要分为两个角度的有应用:一个是利用其分段特性,应用为状态机。一种是利用分段的特性+上下文管理+异步调用 将多个异步逻辑封装到一个 Generator 函数中,从而优化代码逻辑。
实例方法:
- Generator.prototype.next()
- 即 Iterator 的 next,但是可以传递参数,用作 yield 的返回值。
- Generator.prototype.return()
- 主动返回最终值,并提前终结遍历。
- Generator.prototype.throw()
- 函数内部 try…catch… 捕获的异常,要区分全局异常捕获和函数内部异常捕获的区别。
应用
开关状态
利用 while(true) + Iterator.next 用不退出的特性实现开关功能。
//非 Gen
let clock = true;
function toggle() {
clock = !clock;
if (clock) {
console.log('YES');
} else {
console.log('NO');
}
}
console.log(toggle());//NO
console.log(toggle());//YES
console.log(toggle());//NO
console.log(toggle());//YES
// Gen 则提供了基于 Iterator 的状态切换方案
function* Toggle() {
while (true) {
yield 'YES';
yield 'NO'
}
}
const toggle = Toggle();
console.log(toggle.next());//{ value: 'YES', done: false }
console.log(toggle.next());//{ value: 'NO', done: false }
console.log(toggle.next());//{ value: 'YES', done: false }
console.log(toggle.next());//{ value: 'NO', done: false }
流程状态
利用其语法基础提供的状态流程方案:
function* Gen() {
console.log('执行了 1 相关的操作');
yield 1;
console.log('执行了 2 相关的操作');
yield 2;
console.log('执行了 3 相关的操作');
yield 3;
console.log('执行了 4 相关的操作');
return 4;
}
const gen = Gen();
gen.next();
...
gen.next();
...
gen.next();
...
gen.next();
异步函数
当 yield 返回类似 Promise 的操作时就相当于实现了对异步操作的封装:
function* Gen(){
const res = yield new Promise((reslove)=>{
reslove('MX');
});
console.log(res);
}
const gen = Gen();
gen.next().value.then((res)=>{
gen.next('Hello ' + res);
});
//输出 'Hello MX'
特性
执行 Generator 函数会返回一个遍历器对象。
因为 Generator 函数的执行结果是一个遍历器,因此可以用利用所有 Iterator 相关的特性:
function *foo() {
yield 1;
yield 2;
yield 3;
yield 4;
yield 5;
return 6;
}
for (let v of foo()) {
console.log(v);
}
[...foo()];//[1,2,3,4,5]
可以包含多个 yield 语句,但是只能包含一个 return 语句。
gen.next 函数的参数即当前yield的返回值。
function* foo(x) {
var y = 2 * (yield (x + 1));
var z = yield (y / 3);
return (x + y + z);
}
var a = foo(5);
a.next() // Object{value:6, done:false}
a.next() // Object{value:NaN, done:false}
a.next() // Object{value:NaN, done:true}
var b = foo(5);
b.next() // { value:6, done:false }
b.next(12) // { value:8, done:false }
b.next(13) // { value:42, done:true }
yield* 语句
用于嵌套 Generator 函数
function* bar() {
yield 'x';
yield* foo();
yield 'y';
}
// 等同于
function* bar() {
yield 'x';
yield 'a';
yield 'b';
yield 'y';
}
// 等同于
function* bar() {
yield 'x';
for (let v of foo()) {
yield v;
}
yield 'y';
}
for (let v of bar()){
console.log(v);
}
// "x"
// "a"
// "b"
// "y"
Generator 中的 this 问题
由于执行 Generator 后返回的是一个新的继承自 Generator 构造函数的 Iterator 对象实例,所有存在以下两种特殊情况:
- Generator 的返回值拥有 Generator 的原型链上的属性,方法。
```javascript function* g() {} g.prototype.hello = function () { return ‘hi!’; };
let obj = g();
obj instanceof g // true obj.hello() // ‘hi!’
2. Generator 函数内部的 this 并不是 Generator 返回的实例对象。
```javascript
function* g() {
this.a = 11;
}
let obj = g();
obj.a // undefined
async
ES2017 新增了 async
语法,提供了一种更便捷的异步编程方案。
async
本质上就是 Generator 的语法糖实现,是对 Generator 的封装实现。
async
现对与 Generator 的改进:
- 内置执行器
- 更好的意义
- 更广泛的适用性
- 返回值为 Promise,结合了 Promise 的调用优势。
封装案例:
Generator 对异步的实现案例:
var fs = require('fs');
var readFile = function (fileName) {
return new Promise(function (resolve, reject) {
fs.readFile(fileName, function(error, data) {
if (error) reject(error);
resolve(data);
});
});
};
var gen = function* () {
var f1 = yield readFile('/etc/fstab');
var f2 = yield readFile('/etc/shells');
console.log(f1.toString());
console.log(f2.toString());
};
async 的实现案例:
var asyncReadFile = async function () {
var f1 = await readFile('/etc/fstab');
var f2 = await readFile('/etc/shells');
console.log(f1.toString());
console.log(f2.toString());
};
Class
ES6 提供了更接近传统语言的类的定义方案:class
。
//定义类
class Point {
static type = 1;
static classMethod() {
return 'hello';
}
id = 0;
constructor(x, y) {
this.x = x;
this.y = y;
}
get prop() {
return 'getter';
}
set prop(value) {
console.log('setter: '+value);
}
toString() {
return '(' + this.x + ', ' + this.y + ')';
}
}
一个类的定义,包含了 constructor
构造方法、toString
方法、x/y/id
属性、取值函数(getter)和存值函数(setter)、static
修饰的静态函数和静态属性。
这里有几点需要注意的:
prototype
对象的constructor
属性,直接指向“类”的本身。toString
方法是Point
类内部定义的方法,它是不可枚举的。这一点与ES5的行为不一致。Class
不存在变量提升。Class
中定义的所有方法都是共有方法,不提供私有方法的定义规范。- 类和模块的内部,默认就是严格模式,所以不需要使用
use strict
指定运行模式。 - Class之间可以通过
extends
关键字实现继承。 super
这个关键字,既可以当作函数使用,也可以当作对象使用。- ES6 之前,是无法继承 Array 等原生函数的,新的
Class
可以。
new.target属性
new
是从构造函数生成实例的命令。ES6为 new
命令引入了一个 new.target
属性,(在构造函数中)返回 new
命令作用于的那个构造函数。如果构造函数不是通过 new
命令调用的,new.target
会返回 undefined
,因此这个属性可以用来确定构造函数是怎么调用的。
function Person(name) {
if (new.target !== undefined) {
this.name = name;
} else {
throw new Error('必须使用new生成实例');
}
}
// 另一种写法
function Person(name) {
if (new.target === Person) {
this.name = name;
} else {
throw new Error('必须使用new生成实例');
}
}
var person = new Person('张三'); // 正确
var notAPerson = Person.call(person, '张三'); // 报错
需要注意的是,子类继承父类时,new.target
会返回子类。
ES6 下 mixin 的实现
function mix(...mixins) {
class Mix {}
for (let mixin of mixins) {
copyProperties(Mix, mixin);
copyProperties(Mix.prototype, mixin.prototype);
}
return Mix;
}
function copyProperties(target, source) {
for (let key of Reflect.ownKeys(source)) {
if ( key !== "constructor"
&& key !== "prototype"
&& key !== "name"
) {
let desc = Object.getOwnPropertyDescriptor(source, key);
Object.defineProperty(target, key, desc);
}
}
}
上面代码的 mix
函数,可以将多个对象合成为一个类。使用的时候,只要继承这个类即可。
class DistributedEdit extends mix(Loggable, Serializable) {
// ...
}
修饰器
修饰器(Decorator)是一个函数,用来修改一个类的默认行为 ,修饰器是 ES7 的提案,但是 bable 已经实现了对其转码。
修饰器分为 类修饰器、类方法修饰器、类属性修饰器。
类 修饰器
function testable(target,...args) {
target.isTestable = true;
target.args = args;
}
@testable('MX','XF')
class MyTestableClass {}
console.log(MyTestableClass.isTestable) // true
console.log(MyTestableClass.args);//['MX','XF']
修饰器接受的第一个参数即类本身,其他参数即传入的参数。
类方法/类属性修饰器
function readonly(target, name, descriptor){
// descriptor对象原来的值如下
// {
// value: specifiedFunction,
// enumerable: false,
// configurable: true,
// writable: true
// };
descriptor.writable = false;
return descriptor;
}
class Person {
@readonly
name() { return `${this.first} ${this.last}` }
}
方法的装饰器接受三个参数:target(当前类)、name(当前类名)、方法的 descriptor 描述对象。
向修饰器传递参数
class Example {
@dec(1)
@dec(2)
method(){}
}
function dec(id){
console.log('evaluated', id);
return (target, property, descriptor) => console.log('executed', id);
}
当有多个修饰器时,执行顺序从上往下。
注意:修饰器只能用于类和类的方法,不能用函数,因为函数存在变量提升的问题,会导致执行顺序混乱的问题。
模块化
ES6 之前 JS 有两种模块化方案,CommonJS 和 AMD。CommonJS 是应用于后端Nodejs 的模块化方案,AMD 则是应用于前端的模块化方案,其主要区别在于同步/异步。 因为浏览器浏览器中 CommonJS 同步方案的缺陷而产生了 AMD。
//CommonJS 引入方式
var math = require('math');
//AMD 引入模块方式:
require(['math'], function (math) {
});
ES6 在语言标准的层面上,实现了模块功能,而且实现得相当简单,完全可以取代 CommonJS 和 AMD 规范,成为浏览器和服务器通用的模块解决方案。
模块功能主要由两个命令构成:export
和 import
//ES6 模块导出
export default 1;
// ES6 模块引入
import { stat, exists, readFile } from 'fs';
export 的写法
// 写法一
export var m = 1;
// 写法二
var m = 1;
export {m};
// 写法三
var n = 1;
export {n as m};
//export default
export default function () {
console.log('foo');
}
import 的写法
import {firstName, lastName, year} from './profile';
import { lastName as surname } from './profile';
import loadsh from 'loadsh';
import * as circle from './circle';
import 'lodash';//仅执行 loadsh 代码,不引入任何变量。
import export 复合写法
export { foo, bar } from 'my_module';
// 等同于
import { foo, bar } from 'my_module';
export { foo, bar };
import()
由于 import
命令会被 JavaScript 引擎静态分析,先于模块内的其他模块执行。因此无法满足按需加载的场景需求,因此有一个提案建议使用 import 实现异步加载的功能。
const main = document.querySelector('main');
import(`./section-modules/${someVariable}.js`)
.then(module => {
module.loadPageInto(main);
})
.catch(err => {
main.textContent = err.message;
});
加载规则
浏览器加载 ES6 模块,也使用 <script>
标签,但是要加入 type="module"
属性。
<script type="module" src="foo.js"></script>
模块内脚本的注意事项:
- 代码在模块作用域中运行。
- 同一个模块若多次加载,将只执行一次。
- 模块中顶层this 将返回 undefined ,而不是 window。
ES6 & CommonJS
两大差异:
- CommonJS 模块输出的是一个值的拷贝,ES6 模块输出的是值的引用。
- CommonJS 模块是运行时加载,ES6 模块是编译时输出接口。