深刻理解 ES6

本文篇幅较长,有兴趣的能够先收藏再看。本文将重要的 ES6 特性介绍了一遍,而且详细解释了一些重难点。git

let && const

letvar 的声明用法相同,可是多了一个临时死区(Temporal Distonrtion Zone)的概念。github

console.log(a) // -> undefined
var a = 1
console.log(b) // -> Uncaught ReferenceError: b is not defined
let b = 1复制代码

能够发如今声明前使用 let 声明的变量会致使报错,这解决了 JS 不少奇怪的问题。而且使用 let 会生成一个块级做用域,做用域外不能访问该变量。编程

{
let a = 1;
var b = 1;
}
console.log(b); // -> 1
console.log(a); // -> Uncaught ReferenceError: b is not defined复制代码

在 JS 中,声明变量都会提高,不论用什么关键字声明。当使用 let 时变量也会被提高至块级做用域的顶部,可是只提高声明,不提高初始化。而且会产生临时死区,该区域会存放变量,直到执行过声明语句后,方可以使用该变量。json

在循环中 let 会与前面有些不一样,每次迭代都会产生一个新的变量,并用以前的值初始化,如何理解这句话呢,请看如下代码。数组

for(let i = 0; i < 10; i++) {
    console.log(i) // -> 输入 0 - 9
}
// 上面的循环代码能够这样看
{ // 造成块级做用域
    let i = 0
    {
        let ii = i
        console.log(ii)
    }
    i++
    {
        let ii = i
        console.log(ii)
    }
    i++
    {
        let ii = i
        console.log(ii)
    }
    ...
}复制代码

constlet 基本相似,只是用 const 声明必须赋值,而且不得修改绑定,什么意思呢,请看代码。promise

const a = 1;
a = 2 // -> Uncaught TypeError: Assignment to constant variable
// but
const b = {a: 1};
b.a = 2 // 起效复制代码

固然了,有办法让这个不能改变app

const b = Object.freeze({a: 1})
b.a = 2 // 没有报错,可是 b.a 没有被改变复制代码

可是 Object.freeze 只能在这里有效,对于数组这些能够看看这个提案ecmascript

这两个新的声明方式在全局做用域下不会自动加上 window异步

字符串相关

部分新增的字符串函数

let string = 'startend'
string.includes('a') // -> true 是否包含
string.endsWith('end') // -> true 是否由 end 结尾
string.startsWith('start') // -> true 是否由 start 开头复制代码

模板字面量

很棒的新功能,解决了以前不少麻烦的写法。模块化

// 语法就是 `` 代替以前的引号,在 `` 中使用引号不须要转义
let s = `it's string`复制代码

多行字符串

// 这样在语法中就能够换行了
let s = `start \ end`
// 注意在模板字面量中的任何空白符都是起效的
let s = `start \ end`  // -> start end复制代码

占位符和标签模板

let s = 'string'
let message = `start${s}`  // -> startstring
// ${} 就是占位符语法,能够更简便的实现字符串插入

// 定义一个 tag 函数,而后直接在 `` 前使用就能够
let m = tag`s${s}e${message}`
// strings 是一个数组,value 是模板字面量中全部的占位符的值
function tag(strings, ...value) {
    // -> ['s', 'e', '']
    console.log(strings)
    // -> ['string', 'startstring']
    console.log(value)
}
// 上面的 ...value 也是 ES6新出的扩展语句,在这里表明不定参数的写法,用于替换 arguments
// 不定参数使用上也是有限制的,必须放在全部参数的末尾,而且在每一个函数中只能声明一次
// 扩展语句和 arguments 区别就是表明了 strings 参数后面的全部参数
// 除了上面的写法,还能够用于展开能够迭代(有Symbol.iterator属性)的对象
let array = [1, 2, 3]
console.log(...array)
// 该语法能够解决以前不少地方只能传入单个参数,只能使用 apply 解决的问题
Array.prototype.unshift.apply([4, 5], array) // -> [1, 2, 3, 4, 5]
// 如今能够直接这样写
[4, 5].unshift(...array)
// 展开运算不受不定参数的条件限制,能够一块儿用复制代码

函数

默认参数

ES6 容许给函数增长默认参数

function fn(a = 1, b = 2) {}
// 默认值也能够经过调用函数得到,注意必须调用函数
function fn1(a = 1, b = fn()) {}复制代码

新增函数内部方法

在 JS 中,函数有多种用法,能够直接调用,也能够经过 new 构造函数。

