深入剖析ES6新特性

为什么要学习

  • 乍一看这就是说的ES6,ES6对我们而言确实在熟悉不过了,那为什么我们还要继续学习呢?
    • 结合我们在面试过程当中的经验,我们发现很多前端开发者并没理解语言和平台之间的关系
      • 以JS为例,我们日常编写的代码,那些是属于语言层面,那些属于平台层面呢,这点很多人都是乱的
      • 有些同学会觉得,不懂这些依然可以做开发,但事实却不是这样的,比如随着node的重要性越来越重要,搞懂这些对我们整体的提升是很重要的
    • 市面上相对于ES的资料比较零散,每个开发者对ES的理解和掌握程度都不一样,而我们的高阶课程中很依赖这些内容
    • 可以帮助我们写出更现代化、高质量的代码

ECMAScript概述

  • ES也是一门脚本语言,通常我们会把它看作为JS的标准化规范,但实际上JS是ES的扩展语言
    • 因为在ES当中只提供了最基本的语法,换句话说就是约定了我们的代码如何编写,他只停留在了语言层面,并不能应用于实际功能中的开发
    • 而我们经常使用的JS,他实现了ES这种语言的标准并且在这个基础之上做了一些扩展,使得我们可以在浏览器当中操作DOM、BOM,在node环境当中去做一些读写文件的一些操作
    • 总的来说,在浏览器当中的JS他就等于ES + Web API(DOM、BOM),在node当实际上就等于ES + Node API(fs、net)
    • 所以JS语言本身指的就是ES

ES2015 概述

  • 新时代ES标准的代表版本
    • 相比于ES5.1变化很大
    • 自此,标准命名规则发生变化
    • ES6泛指ES5.1之后所有的新版本
      • 比如:“使用ES6的Async和Await”,实际上 Async和Await 是ES2017上的标准
      • 注意分辨资料中的ES6是特指还是泛指
  • ES2015的标准规范长达26章,推荐简单过一遍
    • 这个规格文件中不是仅仅介绍了所引入的新特性,而是包含这个新特性过后,所有的与语言标准规范
    • http://www.ecma-international.org/ecma-262/6.0/
  • 重点介绍ES5.1基础之上的变化
  • 变化归为4类
    • 解决原有语法上的一些问题
      • let与const所提供的块级作用域
    • 对原有语法进行增强
      • 解构、展开、参数把默认值、模板字符串
    • 全新的对象、全新的方法、全新的功能
      • promise
    • 全新的数据类型和数据结构
      • symbol、set、map

ES2015 准备工作

  • 因为我们这里只是介绍语言本身,并不会涉及到运行环境所提供的API,所以任何一个支持2015的环境都可以
  • 为了有更直观的展示所以选择了node.js环境去做具体的尝试
  • 当然也可以使用最新的Chrome去运行

ES2015 let与块级作用域

  • 作用域 — 代码当中的一个成员能起作用的一个范围
  • ES6之前,只有两种作用域
    • 全局作用域
    • 局部(函数)作用域
  • ES2015中新增了一种
    • 块级作用域
      • 块 — 代码中用一对花括号包裹起来的范围
      • 示例
      if(true){
          console.log('zce.me')
      }
      
      for(var i = 0; i < 10; i++){
          console.log('zce.me')
      }
      
  • 以前块没有单独作用域,所以在块里定义的变量,外部也可以访问
    • 这是对复杂代码非常不利的,也是不安全的
if(true){
    var foo = 'zce'
}

console.log(foo)    // zce
  • 而有了块级作用域,我们就可以使用let去声明变量
if(true){
    let foo1 = 'zce'
}

console.log(foo1)    // ReferenceError: foo1 is not defined
  • 在块级作用域内定义的成员,外部是无法访问的
    • 这一特性非常适用于声明for循环当中的计数器
    • 传统的for循环如果出现了循环嵌套的情况,那我们就必须为计数器设置不同的名称,否则就会出现问题
for(var i = 0; i < 3; i++){
    for(var i = 0; i < 3; i++){
        console.log(i)  // 0 1 2 
    }
}

for(let i = 0; i < 3; i++){
    for(let i = 0; i < 3; i++){
        console.log(i)  
    }
    console.log('内层结束 i = ' + i)
}

for(var i = 0; i < 3; i++){
    for(let i = 0; i < 3; i++){
        console.log(i)  
    }
    console.log('内层结束 i = ' + i)
}
  • 注意:虽然let关键词解决了循环嵌套当中我们计数器重名导致的问题,但是还是不建议使用同名计数器,因为这样不利于后期理解代码
  • 除此之外还有一个典型的应用场景 — 循环注册事件
    • 在事件的处理函数中,我们要去访问循环的计数器,这种情况下以前就出现一些问题
var elements = [{}, {}, {}]
for(var i = 0; i < elements.length; i++){
    elements[i].onclick = function(){
        console.log(i)
    }
}

elements[0].onclick()   // 3
elements[1].onclick()   // 3
elements[2].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
elements[1].onclick()   // 1
elements[2].onclick()   // 2
  • 现在有了块级作用域后我们就不必要使用这种方式了
    • 其实这种内部也算是一种闭包的机制
    • 在onclick执行的时候,我们的循环早就结束了,所以我们的 i 实际上已经销毁了,就是因为闭包的机制,我们才可以拿到原本执行循环的时候那个i所对应的值
var elements = [{}, {}, {}]
for(let i = 0; i < elements.length; i++){
    elements[i].onclick = function(){
        console.log(i)
    }
}

elements[0].onclick()   // 0
elements[1].onclick()   // 1
elements[2].onclick()   // 2
  • 在for循环当中还有一个特别之处,在for循环内部实际上有两层作用域
for(let i = 0; i < 3; i++){
    let i = 'foo'
    console.log(i)  // 输出三次foo,表明两个i不会互相影响
}

// for循环可以更改为下边的if判断更加容易理解

let i = 0

if(i < 3){
    let i = 'foo'
    console.log(i)
}
i++ 

if(i < 3){
    let i = 'foo'
    console.log(i)
}
i++ 

if(i < 3){
    let i = 'foo'
    console.log(i)
}
i++ 
  • let的声明不会出现提升的情况
    • 从语法上必须先声明后使用,否则报错
console.log(foo)    // undefined
var foo = 'hello'

