ES6+
什么是ES?什么是JS?ES和JS之间的关系?
ECMAScript也是一门脚本语言,简写为ES,通常会把它看为JavaScript的标准化规范,事实上js是ES的扩展语言。ES只是单纯的语言,js是这门语言的扩展,使我们可以在浏览器中操作BOM和DOM;在node中可以去做读写文件的操作。
浏览器中的js就是ES+webAPI(即BOM和DOM)。
node中的js就是ES+nodeAPI(如fs、net、etc)。
ES新特性
let与块级作用域、const
作用域:函数作用域、全局作用域、块级作用域(ES6新增)
- 函数作用域:变量在定义的函数体内以及函数体内嵌套的函数体中都是有定义的。
- 全局作用域:全局。
- 块级作用域:即{ }中间的区域,如if(){ }块、for(){ }块。
在ES6之后声明变量的关键字有三个var、let、const。
var和let和const
- var:函数作用域;变量声明提升;可以重复定义;声明的变量会作为window的属性。
- let:块级作用域;不会声明提升;不可以重复定义;不会作为window的属性。
- const:在let的基础上添加了只读(浅层只读,深层可读写)的属性。再声明的时候必须同时进行初始化。
编程建议:主用const、配合使用let,不用var。
var函数作用域及其它特性
通过下面的例子可以充分的体会var的函数作用域以及其他特性。
if(true){
var a = 'xiaodong'
}
console.log(a) //xiaodong
for(var i = 0; i < 3; i++){
for(var i = 0; i < 3; i++){
console.log(i)
}
}
//0,1,2
console.log(i) //4
通过if可以模拟一个for循环的过程,可以清晰的看到for循环的两层作用域。
let块级作用域及其它特性
if(true){
let a = 'xiaodong'
}
console.log(a) //a is not defined
for(let i = 0; i < 3; i++){
for(let i = 0; i < 3; i++){
console.log(i)
}
}//0 1 2 0 1 2 0 1 2
console.log(i) //i is not defined
let体会
var elements = [{}, {}, {}]
for(var i = 0; i < elements.length; i++){
elements[i].onclick = function(){
console.log(i)
}
}
elements[0].onclick() //3
var elements = [{}, {}, {}]
for(let i = 0; i < elements.length; i++){
elements[i].onclick = function(){
console.log(i)
}
}
elements[0].onclick() //0
let闭包实现
通过闭包的形式,将每一次传给函数的i作为这个函数执行时的依赖单独保存起来。
var elements = [{}, {}, {}]
for(var i = 0; i < elements.length; i++){
elements[i].onclick = (function(i){
console.log(i)
})(i)
}
elements[0].onclick() //3
解构
数组解构
在应用的例子中体会解构的用法
- 使用解构设置默认值,避免变量undefined
const [a=0, b=0] = [1];
console.log(a,b);//1,0
- 使用解构交换两个变量的值
let [a, b] = [1, 2];//注意这里的省略号不能省略
[a, b] = [b ,a]
console.log(a, b) //2, 1
对象解构
- 对象解构赋值
const {name, age} = {name:'xiaodong', age:18};
console.log(name, age) // xiaodong 18
- 对象解构重命名赋值
const obj = {name:'xiaodong', age:18};
const {name:myname, age:myage} = obj;
console.log(myname, myage) // xiaodong 18
- 解构示例
const name = 'xiaodong'
const {log} = console;
log(name) //xiaodong
模板字符串及标签函数
模板字符串相对传统字符串的区别:
- 支持换行。(再写html字符串的时候非常好用)
- 支持插值表达式。
const [title1, title2] = ['xiaodong', 'lili']
const innerhtml = `
<h1>${title1}</h1>
<h1>${title2}<h1>
`
标签函数
字符串标签就相当于一个函数,给字符串添加标签就相当于调用这个函数,这个函数会接受一个数组参数,这个数组是字符串分解的元素组成的。
const [name, age] = ['xiaodong', 18]
function tagFunc(strings, name, age){
console.log(strings) //[ '我叫', ',今年', '岁' ]
console.log(name) //xiaodong
console.log(age) //18
return strings[0] + name + strings[1] + age + strings[2]
}
//result最终的值就是tagFunc函数的返回值,如果标签函数没有返回值,result就是undefined
const result = tagFunc`我叫${name},今年${age}岁`
console.log(result) //我叫xiaodong,今年18岁
可以看到,模板标签的作用就是对字符串进行加工并返回加工结果。
应用:模板引擎、实现中英文切换(具体怎么实现?demo?)
字符串扩展方法
1. String.includes()
判断字符串是否包含某个字符串,返回布尔值。
2. String.startsWidth()
判断字符串是否以某个字符串开始,返回布尔值。
3. String.endsWidth()
判断字符串是够以某个字符串结尾,返回布尔值。
ES6之前只能用indexOf或者正则表达式
函数参数默认值
function foo(name = 'xiaodong'){
console.log(name);
}
foo() //xiaodong
需要注意的是当函数有多个参数,添加默认值的参数需要放在最后面
之前只能在函数体中通过if逻辑判断来处理参数默认值的问题。
剩余参数
function foo(first, ...args){
console.log(first);
console.log(args)
}
foo(1, 2, 3)
//1
//[ 2, 3 ]
注意当函数接收不只一个参数的时候,需要把剩余参数放到参数的最后面,而且一个函数只能接收一个剩余参数
很多方法函数都是可以接收任意个参数,例如congsole.log()
,在ES6之前只能在函数中通过arguments
对象来使用。(这个arguments
是一个伪数组)
展开数组
const arr = [1, 2, 3]
console.log(arr) //[1, 2, 3]
console.log.apply(console, arr) //1 2 3
console.log(...arr) //1 2 3
这里解释一下第二行为什么输出结果是这个,其实这是一个取巧的方法,利用的是apply接收参数为一个数组的特性,当调用console.log这个方法,并把this只想console然后将arr这个数组传给了更改this指向的console.log方法,自然而然地也就会将数组逐个打印出来了。
箭头函数
用过无数次了,没什么好说的,就做一道题吧。
const arr = [1, 2, 3, 4, 5, 6]
// const arr1 = arr.filter(function(item){
// return item % 2;
// })
const arr1 = arr.filter(item => item % 2)
console.log(arr1) //[1, 3, 5]
需要强调的是箭头函数与传统的函数的区别:箭头函数不会改变this的指向
const person = {
name:'xiaodong',
sayName1:function(){
console.log(`我的名字是${this.name}`)
},
sayName2:() => {
console.log(`我的名字是${this.name}`)
}
}
person.sayName1() //我的名字是xiaodong
person.sayName2() //我的名字是undefined
传统函数在调用的时候,this会指向调用这个函数的对象。但是箭头函数不会,箭头函数没有this的机制,不会改变this的指向,也就是说在箭头函数的外面this只想什么它里面的this就指向什么。
举一个利用箭头函数不影响this的指向的例子
const person = {
name:'xiaodong',
sayNameAsync1:function() {
setTimeout(function(){
console.log(`我的名字是${this.name}`)
},1000)
},
sayNameAsync2:function() {
setTimeout(() => {
console.log(`我的名字是${this.name}`)
},1000)
},
}
person.sayNameAsync1() //我的名字是undefined
person.sayNameAsync2() //我的名字是xiaodong
setTimeout
里面的回调函数最终会被放到全局对象上面被调用,所以当以传统的方式写这个回调函数,当这个回调函数在全局对象上面执行的时候,这个函数的this
就会指向这个全局对象,自然而然的就是undefined
。
但是当用箭头函数,this
的指向并不会发生影响,箭头函数中的this
取决于上一级sayNameAsync2
的执行上下文环境,所以这里面的this
就会指向这个person
对象。
在ES6之前,也就是使用传统function
定义函数的时候,要想实现,只能在这个回调函数外面conost _this = this
,然后用_this
去取person
中的属性。
补充一点感觉很关键在js中有这个说法,看函数的归属,看它在哪定义,找内部this看它在哪执行
对象字面量增强
先百度一下什么是字面量(专业一点。。。狗头)
在计算机科学中,字面量(literal)是用于表达源代码中一个固定值的表示法(notation)。
字符串字面量(stringliteral)是指双引号引住的一系列字符,双引号中可以没有字符,可以只有一个字符,也可以有很多个字符。
#include <stdio.h>
int main(void)
{
int a = 10; // 10为int类型字面量
char a[] = {"Hello world!"} // Hello world 为字符串形式字面量
.............
// 以此类推,不再赘述
return 0;
}
言归正传
- 当对象的键值相同,可以使用省略写法。
- 定义对象的方法可以使用省略的写法。
- 对象字面量的属性名可以使用表达式的返回值。(之前只能在声明对象之后使用方括号的形式)
注意这里方法的省略写法也仅仅就是一个省略写反,他就等同于function()
这种写反,所以这里面的this
和使用传统方法的机制是一样的
对象扩展方法
Object.assign()
将多个源对象中的属性复制到一个目标对象中,然后将目标对象返回。如果有相同的属性用后面的对象的属性去覆盖第一个对象并将第一个对象返回,只会更改第一个对象,后面的对象不会更改。
const obj1 = {
a:1,
b:2,
c:3
};
const obj2 = {
c:9,
e:1,
f:2,
g:3
};
let targetObj = {
c:2,
d:3,
e:4
};
const resultObj = Object.assign(targetObj, obj1, obj2)
console.log(obj1)
console.log(obj2)
console.log(targetObj)
console.log(resultObj === targetObj)
/*log*/
// { a: 1, b: 2, c: 3 }
// { c: 9, e: 1, f: 2, g: 3 }
// { c: 9, d: 3, e: 1, a: 1, b: 2, f: 2, g: 3 }
// true
assign的应用
js中引用数据类型存放的是一组数据的地址值,所以当我们把一个引用数据类型传递给一个函数,并在这个函数中改变了这个参数的时候,函数外面对应的数据也会同时发生变化。
如果我们想不改变之前对象,就可以使用Object.assign()
将对象赋给一个空目标对象,然后对这个目标对象进行相应的操作。
Object.assign()
还可以用来设置一个函数的参数options
的默认值。
const obj = {
name:'xiaodong',
arr:[1, 2, 3]
}
const obj1 = Object.assign({}, obj)
console.log(obj1)
obj1.arr[0] = 4;
obj1.name = 'lili';
console.log(obj)
console.log(obj1)
/*log*/
// { name: 'xiaodong', arr: [ 1, 2, 3 ] }
// { name: 'xiaodong', arr: [ 4, 2, 3 ] }
// { name: 'lili', arr: [ 4, 2, 3 ] }
由上面的例子可以看出,assign相当于对象第一层数据的深拷贝,其实这也正好符合了assign定义的特性,你细品这句话,细细的品:将多个源对象中的属性赋值给一个目标对象。
Object.is()
在ES6之前判断两个数据是否相等有两种方法分别是两个等于号和三个等于号。其中区别就是两个等于号会自动的对两边的变量进行数据类型转换,而三个等号的不会,必须严格相等才会相等。
但是!有问题!
console.log(0 == false) // true
console.log(+0 === -0) // true
console.log(NaN === NaN) // false
Object.is()
也是用来判断两个数据是否相等的,返回布尔值。严格相等。
console.log(Object.is(+0, -0)) // false
console.log(Object.is(NaN, NaN)) // true
Proxy
英文翻译是代理人。
在ES6之前可以使用ES5提供的Object.defaultProperty()
方法来给对象添加属性,并捕获属性的读写过程。比如说vue3.0之前的版本就是使用这个方法实现的数据响应从而完成数据的双向绑定。ES6的Proxy
就是专门为对象来添加代理器的,我们可以使用Proxy
生成的对象的代理对象来捕获对象属性的读写,并在这个过程中还可以进行一些其他的数据处理操作。
const person = {
name:'xiaodong',
age:18
}
const personProxy = new Proxy(person, {
get(target, property){
console.log(target, property) // person对象,属性名
},
set(target, property, value){
console.log(target, property, value) // person对象,属性名,新的值
}
})
console.log(personProxy.name) // undefined
这里最终输出了一个undefined
是因为get
方法返回的值本身就是undefined
const person = {
name:'xiaodong',
age:18
}
const personProxy = new Proxy(person, {
get(target, property){
return property in target ? target[property] : 'default'
// console.log(target, property) // person对象,属性名
},
set(target, property, value){
// console.log(target, property, value) // person对象,属性名,新的值
//在set方法里面可以对属性的赋值做一些例如验证的操作
if(property === 'age'){
if(!Number.isInteger(value)){
//验证设置的年龄是不是一个整数
throw TypeError(`${value} is not a int`)
}
}
}
})
console.log(personProxy.name) // xiaodong
console.log(personProxy.gender) // undefined
personProxy.age = 'aaa' // TypeError: aaa is not a int
从vue3.0开始,vue就开始使用proxy实现数据响应了
Proxy相对于Object.defineProperty的优势
1. Object.defineProperty只能监视到数据的读取或者写入,Proxy可以监听到更多其他的对象操作,比如delete操作、对象方法的调用。
const person = {
name:'xiaodong',
age:18
}
const personProxy = new Proxy(person, {
deleteProperty(target, property){
console.log(target, property)
delete target[property]
}
})
delete personProxy.name
Proxy可以监听的对象的操作:
handler方法 | 触发方式 |
get | 读取某些属性 |
set | 写入某些属性 |
has | in 操作符 |
deleteProperty | delete操作符 |
getPropertyOf | Object.getPropertyOf() |
setPropertyOf | Object.setPropertyOf() |
isExtensible | Object.isExtensible() |
preventExtensions | Object.preventExtensions() |
getOwnPropertyDescriptor | Object.getOwnPropertyDescriptor() |
defineProperty | Object.defineProperty() |
ownKeys | Object.getOwnPropertyNames()、Object.getOwnPropertySymbols() |
apply | 调用第一个函数 |
construct | 用new调用一个函数 |
纳尼?!what?!这都是些啥?看这里吧
https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/Object
2. Proxy可以更好的支持数组对象的监视
之前通过Object.defineProperty监视数组最常见的方式就是通过重新数组的操作方法,这也是vue中使用的方式,大体的思路就是通过自定义的方法去覆盖掉数组原型对象上的push、shift等方法,以此去劫持对应的方法调用的过程。
const arr = []
const arrProxy = new Proxy(arr, {
set(target, property, value){
console.log(target, property, value) // [] 0 233
target[property] = value
return true // 表示设置成功
}
})
arrProxy.push(233)
下标的值是Proxy自己推算出来的。
这里着重提一下最后一行的turn true
。
在照敲代码的时候漏写了这最后的return
,导致报错:
TypeError: ‘set’ on proxy: trap returned falsish for property ‘0’
MDN上明确的说明了set方法应该返回一个布尔值。
返回true表示赋值成功。如果set方法返回false,并且分配发生在严格模式代码中,则会引发TypeError。
3. Proxy是以非侵入的方式监管了对象的读写
即,不需要对对象本身去做任何的操作,就可以监视到它内不成员的读写;而Object.defineProperty就需要通过特定的方式单独去定义对象当中那些需要被监视的属性。
Reflect
Reflect
统一了操作对象的方法。
使用java的方式解读这个Reflect
,它是一个静态类,不能通过new的方式去构建实例对象,只能够调用它的一些静态方法。类似于js的Math对象。
Reflect
静态对象上面挂载了14个(被废弃一个还有13个)用于操作对象的静态方法。
这13种方法对应proxy对应的13个handle处理方法,reflect
上面的方法就是proxy对应处理方法的默认处理方法。
##Reflext
成员方法就是Proxy
处理对象的默认实现##
const person = {
name:'xiaodong',
age:18
}
const personProxy = new Proxy(person, {
//当这里面的各种处理函数没有被重定义的时候,他们的默认执行方法就是Reflect对象的静态方法
//所以一个标准的写法就是,当我们使用自己重定义的处理函数对对象进行监视和数据处理的时候,最终需要调用一次Reflect的静态方法,来保持执行函数的默认功能。
})
const person = {
name:'xiaodonng',
age:18
}
console.log(Reflect.has(person, 'name')) // true
console.log(Reflect.deleteProperty(person, 'age')) // true
console.log(Reflect.ownKeys(person)) // [ 'name' ]
Promise
后面会有详细介绍,现在先看这篇博客吧
class类
ES6之前ECMAScript是通过定义函数以及函数的原型对象来去实现类型,例如:
function Person(name){
//通过this访问当前的实力对象
this.name = name
}
//在这个类型所有的实例之间去共享一些成员,可以使用函数对象的prototype(原型)去实现
Person.prototype.say = function(){
console.log(`hey, my name is ${this.name}`)
}
const xiaodong = new Person('xiaodong')
xiaodong.say()
class给了一个更加清晰的声明类的方式。
class Person {
constructor(name){ // 当前类型的构造函数,可以在这个构造函数中使用this去访问当前类型的实例对象
this.name = name
}
say(){
console.log(`my name is ${this.name}`)
}
}
const xiaodong = new Person('xiaodong');
xiaodong.say() // my name is xiaodong
静态方法
与之相对应的概念叫实例方法,实例方法是通过这个类型构造的实例对象去调用;对象方法是直接通过类型本身去调用就可以。
之前我们定义静态方法是直接在构造函数的对象上挂载方法,因为js中函数也是一个对象,也可以给这个对象添加一些方法成员。
在ES6中新增加了一个专门声明静态成员方法的关键词 static
。
##继续说,由于静态方法是直接挂载到构造对象上面的方法成员,因此静态方法中的this就不会指向当前new的实例,而是一直指向构造对象。##
class Person {
constructor(name){
this.name = name
}
say(){
console.log(`my name is ${this.name}`)
}
static create(name){
return new Person(name)
}
}
const xiaodong = Person.create('xiaodong')
xiaodong.say() // my name is xiaodong
类的继承
在ES6之前通常是使用原型的方式实现继承。
用class实现继承
class Person {
constructor(name){
this.name = name
}
say(){
console.log(`my name is ${this.name}`)
}
static aaa(){
console.log(1)
}
}
class Student extends Person {
constructor(name, number){
super(name);
this.number = number;
}
hello(){
console.log(`my number is ${this.number}`)
}
}
const s = new Student('xiaodong')
s.say() // my name is xiaodong
s.aaa() // TypeError: s.aaa is not a function
静态方法不会继承,Person的静态方法只能通过Person.aaa()来调用
new的步骤:
- 创建空对象;
var obj = {};
- 构建原型链,
设置新对象的constructor属性为构造函数的名称,设置新对象的__proto__属性指向构造函数的prototype对象;
obj.__proto__ = Person.prototype;
- 执行构造函数中的代码,构造函数中的this指向new出对象
- 返回对象,并赋给等号左边的变量
附一张原型链图解:
和我的一篇博客
Set数据结构
set类似于一个数组,区别就是set中的成员是不允许出现重复的,如果重复添加就会在添加的过程中被忽略掉。
const s = new Set()
s.add(1).add(2).add(3).add(2)
console.log(s) // Set { 1, 2, 3 }
s.forEach(item => {
console.log(item) // 1 2 3
})
for(item of s){
console.log(item) // 1 2 3
}
console.log(s.size) // 3
console.log(s.has(2)) // true
console.log(s.delete(2)) // true
console.log(s) // Set { 1, 3 }
console.log(s.clear()) // undefined
console.log(s) // Set {}
使用set数组去重
let arr = [1, 2, 3, 4, 5, 6, 3, 4, 5, 7, 1, 2, 0]
const s = new Set(arr)
// arr = Array.from(s)
arr = [...s]
console.log(arr) // [ 1, 2, 3, 4, 5, 6, 7, 0 ]
Map数据解构
之前的对象的键只能是字符串类型,map的键可以是任意类型。
如果ES5中的对象的键是一个非字符串类型,就会自动的将其转换为字符串作为键保存。
一个需求,保存一份成绩单,每个学生是一个对象并分别对应自己的各科学习成绩。那就得使用ES6中的map,键位不同的学生对象,对应的值位每一个学生的各科成绩。
const m = new Map()
m.set({
name:'xiaodong',
gender:'male'
},{
english:'4',
Mathematics:'100'
})
m.forEach((value, key, target) => {
console.log(value) // { english: '4', Mathematics: '100' }
console.log(key) // { name: 'xiaodong', gender: 'male' }
console.log(target) // Map { { name: 'xiaodong', gender: 'male' } => { english: '4', Mathematics: '100' } }
})
Symbol
表示一个独一无二的值。也就是说通过Symbol创建的每一个值都是唯一的。
从ES6开始普通对象的键可以有两种类型,一种是字符串一种是Symbol。
Symbol作为属性名可以避免属性名冲突的问题(目前最主要的作用)
const obj = {
[Symbol()]:'hahah'
}
console.log(obj)
模拟实现对象的私有成员
const name = Symbol();
const obj = {
[name]:'xiaodong',
say(){
console.log(this[name])
}
}
obj.say()
由于symbol是唯一的,所以我们不可能在创建一个symbol去获取obj的对应属性,因此就可以实现对象的私有成员。
截至ES2019一共有6种原始数据类型
boolean
null
undefined
number
string
symbol
未来还会新增一种叫BigInt的数据类型存放更长的数字
bigint
Symbol补充
Symbol(String)可以接收一个字符串参数,作为该负好的说明,但是就说说明一样的Symbol也是不同的两个数据。
Symbol可以使用Symbol.for(String)的方法,该方法传入一个字符串参数,相同的字符串参数会返回相同的Symbol。
const s1 = Symbol('aaa')
const s2 = Symbol('aaa')
console.log(s1 === s2) // false
const s1 = Symbol.for('aaa')
const s2 = Symbol.for('aaa')
console.log(s1 === s2) // true
这个方法内部维护了一个全局的注册表位字符串和Symbol值提供了一个一一对应的关系。
需要注意的是在这个方法里面维护的是字符串和Symbol对应的关系,如果传入的参数不是一个字符串,它会自动的转换成字符串也就会造成:
const s1 = Symbol.for('true')
const s2 = Symbol.for(true)
console.log(s1 === s2) // true
Symbol补充中的Symbol标识符是干什么的没听懂?
- 通过Symbol实现的属性名是不能通过for in遍历拿到的。
- 通过Object.ownKeys()也是拿不到的。
- 通过JSON.stringify()去格式化对象也是会将Symbol属性忽略掉。
综上所述:Symbol非常适合作为对象的私有属性。
获取Symbol属性需要使用Object.getOwnPropertySymbols(obj),来单独的获取Symbol属性,注意这个方法也只能获取Symbol属性,其他属性获取不到。
fo…of循环
for比较适合遍历普通的数组,for…in循环比较适合遍历键值对。
再有就是一些方法如forEach()方法。
for…of循环可以作为遍历所有数据结构的统一的遍历方式。
for…of可以随时使用break方法终止循环,但是arr.forEach()不能通过break终止。
arr.some(),arr.every()可以通过返回true和false的方式终止循环。
for…of遍历数组获取到item就是数组的元素。
使用for…of遍历数组和map之间的差异
const m = new Map()
m.set({
name:'xiaodong',
gender:'male'
},{
english:'4',
Mathematics:'100'
})
for(item of m){
console.log(item) // [ { name: 'xiaodong', gender: 'male' }, { english: '4', Mathematics: '100' } ]
}
可以看到for…of遍历map拿到的是一个个的数组,所以我们就可以利用数组解构的方式直接获取到他的键或者值。
for([key, value] of m){
console.log(key) // { name: 'xiaodong', gender: 'male' }
console.log(value) // { english: '4', Mathematics: '100' }
}
for…of遍历普通对象(会报错)
const obj = { a:1, b:2 }
for(item of obj){
console.log(item)
}
// TypeError: obj is not iterable
可迭代接口Iterable的实现是for…of遍历的前提,反过来说能用for…of遍历的数据结构内部都已经实现了这个可迭代的接口。
for…of循环的实现原理,就是去调用被便利对象的iterator方法得到一个迭代器,然后循环执行这个迭代器的next()方法去遍历所有的数据。
const s = new Set([1, 2, 3])
const iterator = s[Symbol.iterator]()
console.log(iterator.next())
console.log(iterator.next())
console.log(iterator.next())
console.log(iterator.next())
实现可迭代接口
实际上就是在对象中挂在一个iterator方法,然后在这个方法中返回一个迭代器对象。