在 ES6中,函数内部新增了 [[Call]] 和 [[Construct]] 两个方法。后者会在使用 new 构造函数时执行,其余状况会执行前者方法。

当一个函数必须使用 new 构造时,你可使用这个新属性 new.target 判断

// new.target 只能在函数中使用
function Fn() {
    if (typeof new.target === 'underfined') { throw ....... }
}复制代码

箭头函数

这个特性真的很棒,先介绍下他的几种语法

// 最简单的写法,只有一个参数,单行表达式
value => value
// 多个参数须要使用小括号包裹
(v1, v2) => v2 + v1
// 没有参数须要使用小括号包裹
() => "balabala"
// 多行表达式须要大括号包裹
(v1, v2) => {
    return v1 + v2
}
// 返回一个对象,须要用小括号包裹
() => ({a: 1})
// 当即执行函数,注意普通的当即执行函数的小括号包裹在最外面,箭头函数不须要
((value) => value)("balabala")复制代码

箭头函数和普通函数区别仍是蛮大的,说几个经常使用的

  • 没有 this,不能改变 this 绑定
  • 不能经过 new 调用,固然也没有原型
  • 没有 arguments 对象,不能有相同命名参数

箭头函数虽然没有 this ,可是仍是能够在内部使用 this

  • this 的绑定取决于定义函数时的上下文环境
  • 一旦函数调用,任何改变 this 的方法都无效
// let 有个细节
let x = 11111
let a = {
    x: 1,
    init() {
        // 箭头函数的 this 取决于 init,因此能够打印出 1
        document.addEventListener('click', () => console.log(this.x))
    },
    allowInit: () => {
        // allowInit 直接是个箭头函数,因此这时的 this 变成了 window
        // 可是并不会打印出 11111,忘了 let 的一个细节的能够回到上面看看
        console.log(this.x)) 
    }
    otherInit() {
        // 普通函数的 this 取决于调用函数的位置,this 指向 document
        // 若是想打印出 x,可使用 bind
        document.addEventListener('click', function() {
            console.log(this.x)
        })
    }
}
a.init() // -> 1
a.allowInit() // -> undefined
a.otherInit() // -> undefined复制代码

对象相关

let a = 1
// 当 key 和 value 名字相同时能够简写
let b = { a }
// 对象中的方法也能够简写
let a = {
    init() {}
}
// 对象属性名也能够计算 
let name = 'name'
b[name + '1'] = 2 // === b['name1'] = 2复制代码

ES6 也新增了几个对象方法

Object.is(NaN, NaN) // ->true
// 结果基本于 === 类似,除了 NaN 和 +0 -0
Object.is(+0, -0) // -> false
let o = {a: 1}
let a = Object.assign({}, o) // -> {a: 1}
// 第一个参数为目标参数,后面的参数是不定的,参数属性名若是有重复,后面的会覆盖以前的复制代码

原型相关

ES6 以前改变对象原型很麻烦

let obj = {a: 1}
let obj1 = {a: 2}
// 已 obj 为原型
let a = Object.create(obj)
// 改变 a 的原型为 obj1
Object.setPrototypeOf(a, obj1) // a.a === 2复制代码

访问原型

Object.getPrototypeOf(a) // 访问原型
// ES6 中能够直接经过 super 表明原型
let a = {
    init() {
        return 'Hello'
    }
}
let b = {
    init() {
    // 不能在 super 以前访问 this
        return super.init() + 'World'
    }
}
Object.setPrototypeOf(b, a)
b.init() // -> 'HelloWorld'复制代码

可是 super 不是每一个函数均可以使用的,只有在函数的简写语法中方可以使用。由于在 ES6中新增了一个函数内部属性 [[HomeObject]],这个属性决定了是否能够访问到 super。首先在该属性上调用 Object.getPrototypeOf(绑定的对象),而后找到原型中的同名函数,在设置 this 绑定而且调用函数,其实就是一个新增的语法糖。

解构赋值

该特性能够用于对象,数组和传参。

