Skip to content

let const var

es6新增了两个用于定义变量的关键字
let和const定义的变量不会像var一样作用域在整个函数内,其作用域只在当前块中,如函数,判断,循环等这些语法块中。

js
function blockVar() {
  if (true) var v = 'v';
  console.log(v);
}
function blockLet() {
  if (true) let v = 'v';
  console.log(v);
}
blockVar();
blockLet();

由于let绑定了块级作用域,所以下面代码的行为会有不同

js
for (var i = 0; i < 10; i++) {
  setTimeout(() => {
    console.log(i); 
  }, 100);
} // 10 个 10

for (let i = 0; i < 10; i++) {
  setTimeout(() => {
    console.log(i); 
  }, 100);
} // 0..10

let和const定义的变量在同级语法块中不允许声明多次,但在不同级别的语法块中可以声明相同的变量名,这称为变量遮蔽。

js
function blockVar() {
  var v = 'v';
  var v = 'v1';
  if(true) {
    var v = 'v1';
    console.log(v);
  }
  console.log(v);
}
function blockLet() {
  let v = 'v';
  // let v = 'v1'; // 会报错
  if(true) {
    let v = 'v1';
    console.log(v);
  }
  console.log(v);
}
blockVar();
blockLet();

const用于定义一个常量,这个常量和其他语言中的常量稍有不同,其他语言定义的常量是不允许重新赋值和修改其内容的,而js中const定义的常量不允许重新赋值但可以修改其内容

js
const v = 1;
// v = 2; // 会报错
const obj = { v: 1 };
obj.v = 2;
console.log(obj);

解构

方便变量的赋值

js
const { v, v1 } = { v: 0, v1: 1 };
console.log(v, v1);

function fn({ v, v1}){
  console.log(v, v1);
}
fn({ v: 0, v1: 1 });

let [ a, b, c ] = [ 1, 2, 3 ];
console.log(a, b, c);
// 还有这种玩法
let { length : len } = 'hello';
console.log(len); // 5
// 结构的花样有很多,查看下面更详细的文档,在开发中使用结构可以减少代码量,有助于你写出优雅的
// 逻辑

函数默认值

定义函数时更优雅地初始化参数

js
function fn(x = 1, y = 2){
 console.log(x, y);
}
fn(3);

// 解构并初始化
function fn(x = 1, { y = 2 }){
 console.log(x, y);
}
fn(3, {});
fn(3, { y: 4 });

箭头函数

回调函数更优雅的写法,最重要的,箭头函数可以绑定this到当前作用域对象

js
let obj = {
  call(fn){
    fn('call', 'fn');
  }
}
obj.call(a => console.log(a));
obj.call((a,b) => {
  console.log(a);
  console.log(b);
});

// 绑定当前作用域的this
function obj(fn) {
  this.fn = fn;
  this.fn();
}
function call(){
  new obj(function(){
    console.log(this); // 输出 obj
  });
  new obj(() => {
    console.log(this); // 输出 call
  });
}
new call();

js的继承是基于原型链的,es6新特性中的类同样也是这样的方式,只是将之前的写法变得更优雅了

js
class Father {
  static yell() { // 静态方法
    console.log(`who is your dad`);
  }
  constructor(name) { // 构造方法,用来初始化对象,可以没有
    this.name = name;
  }
  callMe() { // 定义父类方法
    console.log(`${this.constructor.name} name is ${this.name}`);
  }
  calm() {
    console.log('father calm down');
  }
}
class BearSon extends Father { // extends 只是表示第三人称复数,js并不支持一次进行多继承
  constructor(name, sex) {
    super(name); // 调用父类构造器
    this.sex = sex;
  }
  calm() { // 覆盖父类同名方法
    console.log(`bear son ${this.sex} calm down`);
  }
}

Father.yell();
let father = new Father('Anakin Skywalker');
father.callMe();
father.calm();
BearSon.yell();
let bearSon = new BearSon('Luke Skywalker', 'girl');
bearSon.callMe();
bearSon.calm();

