ES 6新特性(07)

Part1 · JavaScript【深度剖析】

ES 6新特性

查看我整理的ES新特性思维导图,参照思维导图可以宏观角度来学习ES6的新特性。传送门:ES新特性思维导图,或直接保存下图。

在这里插入图片描述

文章说明:本专栏内容为本人参加【拉钩大前端高新训练营】的学习笔记以及思考总结,学徒之心,仅为分享。如若有误,请在评论区支出,如果您觉得专栏内容还不错,请点赞、关注、评论。

共同进步!

上一篇:【ECMAScript模板字符串】、【ES6参数】、【展开数组、对象】、【箭头函数】、【对象】、【代理Proxy】、【class类】、【set、map数据结构】

接上一篇文章继续看ES6的特性,本片主要讲Symbol、for…of循环、可迭代接口、迭代器模式、生成器及生成器的应用、ES Modules、ES2016和2017的概述。同样,本篇博客内容较多,可以先Mark后看。

十六、Symbol符号

在ES6之前,对象的属性名都是用字符串表示,而这样会导致,对象的属性名重复造成冲突,例如属性值覆盖等问题。

// shared.js ====================================

const cache = {}

// a.js =========================================

cache['foo'] = Math.random()

// b.js =========================================

cache['foo'] = '123'

console.log(cache['foo'])  // 123

之前的解决方式基本为约定,例如a.js文件中的键名都为a_foo,b.js文件中的键名为b_foo,这样就不会造成属性名重复冲突的问题。而约定只是为了规避这个问题,并没有实际解决这个问题。

ES6中为了解决这个问题,提出了一个新的数据类型**(Symbol)符号**。符号是原始值,且符号实例是唯一、不可变的。符号的用途是确保对象属性使用唯一标识符,不会发生属性冲突的危险。

1.基本使用–Symbol()

const s = Symbol()
console.log(s)
console.log(typeof s)

// 两个 Symbol 永远不会相等

console.log(
  Symbol() === Symbol()
)  // false

调用 Symbol()函数时,也可以传入一个字符串参数作为对符号的描述(description),将来可以通过这个字符串来调试代码。但是,这个字符串参数与符号定义或标识完全无关:

let genericSymbol = Symbol(); 
let otherGenericSymbol = Symbol(); 
let fooSymbol = Symbol('foo'); 
let otherFooSymbol = Symbol('foo'); 
console.log(genericSymbol == otherGenericSymbol); // false
console.log(fooSymbol == otherFooSymbol); // false

符号没有字面量语法,这也是它们发挥作用的关键。按照规范,你只要创建 Symbol()实例并将其用作对象的新属性,就可以保证它不会覆盖已有的对象属性,无论是符号属性还是字符串属性。

let genericSymbol = Symbol(); 
console.log(genericSymbol); // Symbol() 
let fooSymbol = Symbol('foo'); 
console.log(fooSymbol); // Symbol(foo);

最重要的是,Symbol()函数不能与 new 关键字一起作为构造函数使用。这样做是为了避免创建符号包装对象,像使用 Boolean、String 或 Number 那样,它们都支持构造函数且可用于初始化包含原始值的包装对象:

let myBoolean = new Boolean(); 
console.log(typeof myBoolean); // "object" 
let myString = new String(); 
console.log(typeof myString); // "object" 
let myNumber = new Number(); 
console.log(typeof myNumber); // "object" 
let mySymbol = new Symbol(); // TypeError: Symbol is not a constructor

2.使用全局符号注册表–Symbol.for()

如果运行时的不同部分需要共享和重用符号实例,那么可以用一个字符串作为键,在全局符号注册表中创建并重用符号。也就是说可以使用一个字符串参数作为Symbol的描述符,这样在使用过程中可以重用这一定义的symbol。

let fooGlobalSymbol = Symbol.for('foo'); // 创建新符号
let otherFooGlobalSymbol = Symbol.for('foo'); // 重用已有符号
console.log(fooGlobalSymbol === otherFooGlobalSymbol); // true