let obj = {a: 1, b: 2}
// 对象解构使用 {},数组解构使用 [],由于这里是对象解构,c 不是 obj 的属性,因此 underfined
// 数组解构中,若是须要解构的变量大于数组索引,多出来的变量也是 undefined
// 解构必须赋值,不然报错。不能 let {a, b, c};
// 赋值不能为 null 或者 undefined,会报错
let {a, b, c} = obj
// 等于 let a = obj.a,能够看作以前介绍的对象属性简写
console.log(a, b, c) // -> 1, 2, underfined
// 若是已经声明了变量而且想使用解构,必须最外面是小括号
({a, b} = obj)
// 若是不想使用 obj 中的对象名,又想使用解构赋值
let {x: a} = obj
// 若是想使用默认值
let {a = 2, c= 3} = obj // -> 1, 3
// 由于 a 是 obj 中的对象,因此默认值被覆盖
// 解构也能够嵌套
let obj = {data: {code: 1}, message: [1, 2]}
// 这个写法在 json 中很好用
// 注意在这个写法中,data 和 message 都是指代了 obj 的属性,并无被声明变量
let { data: {code}, message: [a] } = obj
console.log(code, a)
// 数组解构和对象解构基本类似,而且简单多了
let message = [1, 2, 3, 4]
// 由于数组取值只能索引取,因此想跳过某几个索引,就用逗号代替
// 一样,数组解构也可使用默认值和嵌套解构,和对象解构如出一辙就不赘述了
let [a, , b] = message // -> 1, 3
// 在上面章节介绍了扩展语法,一样也可使用在数组解构中
// 能够看到 b 变成了一个数组
let [a, ...b] = message // -> 1, [2, 3, 4]
// 传参使用解构可让要传的参数更加清晰
function fn(name, {key, value}) {
    console.log(name, key, value)
}
// 使用,注意:传参解构必须起码传入一个值,不然报错
fn(1, {key: 2, value: 3})
// 由于传参解构相似如下写法
function fn(name, {key, value}) {
    let {key, value} = null // 这个上面讲过不能这样写
}复制代码

Symbol

ES6 新出的第六个原始类型。多用于避免代码冲突,做为一个私有属性使用,不会被属性遍历出来。可使用 Object.getOwnPropertySymbols() 检索 Symbol 属性。

建立和使用

// 建立
 let a = Symbol()
 // 更推荐这种写法,能够更加明确这个Symbol的用途
 // 而且有函数能够经过这个字符串取到相应的Symbol
 let b = Symbol('is b')
 // 使用,通常做为可计算属性使用
 let a = {}
 let b = Symbol('is b')
 a[b] = 1
 // 能够在全局注册表中共享同一个 Symbol,但不推荐使用
 // 不存在 is a 会自动建立
 let a = Symbol.for('is a')复制代码

暴露内部操做

Symbol 中预约义了一些 well-know Symbol,这些 Symbol 定义了一些语言的内部实现

  • Symbol.hasinstance,用于执行 instanceof 时检测对象的继承信息
  • Symbol.isConcatSpreadable,布尔值,用于判断当使用 concat 函数时是否将数组展开
  • Symbol.iterator,迭代器,后面会讲到
  • Symbol.match,Symbol.replace,Symbol.search,Symbol.split,字符串相关方法的对应内部实现
  • Symbol.toPrimitive,返回对象原始值
  • Symbol.toStringTag,调用 toString

Set 和 Map

Set

Set 是新增的无重复的有序集合,多用于集合去重或者判断集合中是否含有某个元素。

// 建立
let set = new Set()
// 添加元素
set.add(1)
set.add('1')
// 重复的元素不会被添加
set.add(1)
// 判断是否包含元素
set.has(1) // -> true
// 判断长度
set.size() // -> 2
// 删除某个元素
set.delete()
// 移除全部元素
set.clear()复制代码

Map

Map 是新增的有序键值对列表,键值能够是任何类型。

// 建立
let map = new Map()
// 设置键值对
map.set('year', "2017")
map.set({}, 'obj')
// 取值
map.get('year') // -> '2017'
// 判断是否有该键值
map.has('year') // -> true
// 得到长度
map.size() // -> 2
// 删除某个键值
map.delete('year')
// 移除全部键值
map.clear()复制代码

迭代器和 Generator 函数

迭代器

顾名思义,用来迭代的。以前介绍过 Symbol.iterator,能够迭代的对象都有这个属性,包括数组,Set,Map,字符串和 NodeList。ES6新增的 for-of 就用到了迭代器的功能,可是默认只有上面这些对象能使用。

let a = [1, 2]
for (let value of a) {
    console.log(value) // -> 1, 2
}
// 上面的代码其实就是调用了数组的默认迭代器
let iterator = a[Symbol.iterator]()
// 当调用 next 时会输出此次迭代的 value 和是否迭代完成
console.log(iterator.next()) // {value: 1, done: false}
console.log(iterator.next()) // {value: 2, done: false}
// 已经没元素能够迭代了
console.log(iterator.next()) // {value: undefined, done: true}
// 数组的默认迭代器只会输出 value,若是想同时输出索引的话
// 这里可使用新特性数组解构 let [index, value]
for (let value of a.entries()) {
    console.log(value) // -> [0, 1] [1, 2]
}复制代码

