JavaScript 深度剖析 - ECMAScript 新特性(2015-2017)

一、ECMAScript 概述

  • 实际上 JavaScript 是 ECMAScript 的扩展语言
  • JavaScript 语言本身指的就是 ECMAScript
  • 2015 年开始 ECMAScript 保持每年一个版本的迭代
    在这里插入图片描述
    在这里插入图片描述

二、ES2015 概述(ES6)

  • 解决原有语法上的一些问题或者不足
  • 对原有语法进行增强
  • 全新的对象、全新的方法、全新的功能
  • 全新的数据类型和数据结构
  • 也有程序员将 ES6 和 ES6 以后的版本统称为 ES6

2.1 let 与块级作用域

  • 作用域:某个成员能够起作用的范围
  • 在ES6之前,ES中只有全局作用域函数作用域两种作用域
  • ECMAScript2015 新增了块级作用域
  • 块指的是 { } 包裹的范围
  • 以前块是没有单独的作用域的,就导致块中定义的成员外部也可以访问到
if (true) {
  // var foo = 'zce'
  let foo = 'zce'
  console.log(foo)
}
console.log(foo); // 报错
  • let 关键词可以解决循环嵌套当中计数器重名导致的问题
// 因为使用 var 声明,是一个全局的成员,内部所声明的 i 会覆盖外层所声明的 i
for (var i = 0; i < 3; i++) {
  for (var i = 0; i < 3; i++) {
    console.log(i)
  }
  console.log('内层结束 i = ' + i)
}
// 0
// 1
// 2
// 内层结束 i = 3

for (var i = 0; i < 3; i++) {
  for (let i = 0; i < 3; i++) {
    console.log(i)
  }
  console.log('内层结束 i = ' + i)
}
// 0
// 1
// 2
// 内层结束 i = 3
// 0
// 1
// 2
// 内层结束 i = 3
// 0
// 1
// 2
// 内层结束 i = 3
  • 循环注册事件时使用
// let 应用场景:循环绑定事件,事件处理函数中获取正确索引
var elements = [{}, {}, {}]
for (var i = 0; i < elements.length; i++) {
  elements[i].onclick = function () {
    console.log(i) 
  }
}
elements[0].onclick() // 3

//可以使用闭包解决
var elements = [{}, {}, {}]
for (var i = 0; i < elements.length; i++) {
  elements[i].onclick = (function (i) {
    return function () {
      console.log(i)
    }
  })(i)
}
elements[0].onclick() // 0

// 使用 let 解决
var elements = [{}, {}, {}]
for (let i = 0; i < elements.length; i++) {
  elements[i].onclick = function () {
    console.log(i)
  }
}
elements[0].onclick() // 0
  • 其他情况
// 两个 let 互不影响
// 第一个let处于:for循环本身的作用域
for (let i = 0; i < 3; i++) {
// 第二个let处于:内层独立作用域
  let i = "foo";
  console.log(i);
}
// foo
// foo
// foo

2.2 const (恒量 / 常量)

  • 在 let 基础上新增了只读特性,变量被声明后不允许在被修改
const name = 'zce'
// 恒量声明过后不允许重新赋值(会报错)
name = 'jack'

// 恒量要求声明同时赋值(会报错)
const name
name = 'zce'
  • const声明的成员不可被修改,只是说不允许在声明过后重新去指向一个新的内存地址,并不是我们不允许修改衡量中的属性成员
// 恒量只是要求内层指向不允许被修改
const obj = {}
// 对于数据成员的修改是没有问题的
obj.name = 'zce'

obj = {} // 但此操作会改变内存地址指向,会报错

2.3 数组的解构

  • 使用解构快速支取数组成员
const arr = [100, 200, 300]
const [foo, bar, baz] = arr
console.log(foo, bar, baz)
// 100,200,300
  • 除此之外还可以在解构位置的变量名之前去添加三个点表示提取从当前位置往后的所有成员,最终所有的结果会放到一个数组当中,注意:此用法只能在数组的最后一个位置使用
const arr = [100, 200, 300]
const [foo, ...rest] = arr
console.log(rest) // [ 200, 300 ]
  • 如果只是想获取其中某个位置所对应的成员,比如只获取第三个成员
const arr = [100, 200, 300]
const [, , baz] = arr
console.log(baz) // 300
  • 如果解构位置的成员个数小于数组的长度,就会按照从前到后的是顺序去提取,多出来的成员就不会被提取,反之如果大于,则提取到的就为 undefined
const arr = [100, 200, 300]
const [foo, bar] = arr
console.log(bar) // 200
const arr = [100, 200, 300]
const [foo, bar, baz, more] = arr
console.log(more) // undefined
  • 在解构位置的后面跟上一个等号,在其后面写上一个默认值,这样如果没有提取到数组当中对应的成员,变量就会得到这个默认值
const arr = [100, 200, 300];
const [foo, bar, baz = 123, more = "default value"] = arr;
console.log(bar, more); // 200 default value
  • 如果拆分一个字符串,通过解构就可以大大简化这样的过程
const path = '/foo/bar/baz'
const [, rootdir] = path.split('/')
console.log(rootdir) // foo

2.4 对象的解构