Symbol.for()对每个字符串键都执行幂等操作。第一次使用某个字符串调用时,它会检查全局运行时注册表(也可以理解为一个映射关系表),发现不存在对应的符号,于是就会生成一个新符号实例并添加到注册表中。后续使用相同字符串的调用同样会检查注册表,发现存在与该字符串对应的符号,然后就会返回该符号实例。

即使采用相同的符号描述,在全局注册表中定义的符号跟使用 Symbol()定义的符号也并不等同:

let localSymbol = Symbol('foo'); 
let globalSymbol = Symbol.for('foo'); 
console.log(localSymbol === globalSymbol); // false

全局注册表中的符号必须使用字符串来创建,因此传递给Symbol的任何参数都会被转换为字符串:

const boolSymbol = Symbol.for(true);
const stringSymbol = Symbol.for('true');
console.log(boolSymbol === stringSymbol); // true

还可以使用**Symbol.keyFor()**来查询全局注册表,这个方法接收符号,返回该全局符号对应的字符串键。如果查询的不是全局符号,则返回 undefined。

// 创建全局符号
let s = Symbol.for('foo'); 
console.log(Symbol.keyFor(s)); // foo 
// 创建普通符号
let s2 = Symbol('bar'); 
console.log(Symbol.keyFor(s2)); // undefined 
// 如果传给 Symbol.keyFor()的不是符号,则该方法抛出 TypeError:
Symbol.keyFor(123); // TypeError: 123 is not a symbol

3.使用符号作为属性

**凡是可以使用字符串或数值作为属性的地方,都可以使用符号。这就包括了对象字面量属性和Object.defineProperty()/Object.defineProperties()**定义的属性。对象字面量只能在计算属性语法中使用符号作为属性。

let s1 = Symbol('foo'), 
    s2 = Symbol('bar'), 
    s3 = Symbol('baz'), 
    s4 = Symbol('qux'); 
let o = { 
 [s1]: 'foo val' 
}; 
// 这样也可以:o[s1] = 'foo val'; 
console.log(o); 
// {Symbol(foo): foo val} 
Object.defineProperty(o, s2, {value: 'bar val'}); 
console.log(o); 
// {Symbol(foo): foo val, Symbol(bar): bar val} 
Object.defineProperties(o, { 
    [s3]: {value: 'baz val'}, 
    [s4]: {value: 'qux val'} 
}); 
console.log(o); 
// {Symbol(foo): foo val, Symbol(bar): bar val, 
// Symbol(baz): baz val, Symbol(qux): qux val}

类似于 Object.getOwnPropertyNames()返回对象实例的常规属性数组Object.getOwnPropertySymbols()返回对象实例的符号属性数组。这两个方法的返回值彼此互斥。Object.getOwnPropertyDescriptors()会返回同时包含常规和符号属性描述符的对象。Reflect.ownKeys()会返回两种类型的键

let s1 = Symbol('foo'), 
	s2 = Symbol('bar'); 
let o = { 
    [s1]: 'foo val', 
    [s2]: 'bar val', 
    baz: 'baz val', 
    qux: 'qux val' 
}; 
console.log(Object.getOwnPropertySymbols(o)); 
// [Symbol(foo), Symbol(bar)] 返回对象实例的符号属性数组
console.log(Object.getOwnPropertyNames(o)); 
// ["baz", "qux"] 返回对象实例的常规属性数组
console.log(Object.getOwnPropertyDescriptors(o)); 
// {baz: {...}, qux: {...}, Symbol(foo): {...}, Symbol(bar): {...}} 返回同时包含常规和符号属性描述符的对象
console.log(Reflect.ownKeys(o)); 
// ["baz", "qux", Symbol(foo), Symbol(bar)]返回两种类型的键**

因为符号属性是对内存中符号的一个引用,所以直接创建并用作属性的符号不会丢失。但是,如果没有显式地保存对这些属性的引用,那么必须遍历对象的所有符号属性才能找到相应的属性键:

let o = { 
    [Symbol('foo')]: 'foo val', 
    [Symbol('bar')]: 'bar val' 
}; 
console.log(o); 
// {Symbol(foo): "foo val", Symbol(bar): "bar val"} 
let barSymbol = Object.getOwnPropertySymbols(o) 
 .find((symbol) => symbol.toString().match(/bar/)); 
