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