const obj = { name: "zce", age: 18 };
const { age } = obj;
console.log(age); // 18
  • 因为解构的变量名同时也也是要去匹配被解构对象的属性名的,所以当前作用域有同名的成员就会产生冲突
const name = "tom";
const { name } = obj;
console.log(name); // 报错
  • 可以采取重命名的方式解决,在成员名的后面加上:然后跟上一个新的名称
const name = "tom";
const obj = { name: "zce", age: 18 };
const { name: objName } = obj;
console.log(name); // tom
console.log(objName); // zce
  • 如果还需要同时添加默认值,可以在冒号后的变量名后面继续跟上等于号去设置对应的默认值
const name = "tom";
const obj = { age: 18 };
const { name: objName = "jack" } = obj;
console.log(objName); // jack
  • 如果代码中用到了大量的 console.log 我们就可以把它解构出来
const { log } = console
log('foo')
log('bar')
log('123')

2.5 模板字符串

在 ECMAScript 中还增强了定义字符串的方式。传统定义字符串需要单引号或者双引号的方式,在新语法中,增加了一种模板字符串的方式,它需要使用反引号反引号去标识

const str = `hello es2015, this is a string`
  • 直接使用和普通字符串也没有任何区别
  • 如果内部需要反引号同样可以通过 \ 转义
const str = `hello es2015,this is a \`string\``
  • 支持多行,可以直接输入换行符
// 允许换行
const str = `hello es2015,
this is a \`string\``
console.log(str)
/*hello es2015,
this is a `string`*/
  • 支持插值表达式
const name = "tom";
const msg = `hey, ${name} --- ${1 + 2} ---- ${Math.random()}`;
console.log(msg);
//hey, tom --- 3 ---- 0.5681677986402371

2.6 带标签的模版字符串

在模板字符串之前添加一个标签,这个标签实际上就是一个特殊的函数,添加这个标签就是调用这个函数

const str = console.log`hello world`;
// [ 'hello world' ]
  • 函数可以接收模板字符串的内容中分割过后的结果以及表达式的返回值
const name = "tom";
const gender = true;
function myTagFunc(strings, name, gender) {
  console.log(strings, name, gender);
  // [ 'hey, ', ' is a ', '.' ] tom true
}
const result = myTagFunc`hey, ${name} is a ${gender}.`;
const name = "tom";
const gender = true;
function myTagFunc(strings, name, gender) {
  return strings[0] + name + strings[1] + gender + strings[2];
}
const result = myTagFunc`hey, ${name} is a ${gender}.`;
console.log(result); // hey, tom is a true.

// 或
const name = "tom";
const gender = true;
function myTagFunc(strings, name, gender) {
  const sex = gender ? "man" : "woman";
  return strings[0] + name + strings[1] + sex + strings[2];
}
const result = myTagFunc`hey, ${name} is a ${gender}.`;
console.log(result); // hey, tom is a man.

2.7 字符串的扩展方法

详细可见字符串的扩展方法

ECMAScript 6 增加了 3 个用于判断字符串中是否包含另一个字符串的方法:startsWith()、endsWith() 和 includes(),都会返回一个表示是否包含的布尔值

  • includes() 检查整个字符串
  • startWith() 检查开始于索引 0 的匹配项
  • endsWith() 检查开始于索引( string.length - substring.length )的匹配项
const message = "Error: foo is not defined.";
console.log(
  message.startsWith("Error"), // true
  message.endsWith("."), // true
  message.includes("foo") // true
);

2.8 参数默认值

以前的写法

function foo(enable) {
  // 短路运算很多情况下是不适合判断默认参数的,例如传入 0 '' false null,都会使用默认值
  // enable = enable || true
  enable = enable === undefined ? true : enable;
  console.log("foo invoked - enable: ");
  console.log(enable);
}
foo(false);
// foo invoked - enable:
// false

ECMAScript2015 写法

// 在没有传递实参,或参数是 undefined 时才会使用默认参数
function foo(enable = true) {
  console.log("foo invoked - enable: ");
  console.log(enable);
}
foo(false);
// foo invoked - enable:
// false

注意:如果有多个参数,带有默认值的形参一定要放在参数的最后,因为参数是按照次序传递的,如果不在最后,默认值将无法正常工作

2.9 剩余参数

对于未知个数的参数,往往我们使用 arguments 对象接收,它是一个伪数组,在 ECMAScript2015 当中新增了一个… 的操作符,这种操作符有两个作用,在这里我们用到 Rest 作用,也就是剩余操作符

// function foo () {
//   console.log(arguments)
// }

function foo (...args) {
  console.log(args)
}
foo(1, 2, 3, 4);
// [ 1, 2, 3, 4 ]

注意:只能出现在形参的最后一位,只能使用一次

2.10 展开数组

… 除了以上讲到的 Rest 的用法还有一个 Spread 的用法,意思就是展开,此处展示函数相关的数组参数展开

  • 以前的写法
const arr = ['foo', 'bar', 'baz']
console.log(
  arr[0],
  arr[1],
  arr[2],
)
  • 如果数组元素个数不固定就不能这样使用,以前我们可以通过 apply 来解决
console.log.apply(console, arr)
  • ECMAScript2015 写法
console.log(...arr); // foo bar baz

2.11 箭头函数

function inc (number) {
  return number + 1
}

