ECMAScript 6 新特性

ECMAScript 2015版本开始,JavaScript开始变得好玩起来。

本文学习阮一峰老师的《ECMAScript 6 入门》的笔记,有兴趣可以直接查看他的原书,感谢阮老师开源。内容主要为ES2015部分。

1、let和const关键字

let声明的变量在块级作用域内有效,且不允许在相同作用域重复声明同一个变量。不存在变量提升,存在暂时性死区,须先声明再使用。如果未声明就使用,直接报错,包括使用typeof进行判断。

// 块作用域
{
  let x = y;  // Error
  let y = 10;  
  var y = 20;  // Error
  var z = 30;
}
y  // Error
z  // 30
let可以避免变量从未声明就使用导致的全局污染情况,也给闭包内循环时变量使用同一块内存提供了一种解决方案。
var arr = []
for(var i = 0; i < 10; i++) {
  arr[i] = function() { console.log(i) }
}
i  // 10  泄漏成全局变量
arr[6]  // 10 使用匿名IIFE可解决

let arr2 = []
for(let i = 0; i < 10; i++) {
  arr2[i] = () => console.log(i)
}
i  // Error
arr2[6]  // 6

块级作用域对变量有效,对函数也是有效的。在JS里,函数也是特殊的变量,ES6对函数使用块级作用域才显得正常。但是ES5块级作用域是不允许声明函数的,ES6规定允许在块级作用内声明函数,声明类似var的作用域,还会提升到所在块级作用域的头部。因此,不建议在块级作用域内声明函数,即便声明也应该写函数表达式,而不是函数声明语句。

{
  function f1() {}  // 不建议
  let f2 = () => {}  // 建议
}

ES6的块级作用域必须有大括号。

if(true) let x = 1;  // error
if(true) { let x = 1; }

const声明一个只读常量,声明之后不能更改。其他特性与let关键字基本相同。

const常量声明是不能保证的。简单数据的指针所指向的内存比较单一,因此等同于常量。而复合类型的数据(主要是对象和数组)所指的内存只是针对这个类型,里面的数据结构的可变性是不能控制的。如果要冻结对象,应该使用object.freeze。

2、变量的解构赋值

解构赋值本质上就是“模式匹配”,只要等号两边的模式相同,左边的变量就会被赋值,允许不完全解构。

let [a, [b], c] = [1, [2, 3], 4]

数组的解构赋值允许有缺省值,缺省值可以是变量、表达式。缺省值是变量须注意是否已声明。缺省值是表达式是惰性求值,只有在用到的时候才会计算值。

// 缺省值
let [x, y = 'b'] = ['a']

// 缺省值是变量
let [x = 1, y = x] = []  // x = 1; y = 1
let [x = y, y = 1] = []  // error

// 缺省值是表达式
let f = () => 'b'
let [x = f()] = [1]

对象的解构赋值与数组的本质是一样的,可以有缺省值。ES6对对象进行了扩展,在不需要改变接收变量的时候,通常都可以简写。

// 完整写法
let { log: log, sin: sin, cos: cos } = Math

// 简写,通常这样写
let { log, sin, cos } = Math

// 假如将console解构,变量冲突
// key保持与右边一直,接收变量自定义
let { log: print } = console

// 缺省值
let {x: y = 3} = {}
解构赋值只要按着规矩来,一般不会出问题。如果要整些骚操作,要作死,那么谁也拦不住。使用已经声明的变量解构,必须小心。
let x
{x} = {x: 1}  // error 大括号在行首,左边的x被引擎解释为在代码块内

let y
({y} = {y: 1})

除字符串(类数组)之外其他数据类型解构赋值时会先转为对象再解构,如果无法转换就会直接报错。解构赋值应用到函数参数上,提取数据尤为有用,加上缺省参数,代码会变得更加清晰。

3、字符串的扩展

ES6加强了对unicode的支持,只要将码点放入大括号就能正确结对该字符。

'\u{1F680}' === '\uD83D\uDE80'

ES6为字符串添加了遍历器接口,可以使用for...of循环遍历,遍历器可识别大于0xFFFF的码点。