console.log(foo1)
let foo1 = 'error'  // ReferenceError: Cannot access 'foo1' before initialization
  • ES2015为什么不是在var上做出升级而是推出了let
    • 升级var会导致很多以前的项目无法正常工作,所以要省用一个新的关键词 — let

ES2015 const

  • 恒量/常量
  • 在let之上,增加了一个只读属性
    • 只读:声明过后不可以再被修改
const name = 'hello'

name = 'rookie' // TypeError: Assignment to constant variable.
  • 声明时必须要有一个初始值,声明和赋值不能像var一样放到两个语句中
const name  // SyntaxError: Missing initializer in const declaration
name = 'hello'
  • const声明的成员不能被修改
    • 只是说我们在声明了过后在重新去执行新的内存地址,并不是说不允许说我们去修改恒量中的属性成员
const obj = {}
obj.name = 'hello'  // 只修改了这块内存空间中的数据,所以是被允许的

obj = {} // TypeError: Assignment to constant variable.
  • 最佳实践,不用var,主用const,配合let
    • 不用var是因为:var里边所谓的一些特性都算是我们开发中的一些陋习
      • 例如:先使用后声明
    • 主用const是因为:const他会让我更明确代码中所声明的这些成员会不会被修改

ES2015 数组的解构

  • ES2015新增了从数组或者对象中获取对象的快捷方式
const arr = [100, 200, 300]

const foo = arr[0]
const bar = arr[1]
const baz = arr[2]
// console.log(foo, bar, baz)  // 100 200 300

const [foo, bar, baz] = arr
console.log(foo, bar, baz)  // 100 200 300
  • 只想获取其中某个位置的值
const [, , baz] = arr
console.log(baz)    // 300
  • 结构位置的变量名之前添加 … 表示提取从当前位置开始往后的所有成员
    • 注意:只能在解构位置的最后一个成员上使用
const [foo, ...baz] = arr
console.log(baz)    // [200, 300]
  • 如果我们解构的成员小于实际的个数,则会按照从前往后的顺序去提取,多出来的成员不会被提取
  • 反之,解构位置的成员大于实际的实际数组的长度,那提取到的就是undefined
    • 类似于访问数组中不存在的下标
const [foo] = arr
console.log(foo)    // 100

const [foo, bar, baz, more] = arr
console.log(more)   // undefined
  • 我们也给提取到的成员设置默认值
const [foo, bar, baz, more] = arr
console.log(more)   // undefined

const [foo, bar, baz, more = 400] = arr
console.log(more)   // 400
  • 这种新语法,在很多场景下,都会给我们带来很多的便捷,例如拆分一个字符串并获取后的指定位置
// 传统做法
const path = '/foo/bar/baz'
const tmp = path.split('/')
const rootdir = tmp[1]
console.log(rootdir)    // foo

// 解构做法
const [, rootdir] = path.split('/')
console.log(rootdir)    // foo

ES2015 对象的解构

  • 除了数组之外,对象也可以结构
  • 但是是根据属性名去匹配提取,而不是位置
  • 因为数组中的元素有下标,也就是有顺序规则,而对象没有下标,所以不能按照位置提取
const obj = {
    name : 'hello',
    age : 18
}

const { name, age } = obj
console.log(name, age)  // hello 18
  • 解构的变量名同时又是用来去匹配我们被解构对象当中的属性名的,所以当我们当前作用域当中有同名的成员会产生冲突
const obj = {
    name : 'hello',
    age : 18
}

const name = 'world'
const { name, age } = obj   // SyntaxError: Identifier 'name' has already been declared
  • 使用重命名解决上述问题
const obj = {
    name : 'hello',
    age : 18
}

const name = 'world'

const { name: objName, age } = obj

console.log(objName, age)   // hello 18
  • 上述可以继续添加默认值
const obj = {
    name : 'hello',
    age : 18
}

const name = 'world'

const { name: objName = 'look', age } = obj

console.log(objName, age)   // hello 18
  • 结构对象的应用场景还是比较多的,但是大部分都是为了简化我们的代码
const { log } = console

log('foo')  // foo

ES2015 模板字符串

  • 在ES2015中,还增强了定义字符串的方式,传统定义我们需要通过单引号或者双引号来表示
  • 在ES2015中新增了一种叫做模板字符串的方式,他需要使用反引号来去标识
const str = 'hello world'

const str1 = `hello world`

console.log(str, str1) // hello world hello world
  • 直接是和普通字符串没有什么区别,但是如果我们要在字符串的内容当中需要反引号,那我们同样也可以使用\去转义
const str1 = `hello \`world\``

console.log(str1) // hello `world`
  • 相比于普通字符串,这种模板字符串多了一些非常有用的方式
    • 传统的字符串,并不支持换行,如果有换行我们需要使用 \n 来表示,模板字符串当中可以支持多行字符串,也就是我们可以在字符串当中直接输入换行符
    • 模板字符串中还支持通过插值表达式的方式在字符串中去嵌入所对应的数值
const name = 'tom'
const msg = `hey, ${name}`
console.log(msg)    // hey, tom
  • 不仅可以嵌入变量,还可以嵌入任何标准的JS语句,语句的返回值最终会被输出到字符串当中的插值表达式所存在的位置
const name = 'tom'
const msg = `hey, ${name} --- ${1 + 2} --- ${Math.random()}`
console.log(msg)    // hey, tom --- 3 --- 0.5301788586742087

ES2015 带标签的模板字符串

  • 定义模板字符串之前,去添加一个标签,这个标签实际上解释一个特殊的函数,添加这个标签实际上就是调用这个函数
const str = console.log`hello world` // [ 'hello world' ]

const name = 'tom'

const gender = true

