ES6新特性
一、let和const
我们知道,var定义的变量是不具备块级作用的。为了解决这个问题,es6引入了let关键字;而为了定义常量,引入了const关键字。
let
- let声明的函数具有块级作用域。
上面代码中用let定义i,会在每次循环中创建一个独立的块级作用域,这样保证了i的独立,上面的代码就是正确的。而如果使用var定义i,那么代码会出错,点击所有的el都会在控制台打印10.for(let i = 0; i < 10; i++){ els[i].addEventListener(function(){ console.log(i); }, false); }
- let声明的变量在同一个作用域不能重复定义,并且需要先定义才能使用
let a = 2; let a = 3; // 报错,不能重复声明 b = 1; // 报错,因为let定义变量不会变量提升,所以此时b未定义 let b;
const
- const声明的非引用类型常量不能修改
const PI = 3.1415926; PI = 3.14; // 报错,不能改变常量
- const声明的引用类型常量内容可变,指向不可变
const arr = ['a', 'b']; arr.push('c'); // arr->['a', 'b', 'c'] // 可以改变内容 arr = ['d']; // 报错,不能改变指向
二、模板字符串
以前,如果要创建多行字符串或者字符串中插入变量,需要使用反斜杠和+
运算符,这让字符串显得十分复杂。es6引入了模板字符串,它使用反引号声明。 - 简单例子
let msg = 'message'; let str = ` <p>${msg}</p> `;
笔记: 模板字符串使用
${}
插入变量,为了方便记忆,我简单把它叫做插值。 - 表达式插值例子
插值不仅仅支持变量,可以是一般的js表达式。let count = 10; let price = 0.25; let message = `${count} items cost $${(count * price).toFixed(2)}.`; console.log(message); // "10 items cost $2.50."
笔记:
- 插值可以是js表达式。
- 当模板字符串中连续出现两个
$
符号,第一个符号表现为美元符号,不是插值。
- 插值嵌套例子
const arr = ['item1', 'item2']; let str = ` The elements of arr are: ${ arr.map(function(item, idx){ return `${idx+1}、${item}`; }).join('\n') } ` console.log(str); /** " The elements of arr are: 1、item1 2、item2" */
- 标签模板
标签其实是函数,而标签模板就是把模板字符串作为实参的一种特殊的函数调用。// tag函数 const transferHtml = function(iterals, ...substitution){ let dict = { "&" : "&", "<" : "<", ">" : ">" }; let result = ""; let keys = Object.keys(dict); let re = new RegExp("["+keys.join("")+"]", "g"); for(let i = 0; i < substitution.length; i++){ let item = substitution[i]; result += iterals[i]; result += item.replace(re, function(match){ return dict[match]; }); } result += iterals[iterals.length-1]; return result; }; transferHtml`${'<p>pppp</p>'}`; // 调用标签模板 // 结果:"<p>pppp</p>"
笔记:
- tag函数的iterals保存着模板标签被插值分割的字符串
(参考split)
,如果插值在字符串最后,那么iterals的最后一个值的空字符串""
;而substitution是插值的集合,它不是数组类型,而是类数组类型(参考substitution)
- 如果需要把for循环代码改为substitution.forEach,那么substitution一定要使用
...
运算符展开这个类数组对象。
- tag函数的iterals保存着模板标签被插值分割的字符串
三、箭头函数
es6简化了函数的语法表示,一定程度上改善了函数调用时this指针指向多变的问题。
- 声明方法
const add = (arg1, arg2) => { return arg1 + arg2; }; /**等价于 function add(arg1, arg2){ return arg1 + arg2; } */ // 如果函数体简单,并且存在返回值可以再简写: // const add = (arg1, arg2) => arg1+arg2;
- this指向
箭头函数的this指向声明它时所在的对象作用域(全局作用域是global对象的作用域)。const func = () => { console.log(this); }; const obj = {}; func.call(obj); // window(浏览器环境下)
笔记:
- 箭头函数的this和调用对象无关(call、apply失效),只有声明时所在作用域有关。
- 箭头函数没有arguments对象,可以用rest参数代替
- 箭头函数不能作为构造函数
- 箭头函数不能使用yield
四、函数参数默认值和rest参数
es6允许函数使用参数默认值,rest参数用于将参数转成数组。
// 默认值
const func1 = (arg="arg") => {
console.log(arg);
};
func1(); // "arg"
func1("arg1"); // "arg1"
// rest参数
const func2 = (...args) => {
console.log(args);
};
func2(1,2,3); // [1, 2, 3];
笔记: rest参数使用
...
操作符指定,它是一个由实参组成的数组
五、解构赋值
es6可以依据结构进行赋值,简化了赋值操作。
- 解析结构对相应变量进行赋值
let {a, b: c, d: d=1} = {a: 'a', 'b': 'b'}; console.log(a + '---' + c + '---' + d); // a---b---1 let [f, g, h=2] = [1, 2]; console.log(f + '---' + g + '---' + h); // 1---2---2
笔记:
{a} = {a: 'a'};
实际上是{a: a} = {a: 'a'}
通过a作为key值查找到对应的value进行赋值。- 数组的解构赋值将对应idx的值进行一一对应,进行相关赋值。
- 对于默认值,如果对应位置等号右边的值为undefined,那么等号左边对应的变量使用默认值。
let [a, b=2, c] = [1, undefined, 3]; console.log(a + '---' + b + '---' + c); // 1---2---3
- 声明变量时使用解构赋值
let a = 1; let b = 2; let obj = {a, b}; console.log(obj); // {a: 1, b: 2}
- 在函数参数中使用解构赋值
function func({a, b} = {'a', 'b'}){ console.log(a, b); } func({a: 1}); // 1 'b'
六、Promise
Promise对象可以将异步编程(特别是ajax)变成链式调用形式,更具可读性和可维护性。 - 简单getJSON函数的封装
const getJSON = (url) => { return new Promise(function(resolve, reject){ // 执行函数 const xmlHttp = window.XMLHttpRequest ? new XMLHttpRequest() : new ActiveXObject("Microsoft.XMLHTTP"); xmlHttp.open('GET', url, true); xmlHttp.send(); xmlHttp.onreadystatechange=function(){ if (xmlHttp.readyState==4 && xmlHttp.status==200){ try{ let response = JSON.parse(xmlHttp.responseText); resolve(xmlHttp.response); // }catch(e){ reject(e); } }else{ reject(new Error(xmlHttp.statusText))); } } }). // 执行函数代码正常时进行后续处理 then(function(res){ console.log(res); }). // 执行函数代码错误时进行的后续错误处理 catch(function(e){ console.log(e); }); }
笔记:
- promise对象在创建时立即执行执行函数,立即执行函数用于分配任务,在执行函数中调用resolve()和reject()函数,将分别触发then和catch函数任务。
- then和catch执行完成会返回一个新的Promise对象实例,故可以链式调用。
- 如果执行函数或者then方法抛出异常,catch方法可以捕获异常并且执行
- 已决或已拒绝Promise对象
// 已决Promise let promise1 = Promise.resolve(<param>); // 已拒绝Promise let promise2 = Promise.reject(<param>); // 已决定了执行后状态的Promise可以直接调用then或者catch promise1.then(function(param){ console.log(param); }); promise2.catch(function(param){ console.log(param); });
- 非Promise的Thenable对象
/** 当一个非Promise对象拥有一个能接受 resolve 与 reject 参数的 then() 方法,这个对象被称为非Promise的Thenable对象; 它能被Promise.resolve和Promise.reject方法调用。 */ let thenable1 = { then: function(resolve, reject) { resolve('resolve'); } }; let thenable2 = { then: function(resolve, reject) { reject('reject'); } }; let t1 = Promise.resolve(thenable1); // 'resolve' t1.then(function(value){ console.log(value); }); let t2 = Promise.resolve(thenable2); // 'reject' t2.catch(function(value){ console.log(value); });
- 串行Promise
/** then方法可以连续多个,使用return返回值进行参数传递 */ let p1 = new Promise(function(resolve, reject) { resolve(42); }); p1.then(function(value) { console.log(value); // 42 return value + 1; // 传递到下一个then的参数 }).then(function(value) { console.log(value); // 43 });
多Promise对象处理
- Promise.all()
使用Promise处理异步操作时,当一个异步操作需要在多个异步操作执行完成后执行,那么可以使用Promise对象all表示这种逻辑。let p1 = new Promise(function(resolve, reject) { resolve(42); }); let p2 = new Promise(function(resolve, reject) { resolve(43); }); let p3 = new Promise(function(resolve, reject) { resolve(44); }); let p4 = Promise.all([p1, p2, p3]); // p4在p1、p2、p3都执行完才执行 p4.then(function(value) { console.log(Array.isArray(value)); // true console.log(value[0]); // 42 console.log(value[1]); // 43 console.log(value[2]); // 44 });
- Promise.race
类似all方法,如果需要表示一个Promise在多个Promise对象中存在一个执行完成才执行的逻辑,可以使用Promise对象的race方法。let p1 = new Promise(function(resolve, reject) { resolve(42); }); let p2 = new Promise(function(resolve, reject) { resolve(43); }); let p3 = new Promise(function(resolve, reject) { resolve(44); }); let p4 = Promise.race([p1, p2, p3]); // p4在p1、p2、p3中的一个执行完才执行 p4.then(function(value) { console.log(value); // 42 });
笔记: all和race创建的Promis都会在一个Promise返回拒绝状态时,立即执行,并呈现拒绝状态,执行catch方法,而不需要等待其它Promise的执行。
七、set和map
es6新增了set和map集合,set是一种成员唯一的类似数组的集合,map是一种类似js字面量对象(JSON)的键值对集合(键不仅可以是字符串还可以是对象)。
- set
// 普通set // --------------------------- let set1 = new Set(); set1.add(5); // 添加2 set1.add(5); // 重复会被忽略 let set2 = new Set([1, 2]); // 使用数组创建Set set2.has(2); // 判断set2是否存在2 set2.delete(2); // 删除2 set2.clear(); // 清空set2 set2.size; // set2的长度 set2.forEach(function(){}, this); // set可以使用forEach let arr = [...set1]; // set转成数组 // Weak Set // Weak Set只能存储引用类型,能够保证set的对象元素被设为null时,会被垃圾回收机制回收。 // ------------------------------ let weakSet1 = new WeakSet(); let weakSet2 = new WeakSet([1, 2]); // weak set的操作与set一致。
- map
// 普通map // ------------------------- let map = new Map(); // 初始化空的map map.set('key', 'value'); // 添加键值对 map.get('key'); // 获取键值对 map.has('key'); // 判断是否存在键值对 map.delete('key'); // 删除键值对 map.clear(); // 清空map map.size; // 键值对的个数 map = new Map([['key1', 'key2'], ['val1', 'val2']]); // 使用数组创建map map.forEach(function(){}, this); // map可以使用forEach // Weak Map // Weak Map只能存储键是引用类型的键值对,作用与weak set一样。 // ----------------------------- let weakMap = new WeakMap(); let key = {}; weakMap.set(key, 'val');
笔记:
- set和map都具有has、delete、clear和forEach方法,也都存在着各自的迭代方式(见迭代器)。
- weakset和weakmap都不能对内容进行过度操控,没有forEach、delete、clear方法,没有迭代器方式,没有size属性。
八、迭代器和生成器
es6新增了迭代器用于处理循环遍历问题,而生成器则是用于生成迭代器的函数。
- 迭代器与迭代对象
// 迭代器是使用next方法一步一步遍历元素的对象 // es5实现迭代器 function createIterator(items){ var i = 0; return { next: function(){ var done = i >= items.length; // 遍历是否完成 var value = !done ? value[i++] : undefined; return { done: done, value: value }; } }; } var iter = createIterator([1,2,3]); // 创建遍历数组的迭代器 iter.next(); // 返回下一个元素 // es6迭代对象 let arr = [1,2,3]; // 数组是可迭代对象,因为具有Symbol.iterator属性 // 该属性是一个函数,它用于创建默认迭代器arr[Symbol.iterator](); // js引擎后台在of关键字使用时,自动调用该函数创建迭代器,并使用next等方法自动遍历可迭代对象 for(let item of arr){ console.log(item); }
- 生成器
// 带星号的函数被称为generator(生成器) // 普通生成器 function *generator(){ yield 1; yield 2; yield 3; } let iter = generator(); // 生成器会依据yield返回迭代器对象,每个yield的返回都会暂停执行函数,等待迭代器对象使用下一个值才执行。 iter.next().value; // 1 iter.next().value; // 2 iter.next().value; // 3 // 传参生成器 function *createIterator(){ // 先执行yield返回后,再执行赋值操作 let first = yield 1; let second = yield frist + 2; let third; try{ third = yield second + 3; }catch(e){ third = 'throw error'; } return; // 提前结束generator yield 2; } let iter = createIterator(); iter.next(2).value; // 1 iter.next(3).value; // first+2 -> 3 iter.next(new Error('error')); // throw error iter.next().value; // undefined // 生成器委托 // 生成器委托其实像函数把相同的代码进行封装,函数里面调用多个函数的情况 function subIterator1(){ yield 1; yield 2; } function subIterator2(){ yield 3; yield 4; } function *createIterator(){ yield *subIterator1(); yield *subIterator2(); } let iter = createIterator(); let item; do{ item = iter.next(); console.log(item.value); }while(!item.done); // 结果: // 1 // 2 // 3 // 4 // undefined
- 任务执行器
function run(taskDef){ let task = taskDef(); // 创建迭代器 let result = task.next(); // 启动任务,步骤执行1次 // 循环步骤 function step(){ // 如果任务未完成{ if(!result.done){ // 是否是执行任务的逻辑函数 if(typeof result.value==='function'){ result.value(function(err, data){ if (err) { result = task.throw(err); return; } result = task.next(data); step(); }); }else{ result = task.next(result.value); step(); } } } // 执行循环函数 step(); } // 使用 let fs = require("fs"); function readFile(filename) { return function(callback) { fs.readFile(filename, callback); }; } run(function*() { let contents = yield readFile("config.json"); doSomethingWith(contents); console.log("Done"); });
笔记:
- 字符串、数组、map、set、object、NodeList都可以使用of进行遍历,它们是可迭代对象。
- values()、keys()、entries()方法都会返回一个可迭代对象
九、对象和类
es6的类使用class关键字来声明,它增强了原型继承方式,可以使用extend关键字来声明继承关系,还增加了新的方法定义方式、super关键字等内容。
- 对象方法简写
let obj = { name: 'obj', getName() { // 简写方式允许使用super(); return this.name; } };
- 对象变量名属性
let lastName = 'lastName'; let obj = { 'firstName': 'first', // [lastName]: 'second' }; // 或者 let suffix = 'Name'; let obj = { // ['first' + suffix]: 'first', // ['second' + suffix]: 'second' };
- 对象相关原型方法
Object.create(Array); // 返回原型为Array的对象,并且可以使用prototype访问;并非只有\_\_proto\_\_属性 Object.getPrototypeOf(obj); // 获取obj的原型对象 Object.setPrototype(obj, Object); // 设置obj的原型对象为Object
- super
// 不使用super访问原型方法 let person = { name: 'Person', getName() { return this.name; } }; let son = { name: 'Son', getName() { // 访问原型中的方法 return Object.getPrototypeOf(this).getName.call(this); } }; Object.setPrototypeOf(son, person); son.getName(); // son /* 然而,如果原型链过长,并不好用; 下面代码出现栈溢出错误。这是因为: mike调用getName时this是mike, 而它的原型是son, 故反复调用son的getName方法 */ let mike = Object.create(Object); Object.setPrototypeOf(mike, son); mike.name = 'mike'; mike.getName();
- 类的声明
// 匿名 let NoName = class { // ... }; // 具名 // alias为别名 let alias = class HasName { // ... };
- 构造器和方法
let variable = "methodName"; class ClassName{ // 构造器 constructor() { this.attr= val; // 成员变量 }, // 普通成员方法 normal() { // ... }, // 访问器属性方法 set setMethod() { // ... }, get getMethod() { // ... return ...; }, // 变量名(可计算成员名)方法 [variable]() { // ... }, // 生成器方法 *generator() { // ... } // 静态方法 相当于 ClassName.staticMethod static staticMethod() { // ... } }; // 方法的使用 let obj = new ClassName(); // 创建对象 obj.normal(); // 调用对象方法 ClassName.staticMethod(); // 调用类的静态方法
- 继承
class Father{ constructor(name) { this.name = "father"; }, getName() { return this.name; }, say() { console.log("I am father."); }, static toString() { return "[Class instanceof Person]"; } }; // Son 继承自 Father,这是原型继承 class Son extends Father{ constructor(name) { // 调用父类的构造器 super(name); // 与Father.call(this)相同 }, // 重写父类的同名方法 say() { console.log("I am son."); }, clone() { // constructor的Symbol.species属性返回类名Son return new this.constructor[Symbol.species](this.name); } }; let son = new Son("son"); son.getName(); // son son.say(); // I am son Son.toString(); // [Class instanceof Person]
笔记: 对象和属性具有许多非常相似的特性,这是因为类是对象的抽象化封装,应该相互记忆
十