Javascript深入理解ES6
参考文献:《深入理解ES6》
目录
第八章:迭代器(iterator)和生成器(generator)
1、什么是变量提升?
es5中,我们使用var来声明变量,但是var会存在 “变量提升”。
什么是变量提升?就是变量会被提升至当前作用域的最顶端
如上图所示,变量value被提升,但是赋值操作被留在了原地,所以其他地方的value值为undefined。
es6引入块级作用域来强化对变量声明周期的控制
2、什么是块级声明?
大白话就是在指定作用域内声明变量,其他作用域无法访问。
书中说的作用域有两种,函数内部,块中(即 { 和 } 中),但是其实有些许不准确。
{...} 并没有形成作用域,而是因为es6中 let、const的特性,让 {...}形成了作用域,如果是在{...}中使用var声明,那么它就不具备作用域的效果。
3、let 声明
先了解下它做重要的2个特性
(1)不存在变量提升
(2)同一个作用域中不能重复定义已经存在的变量
4、const 声明
先了解它最重要的几个特性
(1)不存在变量提升
(2)一旦声明必须赋值初始化,一旦赋值不可修改
(3)同一个作用域中不能重复定义已经存在的变量
值得注意的是,const声明变量赋值为基本类型后,再次修改会报错。但是如果赋值引用类型数据,可以修改其值,原因是引用类型数据存储的只是指向真正数据的地址而已,我们修改数据并不会影响这个地址的变化。
5、临时死区(Temporal Dead Zone 简称TDZ)
通常用来描述let、 const的不提升效果。Javascript 引擎在扫描代码时遇到var声明的变量,就将它提升到顶部。遇到 let 或者 const 声明,就将它放到TDZ中。
访问TDZ中的变量就会报错,只有当执行过变量声明的语句,才会从TDZ中移除,然后正常访问。
这种情况注意一下:
1、es6新增字符串方法
(1)includes() 方法,检测字符串中是否包含指定字符
(2)startsWith() 方法,检测字符串是否以指定字符开头
(3)endsWith() 方法,检测字符串是否以指定字符结尾
以上三个检测字符串的方法都支持第二个参数,表示从下表n的位置开始检索。如果直接传入正则表达式而不是字符串,会报错
(4)repeat() 方法,将一个字符串复制n遍
2、模板字面量
es6新增模板字面量创建字符串,使用方式只是将原本的单/双引号替换成符号 ` hello `
模板字面量的三种用途
(1)可以创建多行字符串
在es6之前,如果使用单、双引号,字符串一定要在同一行才行,不能直接换行。想要换行字符串,按照以下方法:
ES6简化了换行字符串的步骤,直接打换行即可
用et message = `like\nyou` 来换行也可以, \n 表示换行
(2)字符串占位符
可以在字符串中嵌套可访问作用域中的变量、运算、函数调用等
(3)模板标签
每个模板标签都可以执行模板字符串上的转换并返回最终的字符串值
标签指:第一个反撇号(`)前的字符串,如下示例的模板标签是makeStr
1、命名参数的默认值
(1)如何设置默认值
在ES6之前,给函数参数指定默认值的方式如下:
ES6简化了为形参提供默认值的过程,没有传入值则为其提供一个默认值
(2)默认参数对arguments的影响
ES6之前的非严格模式下,形参值的变化会更新到arguments中。(严格模式下,arguments的值还是最开始那个值)
在ES6中,无论是否定义严格模式,arguments都与严格模式下的行为一致
(3)可以使用先定义的参数作为后定义参数的默认值,相反先定义不能使用后定义的参数
2、无命名参数
无论函数已定义的命名函数有多少,都不限制调用时传入的实际参数数量,调用时总是可以传入任意数量的参数
(1)如何设置不定参数
在函数的命名参数前加三个点(...),表示这是一个不定参数,该参数为一个数组,包含着自它之后传入的所有参数。通过这个数组名可以逐一访问里面的参数。
注意:每个函数只能声明一个不定参数,且一定要放在所有参数的最后一个位置,否则会报错
(2)不定参数对arguments对象的影响
无论是否使用不定参数,arguments总是包含所有传入函数的参数
3、展开运算符
展开运算符可以让你指定一个数组,将它们打散后作为各自独立的参数传入函数
4、ES6为所有函数新增了name属性
函数name的值不一定引用同名变量,只作为开发调试的额外信息,所以不要使用name的值来获取对于函数的引用
5、判断函数是否作为构造函数使用(通过new 关键字)
ES5中,对于函数来说,无法知道是通过Person.call() 或者Person.apply() 还是new关键字调用得到的Person实例
ES6中引入new.target元属性(指非对象的属性),当new时,new.target被赋值为new操作符的目标。修改this指向则为undefined。
6、箭头函数
(1)语法
(2)箭头函数和普通函数的区别
- 没有this绑定,不能使用new创建实例
- 调用call、apply、bind方法不会报错,但没有任何效果
- 虽然没有this绑定,但是它的this取决于该函数外部非箭头函数的this
- 箭头函数没有arguments属性,但是它可以访问外围函数的arguments
- 箭头函数没有原型
7、尾调用优化
(1)什么是尾调用
尾调用指的是函数作为另一个函数的最后一条语句被调用。如果是递归,将带来一定消耗。
(2)ES6中,满足条件下,尾调用不会再创建新栈帧表示函数被调用,而是清除并且重用当前栈帧。
- 尾调用不访问当前栈帧的变量(函数不是一个闭包)
- 在函数内部,尾调用是最后一条语句
- 尾调用的结果作为函数值返回
无法优化的几种情况
(3)尾调用的优化是引擎帮助我们做的事情,我们要做的就是让我们写的代码可以被引擎优化。递归是尾调用优化最显著的场景,我们码代码时也要要想想尾调用优化的特性。
1、ES6中对象属性和方法的简写
(1)当对象的属性名与变量名相同时,可以只写属性名
(2)可以直接在对象中定义可变属性名
(3)对象中可以直接写方法,而不用再写function
// es6中 let myName = '良人非良' let hello = '小黄鸭' let obj = { myName, [hello]: 'world', sayHi () { console.log(this[hello]) // world }, } obj.sayHi() console.log(obj[hello]) // world // es6之前 var myName = '良人非良' var hello = '小黄鸭' var obj = { myName: myName, // 必须属性名和属性都写 sayHi: function () { // 必须写属性名,并且定义完整函数 console.log(this[hello]) // world }, } obj[hello] = 'world' // 给对象添加一个可计算的属性(属性为变量) console.log(obj[hello]) // world obj.sayHi()
2、ES6中对象中新加的方法
(1)Object.is() 弥补全等运算的不准确 比如+0和-0是不同的实例,但是+0 === -0 的值为true
选择那种写法根据自己的代码来
console.log(+0 === -0) // true console.log(Object.is(+0, -0)) // false console.log(5 === '5') // false console.log(Object.is(5, '5')) // false console.log(NaN === NaN) // false console.log(Object.is(NaN, NaN)) // true
(2)Object.assign() 合并多个对象的属性,重复属性根据 ’后面覆盖前面‘ 原则,最终返回一个对象,同时第一个对象会跟着改变
let obj1 = { name: '我是老大', age: 18, } let obj2 = { name: '我是老二', hobby: 'eat', } let obj3 = { name: '我是老三', sex: '男', } let resObj = Object.assign(obj1, obj2, obj3) console.log(resObj) // { name: '我是老三', age: 18, hobby: 'eat', sex: '男' } console.log(obj1) // { name: '我是老三', age: 18, hobby: 'eat', sex: '男' }
3、ES6中对象的自由属性被枚举时的返回顺序按照以下规则来排序
(1)属性名为数字键时按升序排序(优先于字符串)
(2)属性名为字符串时按照加入对象时的顺序排序(优先于Symbol键)
(3)所有symbol键按照它们被加入对象的顺序排序
let obj = { b: 1, 9: 1, 0: 1, 3: 1, a: 1, } obj.d = 1 console.log(Object.getOwnPropertyNames(obj)) // [ '0', '3', '9', 'b', 'a', 'd' ]
4、ES6中增强了对象原型
(1)添加了Object.setPrototypeOf(p1,p2)方法,在对象被创建后修改它的原型。接收两个参数
需要改变原型的对象p1
代替p1原型的p2
注意:只有函数有prototype属性,这里说的对象原型,是隐式原型__proto__
let person = { sayHi () { console.log('我是人') } } let cat = { sayHi () { console.log('我是小猫猫') } } let boy = Object.create(person) boy.sayHi() // 我是人 console.log(Object.getPrototypeOf(boy) === person) // true Object.setPrototypeOf(boy, cat) boy.sayHi() // 我是小猫猫 console.log(Object.getPrototypeOf(boy) === cat) // true // Object.getPrototypeOf(obj)为es5中返回指定对象原型的方法 console.log(boy.__proto__ === Object.getPrototypeOf(boy)) // true
(2)引入了关键字 super,指向当前对象的原型
注意:super始终固定,不会因为多重继承而改变
super只能在简写方法的对象中使用(对象中可以直接写方法,而不用再写function),否则报错
let person = { sayHi () { console.log('我是人') } } let boy = { sayHi () { // true super => Object.getPrototypeOf(this) console.log(super.sayHi === Object.getPrototypeOf(this).sayHi) return Object.getPrototypeOf(this).sayHi.call(this) } } Object.setPrototypeOf(boy, person) boy.sayHi() // 我是人
5、给了函数和方法一个正式的定义
(1)定义在对象中的是方法
(2)通过function定义的是函数
1、ES6中的对象解构
(1)对象的解构
语法糖,简化了操作,将要的属性从对象中提取出来。
注意:let { name, age } = obj 中的name,age要和obj中的属性名一致才可以提取成功,否则为undefined
// ES6中获取对象中的属性 let obj = { name: '良人非良', age: 18, hobby: 'eat' } let { name, age, hobb } = obj console.log(name) // 良人非良 console.log(age) // 18 console.log(hobb) // undefined // ES6之前 let obj = { name: '良人非良', age: 18 } let name = obj.name let age = obj.age console.log(name) // 良人非良 console.log(age) // 18
(2)解构中的默认值
可以为解构的值赋默认值,当为undefined时取默认值
注意:值为null时,取的是null值
let obj = { name: '良人非良', age: 18, hobby: 'eat', } let { name, a = 1 } = obj console.log(name) // 良人非良 console.log(a) // 1 obj中并没有属性a,结构出来是undefined,但是我们设置了默认值1
(3)为非同名局部变量赋值
可以理解为给我们解构出来的属性改个名字
let obj = { name: '良人非良', age: 18, hobby: 'eat', a: null, } // 原本的属性名是name let { name } = obj console.log(name) // 良人非良 // 改了新名字,叫newName let { name: newName } = obj console.log(newName) // 良人非良 console.log(name) // 再去获取name,直接报错name is not defined
(4)嵌套对象的解构
let obj = { name: '良人非良', age: 18, hobby: 'eat', boyfriend: { name: '大肥猪', age: 8, } } let { boyfriend: { name, age } } = obj console.log(name) // 大肥猪 console.log(age) // 8
2、数组的解构
(1)解构
注意:数组的解构与变量名字无关,只与变量的下标相关
// 解构出第一个第二个的值赋值给自定义的变量 let arr = [1, 2, 3, 4] let [first, second] = arr console.log(first) // 1 console.log(second) // 2 // 如果只想要指定位置的数据,可以将前面的数据留空 let [, , , four] = arr console.log(four) // 4 // 不定元素的解构:将数组剩余的部分解构赋值给numArr // PS: ...numArr 必须在数组的最后,否则报错 let [num1, ...numArr] = arr console.log(num1) // 1 console.log(numArr) // 234
(2)赋值
// 替换值 let arr = [1, 2, 3] let first = 'hello' let second = 'world'; // 上一行代码记得加上分号,否则会将[]解析成上一行中的内容 [first, second] = arr console.log(first) // 1 console.log(second) // 2 // 交换值 let a = 1 let b = 2; // 上一行代码记得加上分号,否则会将[]解析成上一行中的内容 [ a, b ] = [ b, a ] console.log(a) // 2 console.log(b) // 1
(3)解构中的默认值
注意:值为null时,取的是null值
let arr = [1, undefined, 2, 3] let [first, second = 'hello', , four] = arr console.log(first) // 1 console.log(second) // hello console.log(four) // 3
1、在es6之前的简单类型(原始类型)和为什么使用Symbol?
(1)原始类型:String、Number、Boolean、Null、undefined。
2、创建Symbol
let symName = Symbol() let obj = {} obj[symName] = '良人非良' console.log(symName) // Symbol() console.log(obj[symName]) // 良人非良 console.log(symName in obj) // true console.log(typeof symName) // symbol 使用typeOf判断symbol类型
Symbol()接收一个可选参数,通常用来为这个symbol添加文本描述,便于阅读开发
let symName = Symbol('我是symbol类型,我表示名字') console.log(symName) // Symbol('我是symbol类型,我表示名字')
3、Symbol的使用方法
(1)symbol注册表,使用Symbol.for()方法定义的symbol,会被存在注册表中,当下一次创建symbol,会去表中查找是否注册过相同的symbol,有则取出,无则创建并在表中注册
symbol.for()接收一个参数,标识创建这个symbol的标识字符串(简单理解成就是这个symbol的id值,每次根据这个id值来查找)
let uid = Symbol.for('uid') let uid2 = Symbol.for('uid') console.log(uid === uid2) // true let obj = {} obj[uid2] = '良人非良' console.log(obj[uid]) // 良人非良 console.log(obj[uid2]) // 良人非良
Symbol.keyFor():在注册表中检索和symbol相关的键(就是上面注册时用的身份证id)
let uid = Symbol.for('uid') console.log(Symbol.keyFor(uid)) // uid let uid2 = Symbol.for('uid') console.log(Symbol.keyFor(uid2)) // uid let uid3 = Symbol('uid') console.log(Symbol.keyFor(uid3)) // undefined
(2)Symbol不支持强制转换成String和Number,直接使用运算符报错
let sm = Symbol.for('1') console.log(sm + 'world') // Cannot convert a Symbol value to a string console.log(sm / 1) // Cannot convert a Symbol value to a number
(3)检索Symbol属性
在ES5中,Object.kes() 返回一个包含所有可枚举属性的数组
Object.getOwnPropertyNames() 返回一个包含所有可枚举或不可枚举属性的数组
它们无法识别并检索出Symbol值
let sym = Symbol()
let obj = { a: 1, b: 2,
[sym]: 3 } console.log(Object.keys(obj.__proto__)) // [] console.log(Object.getOwnPropertyNames(obj.__proto__)) // [ // 'constructor', // '__defineGetter__', // '__defineSetter__', // 'hasOwnProperty', // '__lookupGetter__', // '__lookupSetter__', // 'isPrototypeOf', // 'propertyIsEnumerable', // 'toString', // 'valueOf', // '__proto__', // 'toLocaleString' // ]
ES6提供Object.getOwnPropertySymbols()方法用于获取symbol属性的数组,注意:只返回symbol类型
let sym = Symbol() let obj = { a: 1, b: 2, [sym]: 3, } console.log(Object.getOwnPropertySymbols(obj)) // [ Symbol() ]
4、通过well-known Symbol暴露内部操作
1、Set集合
(1)Set集合是一种无重复的有序列表,无法通过索引访问集合中的元素
(2)常用操作
- set.add() 新增一个元素
- set.has() 判断集合中是否存在某个元素
- set.delete() 移除集合某个元素
- set.size 判断集合长度
- set.clear() 移除所有元素
let set = new Set() let o1 = {} let o2 = {} set.add(5) set.add('5') set.add(o1) set.add(o2) console.log(set) // Set { 5, '5', {}, {} } console.log(set.size) // 4 console.log(set.has('5')) // true console.log(set.has(o1)) // true console.log(set.has({})) // false console.log(set.has('xxx')) // false console.log(set.delete(5)) // true
注意:在对象中,属性只能是字符串,所以不管属性是函数、数字,最后都会被解析成对应的字符串
- 遍历Set:forEach()方法
set.forEach((val, index, set) => { console.log(val, index, set) }) // 因为set没有索引,所以第一个值和第二个值相同 // 11 11 Set { 11, 22, 33 } // 22 22 Set { 11, 22, 33 } // 33 33 Set { 11, 22, 33 }
- 将Set转为数组
因为set没有索引,所以需要将set转为数组才可以通过下标访问
let set = new Set([1,2,3]) let arr = [...set] console.log(arr) // [ 1, 2, 3 ]
(3)Weak Set
Set类型被看作是强引用的类型,将对象存储在Set的实例中和存储在变量中一样,只要Set实例的引用存在,垃圾回收器就不能释放该对象的内存空间。所以ES6中引入Weak Set 只存储对象的弱引用。
- Weak Set 只能存储对象,不可以存储原始值
- 集合中的弱引用如果是对象唯一的引用,会被回收释放内存
- Weak Set 不支持迭代,所以for...of、foreach等迭代器不支持
- 不支持size属性
- 不支持clear方法
- has() delete()方法传入一个非对象,返回false
let wset = new WeakSet() let obj = {} wset.add(obj) console.log(wset.has(obj)) // true console.log(wset.has('hello')) // false console.log(wset.delete('hello')) // false obj = null // 移除对象obj的强引用,weak Set中的引用也自动移除 console.log(wset) // false
2、Map集合
(1)存储键值对的有序列表,键和值支持所有类型的数据
(2)常用的方法
- map.set() 往map中添加数据
- map.get() 获取数据
- map.has() 判断指定键名在map中是否存在
- map.delete() 移除指定键名和对应的值
- map.clear() 移除所有的键和值
- map.size() 获取map元素的个数
let map = new Map() map.set('name', '良人非良') map.set('age', 18) map.set('hobby', 'eating') console.log(map.get('name')) // 良人非良 console.log(map.get(1)) // undefined console.log(map.has('hobby')) // true console.log(map.has(2)) // false console.log(map.delete('age')) // true map.clear() console.log(map.size) // 0
简写:可以向map传入一组数组初始化集合
let map = new Map([['name', '良人非良'], ['age', 18]]) // 等同于
let map = new Map() map.set('name', '良人非良') map.set('age', 18)
(3)Weak Map
- 无需列表
- 键名必须是对象,值可以为任意类型
- 如果弱引用之外不存在其他强引用,垃圾回收器会自动回收这个引用
- 不支持clear方法,不支持size属性
第八章:迭代器(iterator)和生成器(generator)
持续更新ing...