console.log(barSymbol); 
// Symbol(bar)

4.常用内置符号

ECMAScript 6 也引入了一批常用内置符号(well-known symbol),用于暴露语言内部行为,开发者可以直接访问、重写或模拟这些行为。这些内置符号都以 Symbol 工厂函数字符串属性的形式存在。这些内置符号最重要的用途之一是重新定义它们,从而改变原生结构的行为。比如,我们知道for-of 循环会在相关对象上使用 Symbol.iterator 属性,那么就可以通过在自定义对象上重新定义Symbol.iterator 的值,来改变 for-of 在迭代该对象时的行为。

for of中的Symbol.iterator我们会在下面的一节讲到

这些内置符号也没有什么特别之处,它们就是全局函数 Symbol 的普通字符串属性,指向一个符号的实例。所有内置符号属性都是不可写、不可枚举、不可配置的。

注意 在提到 ECMAScript 规范时,经常会引用符号在规范中的名称,前缀为@@。比如,@@iterator 指的就是 Symbol.iterator。

5.Symbol方法

Symbol提供了一些方法,方法如下所示,具体使用技巧可查看MDN或《JavaScript高级程序设计第四版

  • Symbol.asyncIterator
  • Symbol.hasInstance
  • Symbol.isConcatSpreadable
  • Symbol.iterator
  • Symbol.match
  • Symbol.replace
  • Symbol.search
  • Symbol.species
  • Symbol.split
  • Symbol.toPrimitive
  • Symbol.toStringTag
  • Symbol.unscopables

十七、for…of循环

在ECMAScript中,遍历数据有很多的方法。例如,for循环通常用来遍历数组,for…in循环通常用来遍历键值对,函数式的遍历方法如:forEach方法。这些方法都会有一定的局限性。所有ES2015引入了一种全新的遍历方式,for…of,其作为以后遍历所有数据结构的统一方式。

for-of 语句是一种严格的迭代语句,用于遍历可迭代对象的元素,语法如下:

for (property of expression) statement
// 示例
for (const el of [2,4,6,8]) { 
	document.write(el); 
}

在这个例子中,我们使用 for-of 语句显示了一个包含 4 个元素的数组中的所有元素。循环会一直持续到将所有元素都迭代完。与 for 循环一样,这里控制语句中的 const 是非必需的。但为了确保这个局部变量不被修改,推荐使用 const。

for-of 循环会按照可迭代对象的 next()方法产生值的顺序迭代元素。关于可迭代对象,将在下面进行详细介绍。

如果尝试迭代的变量不支持迭代,则 for-of 语句会抛出错误。

const arr = [100, 200, 300, 400]

for (const item of arr) {
    console.log(item)
}

// for...of 循环可以替代 数组对象的 forEach 方法

其可替代forEach方法进行遍历,而且优点是可以随时使用break方法终止循环:

arr.forEach(item => {
    console.log(item)
})