function myTagFunc(strings, name, gender){
    // console.log(strings) // [ 'hey, ', ' is a ', '' ]
    // console.log(strings, name, gender)  // [ 'hey, ', ' is a ', '.' ] tom true
    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.
  • 这种标签函数的作用,实际上就是对我们的模板字符串进行加工,例如我们上述代码中的 gender 他直接输出的值就是true或者first,我们可以在函数当中对其经行一些加工,这样可以让他更适合用户的阅读
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.
  • 我们也可以利用这个特性,去实现文本的多语言化,比如翻译成中文或者英文,或者检查我们的模板字符串中是否存在一些不安全的

ES2015 字符串的扩展方法

  • includes() — 匹配字符串中间是否包含某个内容
  • startsWith() — 匹配字符串的开头是否是指定内容
  • endsWith() — 匹配字符串的结尾是否是指定内容
  • 他们是一组方法,可以更方便的去判断我们字符串当时是否包含指定的内容
const message = 'Error:foo is not defined.'

console.log(
    // message.startsWith('Error') // true
    // message.endsWith('.')   // true
    message.includes('foo') // true
)
  • 相比于之前的indexof()或者是使用正则去判断,这样一组方法会让我们的字符串查找便捷很多

ES2015 参数默认值

  • ES2015中为函数的形参列表扩展了一些非常有用的新语法
  • 参数默认值
    • 以前我们想要对函数的参数设置默认值,需要我们在函数内部通过逻辑代码来实现
// function foo(enable) {
//     // enable = enable || true
//     enable = enable === undefined ? true : enable
//     console.log('foo invoked - enable:')   // foo invoked - enable:
//     console.log(enable) // true
// }

/**
 * 这里有个很多人会经常犯的错误,因为很多人喜欢使用短路运算的方式来去设置默认值
 * 
 * 这里这种情况其实不能使用短路运算的方式设置默认值
 * 
 * 因为这样会导致,如果我们传入 false 时,也会使用默认值,这是很明显的错误
 * 
 * 这里我们应该做一个判断,判断其是否为undefined
 * 
 * 因为参数默认值的定义就是我们没有去传递实际参数时所使用参数时的一个值
 * 
 * 没传递实参得到的就应该是undefined
 * 
 * 不过有了参数默认值这一新功能之后,这一切就变得简单了许多
 * */ 

/**
 * 这里的默认值,只会我们没有传递实参或者传递的是一个undefined时才会使用
 * 
 * 需要注意的是:如果说有多个参数的话,那带有默认值的形参,一定要出现在我们参数列表的最后
 * 
 * 因为我们的参数是按照次序传递的,如果我们带有默认值的参数不在最后的话,那我们的默认值无法正常工作
 * */ 
function foo(enable = true) {
    console.log('foo invoked - enable:')   // foo invoked - enable:
    console.log(enable) // false
}

foo(false)

ES2015 剩余参数

  • 在ES中很多方法都可以传递任意一个数的参数
    • 例如我们console对象中的log方法他就可以接收任意一个数的方法,并且最终会把这些参数打印到同一行当中
    • 对于未知个数的参数,以前都是使用ES所提供的arguments对象去接收,arguments实际上是一个伪数组
    • 在ES2015当中新增了一个 … 的操作符,这种操作符有两个作用,这里我们要用到的是剩余操作符
// function foo () {
//     console.log(arguments) // [Arguments] { '0': 1, '1': 2, '2': 3, '3': 4 }
// }

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

foo(1, 2, 3, 4) 
  • 因为是接受的所有参数,所以这种操作符只能出现在我们形参中的最后一位,而且只能使用一次

ES2015 展开数组

  • …操作符除了可以用来收起剩余数据的用法还可以作为展开操作符
const arr = ['foo', 'bar', 'baz']

// console.log(
//     arr[0],
//     arr[1],
//     arr[2]
// )   // foo bar baz

// console.log.apply(console, arr) // foo bar baz

console.log(...arr) // foo bar baz

ES2015 箭头函数

  • 在ES2015中简化了函数表达式的定义方式,他允许我们使用 => 这种类似箭头的符号来定义函数
  • 简化了函数的定义
  • 简化了函数的一些特性
// 普通函数
// function inc (number) {
//     return number + 1
// }

// 箭头函数
const inc = n => n + 1

console.log(inc(100))   // 101
  • 箭头的左边其实就是我们参数的列表,如果有多个参数可以使用小括号包裹
  • 箭头的右边是我们的函数体,这里只有一句表达式,他的执行结果就会作为这个函数的返回值返回,如果在函数体中需要执行多条语句,可以用花括号包裹起来,但是一旦当我们使用了花括号,返回值需要我们手动使用return
const inc = (n, m = 1) => {
    return n + m
}

console.log(inc(100))   // 101
  • 最主要的变化是极大的简化了我们回调函数的编写
const arr = [1, 2, 3, 4, 5, 6, 7]

// 普通函数
arr.filter(function (item) {
    return item % 2
})

// 箭头函数
arr.filter(i => i % 2)

ES2015 箭头函数与this

  • 相比于普通函数,箭头函数还有一个很重要的方法,就是不会改变this的指向
const person = {
    name : 'tom',
    // sayHi : function () {
    //     console.log(`hi, my name is ${this.name}`)   // // hi,my name is tom
    // }
    sayHi : () => {
        console.log(`hi, my name is ${this.name}`)  // hi, my name is undefined
    },
    /**
     * 如果setTimeout传递进去的是一个普通函数,在函数内部就没有办法拿到我们当前作用域的this
     * 
     * 因为这个函数在setTimeout中会放在全局对象上被调用,所以说它拿不到我们当前作用域的this
     * 
     * 他拿到的应该是全局对象
     * 
     * 很多时候我们为了解决这样的问题我们会定义一个_this去保存当前this
     * 
     * 借助于闭包这样的机制,去在内部使用_this
     * 
     * 如果使用箭头函数就没必要这么麻烦了,因为在箭头函数中this始终指向当前所在作用域
     * */ 
    sayHiAsync : function () {
        const _this = this
        setTimeout(function () {
            console.log(_this.name)
        }, 1000)
    }
}

/**
 * 这是箭头函数与普通函数最重要的区别
 * 
 * 因为在箭头函数当中,他没有this的机制,所以它不会改变this的执行
 * 
 * 也就是说,在箭头函数的外边this是什么,在箭头函数里边拿到的就是什么
 * 
 * 任何情况都不会改变
 * */ 

person.sayHi() 

ES2015 对象字面量的增强

  • 对象使我们在ES最常用的数据结构,ES2015升级了我们对象字面量的语法
const bar = '345'

const obj = {
    foo : 123,

    // ES6之前写法
    // bar : bar
    // method1 : function () {
    //     console.log('method1')
    // }

    // ES6写法
    bar,
    method1 () {
        console.log('metohod1')
        console.log(this)
    },

}
// console.log(obj)
obj.method1()

obj[Math.random()] = 123
  • 对象还有一个很重要的变化
    • 它可以使用表达式的返回值,作为对象的属性名
    • 以前如果需要为对象添加一个动态的属性名,只能在对象声明过后,通过索引器的方式,也就是方括号,去动态的添加
    • ES2015之后,对象字面量的属性名,就可以直接通过方括号直接去使用动态的值,这样的特性叫做 — 计算属性名
const obj = {
    foo : 123,
    // ES2015写法
    [Math.random()] : 123
}
// ES2015之前
// obj[Math.random()] = 123

ES2015 Object.assign

  • ES2015中对Object对象提供了一些扩展方法
  • 这里看几个最主要的方法
    • 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(result) // { a: 123, c: 456, b: 789, d: 789 }
console.log(result === target)  // true
  • 该方法还是很常用的,很多时候我们都可以使用它复制一个对象

ES2015 Object.is

  • ES2015中对Object对象新增了一个is方法,用来去判断两个值是否相等
  • 在此之前我们需要判读两个值是否相等时,我们可以使用 == 或者 === 这两者的区别是 == 在比较之前会自动转换数据类型 === 不会
    • == : 0 == false 为 true
    • === :对于数字0的正负没有办法区分
    • === : 两个NaN是不相等的,以前的NaN他代表的是非数字,所以他有无限种可能,但在今天看来NaN它实际上就是一个特别的值,所以说两个NaN应该是完全相等的
  • 在ES2015中提出了一个新的同值比较的算法,我们可以使用Object对象的is方法来解决这个问题
  • 这时 +0 , -0是不相等的,两个NaN是相等的
  • 但在实际开发中我们不会用到这个方法,大多数时候还是使用 ===
console.log(
    0 == false, // true
    0 === false,    // false
    +0 === -0,  // true
    NaN === NaN,    // false
    Object.is(+0, -0),  // false
    Object.is(NaN, NaN) // true
)

ES2015 Proxy

  • 如果我们想要监视某个对象中的属性读写,按我们可以使用ES5提供的Object.defineProperty这样的方法,来去为我们的对象添加属性,这样的话我们就可以捕获到我们对象当中属性的读写过程,这种方法其实应用的非常广泛,在vue3.0以前的版本就是使用这样的方法来去实现数据响应,从而完成双向数据绑定
  • 在ES2015当中,全新设计了一个 Proxy 的类型,他就是专门用来为对象设置访问代理器的,如果不理解什么叫做代理,那可以将其想象为门卫,不管进去拿东西还是放东西,都必须要经过这个代理,通过 Proxy 就可以轻松监视到对象的读写过程
  • 相比于defineProperty,Proxy的功能要更为强大,使用起来更方便
const person = {
    name : 'hello',
    age : 20
}

/**
 * 通过new Proxy()为personProxy创建一个代理对象
 * 
 * 第一个参数为需要代理的目标对象
 * 
 * 第二个参数是代理的处理对象
 * 
 * 这个对象中可以
 * 
 * 通过get方法监视我们属性的访问
 * 通过set方法来去监视我们对象当中设置属性
 * 
 * 的这样一个过程
 * */ 
const personProxy = new Proxy(person, {
    /**
     * get方法接收两个参数
     * 
     * 第一个是我们所代理的这个目标对象
     * 
     * 第二个是外部所访问的这个属性的属性名
     * 
     * 这个方法的返回值将会作为外部访问我们这个属性得到的结果
     * */ 
    get (target, property) {
        /**
         * 内部的正确逻辑应该是先去判断我们代理目标对象当中是否存在这样一个属性
         * 
         * 存在返回对应的值,不存在返回一个默认值或者undefined
         * */ 
        return property in target ? target[property] : 'default'
        // console.log(target, property)   // { name: 'hello', age: 20 } name
        // return 100
    },
    /**
     * 默认接收三个方法
     *      代理目标对象
     *      写入的属性名称
     *      写入的属性值
     * */ 
    set (target, property, value) {
        /**
         * 内部正常的逻辑应该是
         * 
         * 为我们的代理目标设置指定的属性
         * 
         * 不过我们可以先做一些数据校验
         * */ 
        if(property === 'age'){
            if(!Number.isInteger(value)){
                throw new TypeError(`${value} is not an int`)
            }
        }
        
        target[property] = value
        // console.log(target, property, value)    // { name: 'hello', age: 20, gender: true } gender true
    }
})

// personProxy.age = 'agehhhhh'    // TypeError: agehhhhh is not an int
personProxy.age = 12    // 

personProxy.gender = true

// console.log(personProxy.name)   // 100

console.log(personProxy.name)   // hello
console.log(personProxy.xxx)    // default
  • 以上就是proxy的基本用法,以后也会用的越来越多,vue3.0开始就已经开始使用proxy去实现内部的数据响应

ES2015 Proxy 对比 defineProperty

  • Proxy到底有哪些优势
    • 最明显的一点在于Proxy要更为强大一些
      • 具体表现为defineProperty只能监视属性的读写
      • Proxy能够监视带更多对象操作
    const person = {
        name : 'hello',
        age : 20
    }
    
    const personProxy = new Proxy(person, {
        /**
         * 这个方法会在外部对当前这个代理对象进行delete操作时,会自动执行
         * 
         * 接收两个参数,分别为:代理目标对象、要删除的这个属性的名称
         * */ 
        deleteProperty (target, property) {
            console.log('delete', property)
            delete target[property]
        }
    })
    
    delete personProxy.age
    
    console.log(person)
    
    • Proxy更好的支持数组对象的监视
      • 以往通过defineProperty去监视数组的操作,最常见的就是重写数组的操作方法,这也是vue.js中所使用的方式
      • 如何使用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)
      
    • Proxy 是以非侵入的方式监管了整个对象的读写
      • 一个已经定义好的对象,我们不需要对对象本身去做任何的操作就可以监视到他内部成员的读写,而defineProperty他就要求必须要通过特定的方式单独去定义对象当中那些需要被监事的属性,对于一个以及存在的对象,我们要想去监视他的属性,我们需要去做很多额外的操作

