ECMAScript6的新特性总结

ECMAScript 6(简称ES6)是JavaScript语言的下一代标准,于2015年6月正式发布,也称ECMAScript 2015。

一、ES6新增块级作用域

在块级作用域内声明的变量不会被外部所访问,而在此之前,只能用闭包的方式解决全局变量污染的问题;比如新增变量let/cont;

1、let声明

let声明变量的语法与var一样,但它与var声明最大的不同是let声明的变量的作用域会限制在代码块中,不会被外部所访问,例子如下:

let a = 2;
if(a > 1){
	console.log(b); //用let声明的变量没有声明提前这一特性(报错)
	let b = 20;
	console.log(b);   //20
}
console.log(b);  //由于在作用域外部,无法访问(报错)

在比如for循环的应用

var a = [];
for (let i = 0; i < 10; i++) {
    a[i] = function () {
        console.log(i);
    };
}
a[5]();  //5  而如果i的声明使用var的话,所有的a[i]输出的都只会是9;

let 声明使得每次迭代都会创建一个变量 i,所以循环内部创建的函数会获得各自的变量 i 的拷贝。每份拷贝都会在每次迭代的开始被创建并被赋值。

总结如下:

(1)用 let 声明的变量具有块级作用域特性,只能在声明的块中访问,在块外面无法访问;
(2)用let声明的变量不存在声明提前这一特性;
(3)在同一个块中,let声明的变量也不能重复声明;


2、const声明

const 声明的是常量,一旦声明,值将是不可变的;

var message = "Hello!";
let age = 25;

// 以下两行都会报错
const message = "Goodbye!";
const age = 30;
具体特征如下:

(1)const的特性除了声明的是常量外,其他与let一样;
(2)在let和const声明前的这段区域称之为暂存性死区;
(3)使用let和const声明的变量和常量不再是window的属性。 也就是说通过window.a是无法访问到的;

3、变量的解构赋值

ES6 允许按照一定模式,从数组和对象中提取值,对变量进行赋值,这被称为解构;

let [a, [b], c] = [1, [2], 3];
console.log(a);  //1
console.log(b);	 //2
console.log(c);  //3
解构不仅可以用于数组,还可以用于对象;

对象的解构与数组有一个重要的不同就是数组的元素是按次序排列的,变量的取值由它的位置决定;

而对象的属性没有次序,变量必须与属性同名,才能取到正确的值,代码如下:

let { bar, foo } = { foo: "aaa", bar: "bbb" };
foo // "aaa"
bar // "bbb"
 
let { baz } = { foo: "aaa", bar: "bbb" };
baz // undefined
字符串也具备数组的一些特性,所以字符串也可以解构赋值。代码如下:

const [a, b, c, d, e] = 'hello';
a // "h"
b // "e"
c // "l"
d // "l"
e // "o"
函数的参数也可以使用解构赋值,代码如下:

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



二、函数的新增特性

1、允许带默认参数;

function sum(a = 0, b = 0) {
    return a+b
}
2、默认参数对 arguments 对象不产生影响

function foo(a, b = 30) {
	console.log(arguments[0] === a); //true
	console.log(arguments[1] === b); //true
	a = 10;
	b = 20;
	console.log(arguments[0]  === a); //false
	console.log(arguments[1] === b); //false 
}
foo(1, 2);
3、默认参数表达式,可以是一个表达式或者函数调用等
let value = 10;
function getValue() {
	return value++;
}

function add(first, second = getValue()) {  
	return first + second;
}
4、未命名参数的获得方法

ES5可以采用argument[i]方式获得,而ES6则可以才用如下方式获得:

function foo(a, ...b) {
	console.log(a);
	console.log(b instanceof Array);  //true 多余的参数都被放入了b中。b其实就是一个数组;
}
5、函数中的扩展运算符的使用

let values = [25, 50, 75, 100]
console.log(Math.max(...values));  //使用扩展运算符。相当于拆解了数组了;
console.log(Math.max(...values, 200));  //也可以使用扩展运算符和参数的混用,则这个时候就有 5 个数参与比较了;