// 最简方式
const inc = n => n + 1
// 完整参数列表,函数体多条语句,返回值仍需 return
const inc = (n, m) => {
  console.log('inc invoked')
  return n + 1
}
console.log(inc(100))
const arr = [1, 2, 3, 4, 5, 6, 7]
arr.filter(i => i % 2)

2.12 箭头函数与 this

相比于普通函数箭头函数不会改变 this 的指向

  • 普通函数
const person = {
  name: "tom",
  // 在普通函数当中 this始终指向调用这个函数的对象
  sayHi: function () {
    console.log(`hi, my name is ${this.name}`);
  },
};
person.sayHi(); // hi, my name is tom
  • 箭头函数
const person = {
  name: "tom",
  sayHi: () => console.log(`hi, my name is ${this.name}`),
};
person.sayHi(); // hi, my name is undefined
const person = {
  name: "tom",
  sayHiAsync: function () {
    console.log(this);
    // { name: 'tom', sayHiAsync: [Function: sayHiAsync] }
    setTimeout(() => {
      console.log(this);
      // { name: 'tom', sayHiAsync: [Function: sayHiAsync] }
    }, 1000);
  },
};
person.sayHiAsync();

2.13 对象字面量增强

  • 传统写法
const bar = '345'
const obj = {
  foo: 123,
  bar: bar
}
  • ECMAScript2015 写法
// 属性名与变量名相同,可以省略 : bar
const obj = {
  foo: 123,
  bar,
}
  • 对象方法写法
const obj = {
  // method1: function () {
  //   console.log('method111')
  // }

  method1() {
    console.log("method111");
    console.log(this); // this指向当前对象
    // { method1: [Function: method1] }
  },
};
obj.method1();
  • 可以使用表达式的返回值作为对象的属性名

传统写法

const obj = {
  Math.random(): 123 // 此写法不被允许
}
// 只能在对象声明过后通过索引器动态添加(正确写法)
// obj[Math.random()] = 123

ECMAScript2015 写法

// 计算属性名 在[ ]内可以使用任意的表达式,表达式的执行结果将会作为属性的属性名
const obj = {
  // 通过 [] 让表达式的结果作为属性名
  [bar]: 123
}

2.14 对象扩展方法

2.14.1 Object.assign

将多个源对象中的属性复制到一个目标对象中,如果对象之间有相同属性,源对象属性会覆盖目标对象属性

  • 可以传递任意多个对象
  • 返回值为目标对象
  • 第一个参数: 目标对象,
  • 第一个参数以后的参数:源对象
const source1 = {
  a: 123,
  b: 123,
};

const source2 = {
  b: 789,
  d: 789,
};

const target = {
  a: 456,
  c: 456,
};

const result = Object.assign(target, source1, source2);

console.log(target); // { a: 123, c: 456, b: 789, d: 789 }
console.log(result === target); // true

使用场景:使用 Object.assign 将其复制到一个全新的对象上

function func(obj) {
  const funcObj = Object.assign({}, obj);
  funcObj.name = "func obj";
  console.log(funcObj); // { name: 'func obj' }
}
const obj = { name: "global obj" };
func(obj);
console.log(obj); // { name: 'global obj' }

2.14.2 Object.is

判断两个值是否相等

console.log(
  0 == false, //  true
  0 === false, //  false
  +0 === -0, //  true
  NaN === NaN, // false
  NaN === NaN, //  false
  Object.is(+0, -0), //  false
  Object.is(NaN, NaN) //  true
);

2.15 Proxy 代理对象

  • 如果我们想要监视某个对象中的属性读写,我们可以使用 ES5 中提供的Object.defineProperty 这样的方法来去为对象添加属性,这样就可以捕获到对象当中属性的读写过程
  • 在 Vue3.0 以前的版本就是使用这样一个方法实现的数据响应,从而完成双向数据绑定
  • 在 ES2015 当中全新设计了一个叫做 Proxy 的类型:它就是专门为了对象设置访问代理器的
  • 所谓的代理,可以将其理解为门卫,也就是说不管你进去拿东西或是放东西,都需要经过这样一个代理,通过 Proxy 就可以轻松监视到对象的读写过程
  • 相比于 defineProperty,Proxy 的功能更加强大,使用起来也更加方便
  • Vue3.0 就已经开始使用 Proxy 去实现内部的数据响应了
const person = {
  name: "zce",
  age: 20,
};
// 创建一个代理对象 第一个参数:需要代理的目标对象;
// 第二个参数也是一个对象,是代理的处理对象;
const personProxy = new Proxy(person, {
  // 监视属性的访问
  // 接受两个参数:代理的目标对象,外部访问的属性名
  get(target, property) {
    console.log(target, property);
    // { name: 'zce', age: 20 } name
    return 100;
  },
  // 监视属性设置
  set() {},
});
console.log(personProxy.name); // 100
  • get 方法内部的正常逻辑:先判断代理目标对象是否存在这样一个属性,如果存在就返回一个对应的值,反之如果不存在则返回 undefined 或者是一个默认值
const person = {
  name: "zce",
  age: 20,
};
// 创建一个代理对象 第一个参数:需要代理的目标对象;
// 第二个参数也是一个对象,是代理的处理对象;
const personProxy = new Proxy(person, {
  // 监视属性的访问
  get(target, property) {
    return property in target ? target[property] : "default";
  },
  // 监视属性设置
  set() {},
});
console.log(personProxy.name); // zce
console.log(personProxy.xxx); // default
  • set 方法接收三个参数:代理目标对象,要写入的属性名称,要写入的属性值