ES2015 Reflect

  • 统一的对象操作API
  • Reflect属于一个静态类,他不能够通过new的方式去构建一个实例对象,只能够调用这个静态类当中的静态方法
  • Reflect内部封装了一系列对对象的底层操作
    • 具体有14个,其中有一个被废弃掉,还剩13个
    • 这13个方法的方法名,与Proxy对象当中的处理对象里边的方法成员是完全一致的
  • Reflect成员方法就是Proxy处理对象的默认实现
  • Reflect存在的意义是什么?
    • 提供了统一的一套用于操作对象的API
// const obj = {
//     foo : '123',
//     bar : '456'
// }

// const proxy = new Proxy(obj, {
//     get(target, property) {

//         console.log('watch logic~')

//         return Reflect.get(target, property)
//     }
// })

// console.log(proxy.foo)

const obj = {
    name : 'hello',
    age: 18
}

// console.log('name' in obj)
// console.log(delete obj['age'])
// console.log(Object.keys(obj))

console.log(Reflect.has(obj, 'name'))
console.log(Reflect.defineProperty(obj, 'age'))
console.log(Reflect.ownKeys(obj))

ES2015 Promise

  • 一种更优的异步编程解决方案
    • 通过链式调用的方式解决了传统异步编程中回调函数嵌套过深的问题