三、箭头函数

在ES5中,"this"会随着函数调用位置(全局/闭包),和函数调用方式(构造/普通)而改变,ES6箭头函数中的"this"总是指向声明时的那个对象;

箭头函数是使用=>语法的函数简写形式。同时支持表达式体和语句体,与(普通的)函数所不同的是,箭头函数和其上下文中的代码共享同一个具有词法作用域的this;

1、箭头函数可以赋值给变量,也可以像匿名函数一样直接作为参数传递,如下:

var sum = (num1, num2) =>{
	return num1 + num2;
}
console.log(sum(3, 4));
var foo = a=> a+3; //因为只有一个参数,所以()可以省略
console.log(foo(4));
2、自执行方法如下:

var person = (name => {
		return {
			name: name,
			age: 30
		}
	}
)("zebinhao");
console.log(person);


四、对象

ES6允许字面量定义对象时,用表达式作为对象的属性名,即把表达式放在方括号内;如上面提到的函数新增的特性;

1、Object.is()

用来比较两个值是否严格相等。它与严格比较运算符(===)的行为基本一致,不同之处只有两个:一是+0不等于-0,二是NaN等于自身;

console.log(+0 == -0);              // true
console.log(+0 === -0);             // true
console.log(Object.is(+0, -0));     // false

console.log(NaN == NaN);            // false
console.log(NaN === NaN);           // false
console.log(Object.is(NaN, NaN));   // true
2、Object.assign()

该方法用来将源对象(source)的所有可枚举属性,复制到目标对象(target);它至少需要两个对象作为参数,第一个参数是目标对象,后面的参数都是源对象。只要有一个参数不是对象,就会抛出TypeError错误;如果目标对象与源对象有同名属性,或多个源对象有同名属性,则后面的属性会覆盖前面的属性;

var target={name:'张三'}
var source={age:'19',sex:'男'}
Object.assign(target, source); //{"name":"张三","age":"19","sex":"男"}
注:这种复制是浅复制,如果属性值是对象的话,只是copy对象的地址值(引用)

3、Symbol

ES6引入了一种新的原始数据类型Symbol,表示独一无二的ID。凡是属性名属于Symbol类型,就都是独一无二的,可以保证不会与其他属性名产生冲突。


五、字符串扩展功能

1、新增字符串的String.prototype.repeat()重复、String.prototype.padStart()/padEnd()补位(未增加覆盖功能,如数组新增的copyWithin);

'zebin'.repeat(3)  // 'zebinzebinzebin'
'he'.padStart(9, 'ab')  // 'abababahe', 若(1, 'ab') -> 'he'
'he'.padEnd(9, 'ab')  // 'heabababa'
2、新增includes() 、startsWith()、endsWith() 方法

var msg = "zebin hao";
console.log(msg.startsWith("zebin"));       // true
console.log(msg.endsWith("o"));             // true
console.log(msg.includes("h"));             // true
3、让模板字符串的构建和解析更直观

const me = 'me', you = 'you';
str = `I am ${me}.You are ${you}.`
4、String.row()

若使用String.raw 作为模板字符串的前缀,则模板字符串可以是原始(raw)的。反斜线也不再是特殊字符,\n 也不会被解释成换行符:

let raw =String.raw`Not a newline: \n`;
console.log(raw ==='Not a newline: \\n');// true
5、isFinite(),isNaN(),isInteger()

在Number对象上,新提供了Number.isFinite()和Number.isNaN()两个方法,用来检查Infinite和NaN这两个特殊值
Number.isInteger()用来判断一个值是否为整数。需要注意的是,在JavaScript内部,整数和浮点数是同样的储存方法,所以3和3.0被视为同一个值;

6、Math对象新增方法