// 等同于下面基于原型链的方式
function Father(name) {
  this.name = name
}
Father.yell = function() {
  console.log(`who is your dad`);
}
Father.prototype.callMe = function() {
  console.log(`${this.constructor.name} name is ${this.name}`);
}
Father.prototype.calm = function() {
  console.log('father calm down');
}
function BearSon(name, sex) {
  Father.call(this, name);
  this.sex = sex;
}
function extendsStatic(son, father) {
  // 静态方法是附属于某个对象的,因此原型链的继承方式并不会继承静态方法,需要使用工具来处理
  for (let k in father) {
    if (father.hasOwnProperty(k)) {
      son[k] = father[k];
    }
  }
}
extendsStatic(BearSon, Father)
BearSon.prototype = new Father();
BearSon.prototype.constructor = BearSon;
BearSon.prototype.calm = function() {
  console.log(`bear son ${this.sex} calm down`);
}

Father.yell();
let father = new Father('Anakin Skywalker');
father.callMe();
father.calm();
BearSon.yell();
let bearSon = new BearSon('Luke Skywalker', 'girl');
bearSon.callMe();
bearSon.calm();

对象的getter/setter

定义对象属性的赋值和取值的行为,像vuejs之所以能在变量数据修改后执行某些操作就是使用到这个特性

js
let obj = {
  _value: 4,
  get v () {
    return this._value;
  },
  set v (v) {
    this._value = v + 1;
  }
}
console.log(obj.v) // 4
obj.v = 4;
console.log(obj.v) // 5;

模板字符串

用于方便拼接字符串,并保留格式,自动转义换行,模板变量里支持任意的JavaScript表达式

js
let count = 1;
let str = 'count is ' + (count + 1) + '\n next text';
console.log(str);
let strTpl = `count is ${count + 1}
 next text`
console.log(strTpl);

symbol

symbol对象用来定义一个唯一的值,详细用法可以查看es6语法手册,最常用的方式是用Symbol值的唯一性,将私有方法的名字命名为一个Symbol值,这样私有方法将不会被访问到。

js
const bar = Symbol('bar');
const snaf = Symbol('snaf');
export default class myClass{
  // 公有方法
  foo(baz) {
    this[bar](baz);
  }
  // 私有方法
  [bar](baz) {
    return this[snaf] = baz;
  }
};
// 上面代码中,bar和snaf都是Symbol值,导致第三方无法获取到它们,因此达到了私有方法和私有属性
// 的效果。

for of

es6引入了遍历器Iterator的概念,任何可遍历对象都按照Iterator的统一标准实现,即调用对象的某个固定方法,返回的对象有一个next方法,此方法每调用一次,返回一个对象,对象的value表示当前值,done一个boolean值表示遍历是否结束,而这个固定方法es6规定是对象中的Symbol.iterator,iterator是es6规范中预定义的一个Symbol。在es6中,有三类数据结构原生具备Iterator接口:数组、某些类似数组的对象、Set和Map结构。此外Generator函数和字符串对象也实现了Iterator接口。

js
const obj = { // 原生对象不支持Iterator,所以不能被for of使用,但可以把对象模拟成一个Iterator
  [Symbol.iterator] : function () {
    return {
      next: function () {
        return {
          value: 1,
          done: true
        };
      }
    };
  }
};
let iterObj = obj[Symbol.iterator]();
iterObj.next(); // { value: 1, done: true }

let arr = ['a', 'b', 'c'];
let iter = arr[Symbol.iterator]();
iter.next() // { value: 'a', done: false }
iter.next() // { value: 'b', done: false }
iter.next() // { value: 'c', done: false }
iter.next() // { value: undefined, done: true }

不同于for in遍历数组的键for of直接直接数组的值,Iterator便是供for of消费使用的

js
var arr = ['a', 'b', 'c', 'd'];
for (let a in arr) {
  console.log(a); // 0 1 2 3
}
for (let a of arr) {
  console.log(a); // a b c d
}

import/export