对于本身建立的对象都是不可迭代的,固然咱们也可让他变成迭代的

let a = {
    array: [],
    // 这是一个 Generator 函数,立刻就会讲到
    *[Symbol.iterator]() {
        for(let item in this.array) {
            yield item
        }
    }
}
a.array.push(...[1, 2, 3])
for(let item of a) {
    console.log(item)
}复制代码

Generator 函数

用于异步编程。该函数能够暂停和恢复执行,和同步写法很像。

// 星号表示这是一个 Generator 函数
function *gen() {
// 第一次 next 只执行到等号右边
    let first = yield 1
    // 第二次 next 执行 let first = 和 yield 2
    let second = yield 2
    // 不执行接下来的 next 就卡在上一步了
    let thrid = yield 3
}
let g = gen()
g.next() // -> {value: 1, done: false}
g.next() // -> {value: 2, done: false复制代码

接下来看下 Generator 函数如何用于异步

function getFirstName() {
    setTimeout(function(){
        gen.next('alex')
    }, 1000);
}

function getSecondName() {
    setTimeout(function(){
        gen.next('perry')
    }, 2000);
}

function *sayHello() {
    var a = yield getFirstName();
    var b = yield getSecondName();
    // settimeout 原本是异步的,经过 Generator 函数写成了同步写法
    console.log(a, b); // ->alex perry
}

var gen = sayHello();

gen.next();复制代码

JS 中的类不是其余语言中的类,只是个语法糖,写法以下。

class Person {
// 构造函数
    constructor() {
        this.name = name
    }
    sayName() {
        console.log(this.name)
    }
}
let p = new Person('name')
p.sayName() // -> 'name'

// class 就是如下代码的语法糖
// 对应 constructor
function Person(name) {
    this.name = name
}
// 对应 sayName
Person.prototype.sayName = function() {
    console.log(this.name)
}复制代码

类声明相比以前的写法有如下几点优势

  • 类声明和 let 声明同样,有临时死区
  • 类声明中的代码所有运行在严格模式下
  • 必须使用 new 调用

继承

在 ES6 以前写继承很麻烦,既然有个类,那么必然也能够继承类了

class Person {
// 构造函数
    constructor() {
        this.name = name
    }
    sayName() {
        console.log(this.name)
    }
}
// extends 表明继承自Person
class Student extends Person {
    constructor(name, age) {
    // super 的注意事项以前有说过
        super(name)
        // 必须在 super 以后调用 this
        this.age = age
    }
    sayName() {
    // 若是像使用父类的方法就使用这个方法使用
    // 不像使用的话就不写 super,会覆盖掉父类的方法
        super.sayName(this.name)
        console.log(this.age)
    }
}复制代码

Promise

概念

用于异步编程。

// 你可使用 new 建立一个 Promise 对象
let promise = new Promise(function(resolve, reject)) {}
resole() // 表明成功
reject() // 表明失败
promise.then(onFulfilled, onRejected) // 当调用 resole 或者 reject ,then 能够监听到
promise.catch() // reject 或者 throw时能够监听到复制代码

Promise 有三个状态

  • pending,等待状态,也就是既不是 resolve 也不是 reject 状态
  • fulfilled,resolve 之后进入这个状态
  • reject,reject 之后进入这个状态

通常使用状况

function delay() {
// 建立一个 promise
    return new Promise((resolve, reject) => {
    // 当调用 promise 时,里面的内容会当即执行
        console.log('in delay')
        setTimeout(() => {
          resolve(1)
        }, 1000)
    });
}
function otherDelay() {
    return new Promise((resolve, reject) => {
        setTimeout(() => {
          reject(1)
        }, 1000)
    });
}
// 这里会先输出 delay 函数中的 log,而后再输出 outer,接下来1秒之后输出3个1
delay()
// then 能够捕获 resolve 和 reject
    .then((value) => {
        console.log(value)
    })
console.log('outer')

otherDelay()
// 捕获 reject时,若是不须要捕获 resolve 时能够这样写
    .then(null, (value) => {
        console.log(value)
    })
// 捕获 reject 或者 throw 时推荐使用这个写法,缘由后面会说
otherDelay()
    .catch((value) => {
        console.log(value);
    })复制代码