Math.trunc()  //去除一个数的小数部分,返回整数部分
Math.sign()   //判断一个数到底是正数、负数、还是零。它返回五种值:参数为正数,返回+1;参数为负数,返回-1;参数为0,返回0;参数为-0,返回-0;其他值,返回NaN
Math.cbrt	  //计算一个数的立方根
Math.fround   //返回一个数的单精度浮点数形式
Math.hypot    //返回所有参数的平方和的平方根。
Math.expm1(x) //返回ex - 1。
Math.log1p(x) //返回1 + x的自然对数。如果x小于-1,返回NaN。
Math.log10(x) //返回以10为底的x的对数。如果x小于0,则返回NaN。
Math.log2(x)  //返回以2为底的x的对数。如果x小于0,则返回NaN



六、新增基本类型Symbol

ES5基本数据类型有:Number、String、Boolean、Null、Undefined
ES6新增了一种新的数据类型:Symbol
出现的原因:对象的属性使用字符串指定,但是很可能会跟原有的属性名一致,或者后来者造成属性覆盖。以往通常是用 字符串+Date毫秒(或随机数)。为了解决这个头疼的问题,引入了独一无二值数据类型Symbol;

1、创建Symbol

let name = Symbol();   //创建一个Symbol
let person = {};

person[name] = "zebinhao";
console.log(person[name]);     // "zebinhao"
2、判断Symbol

ES6 拓展了 typeof 使其操作 symbol 时返回 “symbol”

3、Symbol.for(字符串)和Symbol.keyFor(symbol类型的值)

Symbol.for(字符串参数):在全局环境中搜索 以该字符串作为参数的Symbol值,如果搜到则返回这个sybol,如果搜不到则创建一个Symbol,并把它注册在全局环境中;

Symbol.keyFor(symbol):返回一个已经注册的symbol的”key”;


七、Set

数据结构Set类似于数组,但是成员的值都是唯一的,没有重复的值,Set函数可以接受一个数组作为参数,用来初始化。向Set加入值的时候,不会发生类型转换,所以5和“5”是两个不同的值;

1、Set结构实例的属性:

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

2、操作方法:

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

3、遍历方法:

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

Set提供了处理一系列值的方式,不过如果想给这些值添加一些附加数据则显得力不从心,所以又提供了一种新的数据结构:Map


八、Map

Map 是一个“超对象”,其 key 除了可以是 String 类型之外,还可以为其他类型(如:对象),他的方法和 Set 差不多:

size:返回成员总数;
set(key, value):设置key所对应的键值,然后返回整个Map结构。如果key已经有值,则键值会被更新,否则就新生成该键;
get(key):读取key对应的键值,如果找不到key,返回undefined;
has(key):返回一个布尔值,表示某个键是否在Map数据结构中;
delete(key):删除某个键,返回true。如果删除失败,返回false;
clear():清除所有成员;
keys():返回键名的遍历器;
values():返回键值的遍历器;
entries():返回所有成员的遍历器;

Map的forEach方法

var map = new Map([
	["name", "zebin"],
	["age", 26],
	["sex", "man"]
]);
map.forEach(function (value, key, ownMap) {
	console.log(`key=${key} ,vlue=${value}`);
	console.log(this);
})


九、Iterator遍历器

统一的接口机制,来处理所有不同的数据结构

Iterator的作用有三个:

(1)是为各种数据结构,提供一个统一的、简便的访问接口;

(2)使得数据结构的成员能够按某种次序排列;

(3)是ES6创造了一种新的遍历命令for...of循环,Iterator接口主要供for...of消费;

Iterator的遍历过程:

创建一个指针,指向当前数据结构的起始位置。也就是说,遍历器的返回值是一个指针对象;
第一次调用指针对象的next方法,可以将指针指向数据结构的第一个成员;
第二次调用指针对象的next方法,指针就指向数据结构的第二个成员;
调用指针对象的next方法,直到它指向数据结构的结束位置;

具体讲下for.....of循环及迭代器:

​ 迭代器只是带有特殊接口(方法)的对象。所有迭代器对象都带有 next() 方法并返回一个包含两个属性的结果对象。这些属性分别是 value 和 done,前者代表下一个位置的值,后者在没有更多值可供迭代的时候为 true 。迭代器带有一个内部指针,来指向集合中某个值的位置。当 next() 方法调用后,指针下一位置的值会被返回;