ES2015 class类

  • 在此之前,ES都是通过定义函数、函数的原型对象来去实现的类型
    • ES2015开始,我们可以使用一个class的关键词来去声明一个类型,这种独立定义类型的语法,相比较之前函数的方式要更容易理解,结构也会更加清晰
// function Person (name) {
//     this.name = name
// }

// Person.prototype.say = function () {
//     console.log(`hi, my name is ${this.name}`)
// }

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

ES2015 静态方法

  • 实例方法 VS 静态方法
    • 实例方法就是要通过这个类型构造的实例对象去调用
    • 静态方法就是直接通过类型本身去调用
    • 之前实现静态方法就是直接在构造函数对象上去挂载方法来去实现,因为在JS当中函数也是对象,他也可以去添加一些方法成员
    • ES2015中新增添加静态成员的static关键词
class Person {
    constructor (name) {
        this.name = name
    }

    say () {
        console.log(`hi, my name is ${this.name}`)
    }

    static create (name) {
        return new Person (name)
    }
}

/**
 * 注意:
 * 
 * 因为我们的静态方法是挂载到类型上边的
 * 所以在静态方法内部他的this 就不会去指向某一个实例对象
 * 而是当前的类型
 * */ 
const tom = Person.create()
tom.say()   // hi, my name is undefined


ES2015 类的继承

  • 继承:面向对象当中一个非常重要的特性
    • 通过这个特性,我们能够抽象出来相似类型之间重复的地方
  • 在ES2015之前,大多数情况我们都会使用原型的方式去实现继承,而在ES2015中他还实现了一个专门用于类型继承的关键词叫做extends
class Person {
    constructor (name) {
        this.name = name
    }

    say () {
        console.log(`hi, my name is ${this.name}`)
    }
}

// 继承Person --- Student类型中拥有了Person类型中所有的成员
class Student extends Person {
    constructor (name, number) {
        // name参数在父类中也需要用到,所以这里我们要用到super对象
        // 始终指向父类,调用它就是指向父类的构造函数
        super(name)
        this.number = number
    }
    hello () {
        super.say()
        console.log(`my school number is ${this.number}`)
    }
}

const s = new Student('tt', '100')
s.hello()
  • 这就是在class中使用extends实现的继承,相比于原型继承要更方便更清楚

ES2015 Set

  • ES2015提供了一个叫做set的全新数据结构
  • 可以理解为集合,与传统数组很类似
    • 但set内部成员是不允许重复的
    • 每一个值在set当中都是唯一的
    • 他是一个类型,我们通过这个类型构造的示例,就可以用来存放不重复的数据
const s = new Set()

// 这个方法返回集合对象本身,所以可以使用链式调用,如果添加了之前添加过的值,那这个值就会被忽略掉
s.add(1).add(1).add(2).add(3)
console.log(s)  // Set { 1, 2, 3 }

// 想要遍历集合中的数据,就可以使用集合对象中的forEach
s.forEach(i => console.log(i))

// 或者也可以使用ES2015中提供的for..of循环
for (let i of s) {
    console.log(i)
}
  • set中一些常用的方法
// 在set对象当中还可以通过size属性来获取整个集合的长度,类似于length
console.log(s.size) // 3

// 判断集合中是否存在某一个值
console.log(s.has(1))

// 删除集合中某一个集合的值
console.log(s.delete(3))    // true
console.log(s)  // Set { 1, 2 }

// 清除当前集合的全部内容
s.clear()
console.log(s)  // Set {}
  • 最常见的场景就是为了数组中的数据去重
const arr = [1, 1, 1, 2, 3, 4, 5]
// const a = new Set(arr)
// console.log(a)  // Set { 1, 2, 3, 4, 5 }

// const a = Array.from(new Set(arr))
// console.log(a)  // [ 1, 2, 3, 4, 5 ]

const a = [...new Set(arr)]
console.log(a)  // [ 1, 2, 3, 4, 5 ]

ES2015 Map

  • Map 与ES2015中的对象非常类似,本质上都是键值对集合,但是对象模式中的键,他只能够是字符串类型,所以我们在存放一些复杂数据类型时会有些问题,
  • 他与对象最大的区别就是它可以使用任意类型的数据作为键,而对象只能够使用字符串作为键
const obj = {}
obj[true] = 'value'
obj[123] = 'value'
obj[{a:1}] = 'value'

// 如果我们添加的这个键不是字符串,内部就会将这个数据转换为字符串
// console.log(Object.keys(obj))   // [ '123', 'true', '[object Object]' ]

/**
 * 假如我们要以对象去存储每个学生的考试成绩
 * 
 * 那如果我们用学生对象作为键不管对象对象当中的属性有什么不同,那每一个对象转成字符串后默认都是一样的
 * 
 * 自然也就没有办法去做到区分,甚至是我们使用'[object Object]'也能够获取到结果
 * 
 * 在ES2015中的map结构就是为了解决这样的问题
 * 
 * map才能算作是严格意义上的键值对集合,用来去映射两个任意类型数据之间的对应关系
 * */ 

const m = new Map()

const tom = {name : 'tom'}

m.set(tom, 90)