以上是最经常使用的 Promise 写法,如今介绍 Promise 链

delay()
// then 会返回一个新的 promise 对象
    .then((value) => {
     // 这样就能够传参了
        return value + 1
    }).then((value) => {
        console.log(value) // -> 2
        // then 里面能够也能够传入一个函数名,会自动调用
        // 若是传入的函数有参数会自动传入
    }).then(delay).then((value) => {
        console.log(value) // -> 1
        // 若是在then 中抛出错误,只有 catch 才能监听到,因此推荐使用 catch 监听错误
        throw new Error('error')
    }).then((value) => {
        console.log(value) // 这个then 不会执行
    }).catch((error) => {
        console.log('catch' + error) // -> catch Error
    })复制代码

Promise 高级用法

开发中可能会有需求,须要一次上传几张图片,所有上传成功之后有个提示,这时候就能够用到 Promise.all()

function updateOne() {
    return new Promise((resolve, reject) => {
        setTimeout(() => {
            resolve('one')    
        }, 1000)
    });
}
function updateTwo() {
    return new Promise((resolve, reject) => {
        setTimeout(() => {
            resolve('two')    
        }, 2000)
    });
}
// all 函数接收一个可迭代对象,注意这里传入函数必须调用
let promise = Promise.all([updateOne(), updateTwo()])
// 只有当 all 中的异步所有完成了才会调用 then
promise
    .then((value) => {
    // value 是个函数,顺序按照 all 里的迭代对象的顺序
        console.log(value) // -> ["one", "two"]
    })复制代码

若是一个异步任务超时了,你想直接取消,能够经过 Promise.race()

// 假设该任务执行时间超过1秒就算超时,应该 cancel
function delay() {
    return new Promise((resolve, reject) => {
        setTimeout(function() {
            resolve('finish')
        }, 1500);
    });
}
function cancel() {
    return new Promise((resolve, reject) => {
        setTimeout(function() {
            resolve('cancel')
        }, 1000);
    });
}
// 接收的参数和 all 相同
let promise = Promise.race([delay(), cancel()])
// race 中只要有一个任务完成,then 就会被调用,这样就能够 cancel 掉全部超时任务
promise
    .then((value) => {
        console.log(value) // -> cancel
    })复制代码

Proxy

Proxy 能够建立一个代替目标对象的代理,拦截语言内部的操做。

let handle = {}
let target = {}
// 这样就建立了target 对象的代理,可是这个代理其实没有任何用处
let p = new Proxy(target, handle)复制代码

上面的代码中能够看到传入了一个 handle 的对象,只有当这个对象中包含一些代理行为的函数时,这个代理才有用。具备的代理行为函数能够去 MDN查看,这里举例几个用法。

let handle = {
    // 改变 set 的内部操做
        set(target, key, value) {
        // 当给 age 属性赋值小于19时报错
            console.log(value)
            if (key === 'age') {
                if (value < 19) {
                    throw new Error('未成年')
                }
            }
        }
    }
let target = {}
let p = new Proxy(target, handle)
p.age = 1 // -> 报错
p.age = 19 // -> 没问题复制代码

模块化

ES6 引入了原生的模块化,这样就能够抛弃以前的 AMD 或者 CMD 规范了,若是对模块化还没什么了解,能够看下我以前的文章 明白 JS 模块化

// example.js 文件下
// export 能够导出任何变量,函数或者类
export var age = 14
export function sum(n1, n2) {
    return n1 + n2
}
export class Person {
    constructor(age) {
        this.age = age
    }
}
// 别的 JS 文件中导入
// 若是想导入整个模块而且本身命名,就能够这样使用
// import 后面表明模块名,from 后面表明要导入的文件地址
import * as Example from './example'
console.log(Example.age) // -> 14
// 固然你也能够只使用模块中的一个功能
// 这里使用了对象解构的方法拿到须要的功能,注意这里名字必须相同,不然使用会报错
import { age, sum } from './example'
console.log(age) // -> 14
console.log(sum(1, 2)) // -> 3
// 如今我只想导出一个功能,而且外部能够随便命名该如何作呢?
// default 一个文件中只能使用一次
export default var age = 14
// MyAge 能够随便本身喜欢命名
import MyAge from './example'
console.log(MyAge) // -> 14复制代码

以上就是本文的所有内容了,感谢你们能看到这里,若是有任何错误或者解释的不明白的,能够留言回复,谢谢!

相关文章
相关标签/搜索