ES6 提供了生成器,能够更方便的创建迭代器,例子如下:

//生成器函数。  注意中间的 * 不能丢
function * createIterator() {
	//每个yield的后面的值表示我们迭代到的值。   yield也定义了我们迭代的顺序。
	yield 3;
	yield 4;
	yield 2;
}
var it = createIterator();
console.log(it.next().value);   // 3
console.log(it.next().value);   // 4
console.log(it.next().value);   // 2
console.log(it.next().value);  //undefined
注:生成器函数由 function 关键字和之后的星号(*)标识,同时还能使用新的 yield 关键字;yield 关键字只能直接用在生成器内部 ,在其它地方甚至是生成器内部的函数中使用都会抛出语法错误;默认情况下只有 数组、set、Map和字符串才可以使用迭代器去迭代;

ES6 新添加的 for-of 循环就是为迭代而设计的,如下:

var arr = ["a", "c", "b", "d"];
for(var item of arr){
	console.log(item)
}
Symbol.iterator该方法能获得默认的迭代方法,如下:
let s = "abcd";
let its = s[Symbol.iterator](); 
console.log(its.next());  //{value: "a", done: false}


十、Class

ES6引入了Class(类)这个概念,作为对象的模板,通过class关键字,可以定义类;在 ES5 版本中,JS是没有类这个概念,其行为最接近的是创建一个构造函数并在构造函数的原型上添加方法,这种实现也被称为自定义的类型创建;

//class关键字必须是小写,后面就是跟的类名
class PersonClass {
    // 等效于 PersonClass 构造函数。
    constructor(name) {  //这个表示类的构造函数。constuctor也是关键字必须小写;
        this.name = name;  //创建属性。  也叫当前类型的自有属性;
    } 
    // 等效于 PersonClass.prototype.sayName.   这里的sayName使用了我们前面的简写的方式;
    sayName() {
        console.log(this.name);
    }
}
let person = new PersonClass("Nicholas");
person.sayName();   // 输出 "Nicholas"
以下是需要牢记的几个关键点:

(1)类声明和函数定义不同,类的声明是不会被提升的。类声明的行为和 let 比较相似,所以当执行流作用到类声明之前类会存在于暂存性死区内;
(2)类声明中的代码自动运行在严格模式下,同时没有任何办法可以手动切换到非严格模式;
(3)所有的方法都是不可枚举的,这和自定义类型相比是个显著的差异,因为后者需要使用 Object.defineProperty() 才能定义不可枚举的方法;
(4)所有的方法都不能使用 new 来调用,因为它们没有内部方法 ;
(5)不使用 new 来调用类构造函数会抛出错误。也就是 必须使用new 类() 的方式使用;
(5)试图在类的方法内部重写类名的行为会抛出错误(因为在类的内部,类名是作为一个常量存在的);

1、class的继承

Class之间可以通过extends关键字,实现继承,子类会继承父类的属性和方法;

class Father{
	constructor(name){
		this.name = name;
	}
	sayName(){
		console.log(this.name);
	}
}
class Son extends Father{  //extents后面跟表示要继承的类型
	constructor(name, age){
		super(name);  //相当于以前的:Father.call(this, name);
		this.age = age;
	}
	//子类独有的方法
	sayAge(){
		console.log(this.age);
	}
}

var son1 = new Son("李四", 30);
son1.sayAge();
son1.sayName();
console.log(son1 instanceof Son);  // true
console.log(son1 instanceof Father);  //true
关于super的使用,有几点需要注意:

(1)只能在派生类中使用 super(),否则(没有使用 extends 的类或函数中使用)会抛出错误;
(2)必须在构造函数的起始位置调用 super(),因为它会初始化 this。任何在 super() 之前访问 this 的行为都会造成错误。也即是说super()必须放在构造函数的首行;
(3)在类构造函数中,唯一能避免调用 super() 的办法是返回一个对象;

2、class的静态方法

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

class Father{
   static foo(){
	   console.log("我是父类的静态方法");
   }
}
class Son extends Father{

}
Son.foo(); //子类也继承了父类的静态方法。  这种方式调用和直接通过父类名调用时一样的。