// console.log(m)  // Map { { name: 'tom' } => 90 }
// console.log(m.get(tom)) // 90
// console.log(m.has('tom'))   // false
// m.delete() // 删除
// m.clear()   // 清空

m.forEach((value, key) => {
    console.log(value, key)
})

ES2015 Symbol

  • 一种全新的数据类型
  • 以前解决键的冲突的问题是使用一种约定,比如在a.js中都使用a_开头,在b.js中使用b_开头
    • 但约定的方式只是规避了问题,没有解决问题,如果有人不遵守约定就还会出现该问题
  • 现在ES2015为了解决这种问题它提供了一种全新的原始数据类型 — Symbol
  • 翻译过来其实就是符号的意思,它的作用呢就是表示一个独一无二的值
// shared.js==========================================
const cache = {}

// a.js==========================================
// cache['foo'] = Math.random()
cache['a_foo'] = Math.random()

// b.js==========================================
// cache['foo'] = '123'
cache['b_foo'] = '123'

// console.log(cache)  // { foo: '123' }
console.log(cache)  // { a_foo: 0.6412025365262253, b_foo: '123' }

const s = Symbol()
console.log(s)  // Symbol()
console.log(typeof s)   // symbol
  • 使用Symbol创建的值,永远都是唯一的,不会重复
console.log(
    Symbol() === Symbol()
)   // false
  • 考虑到在开发过程中的调试,Symbol函数允许我们传入一个字符串,作为这个值的描述文本,对于多次使用Symbol的情况,我们就可以在控制台当中去区分
console.log(Symbol('foo'))  // Symbol(foo)
console.log(Symbol('bar'))  // Symbol('bar')
console.log(Symbol('baz'))  // Symbol('baz')
  • 从ES2015开始,对象就可以直接使用Symbol类型的值作为属性名,也就是说现在我们对象的属性名可以是两种类型的,分别是string和sybol
const obj = {}
obj[Symbol()] = '123'
obj[Symbol()] = '456'
console.log(obj)    // { [Symbol()]: '123', [Symbol()]: '456' }
  • 我们也可以使用计算属性名的方式直接再对象字面量当中使用symbol作为属性名
const obj = {
    [Symbol()]: 123
}
console.log(objectPattern)
  • Symbol除了可以用来避免对象属性名重复产生的问题,我们还可以借助这部分问题的特点去模拟实现对象的私有成员
// a.js=============================================
const name = Symbol()
const person = {
    [name] : 'hello',
    say () {
        console.log(this[name])
    }
}

// b.js=============================================
person.say()    //hello
  • 这种类型的值,目前最主要的作用就是为对象添加一个独一无二的属性名
  • 截止到ES2019,ES一共定义了6种原始数据类型,加上object类型一个是7中数据类型,在未来还会增加一个BigInt的原始数据类型,用来去存放更长的数字,目前是stage-4阶段,预计下一个版本就能被正式标准化,到时候就是8种数据类型

ES2015 Symbol补充

  • Symbol在使用上还有一些值得我们注意的地方

    • 每次通过Symbol创建的对象一定是一个唯一的值,不管我们传入的描述文本是不是相同的,每次去调用Symbol函数他得到的结果都是不相同的,都是全新的一个值
    console.log(
        Symbol() === Symbol()
    )   // false
    
    • 如果我们需要在全局去复用一个相同Symbol的值,我们可以使用全局变量的方式去实现,或者使用Symbol类型提供的一个静态方法
      • for方法接收一个字符串作为参数,相同的字符串就一定会返回相同Symbol类型的值
      • 这个方法内部维护了一个全局的注册表,为我们的字符串和Symbol值提供了一个一一对应的关系
    const s1 = Symbol.for('foo')
    const s2 = Symbol.for('foo')
    console.log(s1 === s2)  // true
    
    • 需要注意的是,在这个方法内部维护的是字符串和Symbol之间的对应关系,也就是说,如果我们传入的不是字符串,那这个方法内部会自动转换为字符串,这样就会导致我们传入布尔值的true和我们传入字符串的true结果拿到的都是一样的
    console.log(
        Symbol.for(true) === Symbol.for('true')
    )   // true
    
    • 而且在Symbol类型当中还提供了很多内置的Symbol常量,用来去作为内部方法的表示,这些标识符可以让自定义对象实现一些js当中内置的接口
    console.log(Symbol.iterator)    // Symbol(Symbol.iterator)
    console.log(Symbol.hasInstance) //Symbol(Symbol.hasInstance)
    
    • 定义一个对象,调用这个对象的toString方法,结果默认是[object Object],我们把这样的字符串叫做对象的toString标签
    const obj = {}
    console.log(obj.toString()) // [object Object]
    
    • 如果我们想要自定义这个对象的标签,我们就可以在这个对象当中去添加一个特定的成员来去表示,考虑到如果使用字符串去添加这种标识符,就有可能跟内部的一些成员产生重复,所以ES要求我们使用Symbol值去实现这样一个接口
    const obj = {
        [Symbol.toStringTag] : 'X'
    }
    console.log(obj.toString()) // [object X]
    
    • 此时我们的toString标签就是我们自定义的,这里的toStringTag就是一个内置的Symbol常量,这种常量在后边为我们对象生成迭代器时会经常用到

    • 使用Symbol值去作为对象的属性名,这个属性我们通过传统的for…in循环是拿不到的,而且我们通过Object.keys方法也是获取不到的

    const obj = {
        [Symbol()] : 'Symbol value',
        foo: 'normal value'
    }
    for (var key in obj) {
        console.log(key)    // foo
    }
    console.log(Object.keys(obj))   // [ 'foo' ]
    
    • 如果我们通过JSON.stringify去序列化我们的对象为一个JSON字符串的话,那Symbol属性也会被忽略掉
    console.log(JSON.stringify(obj))    // {"foo":"normal value"}
    
    • 总之这些特性都使得我们的Symbol类型的属性,他特别适合作为对象的私有属性,当然想要获取这类型的属性名也不是完全没有办法,我们可以使用Object方法里边的getOwnPropertySymbols方法,这个方法类似于keys方法,不同的是keys方法,它只能够获取到对象中所有的字符串属性名,而getOwnPropertySymbols方法获取到的全是Symbol属性的属性名
    console.log(Object.getOwnPropertySymbols(obj))  // [ Symbol() ]
    