const person = {
  name: "zce",
  age: 20,
};
const personProxy = new Proxy(person, {
  // 监视属性读取
  get() {},
  // 监视属性设置
  // 三个参数:代理的目标对象,要写入的属性名称,要写入的属性值
  set(target, property, value) {
    console.log(target, property, value);
    // { name: 'zce', age: 20 } gender true
  },
});
personProxy.gender = true;
  • set 方法内部的正常逻辑:为代理目标设置指定的属性,这里我们可以先做一些数据校验,如果设置的是 age,值就必须位数字,否则报错
  • 完成以后尝试给代理对象设置 age 为一个字符串,此时就会报错,如果设置的是一个正常的数字,结果就可以设置到目标对象上(点击查看 Number.isInteger 知识点
const person = {
  name: "zce",
  age: 20,
};
const personProxy = new Proxy(person, {
  // 监视属性读取
  get() {},
  // 监视属性设置
  // 三个参数:代理的目标对象,要写入的属性名称,要写入的属性值
  set(target, property, value) {
    if (property === "age") {
      // 判断年龄是否为整数
      if (!Number.isInteger(value)) {
        throw new TypeError(`${value} is not an int`);
      }
    }
    target[property] = value;
  },
});
personProxy.age = 18;
console.log(person);
// { name: 'zce', age: 18 }
personProxy.age = "hhh"; // 报错

2.16 Proxy 与 Object.defineProperty() 对比

  • Proxy 更为强大

Object.defineProperty() 只能监视属性的读写,Proxy 能监视到更多对象操作。例如 delete 操作、对对象方法的调用等

const person = {
  name: "zce",
  age: 20,
};
const personProxy = new Proxy(person, {
  // 代理目标对象,要删除的属性名称
  deleteProperty(target, property) {
    console.log("delete", property); // delete age
    delete target[property];
  },
});
delete personProxy.age;
console.log(person); // { name: 'zce' }
  • 除了 delete 以外还有许多其他的对象操作都可以监视到

在这里插入图片描述

  • Proxy 更好的支持数组对象的监视

以往想通过 Object.defineProperty() 去监视数组的操作,最常见的一种方式就是通过重写数组的操作方法(Vue.js使用的方式),大体的思路就是通过自定义的方法去覆盖掉数组原先对象的 push、shift 等方法以此去劫持对应这个方法调用的过程

如何直接使用 Proxy 对象监视数组

const list = [];

const listProxy = new Proxy(list, {
  set(target, property, value) {
    // 监视数据写入
    console.log("set", property, value); // 得到的属性名,属性值
    target[property] = value; // 设置目标对象当中所对应的属性
    return true; // 表示设置成功
  },
});
listProxy.push(100); 
// set 0 100
// set length 1
listProxy.push(100); 
// set 1 100
// set length 2
  • Proxy 是以非侵入的方式监管了对象的读写

对已经定义好的对象,Proxy 不需要对对象本身做任何操作就可以监视到内部成员的读写;
而 Object.defineProperty() 就要求我们必须通过特定的方式单独定义对象中需要被监视的属性

2.17 Reflect(统一的对象操作API)

Reflect 是ES2015 提供的一个全新的内置对象

  • 按照 JAVA 或者 C## 的说法,Reflect 属于一个静态类,也就是说其不能通过 new 的方式去构建一个实例对象,只能调用这个静态类的静态方法 Reflect.get(),例如 Math 对象
  • Reflect 内部封装了一系列对对象的底层操作

在这里插入图片描述

  • Reflect 成员方法就是 Proxy 处理对象的默认实现

这里定义好了一个 Proxy 对象。只不过 Proxy 的处理对象当中没有添加任何的成员,我们可以在 Proxy 对象中去添加不同的方法成员来去监听对象所对应的操作;

如果没有添加具体的处理方法,Proxy 处理对象内部默认实现的逻辑就是调用了 Reflect 对象中所对应的方法

const obj = {
  foo: '123',
  bar: '456'
}
const proxy = new Proxy(obj, {
})
const proxy = new Proxy(obj, {
  get (target, property) {
    return Reflect.get(target, property)
  }
})

这就也表明我们在去实现自定义的 get 或者 set 这样的逻辑时,更标准的做法是先去实现自己所需要的监视逻辑,再去返回通过 Reflect 中对应的方法的一个调用结果

const obj = {
  name: "zce",
  age: 20,
};
const proxy = new Proxy(obj, {
  get(target, property) {
    console.log("watch logic~");
    return Reflect.get(target, property);
  },
});
console.log(proxy.age); // 20
  • Reflect 的意义就是提供了一套用于操作对象的 API,统一了对象的操作方式

如果我们判断这个对象中是否存在某个属性就需要使用 in 操作符,需要去删除则需要使用delete 语句,而如果要获取对象中所有的属性名就需要使用 keys 这样的方法

const obj = {
  name: "zce",
  age: 20,
};
console.log("name" in obj); // true
console.log(delete obj["age"]); // true
console.log(Object.keys(obj)); // [ 'name' ]
console.log(Reflect.has(obj, 'name'))
console.log(Reflect.deleteProperty(obj, 'age'))
console.log(Reflect.ownKeys(obj))

详细介绍请查看 MDN

2.18 Promise

一种更优的异步编程解决方案

  • 通过链式调用的方式解决了在传统 JavaScript 异步编程当中回调函数嵌套过深的问题

详细内容请查看:异步编程手写 Promise 源码

2.19 class 类

以前的写法

function Person (name) {
  // 通过 this 访问当前的实例对象
  this.name = name
}
// 如果需要这个类型所有的实例之间共享成员,需要借助于函数的 prototype
Person.prototype.say = function () {
  console.log(`hi, my name is ${this.name}`)
}
  • 自从 ECMAScript2015 开始我们就可以使用一个 class 的关键词去声明一个类型
  • 这种独立定义类型的语法相比之前函数的方式要更容易理解,结构更加清晰
class Person {
  constructor (name) {
    this.name = name
  }
  say () {
    console.log(`hi, my name is ${this.name}`)
  }
}
const p = new Person('tom')
p.say()// hi, my name is tom

2.20 static 静态成员

在类型中的方法一般分为实例方法静态方法

  • 实例方法:需要通过这个类型构造的实例对象去调用
  • 静态方法:直接通过类型本身去调用

以前实现静态方法就是直接在构造函数对象上挂载方法去实现,因为 JS 当中函数也是对象也可以去添加一些方法成员,在 ES2015 中新增添加静态成员的 static 关键词

注意:由于静态方法是挂载到类型上的,所以在静态方法内部,this 不会指向某个实例对象,而是当前的类型

class Person {
  constructor (name) {
    this.name = name
  }
  say () {
    console.log(`hi, my name is ${this.name}`)
  }
  static create (name) {
    return new Person(name)
  }
}
const tom = Person.create('tom')
tom.say() // hi, my name is tom

2.21 extends(类的继承)

  • 在 S2015 之前可以通过原型的方式去实现继承
  • 而在 ES2015 中实现了一个专门用来类型继承的关键词 extends
class Person {
  constructor(name) {
    this.name = name;
  }
  say() {
    console.log(`hi, my name is ${this.name}`);
  }
  static create(name) {
    return new Person(name);
  }
}

class Student extends Person {
  constructor(name, number) {
    // super 关键词,用于访问和调用对象父类上的函数
    // 可以调用父类的构造函数,也可以调用父类的普通函数
    // 只有调用 super 之后,才能使用 this 关键字,否则会报错
    // 继承中的属性或者方法查找原则:就近原则
    super(name);
    this.number = number;
  }
  hello() {
    // 调用父类方法
    super.say();
    console.log(`my school number is ${this.number}`);
  }
}
const s = new Student("jack", "100");
s.hello();
// hi, my name is jack
// my school number is 100

2.22 Set 数据结构

可以理解为集合,与传统的数组非常类似,但是 set 内部的成员不允许重复,也就是说每一个值在同一个 set 中都是唯一的;

如果添加了之前存在的值,会被忽略

const s = new Set();
// add方法会返回集合对象本身 因此可以链式调用
s.add(1).add(2).add(3).add(4).add(2);
console.log(s); // Set(4) { 1, 2, 3, 4 }
  • 想要遍历集合中的数据可以使用 forEach 方法,传入一个回调函数
s.forEach(i => console.log(i))
  • 或者使用 ES2015 提供的 for…of 循环
// i 是数组中的每一个成员
for (let i of s) {
  console.log(i)
}
  • 可以通过 size() 属性获取集合长度,等价于数组的 length
  • 可以使用 has() 查询是否存在某一个特定的值
  • 以及使用 delete() 和 clear() 删除元素
  • 详细内容请查看:JavaScript-集合引用类型(Set)

  • set 数据结构常见的应用场景是去为数组中的元素去重
  • 同时可以通过使用 ES2015 新增的 Array.from() 方法去把它转化为数组
  • 或者使用 … 这种展开操作符在一个空的数组当中去展开这个 set,这样 set 的成员就可以作为空数组的成员了,这样也可以得到一个数组
const arr = [1, 2, 1, 3, 4, 1]
// const result = Array.from(new Set(arr))
const result = [...new Set(arr)]
console.log(result)
// [ 1, 2, 3, 4 ]

2.23 Map 数据结构

  • 这种结构与 ECMAScript 中的对象非常类似,本质上它们都是键值对集合
  • 但是这种对象结构的键只能是字符串类型,所以说我们去存放一些复杂结构的数据时,会有一些问题
const obj = {};
obj[true] = "value";
obj[123] = "value";
obj[{ a: 1 }] = "value";
console.log(Object.keys(obj));
// [ '123', 'true', '[object Object]' ]
  • 原本我们设置的布尔值、数字、还有对象类型的键都被转化为了字符串
  • 也就是说,如果给对象添加的键不是字符串,内部就会将这个数据 toString 的结果作为键
  • ES2015 的 Map 结构就是为了来解决这个问题的,Map 才能算是严格意义上的键值对集合,用来去映射两个任意类型数据之间的对应关系

  • Map 可以使用任何 JavaScript 数据类型作为键
  • 可以使用 set() 方法再添加键/值对
const m = new Map();
const tom = { name: "tom" };
m.set(tom, 90);
console.log(m);
// Map(1) { { name: 'tom' } => 90 }
  • 如果我们需要去获取其中的数据,可以使用 get() 方法
  • 使用 has() 方法判断某一个键是否存在
  • 使用 delete() 方法去删除掉某一个键
  • 使用 clear() 方法去清空所有的键值
  • 如果需要遍历所有的键值可以使用实例对象的 forEach(value,key) 方法,第一个方法是遍历的值,第二个参数是被遍历的键
  • 详细请查看:JavaScript-集合引用类型(Map- ES6新特性)
const m = new Map();
const tom = { name: "tom" };
m.set(tom, 90);
m.forEach((value, key) => {
  console.log(value, key); // 90 { name: 'tom' }
});

2.24 Symbol(一种全新的原始数据类型)

详细内容请直接查看:JavaScript-语言基础(数据类型)

2.25 for…of 循环

  • ES2015 借鉴了很多其他语言引入了一种全新的遍历方式:for…of 循环
  • 这种循环方式以后会作为遍历所有数据结构的统一方式

  • 不同于传统的 for…in 循环,for…of 循环拿到的是数组的每一个元素,而不是对应的下标
  • 这种方法就可以取代 forEach,而且相比 forEach,for…of 能够使用 break 随时终止循环,而 forEach 不能跳出循环,我们以往通常使用 some()、every() 终止遍历
for (const item of arr) {
  console.log(item);
  if (item > 100) {
    break;
  }
}
  • 除了数组可以被 for…of 循环遍历,伪数组同样适用
  • 例如函数中的 arguments 对象,或者是 DOM 操作时一些元素节点的列表,它们遍历都和普通的数组遍历没有任何区别

  • 遍历 Set 与遍历数组相同
const s = new Set(["foo", "bar"]);
for (const item of s) {
  console.log(item);
}
// foo
// bar
  • 遍历 Map 可以配合数组解构语法,直接获取键值
const m = new Map()
m.set('foo', '123')
m.set('bar', '345')

for (const item of m) {
  console.log(item);
}
// [ 'foo', '123' ]
// [ 'bar', '345' ]

for (const [key, value] of m) {
  console.log(key, value)
}
// foo 123
// bar 345
  • 普通对象不能被直接 for…of 遍历
const obj = { foo: 123, bar: 456 };
for (const item of obj) {
  console.log(item); // 报错
}
  • 而我们上面提到:for…of 循环它可以作为遍历所有数据结构的统一方式,但是它连最基本的普通对象都没有办法遍历,解析请看下节

2.26 Iterable(可迭代接口)

上节提到 for…of 循环是一种数据统一的遍历方式,但是经过尝试,它只能遍历数组之类的数据结构,对于普通的对象,如果直接去遍历就会报出错误

原因:

  • ES 中能够表示有结构的数据类型越来越多,从最早的数组、对象到现在的 set、map,而且开发者还可以组合使用这些类型去定义一些符合自己业务需求的数据结构
  • 为了给各种各样的数据结构提供统一的遍历方式,ES2015 就提出了一个叫做 Iterable 的接口,可以将其理解为规格标准
  • 可迭代接口就是一种可以被 for…of 循环统一遍历访问的规格标准
  • 也就是说能被 for…of 遍历的数据类型内部都已经实现了这个接口,并挂载了一个 iterator() 方法,这个方法需要返回一个带有 next() 方法的对象,我们不断调用这个 next() 方法就可以实现对内部所有数据类型的遍历
const set = new Set(["foo", "bar", "baz"]);

const iterator = set[Symbol.iterator]();

console.log(iterator.next()); // { value: 'foo', done: false }
console.log(iterator.next()); // { value: 'bar', done: false }
console.log(iterator.next()); // { value: 'baz', done: false }
console.log(iterator.next()); // { value: undefined, done: true }
console.log(iterator.next());// { value: undefined, done: true }

  • for…of 内部的工作原理:内部调用被遍历对象的 iterator() 方法得到一个迭代器从而去遍历内部所有的数据,这也就是 Iterable 接口所约定的内容
  • 实现可迭代接口(只要对象也实现了 Iterable 接口,是不是我们就可以实现使用 for…of 循环去遍历呢?)
// 实现了可迭代接口,叫做 Iterable
// 约定内部必须有一个用于返回迭代器的 iterator 方法
const obj = {
  store: ["foo", "bar", "baz"], // 用于存放值得被遍历的数据
  [Symbol.iterator]: function () {
    let index = 0;
    const self = this;
    return {
      // 提供一个 next 方法,用于实现向后迭代的逻辑
      // 带有next()方法的对象叫做 Iterator
      next: function () {
        const result = {
          value: self.store[index],
          done: index >= self.store.length,
        };
        index++;
        return result;
      },
    };
  },
};

for (const item of obj) {
  console.log("循环体", item);
}
// 循环体 foo
// 循环体 bar
// 循环体 baz
  • 迭代器模式

场景:你我协同开发一个任务清单应用


// 我的代码 ===============================

const todos = {
  life: ['吃饭', '睡觉', '打豆豆'],
  learn: ['语文', '数学', '外语'],
  work: ['喝茶'], // 如果我添加了全新的类目


// 你的代码 ===============================

for (const item of todos.life) {
  console.log(item)
}
for (const item of todos.learn) {
  console.log(item)
}
for (const item of todos.work) {  //这里也需要跟着变化
  console.log(item)
}

// 改造
// 此时,如果我的数据结构能够对外提供一个统一的遍历接口,对于调用者而言
// 就不用关心我对象内部的结构是怎样的
// 更不用担心我的内部结构改变过后所产生的影响

// 在 我的代码 中增加 each 方法 ===============================
const todos = {
  life: ['吃饭', '睡觉', '打豆豆'],
  learn: ['语文', '数学', '外语'],
  work: ['喝茶'],

  // 提供统一遍历访问接口
  each: function (callback) {
   // 将所有数组进行合并
    const all = [].concat(this.life, this.learn, this.work)
    for (const item of all) {
      callback(item) //将每一个数据交给回调函数
    }
  }
}

// 你的代码 ===============================
todos.each(function (item) {
  console.log(item)
})
// 吃饭
// 睡觉
// 打豆豆
// 语文
// 数学
// 外语
// 喝茶

实现可迭代接口也是相同的道理,尝试使用迭代器的方法解决这个问题

const todos = {
  life: ["吃饭", "睡觉", "打豆豆"],
  learn: ["语文", "数学", "外语"],
  work: ["喝茶"],

  // 提供迭代器
  [Symbol.iterator]: function () {
    const all = [...this.life, ...this.learn, ...this.work];
    let index = 0;
    return {
      next: function () {
        return {
          value: all[index],
          done: index++ >= all.length,
        };
      },
    };
  },
};

for (const item of todos) {
  console.log(item);
}
// 吃饭
// 睡觉
// 打豆豆
// 语文
// 数学
// 外语
// 喝茶

迭代器这样一个模式它的核心就是对外提供统一遍历接口,让外部不用再去关心这个数据内部的结构是怎样的,这里使用 each() 方法只适用当前这个对象结构,而 ES2015 中的迭代器是语言层面实现的迭代器模式,所以它可以去适用于任何数据结构,只需要通过代码去实现 interator() 方法,实现它的迭代逻辑就可以了

2.27 Generator(生成器函数)

语法

  • 定义生成器函数就是在 function 关键字后加 *
function* foo() {
  console.log("zce");
  return 100;
}

const result = foo();
console.log(result.next()); // 调用生成器对象的next()方法
// zce
// { value: 100, done: true }
  • next() 方法的返回值与迭代器 next() 的返回值有相同的结构
  • 函数的返回值被放到 value 中,这就是因为生成器对象其实也实现了 iterator 接口的协议

  • 生成器函数在实际使用的时候一定会配合 yield 关键词去使用,yield 与 return 关键词非常类似,但是有有很大不同
  • 调用生成器函数会产生一个生成器对象。生成器对象一开始处于暂停执行(suspended)的状态。与迭代器相似,生成器对象也实现了 Iterator 接口,因此具有 next() 方法。调用这个方法会让生成器开始或恢复执行
  • next() 方法的返回值类似于迭代器,有一个 done 属性和一个 value 属性。函数体为空的生成器函数中间不会停留,调用一次 next() 就会让生成器到达 done: true 状态
  • yield 关键字可以让生成器停止和开始执行,也是生成器最有用的地方,会让生成器到达 done: false 状态。生成器函数在遇到 yield 关键字之前会正常执行。遇到这个关键字后,执行会停止,函数作用域的状态会被保留。停止执行的生成器函数只能通过在生成器对象上调用 next() 方法来恢复执行
function* foo() {
  console.log("1111");
  yield 100; // 并不会结束掉这个方法的执行
  console.log("2222");
  yield 200;
  console.log("3333");
  yield 300;
}

const generator = foo();
// 第一次调用,函数体开始执行,遇到第一个 yield 暂停
console.log(generator.next()); 
// 1111
// { value: 100, done: false }

// 第二次调用,从暂停位置继续,直到遇到下一个 yield 再次暂停
console.log(generator.next()); 
// 2222
// { value: 200, done: false }

// 第三次调用,从暂停位置继续,直到遇到下一个 yield 再次暂停
console.log(generator.next()); 
// 3333
// { value: 300, done: false }

// 第四次调用,已经没有需要执行的内容了,所以直接得到 undefined
console.log(generator.next()); 
// { value: undefined, done: true }

生成器应用

  • 场景1:发号器(实现 id 自增)
function* createIdMaker() {
  let id = 1;
  // 此处不必担心会一直循环,因为有 yield,所以会自动暂停
  while (true) {
    yield id++;
  }
}

const idMaker = createIdMaker();

console.log(idMaker.next().value); // 1
console.log(idMaker.next().value); // 2
console.log(idMaker.next().value); // 3
console.log(idMaker.next().value); // 4
  • 场景2:使用 Generator 函数实现 iterator 方法
const todos = {
  life: ["吃饭", "睡觉", "打豆豆"],
  learn: ["语文", "数学", "外语"],
  work: ["喝茶"],
  [Symbol.iterator]: function* () {
    const all = [...this.life, ...this.learn, ...this.work];
    for (const item of all) {
      yield item;
    }
  },
};

for (const item of todos) {
  console.log(item);
}
// 吃饭
// 睡觉
// 打豆豆
// 语文
// 数学
// 外语
// 喝茶

三、ES2016 概述(ES7)

与 ES2015 相比,ES2016 只是一个小版本,仅包含两个小功能

3.1 Array.prototype.includes

  • includes() 方法从数组前头(第一项)开始向后搜索
  • 返回布尔值,表示是否至少找到一个与指定元素匹配的项
const arr = ["foo", 1, NaN, false];

// 找到返回元素下标
console.log(arr.indexOf("foo")); // 0
// 找不到返回 -1
console.log(arr.indexOf("bar")); // -1
// 无法找到数组中的 NaN
console.log(arr.indexOf(NaN)); // -1

// 直接返回是否存在指定元素
console.log(arr.includes("foo")); // true
// 能够查找 NaN
console.log(arr.includes(NaN)); // true

3.2 指数运算符(**)

  • 以前我们需要去进行指数运算需要借助 Math 对象当中的 pow() 方法去实现
  • 而在 ES2016 中新增的指数运算符就是语言本身的运算符
console.log(Math.pow(2, 3)); // 8

console.log(2 ** 3); // 8

四、ES2017 概述(ES8)

同样只是一个小版本,但它带来了非常有用的新功能

4.1 新增 Object 拓展方法

4.1.1 Object.values

  • 与 ES5 的 Object.keys() 方法类似,keys() 方法返回的是所有对象的键组成的数组,而values() 返回的是所有对象的值所组成的数组
const obj = {
  foo: "value1",
  bar: "value2",
};
console.log(Object.values(obj));
// [ 'value1', 'value2' ]

4.1.2 Object.entries

  • entries() 方法是以数组的形式返回对象当中所有的键值对
const obj = {
  foo: "value1",
  bar: "value2",
};
console.log(Object.entries(obj));
// [ [ 'foo', 'value1' ], [ 'bar', 'value2' ] ]

console.log(new Map(Object.entries(obj)));
// Map(2) { 'foo' => 'value1', 'bar' => 'value2' }

4.1.3 getOwnPropertyDescriptors

获取对象当中属性的完整描述信息

  • 从 ES5 之后,可以为对象去定义 getter、setter 属性,但不能通过 Object.assign() 方法去完全复制的
const p1 = {
  firstName: "Lei",
  lastName: "Wang",
  get fullName() {
    return this.firstName + " " + this.lastName;
  },
};

console.log(p1.fullName);

const p2 = Object.assign({}, p1);
p2.firstName = "zce";
console.log(p2);
// { firstName: 'zce', lastName: 'Wang', fullName: 'Lei Wang' }
  • Object.assign() 在复制时只是把 fullName 当作一个普通的属性去复制
  • 这种情况我们就可以使用 getOwnPropertyDescriptors() 方法去获取对象当中的完整描述信息
  • 再使用 Object.defineProperties() 方法将这个描述信息定义到一个新的对象当中
  • 这样我们对于 getter、setter 类型的属性就可以进行复制
const p1 = {
  firstName: "Lei",
  lastName: "Wang",
  get fullName() {
    return this.firstName + " " + this.lastName;
  },
};

const descriptors = Object.getOwnPropertyDescriptors(p1);
const p2 = Object.defineProperties({}, descriptors);
p2.firstName = "zce";
console.log(p2.fullName);
// zce Wang

4.2 新增字符串填充方法(padStart() 和 padEnd() )

padStart() 和 padEnd()方法会复制字符串,如果小于指定长度,则在相应一边填充字符,直至满足长度条件

  • 这两个方法的第一个参数是长度,第二个参数是可选的填充字符串,默认为空格(U+0020)
let stringValue = "foo"; 
console.log(stringValue.padStart(6)); // " foo" 
console.log(stringValue.padStart(9, ".")); // "......foo" 

console.log(stringValue.padEnd(6)); // "foo " 
console.log(stringValue.padEnd(9, ".")); // "foo......"
  • 可选的第二个参数并不限于一个字符。如果提供了多个字符的字符串,则会将其拼接并截断以匹配指定长度。此外,如果长度小于或等于字符串长度,则会返回原始字符串
let stringValue = "foo"; 
console.log(stringValue.padStart(8, "bar")); // "barbafoo" 
console.log(stringValue.padStart(2)); // "foo" 

console.log(stringValue.padEnd(8, "bar")); // "foobarba" 
console.log(stringValue.padEnd(2)); // "foo"
  • 使用场景:对齐输出的字符串长度
const books = {
  html: 5,
  css: 16,
  javascript: 128,
};

for (const [name, count] of Object.entries(books)) {
  console.log(`${name.padEnd(16, "-")}|${count.toString().padStart(3, "0")}`);
}
//html------------|005
//css-------------|016
//javascript------|128

4.3 允许在函数参数中添加尾逗号

可以让源代码管理工具更精确地定位到代码当中实际发生变化的位置,也可以方便开发者调试代码

function foo(
    bar,
    bar2,
){}

4.4 Async / Await

  • 彻底解决了异步编程中回调函数嵌套过深所产生的问题,使代码更加简洁易读
  • 本质就是使用 Promise 的一种语法糖,详细内容请查看:JavaScript 深度剖析 - 异步编程
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

每天内卷一点点

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值