es6引入了模块的感念,import用来导入其他模块中的对象到当前模块,erport用来将当前模块中的对象导出供其它模块使用。v8现在(2017-8)还不支持这两个关键字。import需要放在模块文件开头,export可以放在任意位置,但必须是模块顶层。

js
// export.js
export { a: 'a',  b: 'b' }; // 导出对象,可以是任意对象
// 直接导出时要注意
const str = 'hello world'; export str; // 报错
export const str = 'hello world'; // 正确
function f() {} export f; // 报错
export function f() {}; // 正确
function f() {} export {f}; // 正确
function f() {} export default {f}; // 正确

// import.js
import { a, b as bstr } from './export.js'; // as 给变量取个别名
console.log(a, bstr)

// export.js
export default { a: 'a',  b: 'b' }; // 导出默认对象,此对象将作为导入时的默认对象

// import.js
import obj from './export.js'; // 将默认对象直接命名使用
console.log(obj)

yield generator

generator函数在es6中称为生成器,调用generator函数返回一个迭代器对象,调用它的next方法,返回一个对象,value对应值,done表示是否结束。意味着它可以在for of中进行遍历。与其它迭代器不同在generator函数中要返回迭代数据必须配合yield关键字。调用生成器函数后,函数并不会立即执行,而会暂停在那里,当然变量已经传过去了。调用迭代器的next方法后,函数会执行到下一个yield,再调用next再往下执行,直到结束。

js
function* gen() { // 迭代器的写法,函数function后面加*,可以是 function* / function *,在对象
            // 中的写法 let obj = { * gen(){} }
  console.log('gen');
  yield 'a'; // 可以yield任意对象
  let n = yield { a: 'str' }; // yield出一个值后会暂停,要与生成器内部通讯,调用next时将值通
                     // 过参数传过去,函数恢复执行后,传递的值将会被yield前的变量所
                     // 接收,由于这种机制,生成器函数内部的状态会一直变化。
  yield function b(){ console.log(n); }
  return true; // 最后一个next函数即done为true时的值,若没有返回值或函数已经结束在调用next都
           // 会返回 {value: undefined, done: true}
}
let g = gen();
g.next(); // 'gen' { value: 'a', done: false }
g.next(3); // { value: { a: 'str' }, done: false }
let f = g.next(); { value: f(){...}, done: false }
f(); // 3;
g.next(); // { value: true, done: true }
g.next(); // { value: undefined, done: true }

玩完上面那一把,是不是有一个大胆的想法?可以通过yield返回任意对象,那么是否可以yield一个生成器呢?答案是肯定的,但产出一个生成器还要再对其调用并调用next方法,在实际使用中会嵌套多层生成器,每次都去判断再调用太麻烦,我想要一个简单粗暴的方法,正好,生成器提供了这种机制,可以将next调用委托下去:

js
function* gen() {
  yield 'gen start';
  yield 'gen end';
}
function* ger() {
  yield 'ger start';
  yield* gen();
  yield 'ger end'
}
let g = ger();
g.next(); // ger start
g.next(); // gen start
g.next(); // gen end
g.next(); // ger end

Promise

Promise刚开始是作为一种处理回调参数数据的规范,后面被纳入es6,es6原生支持了Promise对象。MDN上对Promise的介绍Promise超详细的介绍,Promise规范的目的是避免异步函数在书写时嵌套过深的情况,后来与 yield/generator 配合后,可以将异步方法写地更优雅。Promise的基本用法:

js
var fs = require('fs');
function readFile(file) {
	return new Promise((resolve, reject) => {
		fs.readFile(file, (err, data) => {
			if (err) reject(err);
			else resolve(data);
		});
	});
}
function writeFile(file, data) {
	return new Promise((resolve, reject) => {
		fs.writeFile(file, data, (err) => {
			if (err) reject(err);
			else resolve(file, data);
		});
	});
}
readFile('./file')
.then((data) => {
	console.log('read ./file: ', data.toString());
	return writeFile('./newfile', data);
})
.then((file, data) => {
	console.log('write ', file);
})
.catch((err) => {
	console.log('err: ', err);
});

实现一个简单的Promise来透析Promise的原理:

