let/const
var/函数
我们都知道var和函数存在变量提升的问题,它的声明会被提升到作用域的顶端。
if(true) {var value = 1}console.log(value) //1//代码相当于var valueif(true) {value = 1}console.log(value) //1
为了加强对变量生命周期的控制,ES 2015引入了块级作用域,let 和const。
块级作用域在于:
- 函数内部
- 块中的()字符和{}之间的区域
临时死区(Temporal Dead Zone)
简称TDZ,因为 JavaScript 引擎在扫描代码发现变量声明时,要么将它们提升到作用域顶部(遇到 var 声明),要么将声明放在 TDZ 中(遇到 let 和 const 声明)。访问 TDZ 中的变量会触发运行时错误。只有执行过变量声明语句后,变量才会从 TDZ 中移出,然后方可访问。
所以其实let 和const 也是有提升只是存放到了暂时性死区里,在没有变量声明之前访问报错。console.log(typeof value); // Uncaught ReferenceError: value is not definedlet value = 1;
看一个老生常谈的问题 for循环
相当于在 for(let i=0; i<3; i++) 有一个隐藏的作用域。for(let i=0; i<3; i++) {let i = 'foo'console.log(i)}// 拆分就是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)}// 所以说for是双层作用域// for(let ...)这是一层作用域// 里面又是内层作用域
const
再来说下const,const 用于声明常量,其值一旦被设定不能再被修改,否则会报错。不允许声明之后修改内存地址 就是说,不允许修改绑定,但允许修改值这就意味着当const声明对象时const obj = {}obj.name = 'tom'//完全没有问题
建议
在我们开发中主用const,辅助用let,不用var。
变量的解构赋值
从数组和对象中提取值,对变量进行赋值,这被称为解构(Destructuring)。
数组的结构赋值
原来我们数组赋值必须指定值。
const arr = [1,2,3]const f = arr[0]const b = arr[1]const c = arr[2]
ES2015 允许这样写
const arr = [1,2,3]const [f, b, c] = arrconsole.log(f,b,c)//1 2 3
上面代码表示,可以从数组中提取值,按照对应位置,对变量赋值。写法属于“模式匹配”,只要等号两边的模式相同,左边的变量就会被赋予对应的值。
解构不成功
let [f] = [];let [f, b] = [1];
如果解构不成功,变量的值就等于undefined。以上两种情况都属于解构不成功,f/b的值都会等于undefined.
不完全解构
另一种情况是不完全解构,即等号左边的模式,只匹配一部分的等号右边的数组。
const arr = [1,2,3]const [f, b] = arrconsole.log(f,b)//1 2
默认值
解构赋值允许指定默认值。
const arr = [1,2,3]const [f, b, c, d='more'] = arrconsole.log(f,b,c,d)
这种数组的解构赋值很大程度上帮助缩短我们写代码逻辑,比如分割字符串获取字段
const path = 'foo/part/ccc'const tmp = path.split('/')const root = tmp[1]console.log(root)//之后const [,root] = path.split('/')console.log(root)
对象的解构赋值
解构不仅可以用于数组,还可以用于对象,
let { foo, bar } = { foo: 'aaa', bar: 'bbb' };foo // "aaa"bar // "bbb"
对象的解构与数组有一个重要的不同。数组的元素是按次序排列的,变量的取值由它的位置决定;而对象的属性没有次序,变量必须与属性同名,才能取到正确的值。
let { bar, foo } = { foo: 'aaa', bar: 'bbb' };foo // "aaa"bar // "bbb"let { baz } = { foo: 'aaa', bar: 'bbb' };baz // undefined
上面代码的第一个例子,等号左边的两个变量的次序,与等号右边两个同名属性的次序不一致,但是对取值完全没有影响。第二个例子的变量没有对应的同名属性,导致取不到值,最后等于undefined。相当于解构失败,变量的值等于undefined。
其实,对象的解构赋值是下面形式的简写。
let { foo: foo, bar: bar } = { foo: 'aaa', bar: 'bbb' };
也就是说,对象的解构赋值的内部机制,是先找到同名属性,然后再赋给对应的变量。真正被赋值的是后者,而不是前者。
let { foo: baz } = { foo: 'aaa', bar: 'bbb' };baz // "aaa"foo // error: foo is not defined
上面代码中,foo是匹配的模式,baz才是变量。真正被赋值的是变量baz,而不是模式foo。
等以后深入的使用过程中,可以更加理解解构赋值,看个例子
let {log} = consolelog('foo')log('foo1')log('foo3')//foo//foo1//foo3//大大简化了代码的编写 整体体积也减少了很多相当于:const {log:lo} = console //log:相当于模式 后面的lo才是变量lo('fooo')lo('foo1')lo('foo3')//foo//foo1//foo3
对象的解构也可以指定默认值,默认值生效的条件是,对象的属性值严格等于undefined。
var {x = 3} = {};x // 3var {x, y = 5} = {x: 1};x // 1y // 5
字符串的解构赋值
字符串也可以解构赋值。这是因为此时,字符串被转换成了一个类似数组的对象。
const [a, b, c, d, e] = 'hello';a // "h"b // "e"c // "l"d // "l"e // "o"
类似数组的对象都有一个length属性,因此还可以对这个属性解构赋值。
let {length : len} = 'hello';len // 5
函数参数的解构赋值
函数的参数也可以使用解构赋值。
function add([x, y]) {console.log(x+y)}add([1,2])//3
另一个例子
function move ({x=0, y=0}) {return [x, y]}move({x:3, y:8})
函数move的参数是一个对象,通过对这个对象进行解构,得到变量x和y的值。如果解构失败,x和y等于默认值。
一些用途
交换变量的值
let a = 1let y = 2[x, y] = [y, x]
从函数返回多个值
函数只能返回一个值,如果要返回多个值,只能将它们放在数组或对象里返回。有了解构赋值,取出这些值就非常方便。
/ 返回一个数组function example() {return [1, 2, 3];}let [a, b, c] = example();// 返回一个对象function example() {return {foo: 1,bar: 2};}let { foo, bar } = example();
- 提取 JSON 数据 ```javascript let jsonData = { id: 42, status: “OK”, data: [867, 5309] };
let { id, status, data: number } = jsonData;
console.log(id, status, number); // 42, “OK”, [867, 5309]
上面代码可以快速提取 JSON 数据的值。<br />4.输入模块的指定方法```javascriptconst { SourceMapConsumer, SourceNode } = require("source-map");
模板字符串
定义
模板字符串(template string)是增强版的字符串,用反引号(`)标识。它可以当作普通字符串使用,也可以用来定义多行字符串,或者在字符串中嵌入变量。
基本使用
// 普通字符串`In JavaScript '\n' is a line-feed.`// 多行字符串`In JavaScript this isnot legal.`console.log(`string text line 1string text line 2`);// 字符串中嵌入变量let name = "Bob", time = "today";`Hello ${name}, how are you ${time}?`
大括号内部可以放入任意的 JavaScript 表达式,可以进行运算,以及引用对象属性。
const {log} = consoleconst name = 'tom'const msg = `hey,${name}---${Math.random()}------${1+2}`log(msg)//hey,tom---0.8797044709433715------3
模板字符串之中还能调用函数。
function fn() {return "Hello World";}`foo ${fn()} bar`// foo Hello World bar
标签模板
它可以紧跟在一个函数名后面,该函数将被调用来处理这个模板字符串。这被称为“标签模板”功能(tagged template),就是对字符串进行加工。
const {log} = consoleconst str = myTagFunc`hey,${name}is a ${gender}.`function myTagFunc(string, name, gender) {const sex = gender? 'man' : 'woman';return string[0]+ name + string[1] + sex + string[2]}log(str)//hey,tom is a man.
字符串的扩展方法
传统上,JavaScript 只有indexOf方法,可以用来确定一个字符串是否包含在另一个字符串中。ES6 又提供了三种新方法。
- includes():返回布尔值,表示是否找到了参数字符串。
- startsWith():返回布尔值,表示参数字符串是否在原字符串的头部。
endsWith():返回布尔值,表示参数字符串是否在原字符串的尾部。
//字符串的查找方法const message = 'Error:foo is not defined.'const {log} = consolelog(message.startsWith('Error'), //是否是以 Error开头message.endsWith('.'), //是否是以 .结尾message.includes('foo') //字符串中间是否包含 foo)
repeat()方法,返回一个字符串,表示将原字符串重复n次
'x'.repeat(5)//xxxxx
padStart(),padEnd()
如果某个字符串不够指定长度,会在头部或尾部补全。padStart()用于头部补全,padEnd()用于尾部补全。
'x'.padStart(5, 'ab') // 'ababx''x'.padStart(4, 'ab') // 'abax''x'.padEnd(5, 'ab') // 'xabab''x'.padEnd(4, 'ab') // 'xaba'
padStart()和padEnd()一共接受两个参数,第一个参数是字符串补全生效的最大长度,第二个参数是用来补全的字符串。
- trimStart(),trimEnd()
它们的行为与trim()一致,trimStart()消除字符串头部的空格,trimEnd()消除尾部的空格。它们返回的都是新字符串,不会修改原始字符串。
- replaceAll()
字符串的实例方法replace()只能替换第一个匹配
'aabbcc'.replace('b', '_')// 'aa_bcc
上面例子中,replace()只将第一个b替换成了下划线。
如果要替换所有的匹配,不得不使用正则表达式的g修饰符。
'aabbcc'.replace(/b/g, '_')// 'aa__cc'
正则表达式毕竟不是那么方便和直观, 引入了replaceAll()方法,可以一次性替换所有匹配。
'aabbcc'.replaceAll('b', '_')// 'aa__cc'
剩余参数/展开数组
剩余参数
…只可出现在最后并且只有一个
const {log} = console//...只可出现在最后并且只有一个function foo(first, ...args) {// log(Array.from(arguments))log(args)}foo(1,2,3,4)
展开数组
//...展开数组const arr = ['foo', 'bar', 'baz']console.log.apply(console, arr)// 同console.log(...arr)
箭头函数
基本用法
让函数的书写变得很简洁,可读性很好。
我们看一个简单的例子:
function inc (number) {return number + 1}const inc = (n,m)=> {return n + 1}
还有循环中能简化很多代码
const arr = [1,2,3,4,5]const newArr = arr.filter(function(i) {return i % 2})const newArr = arr.filter(i => i % 2 )//[ 1, 3, 5 ]
我们注意一点 如果箭头函数的圆括号后面你省略了{},是默认return 内容得。加上{} 你就得写上return关键词。
箭头函数与this
前言this
在“use strict”严格模式下,没有直接的挂载者(或称调用者)的函数中this是指向window,这是约定俗成的。在“use strict”严格模式下,没有直接的挂载者的话,this默认为undefined。下面讨论的例子都是在非严格模式下。
this具有运行期绑定的特性,是基于函数的执行环境绑定的,在全局函数中,this指向的是window,当函数被作为某个对象调用时,this就等于那个对象。
箭头函数的this
箭头函数的this定义:箭头函数的this是在定义函数时绑定的,不是在执行过程中绑定的。简单的说,函数在定义时,this就继承了定义函数的对象。
例子:
var name = '333'var person = {name: 'tom',sayHi:function() {return function() {return console.log(this.name)}}}person.sayHi()()//333//--------------------------------------var name = '333'var person = {name: 'tom',sayHi:function() {setTimeout(function() {console.log(this.name)},1000)}}person.sayHi()//333
原因是,匿名函数的执行环境是全局的。this只在函数内部起作用。此时的this.name在匿名函数中找不到,所以就从全局中找,找到后打印出来。
原来我们常常通过闭包的特性缓存上下文this
// 箭头函数 与 thisvar name = '333'var person = {name: 'tom',syaHIAsync:function() {const _this = this //通过闭包的特性setTimeout(function() {console.log(_this.name)},1000)}}person.syaHIAsync()//tom
现在可以用箭头函数
// 箭头函数 与 thisvar name = '333'var person = {name: 'tom',syaHIAsync:function() {setTimeout(()=> {console.log(this.name)},1000)}}person.syaHIAsync()//tom
箭头函数的this始终和它最近的外层相同的指向,这样就是箭头函数中的this。
对象字面量
ES6 新推出的新简写法,用来初始化对象并向对象添加方法。
我们经常这样写
// 年货糖果按斤计费let type = 'candy';let weight = '5';let price = '8';const goods = {type: type,weight: weight,price: price}
键值对出现了重复,ES2015 中,如果属性名和和所分配的变量名一样,就可以从对象属性中删掉这些重复的变量名称。
let type = 'candy';let weight = '5';let price = '8';const goods = {type,weight,price, // 如果属性名和字面量一致的话 可以省略total: function(){// ...}}
计算属性名
在之前ES5中,如果属性名是个变量或者需要动态计算,则只能通过 对象.[变量名] 的方式去访问。
而在字面量中是无法使用的。
const p = {name : '李四',age : 20}const attName = 'name';console.log(p[attName]) //这里 attName表示的是一个变量名。//p[attName] === p.name//-----------------------------------------------const p = {attName : '李四', // 这里的attName是属性名,相当于各级p定义了属性名叫 attName的属性。age : 20}console.log(p[attName]) // undefined
在ES2015中,计算属性名 [] 里面可以使用任意的表达式 作为这个属性的属性名称。
上述例子中就可以写为:
const p = {[attName] : '李四', // 引用了变量attName。相当于添加了一个属性名为name的属性age : 20}console.log(p[attName]) // 李四
计算属性名 [] 里面可以使用任意的表达式
// 对象字面量const bar = '123'const obj = {foo: 12,// bar: bar, //1bar, //2}// 如果属性名和字面量一致的话 可以省略 如同 1 2 行一样//计算属性名 [] 里面可以使用任意的表达式 作为这个属性的属性名称obj[Math.random()] = 124obj[1+2] = 3console.log(obj)//{ '3': 3, foo: 12, bar: '123', '0.40770536872805674': 124 }
对象新增方法
Object.assign()
基本用法:
Object.assign()方法用于对象的合并,将源对象(source)的所有可枚举属性,复制到目标对象(target)。
第一个参数是目标参数,把剩下的参数都合并到目标参数里,可以接收多个参数 同名会被覆盖。
//object.assign 方法const source1 = {a:function() {return true},b:5,c:4}const source2 = {d:10,e:11}const target = {a: function() {return false},f: 222}const result = Object.assign(target, source1, source2)console.log(target.a()) //后面的属性会覆盖前面的属性console.log(result === target)//true//true
注意:非对象参数出现在源对象的位置(即非首参数),首先,这些参数都会转成对象,如果无法转成对象,就会跳过。这意味着,如果undefined和null不在首参数,就不会报错。在首参的话就会报错。
浅拷贝
Object.assign()方法实行的是浅拷贝,而不是深拷贝。也就是说,如果源对象某个属性的值是对象,那么目标对象拷贝得到的是这个对象的引用。
const obj1 = {a: {b: 1}};const obj2 = Object.assign({}, obj1);obj1.a.b = 2;obj2.a.b // 2
Object.is()
ES5 比较两个值是否相等,只有两个运算符:相等运算符(==)和严格相等运算符(===)。它们都有缺点,前者会自动转换数据类型,后者的NaN不等于自身,以及+0等于-0。
Object.is就是部署这个算法的新方法。它用来比较两个值是否严格相等,与严格比较运算符(===)的行为基本一致,不同之处只有两个:一是+0不等于-0,二是NaN等于自身。
+0 === -0 //trueNaN === NaN // falseObject.is(+0, -0) // falseObject.is(NaN, NaN) // true
Object.keys()
Object.values()
Object.entries()
Object.fromEntries()
proxy
前言:ES2015之前我们要监视对象属性的读写可以用Object.defineProperty(),但是有一些局限性,而ES2015提供了更好用的更为强大的方法proxy。下面我们来对比下。
1.object.definedProperty 只能监视到属性的读写,proxy 能监听到很多对象的操作。拿对象的删除举例
const person = {name: 'tom',age: 12}const personProxy = new Proxy (person, {deleteProperty(target, property) { //目标对象、属性名console.log(`delete: ${property}`)delete target[property]}})delete personProxy.ageconsole.log(person)// delete: age//{ name: 'tom' }
get方法用于拦截某个属性的读取操作,可以接受三个参数,依次为目标对象、属性名和 proxy 实例本身(严格地说,是操作行为所针对的对象),其中最后一个参数可选。
2.object.definedProperty 是重写数组方法 像push等,而proxy是拦截对数组的操作,对数组的监视
const arr = []const arrProxy = new Proxy(arr, {set(target, property, value) { //目标对象、属性名、属性值console.log('set', property, value)target[property] = valuereturn true}})arrProxy.push(100)
set方法用来拦截某个属性的赋值操作,可以接受四个参数,依次为目标对象、属性名、属性值和 Proxy 实例本身,其中最后一个参数可选。
Proxy的拦截操作
下面是 Proxy 支持的拦截操作一览,一共 13 种。
get(target, propKey, receiver):拦截对象属性的读取,比如proxy.foo和proxy[‘foo’]。
set(target, propKey, value, receiver):拦截对象属性的设置,比如proxy.foo = v或proxy[‘foo’] = v,返回一个布尔值。
has(target, propKey):拦截propKey in proxy的操作,返回一个布尔值。
deleteProperty(target, propKey):拦截delete proxy[propKey]的操作,返回一个布尔值。
ownKeys(target):拦截Object.getOwnPropertyNames(proxy)、Object.getOwnPropertySymbols(proxy)、Object.keys(proxy)、for…in循环,返回一个数组。该方法返回目标对象所有自身的属性的属性名,而Object.keys()的返回结果仅包括目标对象自身的可遍历属性。
getOwnPropertyDescriptor(target, propKey):拦截Object.getOwnPropertyDescriptor(proxy, propKey),返回属性的描述对象。
defineProperty(target, propKey, propDesc):拦截Object.defineProperty(proxy, propKey, propDesc)、Object.defineProperties(proxy, propDescs),返回一个布尔值。
preventExtensions(target):拦截Object.preventExtensions(proxy),返回一个布尔值。
getPrototypeOf(target):拦截Object.getPrototypeOf(proxy),返回一个对象。
isExtensible(target):拦截Object.isExtensible(proxy),返回一个布尔值。
setPrototypeOf(target, proto):拦截Object.setPrototypeOf(proxy, proto),返回一个布尔值。如果目标对象是函数,那么还有两种额外操作可以拦截。
apply(target, object, args):拦截 Proxy 实例作为函数调用的操作,比如proxy(…args)、proxy.call(object, …args)、proxy.apply(…)。
construct(target, args):拦截 Proxy 实例作为构造函数调用的操作,比如new proxy(…args)。
小总结
还是在工作中多用才能记住,相比较于Object.definedProperty(),proxy是以非侵入的方式监管了对对象的读写。
实例
get应用实例
const person = {name: 'tom'}const personProxy = new Proxy(person, {get:function(target, propKey) {if(propKey in target) {return target[propKey]}else {throw new ReferenceError("Prop name \"" + propKey + "\" does not exist.");}}})proxy.name // "tom"proxy.age // 抛出一个错误
set 应用实例
假定Person对象有一个age属性,该属性应该是一个不大于 200 的整数,那么可以使用Proxy保证age的属性值符合要求。
let validator = {set: function(obj, prop, value) {if (prop === 'age') {if (!Number.isInteger(value)) {throw new TypeError('The age is not an integer');}if (value > 200) {throw new RangeError('The age seems invalid');}}// 对于满足条件的 age 属性以及其他属性,直接保存obj[prop] = value;}};let person = new Proxy({}, validator);person.age = 100;person.age // 100person.age = 'young' // 报错person.age = 300 // 报错
其他的不再一一举例了。
Class
基本用法
JavaScript 语言中,生成实例对象的传统方法是通过构造函数。下面是一个例子。
//旧的定义原型对象function Person(name) {this.name = name}Person.prototype.say = function() {console.log(`hi, my name is ${this.name}`)}const person1 = new Person('tom')person1.say()
在ES6 es2015中引入了 Class(类)这个概念,作为对象的模板 ,通过class关键字,可以定义类。
上面代码改写
class Person {// 构造函数constructor(name) {this.name = name}say() {console.log(`hi, my name is ${this.name}`)}}const person1 = new Person('tom')person1.say()
可以看到里面有一个constructor方法,这就是构造方法,而this关键字则代表实例对象。也就是说,ES5 的构造函数Person,对应 ES6 的Person类的构造方法。
class的继承
class 可以通过extends关键字实现继承,这比es5的通过修改原型链实现继承,要清晰方便。下面我们来看个例子:
// extends 继承class Person {// 构造函数constructor(name) {this.name = name}say() { //实例方法console.log(`hi, my name is ${this.name}`)}static create(name) { //静态方法 注意静态方法是通过构造函数直接调用的this指向没有改变return new Person(name)}}class Student extends Person {constructor(name, number) {super(name) //super永远指向父类,调用父类的constructor(name)this.number = number}hello() {super.say()console.log(`my school number is ${this.number}`)}}const student1 = new Student('rose', '111')student1.hello()//hi, my name is rose//my school number is 111
上面代码中,constructor方法和hello方法之中,都出现了super关键字,它在这里表示父类的构造函数,用来新建父类的this对象。
- 子类必须在constructor方法中调用super方法,否则新建实例时会报错。这是因为子类自己的this对象,必须先通过父类的构造函数完成塑造,得到与父类同样的实例属性和方法,如果不调用super方法,子类就得不到this对象
- 在子类的构造函数中,只有调用super之后,才可以使用this关键字,否则会报错。
- 父类的静态方法,也会被子类继承。 ```javascript class A { static hello() { console.log(‘hello world’); } }
class B extends A { }
B.hello() // hello world
<a name="cHhDt"></a>### Reflect<a name="eCJTy"></a>#### 概述Reflect对象与Proxy对象一样,也是 ES6 为了操作对象而提供的新 API。Reflect对象的设计目的有这样几个。1. **将Object对象的一些明显属于语言内部的方法(比如Object.defineProperty),放到Reflect对象上**。现阶段,某些方法同时在Object和Reflect对象上部署,未来的新方法将只部署在Reflect对象上。也就是说,从Reflect对象上可以拿到语言内部的方法。2. **修改某些Object方法的返回结果,让其变得更合理**。比如,Object.defineProperty(obj, name, desc)在无法定义属性时,会抛出一个错误,而Reflect.defineProperty(obj, name, desc)则会返回false。```javascript// 老写法try {Object.defineProperty(target, property, attributes);// success} catch (e) {// failure}// 新写法if (Reflect.defineProperty(target, property, attributes)) {// success} else {// failure}
- 让Object操作都变成函数行为。某些Object操作是命令式,比如name in obj和delete obj[name],而Reflect.has(obj, name)和Reflect.deleteProperty(obj, name)让它们变成了函数行为。 ```javascript // 老写法 ‘assign’ in Object // true
// 新写法 Reflect.has(Object, ‘assign’) // true
// 老写法 const person = { name : ‘tom’ } delete person.name // 新写法 reflect.deleteProperty(person, name) //—————————————————————————— const {log} = console const obj = { name: ‘tom’, age: 12 } const reflect = Reflect.get(obj, ‘name’) const reflect2 = Reflect.deleteProperty(obj, ‘age’) const reflect3 = Reflect.has(obj, ‘name’) log(reflect,reflect2,reflect3)
4. **Reflect对象的方法与Proxy对象的方法一一对应**,只要是Proxy对象的方法,就能在Reflect对象上找到对应的方法。这就让Proxy对象可以方便地调用对应的Reflect方法,完成默认行为,作为修改行为的基础。也就是说,不管Proxy怎么修改默认行为,你总可以在Reflect上获取默认行为。```javascriptProxy(target, {set:function(target, propKey, value) {var success = Reflect.set(target, propKey, value);if(success) {console.log('property ' + name + ' on ' + target + ' set to ' + value);}else {return success;}}})
上面代码中,Proxy方法拦截target对象的属性赋值行为。它采用Reflect.set方法将值赋值给对象的属性,确保完成原有的行为,然后再部署额外的功能。
下面是另一个例子
var loggedObj = new Proxy(obj, {get(target, name) {console.log('get', target, name);return Reflect.get(target, name);},deleteProperty(target, name) {console.log('delete' + name);return Reflect.deleteProperty(target, name);},has(target, name) {console.log('has' + name);return Reflect.has(target, name);}});
上面代码中,每一个Proxy对象的拦截操作(get、delete、has),内部都调用对应的Reflect方法,保证原生行为能够正常执行。添加的工作,就是将每一个操作输出一行日志。
Set/Map数据结构
set
基本用法ES2015 提供了新的数据结构 Set。它类似于数组,但是成员的值都是唯一的,没有重复的值。
Set本身是一个构造函数,用来生成 Set 数据结构。
const s = new Set();[2, 3, 5, 4, 5, 2, 2].forEach(x => s.add(x));// Set(4) {2, 3, 5, 4}for (let i of s) {console.log(i);}// 2 3 5 4
Set函数可以接受一个数组(或者具有 iterable 接口的其他数据结构)作为参数,用来初始化。
add 会返回集合方法本身 所以可以链式调用, 添加了存在的值 就会被忽略。
const s = new Set()s.add(1).add(2).add(3).add(1)//Set { 1, 2, 3 }
用来去重数组
const arr = [1,2,3,4,5,5,3]const newArr = [...new Set(arr)]console.log(newArr)
去重字符串
[...new Set('abckdeedsww')].join('')
Set 内部判断两个值是否不同,使用的算法叫做“Same-value-zero equality”,它类似于精确相等运算符(===),主要的区别是向 Set 加入值时认为NaN等于自身,而精确相等运算符认为NaN不等于自身。
另外,两个对象总是不相等的。
set的属性和方法
s.add(1).add(2).add(2);// 注意2被加入了两次s.size // 2s.has(1) // trues.has(2) // trues.has(3) // falses.delete(2);s.has(2) // false
Array.from方法可以将 Set 结构转为数组。这就又提供了一个数组去重的方法
const newArr = Array.from(new Set([1,2,3,4,5,6,3,5]))console.log(newArr)
set遍历操作
Set 结构的键名就是键值(两者是同一个值)
keys(),values(),entries()
let set = new Set(['red', 'green', 'blue']);for (let item of set.keys()) {console.log(item);}// red// green// bluefor (let item of set.values()) {console.log(item);}// red// green// bluefor (let item of set.entries()) {console.log(item);}// ["red", "red"]// ["green", "green"]// ["blue", "blue"]//其实可以省略values直接遍历,Set 结构的实例默认可遍历for (let item of set) {console.log(item);}// ["red", "red"]// ["green", "green"]// ["blue", "blue"]
Set 可以很容易地实现并集(Union)、交集(Intersect)和差集(Difference)。
let a = new Set([1, 2, 3]);let b = new Set([4, 3, 2]);//并集let union = new Set([...a],[...b])// Set {1, 2, 3, 4}//交集let intersect = new Set([...a].filter(x=> b.has(x)))// set {2, 3}//差集// (a 相对于 b 的)差集let difference = new Set([...a].filter(x => !b.has(x)));// Set {1}
map
JavaScript 的对象(Object),本质上是键值对的集合(Hash 结构),但是传统上只能用字符串当作键。这给它的使用带来了很大的限制。
ES2015 提供了 Map 数据结构。它类似于对象,也是键值对的集合,但是“键”的范围不限于字符串,各种类型的值(包括对象)都可以当作键。也就是说,Object 结构提供了“字符串—值”的对应,Map 结构提供了“值—值”的对应,是一种更完善的 Hash 结构实现。
//对象const obj = {}obj[true] = 'value'obj[123] = 'value2'obj[{a:1}] = 'value3'const {log} = consolelog(obj)//{ '123': 'value2', true: 'value', '[object Object]': 'value3' }//可以看出 对象是调用键值的tostring.转化为string值//---------------------------------------------------------------const m = new Map()const tom = {name: 'tom'}m.set(tom, 100)m.set(Math.random, '随机数')log(m)// Map { { name: 'tom' } => 100, 0.4260789016377904 => '随机数' }log(m.get(tom))// 100for (const [key, value] of m) {console.log(key, value)}//{ name: 'tom' } 100//0.5114862235627582 随机数
Symbol
比如,你使用了一个他人提供的对象,但又想为这个对象添加新的方法(mixin 模式),新方法的名字就有可能与现有方法产生冲突。ES2015就提供了Symbol,表示独一无二的值。它是 JavaScript 语言的第七种数据类型,前六种是:undefined、null、布尔值(Boolean)、字符串(String)、数值(Number)、对象(Object)。
let s = Symbol();console.log(s)
变量s就是一个独一无二的值。typeof运算符的结果,表明变量s是 Symbol 数据类型。
- Symbol 是一个原始类型的值,不能用new
- Symbol 值不是对象,所以不能添加属性。基本上,它是一种类似于字符串的数据类型。
- 可以接受一个字符串作为参数,为新创建的 Symbol 提供描述,用来显示在控制台或者作为字符串的时候使用,便于区分。 ```javascript const s = Symbol() console.log(typeof s) console.log( Symbol() === Symbol() ) //false //——————————————————-
console.log(Symbol(‘foo’)) console.log(Symbol(‘g’)) console.log(Symbol(‘tom’))
//Symbol(foo) //Symbol(g) //Symbol(tom)
//——————————————————-
let s1 = Symbol(‘foo’); let s2 = Symbol(‘foo’);
s1 === s2 // false
//——————————————————
//Symbol 值不能与其他类型的值进行运算,会报错 let sym = Symbol(‘My symbol’);
“your symbol is “ + sym // TypeError: can’t convert symbol to string
//——————————————————-
//Symbol 值可以显式转为字符串 let sym = Symbol(‘My symbol’);
String(sym) // ‘Symbol(My symbol)’ sym.toString() // ‘Symbol(My symbol)’
//———————————————————-
//Symbol 值也可以转为布尔值,但是不能转为数值 let sym = Symbol(); Boolean(sym) // true !sym // false
if (sym) { // … }
Number(sym) // TypeError sym + 2 // TypeError
<a name="Um2g6"></a>#### description 描述```javascriptconst sym = Symbol('foo');sym.description // "foo"
作为属性名Symbol
let mySymbol = Symbol();// 第一种写法let a = {};a[mySymbol] = 'Hello!';// 第二种写法let a = {[mySymbol]: 'Hello!' //在对象的内部,使用 Symbol 值定义属性时,Symbol 值必须放在方括号之中。};// 第三种写法let a = {};Object.defineProperty(a, mySymbol, { value: 'Hello!' });// 以上写法都得到同样结果a[mySymbol] // "Hello!"//--------------------------------------------------------------const log = {};log.levels = {DEBUG: Symbol('debug'),INFO: Symbol('info'),WARN: Symbol('warn')};console.log(log.levels.DEBUG.description, 'debug message');console.log(log.levels.INFO.description, 'info message');//debug debug message//info info message
Symbol 值作为属性名时,该属性是公有属性不是私有属性,可以在类的外部访问。但是不会出现在 for…in 、 for…of 的循环中,也不会被 Object.keys() 、 Object.getOwnPropertyNames() 返回。如果要读取到一个对象的 Symbol 属性,可以通过 Object.getOwnPropertySymbols() 和 Reflect.ownKeys() 取到。
const obj = {}let a = Symbol('a');let b = Symbol('b');obj[a] = 'hello'obj[b] = 'world'const objectSymbols = Object.getOwnPropertySymbols(obj)console.log(objectSymbols)//[ Symbol(a), Symbol(b) ]
同一个Symbol值
重新使用同一个 Symbol 值,Symbol.for()方法可以做到这一点
let s1 = Symbol.for('foo');let s2 = Symbol.for('foo');s1 === s2 // true
Symbol.for()与Symbol()这两种写法,都会生成新的 Symbol。Symbol.for()不会每次调用就返回一个新的 Symbol 类型的值,而是会先检查给定的key是否已经存在,如果不存在才会新建一个值。
for …of
全新的遍历方式 for…of 遍历 可以遍历所有所有数据结构。
一个数据结构只要部署了Symbol.iterator属性,就被视为具有 iterator 接口,就可以用for…of循环遍历它的成员。也就是说,for…of循环内部调用的是数据结构的Symbol.iterator方法。
for…of循环可以使用的范围包括:
- 数组
- Set
- Map 结构
- 某些类似数组的对象(比如arguments对象、DOM NodeList 对象)、 Generator 对象
- 字符串。
```javascript
const arr = [1,2,3,4]
for (const i of arr) {
console.log(i)
if(i>3) {
} } //—————————————————————————————————— const arr = [‘a’,’b’,’c’, ‘d’] for(let i in arr) { console.log(i) } //0 1 2 3 4 for(let i of arr) { console.log(i) } //a b c d //JavaScript 原有的for…in循环,只能获得对象的键名,不能直接获取键值。ES6 提供for…of循环,允许遍历获得键值。break //可以用break随时终止循环 优于forEach
//for…of循环调用遍历器接口,数组的遍历器接口只返回具有数字索引的属性。这一点跟for…in循环也不一样。 let arr1 = [3, 5, 7]; arr1.foo = ‘hello’;
for (let i in arr1) { console.log(i); // “0”, “1”, “2”, “foo” }
for (let i of arr1) { console.log(i); // “3”, “5”, “7” } //———————————————————————————————————- // 可以遍历map set对象 const s = new Set([‘foo’,’bar’]) for (const item of s) { console.log(s) } // 跟数组差不多也是拿到的每一项
const m = new Map() m.set(‘foo’,’124’) m.set(‘foo1’,’4456’) for (const [value, key] of m) { console.log(value, key) }
下面我们具体说说iterator接口。<a name="rwbhy"></a>### iterator 迭代器(遍历器)<a name="o5zQD"></a>#### 前言原来表示“集合”的数据结构,数组(Array)和对象(Object),ES6 又添加了Map和Set。这样就有了四种数据集合,用户还可以组合使用它们,定义自己的数据结构,比如数组的成员是Map,Map的成员是对象。这样就需要一种统一的接口机制,来处理所有不同的数据结构。<a name="b9PEs"></a>#### 概念遍历器(Iterator)就是这样一种机制。**它是一种接口,为各种不同的数据结构提供统一的访问机制**。任何数据结构只要部署 Iterator 接口,就可以完成遍历操作。<br />其实Iterator 接口主要供for...of使用(**一个数据结构只要部署了Symbol.iterator属性,就被视为具有 iterator 接口,就可以用for...of循环遍历**)。<br />我们来看个例子```javascript// iterator 迭代器const set = new Set(['foo','bar','baz'])console.log(set)const iterator = set[Symbol.iterator]()console.log(iterator.next())console.log(iterator.next())console.log(iterator.next())console.log(iterator.next())console.log(iterator.next())//Set { 'foo', 'bar', 'baz' }//{ value: 'foo', done: false }//{ value: 'bar', done: false }//{ value: 'baz', done: false }//{ value: undefined, done: true }//{ value: undefined, done: true }//所以我们能看出来Iterator 的遍历过程
从代码里我们能看出来Iterator的遍历过程
- 创建一个指针对象,指向当前数据结构的起始位置。也就是说,遍历器对象本质上,就是一个指针对象。
- 第一次调用指针对象的next方法,可以将指针指向数据结构的第一个成员。
- 第二次调用指针对象的next方法,指针就指向数据结构的第二个成员。
- 不断调用指针对象的next方法,直到它指向数据结构的结束位置。
返回一个包含value和done两个属性的对象。其中,value属性是当前成员的值,done属性是一个布尔值,表示遍历是否结束。
//模拟实现Iteratorfunction makeIterator(array) {var nextIndex = 0return function() {return nextIndex< array.length?{value: array[nextIndex++],done:false}:{value:undefined,done:true}}}var it makeIterator(['a', 'b'])it.next() // { value: "a", done: false }it.next() // { value: "b", done: false }it.next() // { value: undefined, done: true }
所以只要数据有Symbol.iterator这个属性就可以
//现在是for of 无法循环普通对象 是因为它内部没有实现iterable 接口const obj = {}for( const itme of obj) {console.log(item)}// TypeError: obj is not iterable//假如我们内部实现iterable接口 那这个对象就可以被for of 循环const obj = {store:['foo','bar','baz'],[Symbol.iterator] : function() {let index = 0const self = thisreturn { //从内部再返回一个迭代器next: function() {const result = {value: self.store[index],done: index>=self.store.length //表示迭代有没有结束}index ++return result}}}}for( const item of obj) {console.log(item)}
原生具备 Iterator 接口的数据结构如下。
-array
-map
-set
-map
-string
-TypedArray
-函数的arguments对象
-nodeList 对象
generator
基本概念:
Generator 函数是 ES6 提供的一种异步编程解决方案,语法行为与传统函数完全不同。
特征:
- function关键字与函数名之间有一个星号
- 函数体内部使用yield表达式,定义不同的内部状态(yield在英语里的意思就是“产出”)
看一个例子
//基本使用function* helloWorldGenerator() {yield 'hello';yield 'world';return 'ending';}var hw = helloWorldGenerator(); //调用//-------------------------------------------hw.next() //调用遍历器对象的next方法,使得指针移向下一个状态// { value: 'hello', done: false }hw.next()// { value: 'world', done: false }hw.next()// { value: 'ending', done: true }hw.next()// { value: undefined, done: true }
- 上面代码定义了一个 Generator 函数helloWorldGenerator。
- 调用 Generator 函数后,该函数并不执行,返回的也不是函数运行结果,而是一个指向内部状态的指针对象,也就是上一章介绍的遍历器对象(Iterator Object)
- 每次调用next方法,内部指针就从函数头部或上一次停下来的地方开始执行,直到遇到下一个yield表达式(或return语句)为止。或者说(yield表暂停执行,next恢复执行)
- 每次调用遍历器对象的next方法,返回的value和done。value是yield表达式后面那个表达式的值;done表示是否遍历结束。
感觉像是利用Iterator实现对代码的分段执行能暂停能再开始。
使用案例
// generator 案例//案例1 : 实现一个发号器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)console.log("---------------------------")//案例二 : 实现iterator 迭代器const todos = {left:['吃饭','睡觉','打豆豆'],learn: ['语文','数学','英语'],work: ['喝茶'],// 实现迭代器[Symbol.iterator]: function *() { //因为它本身就返回Iterator迭代器const arr = [].concat(this.left, this.learn, this.work)for (const item of arr) {yield item}}}for (const item of todos) {console.log(item)}// 1// 2// 3// 4// ---------------------------// 吃饭// 睡觉// 打豆豆// 语文// 数学// 英语// 喝茶
再再具体的功能再查阅相关文档的使用吧。
promise已有相关文档介绍,下面介绍下async函数
async函数
含义
ES2017 标准引入了 async 函数,使得异步操作变得更加方便。
async 函数是什么?一句话,它就是 Generator 函数的语法糖。
看个例子:
const fs = require('fs');// 依次读取两个文件const readFile = function (fileName) {return new Promise(function (resolve, reject) {fs.readFile(fileName, function(error, data) {if (error) return reject(error);resolve(data);});});};//generator写法const gen = function* () {const f1 = yield readFile('/etc/fstab');const f2 = yield readFile('/etc/shells');console.log(f1.toString());console.log(f2.toString());};//---------------------------------------------------------//async写法const asyncGen = async function() {const f1 = await readFile('/etc/fastab')const f2 = await readFile('etc/shells')console.log(f1.toString());console.log(f2.toString());}
一比较就会发现,async函数就是将 Generator 函数的星号(*)替换成async,将yield替换成await。
async对generator改进具体是:
- 内置执行器。
Generator 函数的执行必须靠执行器执行得用next() 方法,而async自带执行器,执行就一行就可以
asyncGen()
- 更好的语义。
async和await,比起星号和yield,语义更清楚了。async表示函数里有异步操作,await表示紧跟在后面的表达式需要等待结果。
- 更广的适用性
yield命令后面只能是 Thunk 函数或 Promise 对象,而async函数的await命令后面,可以是 Promise 对象和原始类型的值
- 返回值是 Promise
async函数的返回值是 Promise 对象,这比 Generator 函数的返回值是 Iterator 对象方便多了。你可以用then方法指定下一步的操作。
进一步说,async函数完全可以看作多个异步操作,包装成的一个 Promise 对象,而await命令就是内部then命令的语法糖。
基本用法
指定多少毫秒后输出一个值
function timeout(ms) {return new Promise((resolve)=> {setTimeout(resolve, ms)})}async function asyncPrint(value, ms) {await timeout(ms)console.log(value)}asyncPrint('hello world', 1000)
上面代码指定 1000 毫秒以后,输出hello world。
在我们实际使用上,因为await后面的promise对象,有可能失败,最好把await放到try..catch里
function timeout(ms) {return new Promise((resolve)=> {setTimeout(resolve, ms)})}async function asyncPrint(value, ms) {try {await timeout(ms)console.log(value)}catch(err) {console.log(err)}}asyncPrint('hello world', 1000)
更多详细的使用具体使用的时候再看下。
实现异步的方式 generator promise async,都说async是解决异步的终极方案,可以多多使用。加油下一章我们介绍ES2016和ES2017新增的东西。
