ECMAScript 新特性

183 阅读16分钟

一 ECMAScript和JavaScript的关系

我们常常听到ECMAScript和JavaScript这两者,很多情况我们都分不清楚它们之间有什么区别,甚至很多时候我们常常认为它们是等价的。事实上,ECMAScript也是一门脚本语言,更多的来说是一种语言规范,而JavaScrip可以看做是ECMAScript的扩展语言,ECMAScript只是提供了最基本的语法,而JavaScrip实现了ECMAScript的语言标准,并做了扩展。因此,ECMAScript和JavaScript的关系是,前者是后者的规格,后者是前者的一种实现(另外的ECMAScript方言还有Jscript和ActionScript)。这里有张图形象的展示了ECMAScript和JavaScript的关系:JavaScript = ECMAScript + BOM +DOM。

ab.png

二 ECMAScript的历史

image.png 可以看出ES6从开始制定到最后发布,整整用了15年。而我们常说的ES6最准确的说法应该是ECMAScript 2015(ES2015),只不过从ES2015后不在以年份来命名,而是以版本号来命名了,所以我们也常把ES2015叫做ES6。那为什么ES6这个版本显得如此重要呢?可以说ES6是ECMAScript最重要的一次更新了,我们来看看它做了哪些事:

  • 对原有语法进⾏增强。
  • 解决原有语法上的⼀些问题或者缺陷。
  • 全新的对象、全新的⽅法、全新的功能。
  • 全新的数据类型和数据结构。

三 ES2015(ES6) 的新特性

1. let命令

let用来声明变量。它的用法类似于var,但是所声明的变量,只在let命令所在的代码块内有效。我们来看看它的用法,并和var比较一下:

块级作用域

ES6之前只有全局作用域和函数作用域,但let实际上为JavaScript新增了块级作用域,消除了变量覆盖问题。

function f1() {
  var n = 5;
  if (true) {
    var n = 10;
  }
  console.log(n); // 10
}

function f1() {
  let n = 5;
  if (true) {
    let n = 10;
  }
  console.log(n); // 5
}

不存在变量提升

let不像var那样会发生“变量提升”现象。主要是为了减少运行时错误,防止在变量声明前就使用这个变量,从而导致意料之外的行为。所以,变量一定要在声明后使用,否则报错。

console.log(foo); // 输出undefined
console.log(bar); // 报错ReferenceError

var foo = 2;
let bar = 2;

暂时性死区

只要块级作用域内存在let命令,它所声明的变量就“绑定”(binding)这个区域,不再受外部的影响。 let命令声明变量之前,该变量都是不可用的。这在语法上,称为“暂时性死区”。暂时性死区的本质就是,只要一进入当前作用域,所要使用的变量就已经存在了,但是不可获取,只有等到声明变量的那一行代码出现,才可以获取和使用该变量。

for (var i = 0; i < 3; i++) {
  for (var i = 0; i < 3; i++) {
  }
  console.log( i) //3
}

for (let i = 0; i < 3; i++) {
  for (let i = 0; i < 3; i++) {
  }
  console.log(i) //0,1,2
}

不允许重复声明

let不允许在相同作用域内,重复声明同一个变量。

// 报错
function () {
  let a = 10;
  var a = 1;
}

// 报错
function () {
  let a = 10;
  let a = 1;
}

function func(arg) {
  let arg; // 报错
}

function func(arg) {
  {
    let arg; // 不报错
  }
}

顶层对象的属性

顶层对象,在浏览器环境指的是window对象,在Node指的是global对象。ES5之中,顶层对象的属性与全局变量是等价的。

var a = 1; 
console.log(window.a) //1

let a = 1; 
console.log(window.a) //undefined

顶层对象的属性与全局变量挂钩,被认为是JavaScript语言最大的设计败笔之一。这样的设计带来了几个很大的问题,首先是没法在编译时就报出变量未声明的错误,只有运行时才能知道(因为全局变量可能是顶层对象的属性创造的,而属性的创造是动态的);其次,程序员很容易不知不觉地就创建了全局变量(比如打字出错);最后,顶层对象的属性是到处可以读写的,这非常不利于模块化编程。另一方面,window对象有实体含义,指的是浏览器的窗口对象,顶层对象是一个有实体含义的对象,也是不合适的。