模版字符串是增强版的字符串,用反引号(`)标识,可以是普通字符串,可以定义多行字符串,可以使用(${})嵌入变量、任意的JavaScript表达式包括函数进行运算以及对象属性。它能替代字符串使用+号来组合字符串。

标签模版,模版字符串紧跟在一个函数名后面,该函数将被调用来处理这个模版字符串。它不是模版,而是函数调用的一种特殊形式。

4、正则的扩展

ES6添加u修饰符,用来处理大于\uFFFF的Unicode字符。

添加y修饰符(粘连修饰符),与g修饰符类似,y修饰符是从匹配必须从生效与的第一个位置开始,g只要剩余位置中存在匹配即可。

5、数值的扩展

ES6提供了二进制和八进制的新写法,分别用前缀0b/0B和0o/0O表示。

在Number对象上新增一些方法,包括一些全局方法,如isNaN()可用Number.isNaN(),parseInt()可用Number.parseInt()。

Math对象上新增了了些方法,如Math.trunc()去除一个小数的小数部分返回整数,和parseInt()类似。

新增指数运算符(**),和等号结合(**=)。

6、函数的扩展

ES6允许函数的参数设置默认值,通常会与解构赋值结合起来。缺省参数一般放在参数尾部,如果不这样做,在某些时候,这个参数是没法省略的。这是ES6语法,也就意味着,在作用域内它不能再被let声明。

ES6引入rest参数(...变量名),用于获取函数的多余参数,相当于arguments对象的作用。rest参数之后不能再有其他参数,否则会报错。函数的length属性不包括rest参数。

ES5规定函数内部可设定为严格模式,ES6规定函数参数使用默认值、解构赋值或者扩展运算符,内部不能设定严格模式,否则会报错。

箭头函数,使用=>定义,不需要参数或多个参数就使用圆括号代表参数部分,如果箭头函数多余一条语句就需要用大括号括起来,使用return语句返回值。当返回体是单语句且是对象,必须在对象外部套一个括号,否则会得到错误的反馈。

TIPS:箭头函数本身没有this,它的this是定义时所在的上下文,而不是使用时所在的上下文;不是构造函数不能使用new;不存在arguments,但可以使用rest参数;不能使用yield命令,不能作为Generator函数。

7、数组的扩展

扩展运算符(...),好比是rest参数的逆运算,将数组转为用逗号分隔的参数序列,可用在具有Iterator接口的对象上。

// ES5
function f(x, y, z) {}
var args = [0, 1, 2]
f.apply(null, args)

// ES6
let f = (x, y, z) => {}
let args = [0, 1, 2]
f(...args)

// ES5
var arr1 = [0, 1, 2]
var arr2 = [3, 4, 5]
Array.prototype.push.apply(arr1, arr2)

// ES6
let arr1 = [0, 1, 2]
let arr2 = [3, 4, 5]
arr1.push(...arr2)

数组还扩展了一些实用的方法,如Array.from(),将类数组转为真数组

let arrayLike = { '0': 'a', '1': 'b', length: 2 }
// ES5
var arr1 = [].slice.call(arrayLike)
// ES6
let arr2 = Array.from(arrayLike)

8、对象的扩展

属性简洁表示,属性和值用同一个变量名时可只写属性名。

const foo = 'bar'
// 全写
const baz = {foo:foo}
// 简写
const baz = {foo}

方法名可以省略function关键字

// 全写
const o = {
 method: function() {}
}
// 简写
const o = {
 method() {}
}

JS定义属性的两种方法:obj.key或obj[key],后一种常用在变量的情况,在定义对象时只能用前一种。ES6允许属性名可以用字面量定义对象,变量写入中括号,当变量的值与属性对应的值相同时不能简写。

let propKey = 'foo'
let obj = {
  [propKey]: propKey,
  [propKey] // error
}

this关键字指向函数所在的对象,ES6新增super关键字指向当前对象的原型对象。super关键字表示原型对象时只能用在对象的方法之中。

Object对象新增了一些实用方法:Object.is()、Object.assign()等等。

9、Symbol类型

Symbol值通过Symbol函数生成,表示独一无二的值,现在对象的属性名有两种类型:字符串、Symbol。Symbol不能与其他类型的值进行运算,但可以显示转为字符串。Symbol可以转为布尔值,但是不能转为数值。Symbol的每一个值是不相等的,意味着可以作为标识符用于对象的属性名,保证不会出现相同的属性名。

在很多博客中都在COPY阮老师的一个用Symbol生成唯一常量的例子。这其实挺悲哀的,以前我查资料的时候发现,很多人直接COPY别人的博客,翻来翻来翻去就是一篇文章,完全不带思考能力的。
// 阮老师的例子
const COLOR_RED = Symbol()
const COLOR_GREEN = Symbol()

let getComplement = color => {
  switch(color) {
    case COLOR_RED:
      return COLOR_RED;
    case COLOR_GREEN:
      return COLOR_GREEN;
    default:
      throw new Error('Undefined color');
  }
}
假如这个方法定义在a文件,调用在b文件,就不能用了。Symbol()似乎更适合在私有属性中使用,它是不能共享的,Symbol.for()和没用Symbol之前没太大差。我想如果分模块的话,可以将这个局部常量抛出去或在全局定义这个常量,这样或许可行。

10、Set和Map数据结构

Set本身是一个构造函数,类似于数组,但成员的值是惟一的。

// 数组去重
const set = new Set([1, 2, 3, 4, 4])
[...set]  // [1, 2, 3, 4]
// 去重复字符
[...new Set('abbabc')].join('')  // 'abc'

WeakSet与Set类似,但WeakSet的成员只能是对象,且都是弱引用,垃圾回收机制不考虑WeakSet对该对象的引用,不能被遍历。通常用它来临时存放一组对象,以及存放与对象绑定的信息。

Map也是一个构造函数,类似于对象,但Map的Key不局限于字符串,对同一个Key多次赋值,后面的值会覆盖前面的值。当Key是一个对象时,只有对同一个对象的引用才会被视为同一个键。

WeakMap与Map类似,但WeakMap的Key只接受对象,所指向的对象不计入垃圾回收机制。

11、Proxy

Proxy用于修改某些默认行为,等同于在语言层面做修改,属于一种“元编程”。Proxy相当于在对操作目标做了一层拦截。

12、Reflect

Reflect对象可以拿到语言内部的方法,修改某些object方法的返回结果让其变得合理,让object操作变成函数行为,与Proxy对象的方法对应。

13、Promise

Promise是异步编程的一种解决方案,比传统的回调、事件、发布订阅更为合理,避免了回调地狱的问题。Promise对象是一个构造函数,用来生成实例。

// 定义Promise实例
const getJSON = url => {
  return new Promise((resolve, reject) => {
    const handler = () => {
      if(this.readState !== 4) return
      this.status == 200 ? resolve(this.response) : reject(new Error(this.statusText)
    }
    const client = new XMLHttpRequest()
    client.open('get', url)
    client.onreadystatechange = handler
    client.responseType = 'json'
    client.setRequestHeader('Accept', 'application/json')
    client.send()
  })
}

// 生成实例后then方法进行处理
getJSON ('/posts.jsoin').then(json => {
  // success
}, error => {
  // failure
  // 一般不需要这个回调函数,该用catch来处理
}).catch(error => {
  // failure
}).finally(() => {
  // 一定会执行
})

Promise.all()/Promise.race()将多个Promise实例包装成一个新的Promise实例。

Promise.resolve()/Promise.reject()返回一个新的Promise实例。

14、Generator

Generator函数是另一种异步编程解决方案,是一个内部封装了很多状态的状态机,function和函数名之间有一个星号,内部使用yield表达式定义不同的内部状态。

function* generator() {
  yield 'hello';
  yield 'world';
  return 'ending'; 
}

let g = generator();
g.next()  // {value:'hello', done:false}
g.next()  // {value:'world', done:false}
g.next()  // {value:'ending', done:true}
g.next()  // {value:undefined, done:true}

Generator函数通过next方法将指针移到下一个状态,next传参表示的是上一个yeild表达式的返回值。

function* foo(x) {
  let y = 2 * (yield (x + 1)
  let z = yield (y / 3)
  return (x + y + z)
}

let a = foo(5)
a.next()  // {value: 6, done: false} x = 5
a.next()  // {value: NaN, done: false} y = 2 * undefined
a.next()  // {value: NaN, done: true} 5 + NaN + undefined

let b = foo(5)
b.next()  // {value: 6, done: false} x = 5
b.next(12)  // {value: 8, done: false} y = 2 * 12
b.next(13)  // {value: 42, done: true} 5 + 24 + 13

通过throw方法在函数题外抛出错误,在Generator函数体内捕获。

通过return方法返回给定的值,并终结遍历Generator函数。

yield*表达式,用来在一个Generator函数里执行另一个Generator函数。

作为对象属性的Generator函数,可以简写成下面形式。

let obj = {
  *generator() {
    // ...
  }
}

Generator函数返回一个遍历器,这个遍历器是Generator函数的实例,也继承了Generator函数的prototype对象上的方法,也不能跟new关键字一起用。

// 将Generator函数编程一个正常的实例对象
function* Generator() {
  this.a = 1;
  yield this.b = 2;
  yield this.c = 3;
}

// let obj = {};
// let f = Generator.call(obj);

// let f = Generator.call(Generator.prototype);

let F = () => Generator.call(Generator.prototype);
let f = new F();

f.next();  // {value: 2, done: false}
obj.a  // 1

Generator函数应用:异步操作的同步化表达,Generator的暂停执行效果,减少回调处理;控制流管理,避免回调地狱和大量Promise的语法;在任意对象上部署Iterator接口;当作一个数组结构。

15、async

async函数就是Generator函数的语法糖,将(*)替换成了关键字async,yield替换成了await。当然,async做了优化,内置执行器,返回值是Promise。async函数须等到await异步操作执行完成才返回结果,如果await后的Promise对象抛错,后面的操作就会终止。如果不希望终止,可以在awai后面的Promise对象后做catch拦截,或将语句放入try...catch中。如果不存在继发关系,可以用Promise.all同时触发。

16、Iterator和for...of

Iterator为各种数据结构提供统一简单的访问接口,使得其成员能够按某种次序排列,主要供for...of消费。Iterator部署在数据结构的Symbol.iterator属性,只要具有Symbol.iterator属性就认为是可遍历。Symbol.iterator本身是一个函数,即当前数据结构默认的遍历器生成函数。执行这个属性会返回一个遍历器对象,根本特征是具有next方法。

遍历器对象除了具有next方法,还可以具有return方法和throw方法。如果for...of循环提前退出就会调用return方法,必须返回一个对象。throw方法主要是配合Generator函数,一般遍历器对象用不到这个方法。

for...of循环可以替代数组的forEach方法,forEach不能中断。for...of不能遍历没有部署Iterator的对象,可以利用entries()/keys()/values()达到遍历的目的。

17、Class

JS传统的类的定义方法是通过构造函数,方法封装在构造函数内部或prototype原型链上。

function Point(x, y) {
  this.x = x;
  this.y = y;
  this.distance = function() {
    return Math.sqrt(Math.pow(this.x, 2) + Math.pow(this.y, 2));
  }
}
Point.prototype.toString = function() {
  return '(' + this.x + ', ' + this.y + ')';
}

ES6提供了class关键字,可以理解为一个语法糖,生成实例的时候都需要用new,所有方法都在prototype上面,原型链上都有constructor。ES如果没有显式添加constructor,默认会添加一个空的constructor。class定义的方法不可枚举,ES5写法可以。

在类内部可以使用get和set关键字,对某个属性设置存取值函数,它们都在属性的Descriptor对象上。实例属性可以定义在构造函数的this上面,也可以定义在类的最顶层,此时可以省略this。

class Point {
  constructor(x, y) {
    this.x = x;
    this.y = y;
    this.distance = () => {
      return this.sqrt(Math.pow(this.x, 2) + Math.pow(this.y, 2));
    }
  }
  toString() {
    return `(${this.x}, ${this.y})`;
  }
}

typeof Point  // "function"
Point === Point.prototype.constructor  // true

let p1 = new Point(2, 4);
p1.constructor === Point.prototype.constructor  // true
let p2 = new Point(6, 8);
p1.__proto__ === p2.__proto__  // true

在方法前加上static关键字表示静态方法,该方法不会被实例继承,通过类直接调用。静态方法可以被子类继承,也可以从super对象上调用。

ES6在类的定义上还存在缺陷,缺少私有属性和私有方法的概念,尽管其他方式来达到私有的目的。

Class可以通过extends关键字来实现继承,子类必须在constructor方法中调用super方法才能使用this关键字。super关键字既可以作为函数,也可以当做对象。

class ColorPoint extends Point {
  constructor(x, y, color) {
    super(x, y);
    this.color = color;
  }
  toString() {
    return `${this.color} ${super.toString()}`;
  }
}

Object.getPrototypeOf(ColorPoint) === Point  // true

Class同时拥有prototype和__proto__,子类的__proto__表示构造函数的继承,总是指向父类,子类的prototype的__proto__表示方法的继承,总是指向父类的prototype。

ColorPoint.__proto__ === Point  // true
ColorPoint.prototype.__proto__ === Point.prototype  // true

子类实例的__proto__的__proto__指向父类实例的__proto__,也就是子类的原型的原型是父类的原型。

let p = new Point(2, 3);
let cp = new ColorPoint(2, 3, 'red');

cp.__proto__ === p.__proto__  // false
cp.__proto__.__proto__ === p.__proto__  // true

Mixin指的是多个对象合成一个新的对象,新对象具有各个组成成员的接口。

18、Module

模块功能主要有两个关键字构成:export和import,export用于模块的接口导出,import用于输入模块提供的功能。

// profile.js
export let firstName = 'Michael';
export let lastName = 'Jackson';
export let year = 1958;

// 与上面写法等价
let firstName = 'Michael';
let lastName = 'Jackson';
let year = 1958;
export {firstName, lastName, year};

import可以使用as关键字将输入的变量重命名,也可以通过解构赋值来达到目的。import模块可以省略.js后缀。除了指定加载某个输出值,还可以使用整体加载,即使用星号(*)指定一个对象。import()加载成功后会作为一个对象当作then方法的参数,可以使用对象结构赋值的语法输出接口,default可以用参数直接获得。同时加载多个模块可以采用Promise.all()。

import {firstName: surname} from './profile'
import {firstName as surname} from './profile'

// 整体加载
import * as profile from './profile'

import 'loadsh'

为了用户可以不用阅读文档就能加载模块,可以使用export default为模块指定默认输出。

// 默认模块
import _ from 'loadsh'

// 默认模块和其他模块
import _, {each, forEach} from 'lodash'

在一个模块中先输入后输出一个模块,import语句可以和export语句写在一起。

export {foo, bar} from 'my_module'

// 可以理解为
import {foo, bar} from 'my_module'
export {foo, bar}

// 具名接口改为默认接口
export {es6 as default } from './someModule'

// 默认接口改为具名接口
export {default as es6 } from './someModule'

// export * 会忽略default
export * from 'circle'
export let e = 2.71828182846
export default x => Math.exp(x)

跨模块常量,可以专门建一个或者多个文件模块来共享这些值。

TIPS:ES2015及之后的新版本在浏览器的部署上不是同步的,如果要使用就需要通过转码器编译成ES5的通行标准。大家普遍都是通过babel来完成转换,编译后的代码在大多数浏览器时可以执行的。如果还需要兼容像IE这种货色,这还需要polyfill等方式来解决。

最后关于ES6版本的说法,我更倾向阮一峰老师##传送门##

ES6 既是一个历史名词,也是一个泛指,含义是 5.1 版以后的 JavaScript 的下一代标准,涵盖了 ES2015、ES2016、ES2017 等等,而 ES2015 则是正式名称,特指该年发布的正式版本的语言标准。

转载于:https://my.oschina.net/u/3830333/blog/3094554

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值