js
class myPromise{
	constructor(init) {
		init(this.resolve.bind(this), this.reject.bind(this));
	}
	then(fn) {
		if (!this.resolveds) {
			this.resolveds = [];
		}
		this.resolveds.push(fn);
		return this;
	}
	catch(fn) {
		this.rejected = fn;
	}
	resolve(...args) {
		if (this.resolveds) {
			let arg = args;
			for (let i = 0; i < this.resolveds.length; i++) {
				let resolved = this.resolveds[i];
				let res = resolved(...arg);
				if (res instanceof myPromise) {
					i = i+1;
					res.then((...args) => {
						if (typeof this.resolveds[i] == 'function') {
							this.resolveds[i](...args);
						}
					});
				} else {
					arg = res;
				}
			}
		}
	}
	reject(err) {
		if (this.rejected) {
			this.rejected(err);
		}
	}
}

var fs = require('fs');
function readFile(file) {
	return new myPromise((resolve, reject) => {
		fs.readFile(file, (err, data) => {
			if (err) reject(err);
			else resolve(data);
		});
	});
}
function writeFile(file, data) {
	return new myPromise((resolve, reject) => {
		fs.writeFile(file, data, (err) => {
			if (err) reject(err);
			else resolve(file, data);
		});
	});
}
readFile('./file')
.then((data) => {
	console.log('read ./file: ', data.toString());
	return writeFile('./newfile', data);
})
.then((file, data) => {
	console.log('write ', file);
})
.catch((err) => {
	console.log('read ./file err: ', err);
});

实际的Promise实现要复杂地多,会进行三种状态之间地转换:pending: 初始状态,不是成功或失败状态。fulfilled: 意味着操作成功完成。rejected状态。: 意味着操作失败。状态只能由pending状态转换成fulfilled状态或rejected状态。
Promise经常用来将一个异步函数进行封装后,配合生成器函数,使得代码书写时看起来像同步的。在之前的介绍中生成器函数需要一个运行器来运行,co库便是这样的运行器,co库的介绍,简单使用下co:

js
let fs = require('fs')
let co = require('co');
function readFile(file) {
	return new Promise((resolve, reject) => {
		fs.readFile(file, (err, data) => {
			if (err) reject(err);
			else resolve(data);
		});
	});
}
co(function* () {
  let file = yield readFile('./file');
  console.log(file);
})(function(err, res){
  console.log(res);
});

实现一个简单的co库:

js
function co(gen) {
  return function(fn) {
		let g = gen();
		next();
		function next(err, result) {
			if(err){
				return fn(err);
			}
			let step = g.next(result);
			if (!step.done) {
				if (typeof step.value == 'function') {
					step.value(next);
				} else if (step.value instanceof Promise) {
					step.value.then(next);
				} else {
					next(null, step.value);
				}
			} else {
				fn(null, step.value);
			}
		}
  }
}

将一个异步方法,转成Promise对象,其实过程都一样,nodejs中的所有异步方法,都遵循了一个规范,若异步函数执行时发生了异常,则回调函数的第一个参数肯定是一个错误对象,在回调函数内部需要对错误进行处理。基于这种特性,nodejs提供了一个工具方法,util.promisify,只要满足nodejs异步函数格式的,这个工具类都可以将其转换成Promise对象。

async/await

async/await是es7中的规范,目的是为了进行优雅的异步编程。async/await其实就是上面生成器加Promise结合的语法糖。async定义一个异步函数,返回一个Promise对象。await只能在async函数中使用,像yield一样会将函数暂停,await命令后面可以是Promise和原始类型。并且不需要额外的运行器来执行。async/await的介绍。async/await简单用法:

js
var fs = require('fs');

var readFile = function (fileName) {
  return new Promise(function (resolve, reject) {
    fs.readFile(fileName, function(error, data) {
      if (error) return reject(error);
      resolve(data);
    });
  });
};

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

介绍es6新特性的文章,推荐阅读
阮一峰的es6教程
深入浅出es6
扫一下api,有一些新函数可用