for (const item of arr) {
  console.log(item)
    if (item > 100) {
    break
  }

// forEach 无法跳出循环,必须使用 some 或者 every 方法

除了数组可以使用for…of遍历,一些伪数组同样也可以进行循环遍历,例如:函数中arguments对象、DOM中元素节点列表,他们与普通数组对象没有任何区别,这里就不单独演示了。

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', '345')

for (const [key, value] of m) {
    // 使用数组展开方法
    console.log(key, value)
}

// foo 123
// bar 345

普通对象不能被for…of遍历,至于原因,请看下面的可迭代接口,其中包含了一下Symbol.iterator:

// 普通对象不能被直接 for...of 遍历

const obj = { foo: 123, bar: 456 }

for (const item of obj) {
  console.log(item)
}

十八、可迭代接口

ES中能够表示有结构的数据类型越来越多,从最早的数组和对象,到现在新增了set和map,并且还可以组合使用这些类型。为了提供一种统一的遍历方式,ES2015提供了一种统一的Iterable接口。例如ES中任意一种数据类型都有toString方法,这就是他们都实现了统一的规格标准(统一的接口)

实现Iterable接口就是for…of的前提,只要数据结构实现了可迭代接口,他就能被for…of遍历,也就是说之前的所有数据类型都实现了可迭代接口。

1.Iterator

在chrome浏览器的控制台进行测试:

Chrome-console控制台

console.log([]);  // 打印数组
[]
    length: 0
    __proto__: Array(0)
        ...
        Symbol(Symbol.iterator): ƒ values()  // Symbol.iterator可迭代接口
        Symbol(Symbol.unscopables): {copyWithin: true, entries: true, fill: true, find: true, findIndex: true,}
        __proto__: Object
console.log(new Set());  // 打印Set
Set(0) {}
    [[Entries]]
    size: (...)
    __proto__: Set
        add: ƒ add()
        ...
        Symbol(Symbol.iterator): ƒ values()  // Symbol.iterator可迭代接口
        Symbol(Symbol.toStringTag): "Set"
        get size: ƒ size()
        __proto__: Object
console.log(new Map());  // 打印Map
Map(0) {}
    [[Entries]]
    size: (...)
    __proto__: Map
        clear: ƒ clear()
        ...
        Symbol(Symbol.iterator): ƒ entries()  // Symbol.iterator可迭代接口
        Symbol(Symbol.toStringTag): "Map"
        get size: ƒ size()
        __proto__: Object

继续看看Symbol.iterator到底实现了什么:

Chrome-console控制台

const arr = ['foo', 'bar', 'baz']
undefined
arr[Symbol.iterator]()
Array Iterator {}
	__proto__: Array Iterator
    	next: ƒ next()
		arguments: (...)
        caller: (...)
        length: 0
        name: "next"
		...
const iterator = arr[Symbol.iterator]()
undefined
iterator.next()
{value: "foo", done: false}

value中的就是数组中的第一个元素,done为false,当再次调用时,结果为相同的结构,此时的done为false。

Chrome-console控制台

iterator.next()
{value: "bar", done: false}
iterator.next()
{value: "bar", done: true}

done属性的作用就是表示数组内部的属性是否全部遍历完成。

模拟迭代器:

// 迭代器(Iterator)

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 }

while (true) {
  const current = iterator.next()
  if (current.done) {
    break // 迭代已经结束了,没必要继续了
  }
  console.log(current.value)
}

2.实现iterator接口

// 实现可迭代接口(Iterable)

const obj = {
    // 实现了可迭代接口,Iterable,约定内部必须有用于范湖迭代器的iterator方法
    [Symbol.iterator]: function () {
        // 实现了迭代器接口,iterator其内部有用于迭代的next方法
        return {
            next: function () {
                // 迭代结果接口,iterationResult,约定对象内部必须要有value属性,来表示当前被迭代到的数据,值为任意类型,done属性用来表示迭代是否结束
                return {
                    value: 'zce',
                    done: true
                }
            }
        }
    }
}
const obj = {
    store: ['foo', 'bar', 'baz'],

    [Symbol.iterator]: function () {
        let index = 0
        const self = this

        return {
            next: function () {
                const result = {
                    value: self.store[index],
                    done: index >= self.store.length
                }
                index++
                return result
            }
        }
    }
}

3.迭代器模式

迭代器模式(特别是在 ECMAScript 这个语境下)描述了一个方案,即可以把有些结构称为“可迭代对象”(iterable),因为它们实现了正式的 Iterable 接口,而且可以通过迭代器 Iterator 消费。

// 迭代器设计模式

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

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

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

  // 提供统一遍历访问接口
  each: function (callback) {
    const all = [].concat(this.life, this.learn, this.work)
    for (const item of all) {
      callback(item)
    }
  },

  // 提供迭代器(ES2015 统一遍历访问接口)
  [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)
// }
// 实现统一遍历接口之后
todos.each(function (item) {
  console.log(item)
})

console.log('-------------------------------')

for (const item of todos) {
  console.log(item)
}

十九、生成器及生成器的应用

生成器是 ECMAScript 6 新增的一个极为灵活的结构,拥有在一个函数块内暂停和恢复代码执行的能力。这种新能力具有深远的影响,比如,使用生成器可以自定义迭代器和实现协程。其可以避免异步编程中回调嵌套过深的问题,提供更好的额异步编程解决方案。

