Node.js从入门到实战(五)ECMAScript6一页纸总结(很大的一页纸)
2018-01-24 23:42
656 查看
转载:30分钟掌握ES6/ES2015核心内容(上)
转载:30分钟掌握ES6/ES2015核心内容(下)
转载:ECMAScript 6 入门
ECMAScript6,即ES6,也称ES2015,是ECMAScript的第六次修订,于2015年完成,并且运用的范围逐渐开始扩大,因为其相对于ES5更加简洁,提高了开发速率,开发者也都在陆续进行使用,但是由于ES6还存在一些支持的问题,所以一般即使是使用ES6开发的工程,也需要使用Babel进行转换。
Babel是一个广泛使用的ES6转码器,可以将ES6代码转为ES5代码,从而在现有环境执行。这一过程叫做“源码到源码”编译, 也被称为转换编译。
一般来说Babel作为依赖包被引入ES6工程中,此处不再介绍以cli方式使用的ES6,如果你需要以编程的方式来使用 Babel,可以使用
babel-core 这个包。babel-core 的作用是把 js 代码分析成
ast ,方便各个插件分析语法进行相应的处理。有些新语法在低版本 js 中是不存在的,如箭头函数,rest 参数,函数默认值等,这种语言层面的不兼容只能通过将代码转为 ast,分析其语法后再转为低版本 js。babel的使用过程如下:
1. 首先安装 babel-core。
$ npm install babel-core2. 在文件开头引入babel:
var babel = require("babel-core");3. 文件转换
字符串形式的 JavaScript 代码可以直接使用 babel.transform 来编译。
babel.transform("code();", options);
// => { code, map, ast }如果是文件的话,可以使用异步 api:
1. 添加依赖
在Node.js工程package.json包中添加如下依赖:
2. 配置dev脚本
"scripts": {
"serve-dev": "NODE_ENV=development nodemon ./src/index.js --exec babel-node",
},接下来罗列一下ES6的语法要点参考备用。
首先来看下面这个例子:
let name = 'zach'
while (true) {
let name = 'obama'
console.log(name) //obama
break
}
console.log(name) //zach
另外一个var带来的不合理场景就是用来计数的循环变量泄露为全局变量,看下面的例子:
var a = [];
for (var i = 0; i < 10; i++) {
a[i] = function () {
console.log(i);
};
}
a[6](); // 10上面代码中,变量i是var声明的,在全局范围内都有效。所以每一次循环,新的i值都会覆盖旧值,导致最后输出的是最后一轮的i的值。而使用let则不会出现这个问题。
let [a, b, c] = [1, 2, 3];
let [foo, [[bar], baz]] = [1, [[2], 3]];
foo // 1
bar // 2
baz // 3
let [ , , third] = ["foo", "bar", "baz"];
third // "baz"
let [head, ...tail] = [1, 2, 3, 4];
head // 1
tail // [2, 3, 4]解构不成功时变量赋值为undefined
let [x, y] = [1, 2, 3];
x // 1
y // 2
let [a, , d] = [1, [2, 3], 4];
a // 1
b // 2
d // 4解构赋值允许指定默认值。ES6 内部使用严格相等运算符(===),判断一个位置是否有值。所以,只有当一个数组成员严格等于undefined,默认值才会生效。
let [x = 1] = [undefined];
x // 1
let [x = 1] = [null];
x // null如下是解构赋值的应用实例:
let { foo: baz } = { foo: 'aaa', bar: 'bbb' };
baz // "aaa"
let obj = { first: 'hello', last: 'world' };
let { first: f, last: l } = obj;
f // 'hello'
l // 'world'
let arr = [1, 2, 3];
let {0 : first, [arr.length - 1] : last} = arr; //属性名表达式
first // 1
last // 3
function add([x, y]){
return x + y;
}
add([1, 2]); // 3 //函数入参也可进行解构
// 返回一个数组
function example() {
return [1, 2, 3];
}
let [a, b, c] = example();
let jsonData = {
id: 42,
status: "OK",
data: [867, 5309]
};
let { id, status, data: number } = jsonData;
// 获取键名
for (let [key] of map) {
// ...
}
// 获取键值
for (let [,value] of map) {
// ...
}
如下两种函数的定义方法在解构赋值时具备不同的返回值:
function m1({x = 0, y = 0} = {}) {
return [x, y];
}
// 写法二
function m2({x, y} = { x: 0, y: 0 }) {
return [x, y];
}
// 普通字符串
`In JavaScript '\n' is a line-feed.`
// 多行字符串
`In JavaScript this is
not legal.`
console.log(`string text line 1
string text line 2`);
// 字符串中嵌入变量
let name = "Bob", time = "today";
`Hello ${name}, how are you ${time}?`如果在模板字符串中需要使用反引号,则前面要用反斜杠转义。
let greeting = `\`Yo\` World!`;大括号内部可以放入任意的 JavaScript 表达式,可以进行运算,以及引用对象属性。如果大括号中的值不是字符串,将按照一般的规则转为字符串。比如,大括号中是一个对象,将默认调用对象的toString方法。
let a = 5;
let b = 10;
tag`Hello ${ a + b } world ${ a * b }`;
// 等同于
tag(['Hello ', ' world ', ''], 15, 50);模板字符串前面有一个标识名tag,它是一个函数。整个表达式的返回值,就是tag函数处理模板字符串后的返回值。函数tag依次会接收到多个参数。
// arguments变量的写法
function sortNumbers() {
return Array.prototype.slice.call(arguments).sort();
}
// rest参数的写法
const sortNumbers = (...numbers) => numbers.sort();
arguments对象不是数组,而是一个类似数组的对象。所以为了使用数组的方法,必须使用Array.prototype.slice.call先将其转为数组。rest 参数就不存在这个问题,它就是一个真正的数组,数组特有的方法都可以使用。
console.log(1, ...[2, 3, 4], 5)
// 1 2 3 4 5扩展运算符提供了复制数组的简便写法。
const a1 = [1, 2];
// 写法一
const a2 = [...a1];
// 写法二
const [...a2] = a1;扩展运算符提供了数组合并的新写法。
(1)函数体内的this对象,就是定义时所在的对象,而不是使用时所在的对象。
(2)不可以当作构造函数,也就是说,不可以使用new命令,否则会抛出一个错误。
(3)不可以使用arguments对象,该对象在函数体内不存在。如果要用,可以用 rest 参数代替。
(4)不可以使用yield命令,因此箭头函数不能用作 Generator 函数。
上面四点中,第一点尤其值得注意。this对象的指向是可变的,但是在箭头函数中,它是固定的。
const foo = 'bar';
const baz = {foo};
baz // {foo: "bar"}
ES6 允许在对象之中,直接写变量。这时,属性名为变量名, 属性值为变量的值。下面是另一个例子。
function f(x, y) {
return {x, y};
}
// 等同于
function f(x, y) {
return {x: x, y: y};
}
f(1, 2) // Object {x: 1, y: 2}函数的name属性,返回函数名。对象方法也是函数,因此也有name属性。
const person = {
sayName() {
console.log('hello!');
},
};
person.sayName.name // "sayName"在对象的继承、原型和构造函数上ES6提供了更接近传统语言的写法,引入了Class(类)这个概念。新的class写法让对象原型的写法更加清晰、更像面向对象编程的语法,也更加通俗易懂。
class Animal {
constructor(){
this.type = 'animal'
}
says(say){
console.log(this.type + ' says ' + say)
}
}
let animal = new Animal()
animal.says('hello') //animal says hello
class Cat extends Animal {
constructor(){
super()
this.type = 'cat'
}
}
let cat = new Cat()
cat.says('hello') //cat says hello上面代码首先用class定义了一个“类”,可以看到里面有一个constructor方法,这就是构造方法,而this关键字则代表实例对象。简单地说,constructor内定义的方法和属性是实例对象自己的,而constructor外定义的方法和属性则是所有实例对象可以共享的。
Class之间可以通过extends关键字实现继承,这比ES5的通过修改原型链实现继承,要清晰和方便很多。上面定义了一个Cat类,该类通过extends关键字,继承了Animal类的所有属性和方法。
super关键字,它指代父类的实例(即父类的this对象)。子类必须在constructor方法中调用super方法,否则新建实例时会报错。这是因为子类没有自己的this对象,而是继承父类的this对象,然后对其进行加工。如果不调用super方法,子类就得不到this对象。
ES6的继承机制,实质是先创造父类的实例对象this(所以必须先调用super方法),然后再用子类的构造函数修改this。
(1)for...in
for...in循环遍历对象自身的和继承的可枚举属性(不含 Symbol 属性)。
(2)Object.keys(obj)
Object.keys返回一个数组,包括对象自身的(不含继承的)所有可枚举属性(不含 Symbol 属性)的键名。
(3)Object.getOwnPropertyNames(obj)
Object.getOwnPropertyNames返回一个数组,包含对象自身的所有属性(不含 Symbol 属性,但是包括不可枚举属性)的键名。
(4)Object.getOwnPropertySymbols(obj)
Object.getOwnPropertySymbols返回一个数组,包含对象自身的所有 Symbol 属性的键名。
(5)Reflect.ownKeys(obj)
Reflect.ownKeys返回一个数组,包含对象自身的所有键名,不管键名是 Symbol 或字符串,也不管是否可枚举。
以上的 5 种方法遍历对象的键名,都遵守同样的属性遍历的次序规则。
首先遍历所有数值键,按照数值升序排列。
其次遍历所有字符串键,按照加入时间升序排列。
最后遍历所有 Symbol 键,按照加入时间升序排列。
Symbol 值通过Symbol函数生成。这就是说,对象的属性名现在可以有两种类型,一种是原来就有的字符串,另一种就是新增的 Symbol 类型。凡是属性名属于 Symbol 类型,就都是独一无二的,可以保证不会与其他属性名产生冲突。
let s1 = Symbol('foo');
let s2 = Symbol('bar');
s1 // Symbol(foo)
s2 // Symbol(bar)
s1.toString() // "Symbol(foo)"
s2.toString() // "Symbol(bar)"由于每一个 Symbol 值都是不相等的,这意味着 Symbol 值可以作为标识符,用于对象的属性名,就能保证不会出现同名的属性。这对于一个对象由多个模块构成的情况非常有用,能防止某一个键被不小心改写或覆盖(Symbol 值作为对象属性名时,不能用点运算符,因为点运算符后面总是字符串,所以不会读取mySymbol作为标识名所指代的那个值,导致a的属性名实际上是一个字符串,而不是一个
Symbol 值)。
// mod.js
const FOO_KEY = Symbol.for('foo');
function A() {
this.foo = 'hello';
}
if (!global[FOO_KEY]) {
global[FOO_KEY] = new A();
}
module.exports = global[FOO_KEY];上面代码中,可以保证global[FOO_KEY][b]不会被无意间覆盖,但还是可以被改写。
const s = new Set();
[2, 3, 5, 4, 5, 2, 2].forEach(x => s.add(x));
for (let i of s) {
console.log(i);
}
// 2 3 5 4Set 函数可以接受一个数组(或者具有 iterable 接口的其他数据结构)作为参数,用来初始化。
// 例一
const set = new Set([1, 2, 3, 4, 4]);
[...set]
// [1, 2, 3, 4]
// 例二
const items = new Set([1, 2, 3, 4, 5, 5, 5, 5]);
items.size // 5Set的遍历顺序就是插入顺序。这个特性有时非常有用,比如使用 Set 保存一个回调函数列表,调用时就能保证按照添加顺序调用。
keys方法、values方法、entries方法返回的都是遍历器对象,由于 Set 结构没有键名,只有键值(或者说键名和键值是同一个值),所以keys方法和values方法的行为完全一致。
ES6 提供了 Map 数据结构。它类似于对象,也是键值对的集合,但是“键”的范围不限于字符串,各种类型的值(包括对象)都可以当作键。也就是说,Object 结构提供了“字符串—值”的对应,Map 结构提供了“值—值”的对应,是一种更完善的 Hash 结构实现。如果你需要“键值对”的数据结构,Map 比 Object 更合适。
(1)Map 转为数组
前面已经提过,Map 转为数组最方便的方法,就是使用扩展运算符(...)。
将数组传入 Map 构造函数,就可以转为 Map。
如果所有 Map 的键都是字符串,它可以转为对象。
function objToStrMap(obj) {
let strMap = new Map();
for (let k of Object.keys(obj)) {
strMap.set(k, obj[k]);
}
return strMap;
}
objToStrMap({yes: true, no: false})
// Map {"yes" => true, "no" => false}
(5)Map 转为 JSON
Map 转为 JSON 要区分两种情况。一种情况是,Map 的键名都是字符串,这时可以选择转为对象 JSON。
JSON 转为 Map,正常情况下,所有键名都是字符串。
// ES6模块
import { stat, exists, readFile } from 'fs';上面代码的实质是从fs模块加载 3 个方法,其他方法不加载。这种加载称为“编译时加载”或者静态加载,即 ES6 可以在编译时就完成模块加载,效率要比 CommonJS 模块的加载方式高。当然,这也导致了没法引用 ES6 模块本身,因为它不是对象。
由于 ES6 模块是编译时加载,使得静态分析成为可能。有了它,就能进一步拓宽 JavaScript 的语法,比如引入宏(macro)和类型检验(type system)这些只能靠静态分析实现的功能。
除了静态加载带来的各种好处,ES6 模块还有以下好处。
不再需要UMD模块格式了,将来服务器和浏览器都会支持 ES6 模块格式。目前,通过各种工具库,其实已经做到了这一点。
将来浏览器的新 API 就能用模块格式提供,不再必须做成全局变量或者navigator对象的属性。
不再需要对象作为命名空间(比如Math对象),未来这些功能可以通过模块提供。
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和import。export命令用于规定模块的对外接口,import命令用于输入其他模块提供的功能。一个模块就是一个独立的文件。该文件内部的所有变量,外部无法获取。如果你希望外部能够读取模块内部的某个变量,就必须使用export关键字输出该变量。下面是一个
JS 文件,里面使用export命令输出变量。
// profile.js
export var firstName = 'Michael';
export var lastName = 'Jackson';
export var year = 1958;上面代码是profile.js文件,保存了用户信息。ES6 将其视为一个模块,里面用export命令对外部输出了三个变量。export的写法,除了像上面这样,还有另外一种。
// profile.js
var firstName = 'Michael';
var lastName = 'Jackson';
var year = 1958;
export {firstName, lastName, year};export命令除了输出变量,还可以输出函数或类(class)。通常情况下,export输出的变量就是本来的名字,但是可以使用as关键字重命名。
export命令可以出现在模块的任何位置,只要处于模块顶层就可以。如果处于块级作用域内,就会报错,下一节的import命令也是如此。这是因为处于条件代码块之中,就没法做静态优化了,违背了 ES6 模块的设计初衷。
// export-default.js
export default function () {
console.log('foo');
}上面代码是一个模块文件export-default.js,它的默认输出是一个函数。其他模块加载该模块时,import命令可以为该匿名函数指定任意名字。
// import-default.js
import customName from './export-default';
customName(); // 'foo'上面代码的import命令,可以用任意名称指向export-default.js输出的方法,这时就不需要知道原模块输出的函数名。需要注意的是,这时import命令后面,不使用大括号。
export default命令用于指定模块的默认输出。显然,一个模块只能有一个默认输出,因此export default命令只能使用一次。所以,import命令后面才不用加大括号,因为只可能唯一对应export default命令。本质上,export default就是输出一个叫做default的变量或方法,然后系统允许你为它取任意名字。
export { foo, bar } from 'my_module';
// 等同于
import { foo, bar } from 'my_module';
export { foo, bar };
这样的设计,固然有利于编译器提高效率,但也导致无法在运行时加载模块。在语法上,条件加载就不可能实现。如果import命令要取代 Node 的require方法,这就形成了一个障碍。因为require是运行时加载模块,import命令无法取代require的动态加载功能。
import()加载模块成功以后,这个模块会作为一个对象,当作then方法的参数。因此,可以使用对象解构赋值的语法,获取输出接口。
如上就是ES6中比较容易识别出的关键点,实际上在两天半的ES6使用中也确实见到了如上的用法,感谢阮一峰老师的博客,先总结到这里,留待后补?
转载:30分钟掌握ES6/ES2015核心内容(下)
转载:ECMAScript 6 入门
一、ES5/ES6和babel
ECMAScript5,即ES5,是ECMAScript的第五次修订,于2009年完成标准化,现在的浏览器已经相当于完全实现了这个标准。ECMAScript6,即ES6,也称ES2015,是ECMAScript的第六次修订,于2015年完成,并且运用的范围逐渐开始扩大,因为其相对于ES5更加简洁,提高了开发速率,开发者也都在陆续进行使用,但是由于ES6还存在一些支持的问题,所以一般即使是使用ES6开发的工程,也需要使用Babel进行转换。
Babel是一个广泛使用的ES6转码器,可以将ES6代码转为ES5代码,从而在现有环境执行。这一过程叫做“源码到源码”编译, 也被称为转换编译。
一般来说Babel作为依赖包被引入ES6工程中,此处不再介绍以cli方式使用的ES6,如果你需要以编程的方式来使用 Babel,可以使用
babel-core 这个包。babel-core 的作用是把 js 代码分析成
ast ,方便各个插件分析语法进行相应的处理。有些新语法在低版本 js 中是不存在的,如箭头函数,rest 参数,函数默认值等,这种语言层面的不兼容只能通过将代码转为 ast,分析其语法后再转为低版本 js。babel的使用过程如下:
1. 首先安装 babel-core。
$ npm install babel-core2. 在文件开头引入babel:
var babel = require("babel-core");3. 文件转换
字符串形式的 JavaScript 代码可以直接使用 babel.transform 来编译。
babel.transform("code();", options);
// => { code, map, ast }如果是文件的话,可以使用异步 api:
babel.transformFile("filename.js", options, function(err, result) { result; // => { code, map, ast } });或者是同步 api:
babel.transformFileSync("filename.js", options); // => { code, map, ast }或者在development环境下可以使用bable-node和bable-register的方式配置,过程如下:
1. 添加依赖
在Node.js工程package.json包中添加如下依赖:
"devDependencies": { "babel-cli": "^6.26.0", "babel-eslint": "^8.0.1", "babel-plugin-transform-flow-strip-types": "^6.22.0", "babel-preset-es2015": "^6.24.1", "babel-register": "^6.26.0", ... }
2. 配置dev脚本
"scripts": {
"serve-dev": "NODE_ENV=development nodemon ./src/index.js --exec babel-node",
},接下来罗列一下ES6的语法要点参考备用。
二、let, const
这两个的用途与var类似,都是用来声明变量的,但在实际运用中他俩都有各自的特殊用途。首先来看下面这个例子:
var name = 'zach' while (true) { var name = 'obama' console.log(name) //obama break } console.log(name) //obama使用var 两次输出都是obama,这是因为ES5只有全局作用域和函数作用域,没有块级作用域,这带来很多不合理的场景。第一种场景就是你现在看到的内层变量覆盖外层变量。而let则实际上为JavaScript新增了块级作用域。用它所声明的变量,只在let命令所在的代码块内有效。
let name = 'zach'
while (true) {
let name = 'obama'
console.log(name) //obama
break
}
console.log(name) //zach
另外一个var带来的不合理场景就是用来计数的循环变量泄露为全局变量,看下面的例子:
var a = [];
for (var i = 0; i < 10; i++) {
a[i] = function () {
console.log(i);
};
}
a[6](); // 10上面代码中,变量i是var声明的,在全局范围内都有效。所以每一次循环,新的i值都会覆盖旧值,导致最后输出的是最后一轮的i的值。而使用let则不会出现这个问题。
var a = []; for (let i = 0; i < 10; i++) { a[i] = function () { console.log(i); }; } a[6](); // 6const也用来声明变量,但是声明的是常量。一旦声明,常量的值就不能改变。
const PI = Math.PI PI = 23 //Module build failed: SyntaxError: /es6/app.js: "PI" is read-only
三、解构赋值
ES6 允许按照一定模式,从数组和对象中提取值,对变量进行赋值,这被称为解构(Destructuring)。let [a, b, c] = [1, 2, 3];
let [foo, [[bar], baz]] = [1, [[2], 3]];
foo // 1
bar // 2
baz // 3
let [ , , third] = ["foo", "bar", "baz"];
third // "baz"
let [head, ...tail] = [1, 2, 3, 4];
head // 1
tail // [2, 3, 4]解构不成功时变量赋值为undefined
let [x, y, ...z] = ['a']; x // "a" y // undefined z // []存在不完全解构的情况如下:
let [x, y] = [1, 2, 3];
x // 1
y // 2
let [a, , d] = [1, [2, 3], 4];
a // 1
b // 2
d // 4解构赋值允许指定默认值。ES6 内部使用严格相等运算符(===),判断一个位置是否有值。所以,只有当一个数组成员严格等于undefined,默认值才会生效。
let [x = 1] = [undefined];
x // 1
let [x = 1] = [null];
x // null如下是解构赋值的应用实例:
let { foo: baz } = { foo: 'aaa', bar: 'bbb' };
baz // "aaa"
let obj = { first: 'hello', last: 'world' };
let { first: f, last: l } = obj;
f // 'hello'
l // 'world'
let arr = [1, 2, 3];
let {0 : first, [arr.length - 1] : last} = arr; //属性名表达式
first // 1
last // 3
function add([x, y]){
return x + y;
}
add([1, 2]); // 3 //函数入参也可进行解构
// 返回一个数组
function example() {
return [1, 2, 3];
}
let [a, b, c] = example();
let jsonData = {
id: 42,
status: "OK",
data: [867, 5309]
};
let { id, status, data: number } = jsonData;
// 获取键名
for (let [key] of map) {
// ...
}
// 获取键值
for (let [,value] of map) {
// ...
}
如下两种函数的定义方法在解构赋值时具备不同的返回值:
function m1({x = 0, y = 0} = {}) {
return [x, y];
}
// 写法二
function m2({x, y} = { x: 0, y: 0 }) {
return [x, y];
}
// x 有值,y 无值的情况 m1({x: 3}) // [3, 0] m2({x: 3}) // [3, undefined] // x 和 y 都无值的情况 m1({}) // [0, 0]; m2({}) // [undefined, undefined] m1({z: 3}) // [0, 0] m2({z: 3}) // [undefined, undefined]
四、模板字符串
模板字符串(template string)是[b]增强版的字符串,用反引号(`)标识。它可以当作普通字符串使用,也可以用来定义多行字符串,或者在字符串中嵌入变量。// 普通字符串
`In JavaScript '\n' is a line-feed.`
// 多行字符串
`In JavaScript this is
not legal.`
console.log(`string text line 1
string text line 2`);
// 字符串中嵌入变量
let name = "Bob", time = "today";
`Hello ${name}, how are you ${time}?`如果在模板字符串中需要使用反引号,则前面要用反斜杠转义。
let greeting = `\`Yo\` World!`;大括号内部可以放入任意的 JavaScript 表达式,可以进行运算,以及引用对象属性。如果大括号中的值不是字符串,将按照一般的规则转为字符串。比如,大括号中是一个对象,将默认调用对象的toString方法。
标签模板
模板字符串它可以紧跟在一个函数名后面,该函数将被调用来处理这个模板字符串。这被称为“标签模板”功能(tagged template)。let a = 5;
let b = 10;
tag`Hello ${ a + b } world ${ a * b }`;
// 等同于
tag(['Hello ', ' world ', ''], 15, 50);模板字符串前面有一个标识名tag,它是一个函数。整个表达式的返回值,就是tag函数处理模板字符串后的返回值。函数tag依次会接收到多个参数。
function tag(stringArr, value1, value2){ // ... } // 等同于 function tag(stringArr, ...values){ // ... }tag函数的第一个参数是一个数组,该数组的成员是模板字符串中那些没有变量替换的部分,也就是说,变量替换只发生在数组的第一个成员与第二个成员之间、第二个成员与第三个成员之间,以此类推。tag函数的其他参数,都是模板字符串各个变量被替换后的值。由于本例中,模板字符串含有两个变量,因此tag会接受到value1和value2两个参数。也就是说,tag函数实际上以下面的形式调用。
tag(['Hello ', ' world ', ''], 15, 50)
五、rest参数
ES6 引入 rest 参数(形式为...变量名),用于获取函数的多余参数,这样就不需要使用arguments对象了。rest 参数搭配的变量是一个数组,该变量将多余的参数放入数组中。// arguments变量的写法
function sortNumbers() {
return Array.prototype.slice.call(arguments).sort();
}
// rest参数的写法
const sortNumbers = (...numbers) => numbers.sort();
arguments对象不是数组,而是一个类似数组的对象。所以为了使用数组的方法,必须使用Array.prototype.slice.call先将其转为数组。rest 参数就不存在这个问题,它就是一个真正的数组,数组特有的方法都可以使用。
扩展运算符
rest函数的实现也是基于扩展运算符,扩展运算符(spread)是三个点(...)。它好比 rest 参数的逆运算,将一个数组转为用逗号分隔的参数序列。console.log(1, ...[2, 3, 4], 5)
// 1 2 3 4 5扩展运算符提供了复制数组的简便写法。
const a1 = [1, 2];
// 写法一
const a2 = [...a1];
// 写法二
const [...a2] = a1;扩展运算符提供了数组合并的新写法。
// ES5 [1, 2].concat(more) // ES6 [1, 2, ...more] var arr1 = ['a', 'b']; var arr2 = ['c']; var arr3 = ['d', 'e']; // ES5的合并数组 arr1.concat(arr2, arr3); // [ 'a', 'b', 'c', 'd', 'e' ] // ES6的合并数组 [...arr1, ...arr2, ...arr3] // [ 'a', 'b', 'c', 'd', 'e' ]
六、箭头函数
ES6 允许使用“箭头”(=>)定义函数。var f = v => v; //上面的箭头函数等同于: var f = function(v) { return v; };如果箭头函数的代码块部分多于一条语句,就要使用大括号将它们括起来,并且使用return语句返回。如果箭头函数不需要参数或需要多个参数,就使用一个圆括号代表参数部分。
var f = () => 5; // 等同于 var f = function () { return 5 };由于大括号被解释为代码块,所以如果箭头函数直接返回一个对象,必须在对象外面加上括号,否则会报错。
// 报错 let getTempItem = id => { id: id, name: "Temp" }; // 不报错 let getTempItem = id => ({ id: id, name: "Temp" });箭头函数可以与变量解构结合使用。
const full = ({ first, last }) => first + ' ' + last; // 等同于 function full(person) { return person.first + ' ' + person.last; }箭头函数的一个用处是简化回调函数。
// 正常函数写法 [1,2,3].map(function (x) { return x * x; }); // 箭头函数写法 [1,2,3].map(x => x * x);箭头函数有几个使用注意点。
(1)函数体内的this对象,就是定义时所在的对象,而不是使用时所在的对象。
(2)不可以当作构造函数,也就是说,不可以使用new命令,否则会抛出一个错误。
(3)不可以使用arguments对象,该对象在函数体内不存在。如果要用,可以用 rest 参数代替。
(4)不可以使用yield命令,因此箭头函数不能用作 Generator 函数。
上面四点中,第一点尤其值得注意。this对象的指向是可变的,但是在箭头函数中,它是固定的。
function foo() { setTimeout(() => { console.log('id:', this.id); }, 100); } var id = 21; foo.call({ id: 42 });上面代码中,setTimeout的参数是一个箭头函数,这个箭头函数的定义生效是在foo函数生成时,而它的真正执行要等到 100 毫秒后。如果是普通函数,执行时this应该指向全局对象window,这时应该输出21。但是,箭头函数导致this总是指向函数定义生效时所在的对象(本例是{id: 42}),所以输出的是42。
七、ES6对象
ES6 允许直接写入变量和函数,作为对象的属性和方法。这样的书写更加简洁。const foo = 'bar';
const baz = {foo};
baz // {foo: "bar"}
ES6 允许在对象之中,直接写变量。这时,属性名为变量名, 属性值为变量的值。下面是另一个例子。
function f(x, y) {
return {x, y};
}
// 等同于
function f(x, y) {
return {x: x, y: y};
}
f(1, 2) // Object {x: 1, y: 2}函数的name属性,返回函数名。对象方法也是函数,因此也有name属性。
const person = {
sayName() {
console.log('hello!');
},
};
person.sayName.name // "sayName"在对象的继承、原型和构造函数上ES6提供了更接近传统语言的写法,引入了Class(类)这个概念。新的class写法让对象原型的写法更加清晰、更像面向对象编程的语法,也更加通俗易懂。
class Animal {
constructor(){
this.type = 'animal'
}
says(say){
console.log(this.type + ' says ' + say)
}
}
let animal = new Animal()
animal.says('hello') //animal says hello
class Cat extends Animal {
constructor(){
super()
this.type = 'cat'
}
}
let cat = new Cat()
cat.says('hello') //cat says hello上面代码首先用class定义了一个“类”,可以看到里面有一个constructor方法,这就是构造方法,而this关键字则代表实例对象。简单地说,constructor内定义的方法和属性是实例对象自己的,而constructor外定义的方法和属性则是所有实例对象可以共享的。
Class之间可以通过extends关键字实现继承,这比ES5的通过修改原型链实现继承,要清晰和方便很多。上面定义了一个Cat类,该类通过extends关键字,继承了Animal类的所有属性和方法。
super关键字,它指代父类的实例(即父类的this对象)。子类必须在constructor方法中调用super方法,否则新建实例时会报错。这是因为子类没有自己的this对象,而是继承父类的this对象,然后对其进行加工。如果不调用super方法,子类就得不到this对象。
ES6的继承机制,实质是先创造父类的实例对象this(所以必须先调用super方法),然后再用子类的构造函数修改this。
八、遍历方法
ES6 一共有 5 种方法可以遍历对象的属性。(1)for...in
for...in循环遍历对象自身的和继承的可枚举属性(不含 Symbol 属性)。
(2)Object.keys(obj)
Object.keys返回一个数组,包括对象自身的(不含继承的)所有可枚举属性(不含 Symbol 属性)的键名。
(3)Object.getOwnPropertyNames(obj)
Object.getOwnPropertyNames返回一个数组,包含对象自身的所有属性(不含 Symbol 属性,但是包括不可枚举属性)的键名。
(4)Object.getOwnPropertySymbols(obj)
Object.getOwnPropertySymbols返回一个数组,包含对象自身的所有 Symbol 属性的键名。
(5)Reflect.ownKeys(obj)
Reflect.ownKeys返回一个数组,包含对象自身的所有键名,不管键名是 Symbol 或字符串,也不管是否可枚举。
以上的 5 种方法遍历对象的键名,都遵守同样的属性遍历的次序规则。
首先遍历所有数值键,按照数值升序排列。
其次遍历所有字符串键,按照加入时间升序排列。
最后遍历所有 Symbol 键,按照加入时间升序排列。
九、Symbol
ES6 引入了一种新的原始数据类型Symbol,表示独一无二的值。它是 JavaScript 语言的第七种数据类型,前六种是:undefined、null、布尔值(Boolean)、字符串(String)、数值(Number)、对象(Object)。Symbol 值通过Symbol函数生成。这就是说,对象的属性名现在可以有两种类型,一种是原来就有的字符串,另一种就是新增的 Symbol 类型。凡是属性名属于 Symbol 类型,就都是独一无二的,可以保证不会与其他属性名产生冲突。
let s1 = Symbol('foo');
let s2 = Symbol('bar');
s1 // Symbol(foo)
s2 // Symbol(bar)
s1.toString() // "Symbol(foo)"
s2.toString() // "Symbol(bar)"由于每一个 Symbol 值都是不相等的,这意味着 Symbol 值可以作为标识符,用于对象的属性名,就能保证不会出现同名的属性。这对于一个对象由多个模块构成的情况非常有用,能防止某一个键被不小心改写或覆盖(Symbol 值作为对象属性名时,不能用点运算符,因为点运算符后面总是字符串,所以不会读取mySymbol作为标识名所指代的那个值,导致a的属性名实际上是一个字符串,而不是一个
Symbol 值)。
let mySymbol = Symbol(); // 第一种写法 let a = {}; a[mySymbol] = 'Hello!'; // 第二种写法 let a = { [mySymbol]: 'Hello!' }; // 第三种写法 let a = {}; Object.defineProperty(a, mySymbol, { value: 'Hello!' }); // 以上写法都得到同样结果 a[mySymbol] // "Hello!"Object.getOwnPropertySymbols方法返回一个数组,成员是当前对象的所有用作属性名的 Symbol 值。
const obj = {}; let a = Symbol('a'); let b = Symbol('b'); obj[a] = 'Hello'; obj = 'World'; const objectSymbols = Object.getOwnPropertySymbols(obj); objectSymbols // [Symbol(a), Symbol(b)]Symbol可以用于实现单例模式:
// mod.js
const FOO_KEY = Symbol.for('foo');
function A() {
this.foo = 'hello';
}
if (!global[FOO_KEY]) {
global[FOO_KEY] = new A();
}
module.exports = global[FOO_KEY];上面代码中,可以保证global[FOO_KEY][b]不会被无意间覆盖,但还是可以被改写。
十、Set和Map
ES6 提供了新的数据结构 Set。它类似于数组,但是成员的值都是唯一的,没有重复的值。Set 本身是一个构造函数,用来生成 Set 数据结构。const s = new Set();
[2, 3, 5, 4, 5, 2, 2].forEach(x => s.add(x));
for (let i of s) {
console.log(i);
}
// 2 3 5 4Set 函数可以接受一个数组(或者具有 iterable 接口的其他数据结构)作为参数,用来初始化。
// 例一
const set = new Set([1, 2, 3, 4, 4]);
[...set]
// [1, 2, 3, 4]
// 例二
const items = new Set([1, 2, 3, 4, 5, 5, 5, 5]);
items.size // 5Set的遍历顺序就是插入顺序。这个特性有时非常有用,比如使用 Set 保存一个回调函数列表,调用时就能保证按照添加顺序调用。
keys方法、values方法、entries方法返回的都是遍历器对象,由于 Set 结构没有键名,只有键值(或者说键名和键值是同一个值),所以keys方法和values方法的行为完全一致。
ES6 提供了 Map 数据结构。它类似于对象,也是键值对的集合,但是“键”的范围不限于字符串,各种类型的值(包括对象)都可以当作键。也就是说,Object 结构提供了“字符串—值”的对应,Map 结构提供了“值—值”的对应,是一种更完善的 Hash 结构实现。如果你需要“键值对”的数据结构,Map 比 Object 更合适。
const map = new Map([ ['name', '张三'], ['title', 'Author'] ]); map.size // 2 map.has('name') // true map.get('name') // "张三" map.has('title') // true map.get('title') // "Author"与其他数据结构的互相转换
(1)Map 转为数组
前面已经提过,Map 转为数组最方便的方法,就是使用扩展运算符(...)。
const myMap = new Map() .set(true, 7) .set({foo: 3}, ['abc']); [...myMap] // [ [ true, 7 ], [ { foo: 3 }, [ 'abc' ] ] ](2)数组 转为 Map
将数组传入 Map 构造函数,就可以转为 Map。
new Map([ [true, 7], [{foo: 3}, ['abc']] ]) // Map { // true => 7, // Object {foo: 3} => ['abc'] // }(3)Map 转为对象
如果所有 Map 的键都是字符串,它可以转为对象。
function strMapToObj(strMap) { let obj = Object.create(null); for (let [k,v] of strMap) { obj[k] = v; } return obj; } const myMap = new Map() .set('yes', true) .set('no', false); strMapToObj(myMap) // { yes: true, no: false }(4)对象转为 Map
function objToStrMap(obj) {
let strMap = new Map();
for (let k of Object.keys(obj)) {
strMap.set(k, obj[k]);
}
return strMap;
}
objToStrMap({yes: true, no: false})
// Map {"yes" => true, "no" => false}
(5)Map 转为 JSON
Map 转为 JSON 要区分两种情况。一种情况是,Map 的键名都是字符串,这时可以选择转为对象 JSON。
function strMapToJson(strMap) { return JSON.stringify(strMapToObj(strMap)); } let myMap = new Map().set('yes', true).set('no', false); strMapToJson(myMap) // '{"yes":true,"no":false}'另一种情况是,Map 的键名有非字符串,这时可以选择转为数组 JSON。
function mapToArrayJson(map) { return JSON.stringify([...map]); } let myMap = new Map().set(true, 7).set({foo: 3}, ['abc']); mapToArrayJson(myMap) // '[[true,7],[{"foo":3},["abc"]]]'(6)JSON 转为 Map
JSON 转为 Map,正常情况下,所有键名都是字符串。
function jsonToStrMap(jsonStr) { return objToStrMap(JSON.parse(jsonStr)); } jsonToStrMap('{"yes": true, "no": false}') // Map {'yes' => true, 'no' => false}
十一、模块(module)体系
ES6 模块不是对象,而是通过export命令显式指定输出的代码,再通过import命令输入。// ES6模块
import { stat, exists, readFile } from 'fs';上面代码的实质是从fs模块加载 3 个方法,其他方法不加载。这种加载称为“编译时加载”或者静态加载,即 ES6 可以在编译时就完成模块加载,效率要比 CommonJS 模块的加载方式高。当然,这也导致了没法引用 ES6 模块本身,因为它不是对象。
由于 ES6 模块是编译时加载,使得静态分析成为可能。有了它,就能进一步拓宽 JavaScript 的语法,比如引入宏(macro)和类型检验(type system)这些只能靠静态分析实现的功能。
除了静态加载带来的各种好处,ES6 模块还有以下好处。
不再需要UMD模块格式了,将来服务器和浏览器都会支持 ES6 模块格式。目前,通过各种工具库,其实已经做到了这一点。
将来浏览器的新 API 就能用模块格式提供,不再必须做成全局变量或者navigator对象的属性。
不再需要对象作为命名空间(比如Math对象),未来这些功能可以通过模块提供。
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和import。export命令用于规定模块的对外接口,import命令用于输入其他模块提供的功能。一个模块就是一个独立的文件。该文件内部的所有变量,外部无法获取。如果你希望外部能够读取模块内部的某个变量,就必须使用export关键字输出该变量。下面是一个
JS 文件,里面使用export命令输出变量。
// profile.js
export var firstName = 'Michael';
export var lastName = 'Jackson';
export var year = 1958;上面代码是profile.js文件,保存了用户信息。ES6 将其视为一个模块,里面用export命令对外部输出了三个变量。export的写法,除了像上面这样,还有另外一种。
// profile.js
var firstName = 'Michael';
var lastName = 'Jackson';
var year = 1958;
export {firstName, lastName, year};export命令除了输出变量,还可以输出函数或类(class)。通常情况下,export输出的变量就是本来的名字,但是可以使用as关键字重命名。
function v1() { ... } function v2() { ... } export { v1 as streamV1, v2 as streamV2, v2 as streamLatestVersion };export语句输出的接口,与其对应的值是动态绑定关系,即通过该接口,可以取到模块内部实时的值。
export命令可以出现在模块的任何位置,只要处于模块顶层就可以。如果处于块级作用域内,就会报错,下一节的import命令也是如此。这是因为处于条件代码块之中,就没法做静态优化了,违背了 ES6 模块的设计初衷。
// 报错 var m = 1; export m; // 写法一 export var m = 1; // 写法二 var m = 1; export {m};
import 命令
使用export命令定义了模块的对外接口以后,其他 JS 文件就可以通过import命令加载这个模块。// main.js import {firstName, lastName, year} from './profile.js'; function setName(element) { element.textContent = firstName + ' ' + lastName; }上面代码的import命令,用于加载profile.js文件,并从中输入变量。import命令接受一对大括号,里面指定要从其他模块导入的变量名。大括号里面的变量名,必须与被导入模块(profile.js)对外接口的名称相同。如果想为输入的变量重新取一个名字,import命令要使用as关键字,将输入的变量重命名。
import { lastName as surname } from './profile.js';import命令具有提升效果,会提升到整个模块的头部,首先执行。
export default 命令
使用import命令的时候,用户需要知道所要加载的变量名或函数名,否则无法加载。但是,用户肯定希望快速上手,未必愿意阅读文档,去了解模块有哪些属性和方法。为了给用户提供方便,让他们不用阅读文档就能加载模块,就要用到export default命令,为模块指定默认输出。// export-default.js
export default function () {
console.log('foo');
}上面代码是一个模块文件export-default.js,它的默认输出是一个函数。其他模块加载该模块时,import命令可以为该匿名函数指定任意名字。
// import-default.js
import customName from './export-default';
customName(); // 'foo'上面代码的import命令,可以用任意名称指向export-default.js输出的方法,这时就不需要知道原模块输出的函数名。需要注意的是,这时import命令后面,不使用大括号。
export default命令用于指定模块的默认输出。显然,一个模块只能有一个默认输出,因此export default命令只能使用一次。所以,import命令后面才不用加大括号,因为只可能唯一对应export default命令。本质上,export default就是输出一个叫做default的变量或方法,然后系统允许你为它取任意名字。
export 与 import 的复合写法
如果在一个模块之中,先输入后输出同一个模块,import语句可以与export语句写在一起。export { foo, bar } from 'my_module';
// 等同于
import { foo, bar } from 'my_module';
export { foo, bar };
import和require的区别
引擎处理import语句是在编译时,这时不会去分析或执行if语句,所以import语句放在if代码块之中毫无意义,因此会报句法错误,而不是执行时错误。也就是说,import和export命令只能在模块的顶层,不能在代码块之中(比如,在if代码块之中,或在函数之中)。这样的设计,固然有利于编译器提高效率,但也导致无法在运行时加载模块。在语法上,条件加载就不可能实现。如果import命令要取代 Node 的require方法,这就形成了一个障碍。因为require是运行时加载模块,import命令无法取代require的动态加载功能。
const path = './' + fileName; const myModual = require(path);上面的语句就是动态加载,require到底加载哪一个模块,只有运行时才知道。import语句做不到这一点。
import()加载模块成功以后,这个模块会作为一个对象,当作then方法的参数。因此,可以使用对象解构赋值的语法,获取输出接口。
import('./myModule.js') .then(({export1, export2}) => { // ...· });上面代码中,export1和export2都是myModule.js的输出接口,可以解构获得。如果模块有default输出接口,可以用参数直接获得。
如上就是ES6中比较容易识别出的关键点,实际上在两天半的ES6使用中也确实见到了如上的用法,感谢阮一峰老师的博客,先总结到这里,留待后补?
相关文章推荐
- Node.js从入门到实战(六)React一页纸总结(很大的一页纸)
- Node.js从入门到实战(七)Solr查询规则总结
- 以太坊 DApp 开发入门实战! 用Node.js和truffle框架搭建——区块链投票系统!
- Node.js从入门到实战(一)Intellj Idea 2017下的第一个Node.js工程
- Node.js入门总结
- 以太坊 DApp 开发入门实战! 用Node.js和truffle框架搭建——区块链投票系统!
- Node.js从入门到实战(八)Solr的层级
- GitChat · 前端 | JavaScript 进阶之 Vue.js + Node.js 入门实战开发
- Node.js从入门到实战(四)Node.js / JavaScript / ECMAScript的关系
- IT营最新Node.js入门到实战项目视频教程免费下载
- node.js入门总结
- Node.JS入门到实战_网络爬虫{H5公开课}
- Node.js从入门到实战(三)Npm使用介绍
- Node.js从入门到实战(二)Node.js基本用法
- JavaScript 进阶之 Vue.js + Node.js 入门实战开发
- 【vuejs路由】vuejs 路由基础入门实战操作详细指南
- (pomelo系列入门教程)深入浅出node.js游戏服务器开发——Pomelo框架的设计动机与架构介绍
- React.js 入门与实战课程思维导图
- node.js获取参数的常用方法(总结)
- Node.js 入门简介