for…of循环

  • 在ES中遍历数据有很多种方法
    • for — 适合普通的数组
    • for…in — 适合键值对
    • 函数式的遍历方法 — 数组对象的forEach方法
  • 因为都会有一些局限性,所以ES2015引入了一种全新的for…of循环
  • 以后会作为遍历所有数据结构的统一方式
  • 不同于传统的for…in循环,for…of拿到的是我们数组中的每一个元素,而不是对应的下标,这种循环方式就可以取代我们之前常用的forEach方法
for (const item of arr) {
    console.log(item)   // 100 200 300 400
}

arr.forEach(item => {
    console.log(item)   // 100 200 300 400
})
  • 相比于forEach,for…of他可以使用break关键词,随时去终止循环
for (const item of arr) {
    console.log(item)   // 100 200
    if (item > 100) {
        break
    }
}

arr.forEach()    // 不能跳出循环
  • 以前为了能够终止遍历,我们必须要用数组实例的some()或者是every(),在some中返回true,在every中返回false,而在forEach()无论返回什么都不能终止遍历

  • 伪数组也可以使用for…of循环遍历

  • ES2015新增的set和map对象

    • 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', '456')
    
    for (const item of m) {
        console.log(item)   // [ 'foo', '123' ] [ 'bar', '456' ]
    }
    
    for (const [key, value] of m) {
        console.log(key, value)   // foo 123    bar 456
    }
    
  • 遍历普通对象

const obj = { foo: 123, bar: 456 }
for (const item of obj) {   // TypeError: obj is not iterable --- obj对象是不可被迭代的
    console.log(item)
}

ES2015 可迭代接口

  • for…of循环是一种数据统一的遍历方式,对于普通对象直接遍历会报错
  • ES中能够表示有结构的数据类型越来越多,从最早的数组和对象,到现在新增了set、map而且我们开发者还可以组合使用这些类型,去定义一些符合自己业务需求的数据结构
  • 为了给各种各样的数据结构提供统一的遍历方式,ES2015提出了一种Iterable的数据接口 — 可迭代的
  • 可以把它理解为一个规格标准,例如我们在ES标准中任意一种数据类型他都有toString方法,这就是因为它们都实现了统一的规格标准
  • 编程语言当中更专业的说法 — 他们都实现了统一的接口
  • 实现Iterable接口就是for…of 的前提
// 以下操作都是直接在浏览器的开发人员工具操作

const arr = ['foo', 'bar', 'baz']

const iterator = arr[Symbol.iterator]()

iterator.next() // {value: "foo", done: false}
iterator.next() // {value: "bar", done: false}
iterator.next() // {value: "baz", done: false}
iterator.next() // {value: undefined, done: true}

// 以下操作都在node环境中操作
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 }
  • 这就是for…of循环内部的工作原理

ES2015 实现可迭代接口

  • 在ES去实现Iterable接口,他实际上就是在这个对象当中去挂在一个Iterable方法,然后在这个方法中返回一个迭代器对象
/**
 * 这里一共三层对象:
 * 
 * 首先是最外成自定义的对象,它实现了,可迭代接口 --- Iterable
 * 这里约定了内部必须要有一个用于返回迭代器的iterator方法
 * 
 * 其次就是中间iterator返回的这个对象,他实现了迭代器接口 --- Iterator
 * 这个接口约定了的内部必须要有一个用于迭代的next方法
 * 
 * 最后就是在next方法当中返回的这个对象,这个对象实现的实际上就是迭代结果接口 --- IterationResult
 * 这个接口约定了我们在这个对象内部必须要有一个value属性用来去表示当前被迭代到的数据,他得值可以是任意类型
 * 除此之外还需要有一个done属性,它是用来表示迭代有没有结束
 * */
const obj = {
    // 存放一些值得被遍历的数据
    store: ['foo', 'bar', 'baz'],

    // 因为这是Symbol提供的一个常量,所以需要使用计算属性名的方式定义到字面量当中,成员的值就是一个函数
    [Symbol.iterator]: function () {
        let index = 0; // 下标 --- 默认为0
        let self = this; // 存储一下this

        // 在成员内部要返回一个实现迭代器接口的对象,也就是说在这个对象中要提供一个next方法,用于实现向后迭代的逻辑
        return {
            // next方法内部需要返回一个迭代结果对象
            next: function () {
                // 这个对象需要有两个成员,分别是value和done
                const result = {
                    value: self.store[index],
                    done: index >= self.store.length
                }
                index++;
                return result
            }
        }
    }
}

for (const item of obj) {
    console.log('循环体', item)
}

ES2015 迭代器模式

  • 如何让我们的自定义对象去实现可迭代接口,从而实现可使用for…of循环迭代我们的对象 — 其实这就是设计模式中的迭代器模式
// 场景:你我协同开发一个任务清单应用

// 我的代码==================================================
    // 设计一个用于存放所有任务的对象

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

        each : function (callback) {
            const all = [].concat(this.life, this.learn, this.work)

            for (const item of all) {
                callback(item)
            }
        },

        [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.life) {
        console.log(item)
    }
    for (const item of todos.learn) {
        console.log(item)
    }
    for (const item of todos.work) {
        console.log(item)
    }

    console.log('=============================================')


    todos.each(function (item) {
        console.log(item)
    })

    console.log('=============================================')

    for ( const item of todos ) {
        console.log(item)
    }
  • 这样一个模式的核心就是对外提供统一遍历接口,让外部不用再去担心这个数据的内部结构是怎样的
  • 只不过我们这里是用的 each 方法 它只适用于我们当前这个对象的数据结构,而ES2015当中的迭代器,他是语言层面的迭代器模式,所以说它可以适用于任何数据结构,只需要通过代码去实现iterator方法,实现它的迭代逻辑就可以了

ES2015 生成器

  • 生成器函数 — Genertor
  • 目的在于:避免异步编程中回调嵌套过深,提供更好的异步编程解决方案
  • 生成器对象其实也实现了迭代器接口的协议
// function * foo () {
//     console.log('hello')
//     return 100
// }

// const result = foo()

// // console.log(result) // Object [Generator] {}

// console.log(result.next()) // hello { value: 100, done: true }