ES6为了改变这一点,一方面规定,为了保持兼容性,var命令和function命令声明的全局变量,依旧是顶层对象的属性;另一方面规定,let命令、const命令、class命令声明的全局变量,不属于顶层对象的属性。也就是说,从ES6开始,全局变量将逐步与顶层对象的属性脱钩。

2. const命令

const和let实际上是差不多的,let的特性const也是具备的,只是const声明的是只读的常量。一旦声明,常量的值就不能改变。所以我们只会讨论const与let不一致的地方

const a =1;
a=2; // TypeError: Assignment to constant variable.

事实上const声明的数据更具体的含义指的是数据的地址不可变,对于基本类型来说也就是值不可变,但对于复杂类型来说,指的就是引用地址不能变,只有引用地址不变,是可以被修改的,例如:

const foo = {};
foo.prop = 123; //为foo添加一个成员属性,引用地址没有发生改变,可以操作

foo = {}; // TypeError: "foo" is read-only(引用地址发生改变,所以这个是不被允许的)

3. 变量的解构赋值

数组的解构赋值

只要某种数据结构具有Iterator接口,都可以采用数组形式的解构赋值。

var [a, b, c] = [1, 2, 3];
let [foo, [[bar], baz]] = [1, [[2], 3]];
let [ , , third] = ["foo", "bar", "baz"]; // third =  "baz"
let [head, ...tail] = [1, 2, 3, 4]; //head =1   tail = [2, 3, 4]
let [x, y, ...z] = ['a']; //a='a' y = undefined  z=[]

var [foo = true] = []; // foo = true

//ES6内部使用严格相等运算符(===),判断一个位置是否有值。所以,如果一个数组成员不严格等于undefined,默认值是不会生效的。
var [x = 1] = [undefined]; //x =1
var [x = 1] = [null];// x = null


let [x = 1, y = x] = [2];    // x=2; y=2
let [x = y, y = 1] = [];     // ReferenceError

对象的解构赋值

var { foo, bar } = { foo: "aaa", bar: "bbb" };

let obj = { first: 'hello', last: 'world' };
let { first: f, last: l } = obj; //变量重命名

var {x = 3} = {};//x=3

var {x, y = 5} = {x: 1};//x=1 y=5

var {x:y = 3} = {};//y=3

var {x:y = 3} = {x: 5};//y=5

字符串的解构赋值

const [a, b, c, d, e] = 'hello';
a // "h"
b // "e"
c // "l"
d // "l"
e // "o"

let {length : len} = 'hello';
len // 5

4.字符串的扩展

字符串的遍历器接口

ES6为字符串添加了遍历器接口(Iterator),使得字符串可以被for...of循环遍历。

for (let codePoint of 'foo') {
  console.log(codePoint)
}

at()

给定字符串位置,返回正确的字符。

'abc'.at(0) // "a"
'𠮷'.at(0) // "𠮷"

startsWith(), endsWith()

  • startsWith():返回布尔值,表示参数字符串是否在源字符串的头部。
  • endsWith():返回布尔值,表示参数字符串是否在源字符串的尾部。
var s = 'Hello world!';

s.startsWith('Hello') // true
s.endsWith('!') // true

repeat()

repeat方法返回一个新字符串,表示将原字符串重复n次。

'x'.repeat(3) // "xxx"
'na'.repeat(0) // ""
'na'.repeat(2.9) // "nana" 参数如果是小数,会被取整。
'na'.repeat(Infinity)// RangeError 参数是负数或者Infinity,会报错
'na'.repeat(-1)// RangeError 参数是负数或者Infinity,会报错
'na'.repeat(-0.9) // "" 但是如果参数是0到-1之间的小数,则等同于0
'na'.repeat(NaN) // "" 参数NaN等同于0
'na'.repeat('na') // ""   参数是字符串,则会先转换成数字。
'na'.repeat('3') // "nanana"  参数是字符串,则会先转换成数字。

模板字符串