1.基本用法

生成器的形式是一个函数,函数名称前面加一个星号(*)表示它是一个生成器。只要是可以定义函数的地方,就可以定义生成器。

// 生成器函数声明
function* generatorFn() {}

// 生成器函数表达式
let generatorFn = function* () {}
// 作为对象字面量方法的生成器函数
let foo = {
    * generatorFn() {}
}

// 作为类实例方法的生成器函数
class Foo {
    * generatorFn() {}
}

// 作为类静态方法的生成器函数
class Bar {
    static* generatorFn() {}
}

调用生成器函数会产生一个生成器对象。生成器对象一开始处于暂停执行(suspended)的状态。与迭代器相似,生成器对象也实现了 Iterator 接口,因此具有 next()方法。调用这个方法会让生成器开始或恢复执行。

function* foo() {
  console.log('zce')
  return 100
}

const result = foo()
console.log(result)  // Object [Generator] {}
console.log(result.next())  // { value: 100, done: false }

yield 关键字可以让生成器停止和开始执行,也是生成器最有用的地方。生成器函数在遇到 yield关键字之前会正常执行。遇到这个关键字后,执行会停止,函数作用域的状态会被保留。停止执行的生成器函数只能通过在生成器对象上调用 next()方法来恢复执行:

function* foo() {
    console.log('1111')
    yield 100
    console.log('2222')
    yield 200
    console.log('3333')
    yield 300
}

const generator = foo()

console.log(generator.next()) // 第一次调用,函数体开始执行,遇到第一个 yield 暂停
console.log(generator.next()) // 第二次调用,从暂停位置继续,直到遇到下一个 yield 再次暂停
console.log(generator.next()) // 。。。
console.log(generator.next()) // 第四次调用,已经没有需要执行的内容了,所以直接得到 undefined

2.实际应用

在生成器对象上显式调用 next()方法的用处并不大。其实,如果把生成器对象当成可迭代对象,那么使用起来会更方便:

function* generatorFn() { 
    yield 1; 
    yield 2; 
    yield 3; 
} 
for (const x of generatorFn()) { 
    console.log(x); 
} 
// 1 
// 2 
// 3
// Generator 应用

// 案例1:发号器

function* createIdMaker() {
    let id = 1
    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和ES2017概述

1.ES2016

新增数组实例对象的includes方法,检查数组中是否包含某个指定元素

const arr = ['foo', 1, NaN, false]

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

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

新增指数运算符

console.log(Math.pow(2, 10))
console.log(2 ** 10)  // 语言本身的运算符与加减乘除相同

2.ES2017

object对象的三个扩展方法

  • Object.values
  • Object.entries
  • Object.getOwnPropertyDescriptiors
const obj = {
    foo: 'value1',
    bar: 'value2'
}
// Object.values -------------------返回对象中所有值组成的数组----------------------------------------
console.log(Object.values(obj))

// Object.entries ------------------以数组的形式返回对象中所有的键值对----------------------------------------
console.log(Object.entries(obj))
// 可以直接使用for...of遍历
for (const [key, value] of Object.entries(obj)) {
    console.log(key, value)
}
// 将对象转换为Map类型的对象
console.log(new Map(Object.entries(obj)))

// Object.getOwnPropertyDescriptors ----------获取对象中属性的完整描述信息------------------------------

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)

const descriptors = Object.getOwnPropertyDescriptors(p1)
console.log(descriptors)
const p2 = Object.defineProperties({}, descriptors)
p2.firstName = 'zce'
console.log(p2.fullName)

字符串方法

  • String.prototype.padStart
  • String.prototype.padEnd
const books = {
    html: 5,
    css: 16,
    javascript: 128
}

for (const [name, count] of Object.entries(books)) {
    console.log(name, count)
}

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

今日分享截止到这里,明天更新:TypeScript部分。

记录:2020/11/07

  • 7
    点赞
  • 12
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 11
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

5coder

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

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

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

打赏作者

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

抵扣说明:

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

余额充值