function * foo () {
    console.log(111111)
    yield 100
    console.log(222222)
    yield 200
    console.log(333333)
    yield 300
}
const generator = foo()
console.log(generator.next())   //111111    { value: 100, done: false }
console.log(generator.next())   //222222    { value: 200, done: false }
console.log(generator.next())   //333333    { value: 300, done: false }
  • 生成器函数它会自动帮我们返回一个生成器对象,调用这个对象的next方法才会让这个函数的函数体开始执行,执行过程中遇到yield关键词,函数的执行过程就会被暂停下来,而且yield后边的值,将会作为,next的结果返回,如果继续调用生成器的next,那函数就会从暂停的位置继续执行,一直到函数完全结束,next返回的done的值就会变为true
  • 最大的特点就是惰性执行

ES2015 生成器应用

// 发号器

function * createIdMaker () {
    let id = 1
    while (true) {
        yield id++
    }
}

const idMaker = createIdMaker()

console.log(idMaker.next().value)
console.log(idMaker.next().value)
console.log(idMaker.next().value)
console.log(idMaker.next().value)

// ========================================
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)
}
  • 它最重要的目的还是为了去解决异步编程当中异步回调所导致的问题

ES Modules

  • 语言层面的模块化标准

ES2016 概述

  • ES2016相比于ES2015仅包含两个小功能
    • 数组实例对象的includes方法,这方法让我们去检查数组是否中包含制定元素变得更加简单
      • 在此之前如果我们类似需求时,我们就必须要使用数组对象的indexof方法,因为这个方法可以帮我们找到元素在数组中的下标,在没有找到时返回-1
      • 但是这种方式存在一定弊端:他不能去用于查找数组中的NaN
      • 有了includes直接就返回一个true或者false,相对于indexof还可以查找NaN
    • 指数运算符
      • 以前我们需要指数运算时,我们需要借助于Math对象当中的pow方法
      • ES2016新增的指数运算符就是语言本身的运算符,就像是我们之前是用的+ - * /一样
      • 使用起来 就是积数 两个**,后边跟上指数
const arr = ['foo', 1, NaN, false]

console.log(arr.indexOf('foo')) //0
console.log(arr.indexOf('bar')) // -1
console.log(arr.indexOf(NaN))   // -1
console.log(arr.includes('foo'))    // true
console.log(arr.includes(NaN))  // true


console.log(Math.pow(2, 10))    // 1024

console.log(2 ** 10)    // 1024

ES2017 概述

  • 对Object对象的三个扩展方法
    • Object.values
      • 和Object.keys方法非常类似,但是Object.keys返回的是对象当中所有键组成的一个数组
      • Object.values返回的是这个对象当中所有的值组成的数组
    console.log(Object.values(obj)) // [ 'value1', 'value2' ]
    
    • Object.entries
      • 以数组的形式返回我们对象当中所有的键值对
      • 这使得我们可以直接通过for…of循环遍历我们的普通对象
      • map的构造函数就是需要这种格式的数组,所以我们可以借助他将一个对象转换成一个map类型的对象
    console.log(Object.entries(obj))    // [ [ 'foo', 'value1' ], [ 'bar', 'value2' ] ]
    
    for (const [key, value] of Object.entries(obj)) {
        console.log(key, value) // foo value1   bar value2
    }
    console.log(new Map(Object.entries(obj)))   // Map { 'foo' => 'value1', 'bar' => 'value2' }
    
    • Object.getOwnPropertyDescriptors
      • 帮我们获取对象当中属性的完整描述信息
      • ES5过后,我们就可以为对象去定义get或者set属性
      • 这两个属性是不能直接通过Object.assign完全复制的
      • 主要就是配合ES5新增的get和set去使用
    const p1 = {
        firstName: 'xx',
        lastName: 'GD',
        get fullName () {
            // 相当于为外界提供了一个只读属性
            return this.firstName + ' ' + this.lastName
        }
    }
    
    // console.log(p1.fullName)    // xx GD
    
    // 复制一个P2,并且修改内部的firstName
    // const p2 = Object.assign({}, p1)
    // p2.firstName = 'hello'
    /**
    * 修改后应该为 hello Gd,但是得到的确实p1当中的fullName
    * 
    * 这是因为Object.assign()在复制是,他只是把fullName当做一个普通的属性去复制
    * 
    * 这种情况我们就可以使用Object.getOwnPropertyDescriptors来解决
    * */ 
    // console.log(p2.fullName) 
    
    const descriptors = Object.getOwnPropertyDescriptors(p1)
    // console.log(descriptors)
    const p2 = Object.defineProperties({}, descriptors)
    p2.firstName = 'hello'
    console.log(p2.fullName)    //hello GD
    
    • 字符串填充
      • String.prototype.padStart
      • String.prototype.padEnd
        const books = {
        html: 5,
        css: 16,
        JavaScript: 128
    }
    
    // for ( const [name, count] of Object.entries(books) ) {
            /**
            * html 5
            * css 16
            * JavaScript 128
            * */ 
    //     console.log(name, count)    
    // }
    
    for ( const [name, count] of Object.entries(books) ) {
        /**
        * html------------ | 005
        * css------------- | 016
        * JavaScript------ | 128
        */
        console.log(`${name.padEnd(16, '-')} | ${count.toString().padStart(3, '0')}`)    
    }
    
    • 在ES2017中还有一个很小的变化
      • 它允许我们在函数参数中添加尾逗号
      • 我们很多人在去定义数组或者定义对象时,最后一个元素后边呢我们都回去添加一个逗号,这里的原因是一样的
    /**
     * 在执行层面,最后一个数组添加逗号和不添加逗号效果是一样的
    * 
    * 但是很多人都会这样去使用,因为这样有两个好处
    * 
    * 1. 如果我想重新去排列这个数组当中元素的顺序,因为每一行后边都有 ,
    *      也就是说格式是一致的,调整起来就很容易
    * 2. 我们如果说去修改了这个数组当中元素的个数,比如说添加一个新元素
    *      这个时候我们只需要再去新建一行就可以
    * */ 
    const arr = [
        100,
        200,
        300,
    ]
    

Async / Await

  • 这是ES2017最重要的功能
  • 两者配合可以彻底解决异步编程中回调函数嵌套过深的问题
  • 可以让我们的代码更加简洁易读
  • 本质上就是使用Promise的一种语法糖而已
  • 这样一个特性在之前的博客中已经介绍过了,可以移步翻阅
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值