模板字符串(template string)是增强版的字符串,用反引号(`)标识。它可以当作普通字符串使用,也可以用来定义多行字符串,或者在字符串中嵌入变量。

const name = 'tom'
// 可以通过 ${} 插入表达式,表达式的执行结果将会输出到对应位置
const msg = `hey, ${name} --- ${1 + 2} ---- ${Math.random()}`
console.log(msg)

const name = 'Tom'
const age = 18

  function fn(strArr,name,age){
    console.log(strArr,name,age) //[ '你好,我叫', ' , 今年', '。' ] Tom 18
    return strArr[0] +name+  strArr[1] + age + strArr[2]
  }

  let r = fn`你好,我叫${name} , 今年${age}。`
  console.log(r) //你好,我叫Tom , 今年18.

5. 数组的扩展

Array.from()

用于将两类对象转为真正的数组:类似数组的对象(array-like object)和可遍历(iterable)的对象(包括ES6新增的数据结构Set和Map)。

所谓类似数组的对象,本质特征只有一点,即必须有length属性。因此,任何有length属性的对象,都可以通过Array.from方法转为数组,而此时扩展运算符就无法转换。

Array.from('hello') // ['h', 'e', 'l', 'l', 'o']
Array.from([1, 2, 3], (x) => x * x) // [1, 4, 9]
Array.from({ length: 3 });// [ undefined, undefined, undefined ]

Array.of()

用于将一组值,转换为数组。

Array.of(3, 11, 8) // [3,11,8]
Array.of() // []
Array.of(undefined) // [undefined]

//模拟实现
function ArrayOf(){
  return [].slice.call(arguments);
}

数组实例的find()和findIndex()

数组实例的find方法,用于找出第一个符合条件的数组成员。它的参数是一个回调函数,所有数组成员依次执行该回调函数,直到找出第一个返回值为true的成员,然后返回该成员。如果没有符合条件的成员,则返回undefined。

数组实例的findIndex方法的用法与find方法非常类似,返回第一个符合条件的数组成员的位置,如果所有成员都不符合条件,则返回-1。

find和findIndex方法的回调函数可以接受三个参数,依次为当前的值、当前的位置和原数组。

[1, 5, 10, 15].find(function(value, index, arr) {
  return value > 9;
}) // 10

[1, 5, 10, 15].findIndex(function(value, index, arr) {
  return value > 9;
}) // 2

[NaN].indexOf(NaN)
// -1

[NaN].findIndex(y => Object.is(NaN, y))
// 0

数组实例的fill()

fill方法使用给定值,填充一个数组。

['a', 'b', 'c'].fill(7)// [7, 7, 7]

new Array(3).fill(7)// [7, 7, 7]

// fill方法还可以接受第二个和第三个参数,用于指定填充的起始位置和结束位置。
['a', 'b', 'c'].fill(7, 1, 2)// ['a', 7, 'c']

数组实例的keys()

keys()用于遍历数组。返回一个遍历器对象,可以用for...of循环进行遍历,keys()是对键名的遍历

for (let index of ['a', 'b'].keys()) {
  console.log(index);
}

6. 对象的扩展

属性的简洁表示法

 let name = 'Tom'
  let fn =  function () {
    console.log("fn执行")
  }
  let obj ={
    name,
    fn
  }
  console.log(obj.name)
  obj.fn()

Object.is()

用来比较两个值是否严格相等。

Object.is('foo', 'foo')
// true
Object.is({}, {})
// false

+0 === -0 //true
NaN === NaN // false

Object.is(+0, -0) // false
Object.is(NaN, NaN) // true

Object.assign()

用于对象的合并,将源对象(source)的所有可枚举属性,复制到目标对象(target)。

var target = { a: 1 };
var source1 = { b: 2 };
var source2 = { c: 3 };
let obj = Object.assign(target, source1, source2);//obj===target  {a:1, b:2, c:3}

Object.assign(undefined) // 报错
Object.assign(null) // 报错

let obj = {a: 1};
Object.assign(obj, undefined) === obj // true
Object.assign(obj, null) === obj // true

Object.assign({ a: 'b' }, { [Symbol('c')]: 'd' }) // { a: 'b', Symbol(c): 'd' }

//Object.assign方法实行的是浅拷贝,而不是深拷贝。
//Object.assign拷贝得到的是这个对象的引用。这个对象的任何变化,都会反映到目标对象上面。
let obj1 = {a:1,b:{c:2}}
let obj2 = Object.assign({},obj1)
obj1.b.c =9
console.log(obj2.b.c) //9

//Object.assign把数组视为属性名为0、1、2的对象,因此目标数组的0号属性4覆盖了原数组的0号属性1。
Object.assign([1, 2, 3], [4, 5]) // [4, 5, 3]

属性的可枚举性

对象的每个属性都有一个描述对象(Descriptor),用来控制该属性的行为。Object.getOwnPropertyDescriptors(ES8)方法可以获取该属性的描述对象。

let obj = { foo: 123 };
Object.getOwnPropertyDescriptor(obj, 'foo')
//  {
//    value: 123,
//    writable: true,
//    enumerable: true,
//    configurable: true
//  }

Object.getOwnPropertyDescriptor(Object.prototype, 'toString').enumerable
// false

Object.getOwnPropertyDescriptor([], 'length').enumerable
// false

//toString和length属性的enumerable都是false,因此for...in不会遍历到这两个继承自原型的属性。

//所有Class的原型的方法都是不可枚举的。
Object.getOwnPropertyDescriptor(class {foo() {}}.prototype, 'foo').enumerable
// false

属性的遍历

  • for...in

    for...in循环遍历对象自身的和继承的可枚举属性(不含Symbol属性)。
    
  • Object.keys(obj)

    Object.keys返回一个数组,包括对象自身的(不含继承的)所有可枚举属性(不含Symbol属性)。
    
  • Object.getOwnPropertyNames(obj)

    Object.getOwnPropertyNames返回一个数组,包含对象自身的所有属性(不含Symbol属性,但是包括不可枚举属性)。
    
  • Object.getOwnPropertySymbols(obj)

    Object.getOwnPropertySymbols返回一个数组,包含对象自身的所有Symbol属性。
    
  • Reflect.ownKeys(obj)

    Reflect.ownKeys返回一个数组,包含对象自身的所有属性,不管是属性名是Symbol或字符串,也不管是否可枚举。
    
const obj = {};
Object.defineProperties(obj, {
    property1: {enumerable: true, value: 1},
    property2: {enumerable: false, value: 2},
    [Symbol("property3")]: {enumerable: true, value: 3},
    [Symbol("property4")]: {enumerable: false, value: 4},
    10:{enumerable: true, value: 5},
    11:{enumerable: false, value: 6},
    "a":{enumerable: false, value: 7},
    "b":{enumerable: true, value: 8},
});
for(let key in obj){
  console.log(key) //10 property1 b
}
console.log(Object.keys(obj)); //["10", "property1", "b"]
console.log(Object.getOwnPropertyNames(obj));//["10", "11", "property1", "property2", "a", "b"]
console.log(Object.getOwnPropertySymbols(obj));//[Symbol(property3), Symbol(property4)]
console.log(Reflect.ownKeys(obj));//["10", "11", "property1", "property2", "a", "b", Symbol(property3), Symbol(property4)]

7. Symbol

ES6引入了一种新的原始数据类型Symbol,表示独一无二的值。它是JavaScript语言的第七种数据类型,前六种是:Undefined、Null、布尔值(Boolean)、字符串(String)、数值(Number)、对象(Object)。

typeof Symbol() //"symbol"
Symbol() == Symbol() //false

const name = Symbol()
let obj = {
  [name]: "Tom"
}
obj[name] // 'Tom'
obj.name //undefined


//Symbol值不能与其他类型的值进行运算,会报错。
var sym = Symbol('My symbol');
"your symbol is " + sym // TypeError: can't convert symbol to string

//Symbol值也可以转为布尔值,但是不能转为数值。
var sym = Symbol();
Boolean(sym) // true
Number(sym) // TypeError

//有时,我们希望重新使用同一个Symbol值,Symbol.for方法可以做到这一点
Symbol.for("bar") === Symbol.for("bar")//true

//Symbol.keyFor方法返回一个已登记的 Symbol 类型值的key。
var s1 = Symbol.for("foo");
Symbol.keyFor(s1) // "foo"

8. Set和Map数据结构

Set

ES6提供了新的数据结构Set。它类似于数组,但是成员的值都是唯一的,没有重复的值。Set本身是一个构造函数,用来生成Set数据结构。Set函数可以接受一个数组(或类似数组的对象)作为参数,用来初始化。

向Set加入值的时候,不会发生类型转换,所以5和"5"是两个不同的值。

  • add(value):添加某个值,返回Set结构本身。
  • delete(value):删除某个值,返回一个布尔值,表示删除是否成功。
  • has(value):返回一个布尔值,表示该值是否为Set的成员。
  • clear():清除所有成员,没有返回值。
//数组去重
 Array.from(new Set([1,2,1,2,3])) //[1,2,3] 
 
 
 let s = new Set();
 s.add(1).add(2).add(2); //Set(2) {1, 2}
// 注意2被加入了两次

s.size // 2

s.has(1) // true
s.has(2) // true
s.has(3) // false

s.delete(2);
s.has(2) // false

WeakSet

WeakSet结构与Set类似,也是不重复的值的集合。但是,它与Set有两个区别。

  • WeakSet的成员只能是对象,而不能是其他类型的值。
  • WeakSet中的对象都是弱引用,即垃圾回收机制不考虑WeakSet对该对象的引用,也就是说,如果其他对象都不再引用该对象,那么垃圾回收机制会自动回收该对象所占用的内存,不考虑该对象还存在于WeakSet之中。这个特点意味着,无法引用WeakSet的成员,因此WeakSet是不可遍历的。

WeakSet可以接受一个数组或类似数组的对象作为参数。

var ws = new WeakSet();

ws.add(1); // TypeError: Invalid value used in weak set
ws.add(Symbol()); // TypeError: Invalid value used in weak set

var a = [[1,2], [3,4]];
ws.add(a); //WeakSet {Array(2), Array(2)}

var b = [3, 4];
ws.add(b); //WeakSet {Array(2)}


var obj = {};
var foo = {};
ws.add(window);
ws.add(obj);
ws.has(window); // true
ws.has(foo);    // false
ws.delete(window);
ws.has(window);    // false

Map

ES6提供了Map数据结构。它类似于对象,也是键值对的集合,但是“键”的范围不限于字符串,各种类型的值(包括对象)都可以当作键。

const obj = {}
obj[{ a: 1 }] = 'value'
console.log(Object.keys(obj)) //["[object Object]"]



var m = new Map();
var o = {p: 'Hello World'};
m.set(o, 'content')
m.get(o) // "content"
m.has(o) // true
m.delete(o) // true
m.has(o) // false

//Map也可以接受一个数组作为参数
var map = new Map([
  ['name', '张三'],
  ['title', 'Author']
]);
map.size // 2
map.has('name') // true
map.get('name') // "张三"
map.has('title') // true
map.get('title') // "Author"

//如果对同一个键多次赋值,后面的值将覆盖前面的值。
let map = new Map();
map
.set(1, 'aaa')
.set(1, 'bbb');
map.get(1) // "bbb"

new Map().get('asfddfsasadf') //undefined

//内存地址是不一样的
var map = new Map();
map.set(['a'], 555);
map.get(['a']) // undefined

let map = new Map();
map.set(NaN, 123);
map.get(NaN) // 123
map.set(-0, 123);
map.get(+0) // 123

WeakMap

WeakMap结构与Map结构基本类似,唯一的区别是它只接受对象作为键名(null除外),不接受其他类型的值作为键名,而且键名所指向的对象,不计入垃圾回收机制。WeakMap只有四个方法可用:get()、set()、has()、delete()。

9. Proxy

Proxy 可以理解成,在目标对象之前架设一层“拦截”,外界对该对象的访问,都必须先通过这层拦截,因此提供了一种机制,可以对外界的访问进行过滤和改写。Proxy 这个词的原意是代理,用在这里表示由它来“代理”某些操作,可以译为“代理器”。

let obj ={b:2}
let proxy = new  Proxy(obj,{
  get(target, property,receiver){
    //target是实例化Proxy时使用的对象,property是这次读取操作中想要获取的属性名,receiver则是这个实例化的Proxy自身,即proxy。
    console.log(target, property,receiver,"get") //{b: 2, a: 1}  "a"  Proxy {b: 2, a: 1}  "get"
   // return target[property] //返回值作为访问的值

   //Reflect内部封装了一系列对对象的操作,所以我们在实现自己逻辑后,可以调用Reflect
   return Reflect.get(target, property,receiver)
  },
  set(target, property, value,receiver){
    console.log(target, property, value,receiver,"set") //{b: 2} "a" 1 Proxy {b: 2} "set"
    //  target[property] = value
    //  return true
    return Reflect.set(target, property, value,receiver)
  },
  deleteProperty(target,property){
    console.log(target, property,"deleteProperty") //{b: 2} "a" "deleteProperty"
    // delete target[property]
    return Reflect.deleteProperty(target,property)
  }
})
proxy.a =1
console.log(proxy.a) //1
delete proxy.a
console.log(proxy.a) //undefined
  1. Iterator和for...of循环 Iterator的作用有三个:一是为各种数据结构,提供一个统一的、简便的访问接口;二是使得数据结构的成员能够按某种次序排列;三是ES6创造了一种新的遍历命令for...of循环,Iterator接口主要供for...of消费。总之,能够提供Iterator接口的数据都能被for...of遍历。

在我们的数组(Array),对象(Object),Map和Set四种数据集合中,只有对象没有部署Iterator接口,所以不能被for...of遍历。


const obj = { foo: 123, bar: 456 }
for (const item of obj) { //报错 obj is not iterable
  console.log(item)
}

//接下来我们为obj部署Iterator,使它能被遍历
obj[Symbol.iterator ] = function(){
  let index = 0
  const key = Object.keys(this);
  return{
    next:function(){
      return index<key.length?{value:obj[key[index++]],done:false}:{value:undefined,done:true}
    }
  }
}

//在打印一下
for (const item of obj) { //123  456
  console.log(item)
}

四 ES2016(ES7) 的新特性

1.求幂运算符(**)

Math.pow(3, 2) === 3 ** 2    // 9

2.数组实列的includes()

  • includes():返回布尔值,表示是否找到了参数字符串。
[1, 2, 3].indexOf(3) > -1 // true
等同于:
[1, 2, 3].includes(3) // true

[1, 2, NaN].includes(NaN)     // true
[1, 2, NaN].indexOf(NaN)  // -1

五 ES2017(ES8) 的新特性

1. 数组实例的entries()和values()

entries()和values()——用于遍历数组。它们都返回一个遍历器对象,可以用for...of循环进行遍历,唯一的区别是values()是对键值的遍历,entries()是对键值对的遍历。


for (let elem of ['a', 'b'].values()) {
  console.log(elem);
}
// 'a'
// 'b'

for (let [index, elem] of ['a', 'b'].entries()) {
  console.log(index, elem);
}
// 0 "a"
// 1 "b"

2.Object.getOwnPropertyDescriptors

Object.getOwnPropertyDescriptors方法可以获取该属性的描述对象。

const obj = {
  foo: 123,
  get bar() { return 'abc' }
};

Object.getOwnPropertyDescriptors(obj)
// { foo:
//    { value: 123,
//      writable: true,
//      enumerable: true,
//      configurable: true },
//   bar:
//    { get: [Function: bar],
//      set: undefined,
//      enumerable: true,
//      configurable: true } }

3.padStart(),padEnd()

ES8推出了字符串补全长度的功能。如果某个字符串不够指定长度,会在头部或尾部补全。padStart用于头部补全,padEnd用于尾部补全。

'x'.padStart(5, 'ab') // 'ababx'
'x'.padStart(4, 'ab') // 'abax'

'x'.padEnd(5, 'ab') // 'xabab'
'x'.padEnd(4, 'ab') // 'xaba'

4.async、await异步解决方案

console.log('A')
setTimeout(()=>console.log('B'),1000)
const start = new Date()
while(new Date- start<3000){}
console.log("C")
setTimeout(()=>console.log('D'),0)
new Promise((resolve,reject)=>{
  console.log('E')
  foo.bar(100)
})
.then(()=>console.log("F"))
.then(()=>console.log("G"))
.catch(()=>console.log("H"))
console.log('I')
//ACEIHBD

六 附录

ES6入门文档