3、new.target属性

new是从构造函数生成实例的命令。ES6为new命令引入了一个new.target属性,(在构造函数中)返回new命令作用于的那个构造函数。如果构造函数不是通过new命令调用的,new.target会返回undefined,因此这个属性可以用来确定构造函数是怎么调用的;

(1)Class内部调用new.target,返回当前Class。
(2)子类继承父类时,new.target会返回子类。


十一、模块

ES6 模块加载的机制是值的应用,而不像CommonJS 是值的拷贝;这意味着, ES6 模块内的值的变换会影响模块外对应的值;ES6 遇到 import 时不会立刻执行这个模块,只生成一个动态引用,需要用的时候再去里面找值;所以说 ES6的模块是动态引用,不会缓存值,例子如下:

// foo.js
export let counter = 3;
export function inc(){
  counter++;
}
// main.js
import {counter, inc} from './foo';
console.log(counter);    //3
inc();
console.log(counter);    //4

模块功能主要由两个命令构成:export和import。
export命令用于用户自定义模块,规定对外接口;
import命令用于输入其他模块提供的功能,同时创造命名空间(namespace),防止函数名冲突。

ES6模块具有以下特性:
(1)使用export关键词导出对象。这个关键字可以无限次使用;
(2)使用import关键字将其它模块导入某一模块中。它可用来导入任意数量的模块;
(3)支持模块的异步加载;
(4)为加载模块提供编程支持;



十二、Promise + Generator

ES6的异步编程的方式是Generator + promise;

ES6以前,异步编程通常使用回调函数的方式实现。层层嵌套,并且每层都要手动捕捉错误,很多外部库通过promise对象来降低代码的耦合性;jQuery的Deferred对象是其中一种非标准的实现;ES6新增Promise类型,使用面向对象式的方式构造,需要用new操作符调用,返回一个promise对象。内部有三种状态:Pending(进行中) Resolved(已完成) Rejected(已失败)。当成功或失败触发后,状态被冻结;

Promise有Promise.prototype.then(successFun, errorFun)接口,返回的是一个新的Promise实例,可以链式绑定回调函数。若成功则触发前者,失败触发后者,无论前者还是后者,顺利执行后都继续触发下一个then的successFun(但当then(fn, null)或then(null, fn)触发到null/undefined的情况,则以相同的状态和参数触发下一个then),若执行过程中抛出错误,则触发下一个then的errorFun。

若回调函数无返回值,参数一直原样传递到下一个回调,执行出错,则传递error对象。若回调返回promise对象,则等待该promise对象来调用then返回的promise。若返回其他值,则改变需要传递的参数为返回值给下一个回调。

var promise = new Promise(function(resolve, reject) {
  // ... some code

  if (/* 异步操作成功 */){
    resolve(value);
  } else {
    reject(error);
  }
});
promise.then(succFun1).then(successFun2).catch(errorFun);
而Generator如之前提到的迭代方法,其有出色的控制流管理,但仅仅如此还不够,因为必须要ES6调用栈的代码去控制Generator的next调用的时机。如果yield后跟着promise对象,则需要等该对象锁定状态后才能调用next,可以配合then完成这件事,递归实现自动的next流程控制。

function getFoo (){
return new Promise(function(resolve, reject){
    resolve('foo');
});
}
var g = function*(){
try{
	var foo =yield getFoo();
    console.log(foo);
}catch(e){
    console.log(e);
}
};
function run (generator){
	var it = generator();
	function go(result){
		if(result.done)
			return result.value;
		return result.value.then(function(value){
			return go(it.next(value));
		},function(error){
			return go(it.throw(value));
		});
	}
	go(it.next());
}
run(g);


题外话:我觉得JavaScript越来越像java了,不仅在语法结构上,类型上,用法上,都在向java靠拢,开始慢慢摆脱弱类型语言,走向强类型语言方向。我本身具备java/nodejs等后台语言基础,所以在看ECMAScript6的时候,总感觉有后端语言的影子......


你可能感兴趣的:(web前端)