😭这些功能用了这么久,现在才知道是 ES6 才发布的

66 阅读19分钟

前言

在日常开发中,有些语法稀里糊涂的就用着了,用着还挺方便好用的,但是如果不深入探索就不知道它出自哪里,原理是什么,下面就是整理的一些比较常用的ES6语法

let 和 const

ES6 新增了 let 和 const,它们的用法类似于 var,但是它们所声明的变量和常量只能在块级作用域有效,也会就是大括号{}内,那么它们三者之间的有什么区别呢?请看下面解释:

var 声明的是全局变量,存在变量提升

let 声明的是局部变量,不允许重复声明,

存在暂时性死区:

ES6 明确规定,如果区块中存在 letconst 命令,这个区块对这些命令声明的变量,从一开始就形成了封闭作用域。凡是在声明之前就使用这些变量,就会报错。

var temp = 123;
if(true){
temp = 'abc'; //ReferenceError
let temp;
console.log(temp1) //ReferenceError
const temp1 =234
}

const 声明的是常量,一旦声明就必须赋值,声明的值不能修改

{
console.log(temp2) //undefined
var temp2 = 123;
temp = 'abc'; //ReferenceError
let temp;
console.log(temp1); //ReferenceError
const temp1 =234;
const temp3; //Uncaught SyntaxError: Missing initializer in const declaration
}

解构赋值

解构赋值语法是一种 Javascript 表达式。可以将数组中的值或对象的属性取出,赋值给其他变量。

let a,b,rest;
[a,b] = [10,20];
console.log(a)  //10
console.log(b)  //20
[a,b,...rest] = [10,20,30,40,50];
console.log(rest)  //[30,40,50]

数组解构:在赋值的左侧定义了要从原变量中取出哪些值,赋值对应着相应的下标

const x = [1, 2, 3, 4, 5];
const [y, z] = x;
console.log(y); // 1 y的下标为0,右侧数组1的下标为0
console.log(z); // 2

对象解构:与数组解构不同的是,它不是根据次序(下标)进行匹配,而是只需要变量名属性名相同即可

const obj = {a:1,b:2}
const {a,b} = obj
console.log(a,b)  //1,2

const {b} = obj 
console.log(b)  //2

此外我们还可以设置默认值

每个解构属性都可以有一个默认值。当属性不存在或值为 undefined 时,将使用默认值。如果属性的值为 null,则不使用它、

const [a = 1] = []   //a=1
const {b = 2} = {b:undefined} // b = 2
const {c = 3} = {c:null}   //c = null
const {a} = {b:2}  //a = undefined
const {a,b = 2} = {a:1}  //a = 1  ,b = 2

扩展运算符

对象的扩展运算符

对象中的扩展运算符(...)用于取出参数对象中的所有可遍历属性,拷贝到当前对象之中

const bar = {a:1,b:2}
const bab = {...bar}  //{a:1,b:2}

实际上等价于

const bar = {a:1,b:2};
const bab = object.assign({},bar)  // {a:1,b:2}

有的同学不是很了解Object.assign的作用或者概念。

Object.assign()  方法将所有可枚举Object.propertyIsEnumerable() 返回 true)的自有Object.hasOwnProperty() 返回 true)属性从一个或多个源对象复制到目标对象,返回修改后的对象。如果目标对象与源对象具有相同的 key,则目标对象中的属性将被源对象中的属性覆盖,后面的源对象的属性将类似地覆盖前面的源对象的属性。

const target = {a:1,b:2}
const source = {b:4,c:5}
const returnTarget = Object.assign(target,source)
console.log(target)  //{a:1,b:4,c:5}

数组的扩展运算符

扩展运算符可以将数组转化为参数序列

function add(x,y){
return x+y;
}
const arr1 = [4,5]
add(...arr1)  //9
console.log(...arr1)  //4,5

需要注意一点的就是扩展运算符对对象实例的拷贝属于一种浅拷贝,对于基本数据类型的拷贝会完整的复制一份,复制后修改不会改变原来的对象,对于引用数据类型,在拷贝时拷贝的是对象的引用,当原对象发生变化时,拷贝对象也跟着变化

const obj1 = {a:1,b:2}
const obj2 = {...obj1}
obj2.a = 3
console.log(obj1)  //{a:1,b:2}
console.log(obj2)  //{a:3,b:2}


const obj1 = {a:1,b:2,c:{d:"haha"}}
const obj2 = {...obj1}
obj2.c.d = "xixi"
console.log(obj1)  //{a:1,b:2,c:{d:"xixi"}}
console.log(obj2)  //{a:1,b:2,c:{d:"xixi"}}

若扩展运算符用于数组赋值,只能放在参数的最后一位,否则报错

const [...rest,last] = [1,2,3,4,5]  //报错
const [frist,...rest,last] = [1,2,3,4,5]  //报错
const [frist,...rest] = [1,2,3,4,5]  //不报错

Set 和 Map

Set 是什么呢

Set对象是值的集合,你可以按照插入的顺序迭代它的元素。Set 中的元素只会出现一次,即 Set 中的元素是唯一的。

学习Set需要掌握它的属性和方法

Set 实例的属性和方法

Set本身是一个构造函数,用于生成Set数据结构

Set().size :返回Set实例的成员总数。

Set().add()如果没有相同的元素,则将在Set对象尾部添加一个元素

Set().clear 从对象中移除所有元素

Set().delete(value) 删除与之关联的元素value并返回一个布尔值来表示是否移除成功

Set().has(value) 判断对象中是否存在具有给定值得元素,返回一个布尔值

const set = new Set()
set.add(1).add(2).add(2).add(4);//2被添加了两次
set.size   //3
set.has(1)  //true
set.has(3)  //false
set.delete(2) //true
set.has(2)  //false
set.clear  
set.has(1)  //false

由于Set中的元素是唯一的特性,常被用于数组的去重

const arr1 = [1,1,2,2,3,3,5,5,5,5,5]
console.log([... new Set(arr1)])  //[1,2,3,5]

然而Map又是什么呢

它类似于对象,也是键值对的集合,但是“键”的范围可以是字符串,数字,布尔值,甚至是对象都可以当做键。它跟对象的区别是,Object结构提供了“字符串--值”的对应,Map结构提供了“值--值”的对应

Map 对象保存键值对,并且能够记住键的原始插入顺序。任何值(对象或者基本类型)都可以作为一个键或一个值。

Map 对象是键值对的集合。Map 中的一个键只能出现一次;它在 Map 的集合中是独一无二的。Map 对象按键值对迭代——一个 for...of 循环在每次迭代后会返回一个形式为 [key,value] 的数组。迭代按插入顺序进行,即键值对按 set() 方法首次插入到集合中的顺序(也就是说,当调用 set() 时,map 中没有具有相同值的键)进行迭代。

const map = new Map()
map.set('a',1)
map.set('b',2)
map.set('a',3)
console.log(map) // {'a' => 3, 'b' => 2}

Map 的属性和方法

1、Map().size 返回 Map结构的成员总数。

const map = new Map()
map.set('foo',true)
map.set('bar',false)
map.size  //2

2、Map().set(key,value) 设置键名 key 对应的键值为value,然后返回整个Map结构,如果Key已经有值,则键值会被更新,否则就新生成该键。

const map = new Map()
map.set('a',1)
map.set('b',2)
map.set('a',3)
console.log(map) // {'a' => 3, 'b' => 2}

3、Map().get 方法读取key对应的键值,如果找不到key,则返回undefined

const map = new Map()
map.set('a',1)
map.set('b',2)
console.log(map.get('a')) // 1

4、Map().has(key) 返回一个布尔值,表示某个键是否在当前Map

const map = new Map()
map.set('a',1)
map.set('b',2)
map.has('a') // true
map.has('c') //false

5、Map().delete(key) 删除某个键,返回true。如果删除失败,返回false

const map = new Map()
map.set('a',1)
map.set('b',2)
map.has('a') // true
map.delete('a') //true
map.has('a') // false

6、Map().clear() 清除所有成员,没有返回值

const map = new Map()
map.set('a',1)
map.set('b',2)
map.clear() 
map.has('a')  //false

Promise对象

Promise的概念:

Promise 是异步编程的一种解决方案,比传统的解决方案--回调函数和事件--更加合理和更加强大。有了Promise对象,就可以把异步操作以同步操作的流程表达出来,避免了层层嵌套的回调函数(回调地狱)。

Promise是什么呢?

Promise对象简单来说就是一个容器,里面保存着某个未来才会结束的事件(通常是一个异步操作)的结果。从语法上说,Promise是一个对象,从它可以获取异步操作的消息。并且Promise提供统一的API,各种异步操作都可以以同样的方法处理

Promise 对象有以下两个特点:

  1. 对象的状态不受外界影响Promise对象代表一个异步操作,有三种状态:pending(进行中)、fulfilled (已成功)和rejected(已失败)。只有异步操作的结果,可以决定当前是哪一种状态,任何其他操作都无法改变这个状态。Promise 的英文意思是“承诺”,就是答应了就不会反悔的意思

  2. 一旦状态改变就不会再次发生改变:任何时候都可以得到这个结果。Promise对象的状态只有两种可能:从pending 变为 fulfilled 或者从 pending 变为 rejected 。只要这两种情况发生改变,状态就会凝固,不会再变了,会一直保持这个结果,如果改变已经发生,你再对 Promise 对象添加回调函数,也会立即得到这个结果。这与事件完全不同(事件的特点是,如果你错过了它,再去监听,是得不到结果的)。

Promise构造函数

ES6规定,Promise对象是一个构造函数,用于生成 Promise实例

const promise = new Promise((resolve,reject) =>{
if('异步操作成功'){
resolve(value)
}else{
reject(error) //失败执行reject
}
})

我举个例子吧,就用 技术蛋老师 的那个例子会比较有趣:

一个女人问男人,我怀孕了你会跟我结婚吗,男人说会的,这时候就是给了女人一个承诺(Promise),但是女人到底有没有怀孕男人现在是不知道的,如果怀孕了就是叫女人为孩子他妈(fulfilled),如果没有怀孕就叫女人为老婆(rejected),男人还是要兑现承诺跟女人结婚,十个月之后,如果孩子生出来了就叫女人为“孩子他妈”(then),女人没有生出孩子来就只能叫女人为“老婆”了(catch

Promise在一开始都是 pending状态,之后执行完逻辑后就会变成 setted(fulfilled或rejected)

Promise 进入到 fulfilled 状态是会调用 then 函数

Promise 进入到 rejected 状态是会调用 catch 函数

Promise 进入到 setted 状态是会调用 finally 函数

const isMaternity = true
function Marry(){
return new Promise((resolve,reject) =>{
setTimeout (() =>{
if(isMaternity){
resolve()
}else{
reject()
}
},1000)
})
}
Marry().then(res =>{
console.log('孩子他妈')
})
.catch(()=>{
console.log('老婆')
})
.finally(() =>{
console.log('结婚')
})

箭头函数

箭头函数表达式的语法比较普通函数表达式更加简洁,并且没有自己的 thisarguments , supernew.target 。箭头函数表达式更加适用于那些本来需要匿名函数的地方,并且它不能用作构造函数

引入箭头函数有两个主要的作用:更加简短的函数并且不绑定 this

  1. 更短的函数
const arr1 = ['HelloWord' ,'javascript']
arr1.map(function (item) {
    return item.length
}) 
//返回数组[9,10]

//把上面的普通函数换成箭头
arr1.map((item)=> {
    return item.length
}) //[9,10]
//如果只有一个参数可以把括号去掉
arr1.map(item => {
    return item.length
}) //[9,10]
//当箭头函数的函数体只有一个 return 语句时,可以省略 `return` 关键词和方法体的花括号
arr1.map(item =>  item.length) //[9,10]
  1. 没有单独的 this

在箭头函数出现之前,每一个新函数根据它是被如何调用的来定义函数的 this值:

  1. 如果该函数是一个构造函数,this 指针指向一个新的对象
  2. 在严格模式下的函数调用下,this 指向 undefined
  3. 如果该函数是一个对象的方法,则它的 this 指针指向这个对象
function Person(){
//Person()构造函数定义 `this` 作为它自己的实例
this.age = 0
 setInterval(function growUp() {
 //在非严格模式,growUp() 函数定义  `this` 作为全局对象,与在 Person() 构造函数中定义的 `this`并不同。
 this.age++
 },1000);
 }
 var p = new Person();

箭头函数不会创建自己的 this 它只会从自己的作用域的上一层继承 this 。因此,在下面的代码中,传递给 setInterval 的函数内的 this 与封闭函数的this值相同:

function Person(){
 this.age = 0;
 setInterval(() =>{
this.age++;  //|this| 正确地指向 p 实例
},1000);
}
var p = new Person()

现在我们大概了解了箭头函数的this,那么它跟普通函数有什么区别呢?

普通函数中的 this

  1. 总是代表着它的直接使用者,如 obj.fn ,fn 里的最外层this就是指向 obj
  2. 默认情况下,没有直接调用者,this 指向 window
  3. 严格模式下,this 为 undefined
  4. 当使用 call 、apply 、bind绑定的,this指向绑定对象 :

call 方法的第一个参数是 This 的指向,后面传入的是一个参数列表。当第一个参数为 null 、undefined 的时候,默认指向 windown 。如 getColor(obj,'green','red','blue')

apply方法接收两个参数,第一个参数是 this 的指向,第二个参数是一个参数数组。当第一个参数为 null 、undefined 的时候,默认指向 windown 。 getColor(obj,['green','red','blue])

bind 方法和 call 方法很相似,第一个参数是 this 的指向,从第二个开始接收的是参数列表。区别在于 bind 方法返回值是函数以及 bind 接收的参数列表的使用

var id = 'global'
let fun1 = () =>{
    console.log(this.id)
}
fun1()
fun1.call({id:'obj'})  //global
fun1.apply({id:'obj'})  //global
fun1.bind({id:'obj'})  //global

箭头函数中的 this

  1. 默认指向定义它时,所处上下文的对象的 this 指向。即ES6 箭头函数里 this 的指向就是上下文里对象 this 指向,偶尔没有上下文对象,this就指向 windown
  2. 即使是 call 、 apply 和 bind 等方法也不能改变箭头函数 this 的指向
//fn 是全局函数,没有直接调用它的对象,也没有使用严格模式,this 指向 windown
function fn(){
console.log(this) //windown  
}
fn()

 //fn 是全局函数,没有直接调用它的对象,但是指定了严格模式,this 指向 undefined
 function fn(){
 'use strict'
 console.log(this)
 }
 fn()
 
 //fn 直接调用者是 obj ,第一个 this 指向 obj,setTimeout 里匿名函数没有直接调用者, this 指向 windown
 const obj = {
 num:10
 fn:function() {
 console.log(this)  //obj
 setTimeout(function () {
 console.log(this)  //windown
 })
 }
 }
 obj.hello();
 
 //fn 直接调用者是 obj ,第一个 this 指向 obj,setTimeout箭头函数,this 指向最近的函数的 this 指向,也就是 obj 
 const obj = {
 num: 10
 fn:function (){
 console.log(this);  //obj
 setTimeout(() =>{
 console.log(this)  //obj
 });
 }
 }
 obj.hello()
 
 
 //diameter是普通函数,里面的this指向直接调用它的对象obj。perimeter是箭头函数,this应该指向上下文函数this的指向,这里上下文没有函数对象,就默认为window,而window里面没有radius这个属性,就返回为NaN
 const obj = { radius: 10, diameter() { return this.radius * 2 }, perimeter: () => 2 * Math.PI * this.radius } console.log(obj.diameter()) // 20 console.log(obj.perimeter()) // NaN

再温习一个箭头函数的概念:

箭头函数没有自己的 this ,它会捕获自己在定义时(注意,是定义时,而不是调用时)所处的外层执行环境的 this ,并继承这个 this 值。所以箭头函数中 this 的指向在它被定义的时候就已经确定了,之后永远不会改变

var id = 'golba'
function fun1(){
    setTimeout(function(){
        console.log(this.id)
    },2000)
}
function fun2(){
    setTimeout(()=>{
        console.log(this.id);
    },2000)
}
fun1.call({id:'obj'})  //golbal
fun2.call({id:'obj'})  //obj

上面这个例子,函数fun1中的setTimeout中使用普通函数,2秒后函数执行时,这时函数其实是在全局作用域执行的,所以this指向Window对象,this.id就指向全局变量id,所以输出'Global'。 但是函数fun2中的setTimeout中使用的是箭头函数,这个箭头函数的this在定义时就确定了,它继承了它外层fun2的执行环境中的this,而fun2调用时thiscall方法改变到了对象{id: 'Obj'}中,所以输出'Obj' (注意上面的定义时而不是调用时)

var id = 'global'
var obj = {
    id:'obj',
    a:function(){
        console.log(this.id)
    },
    b:() =>{
        console.log(this.id)
    }
}
obj.a() //obj
obj.b()  //global

对象obj的方法a使用普通函数定义的,普通函数作为对象的方法调用时,this指向它所属的对象。所以,this.id就是obj.id,所以输出'OBJ'。 但是方法b是使用箭头函数定义的,箭头函数中的this实际是继承的它定义时所处的全局执行环境中的this,所以指向Window对象,所以输出'GLOBAL'。(这里要注意,定义对象的大括号{}是无法形成一个单独的执行环境的,它依旧是处于全局执行环境中!!

箭头函数继承而来的this指向永远不变,上面的例子可以看出箭头函数继承来的 this 指向永远不变。对象 obj 的方法 b 是使用箭头函数定义的,这个函数的 this 就永远指向它定义时所处的全局执行环境的 this ,即便这个函数是作为对象 obj 的方法调用,this依旧指向 `windown·

我们可能听说过类,在实际开发中一般比较少会用到类。那么类是什么呢?

实际上,类是“特殊的函数”,就像你能够定义的函数表达式和函数声明一样,类语法有两部分组成:类表达式和类声明

类声明

定义类的一种方法是类声明。要声明一个类,你可以使用带有 class 关键字的类名

class Person{
constructor (name,age){
this.name = name
this.age = age
}
}

函数声明类声明之间的一个重要区别是:函数声明会提升,而类不会。你要先声明你的类,然后再访问它,否则类似以下的代码将抛出 ReferenceError

let p = new Person()  //ReferenceError
class Person{}

//...................
let p1 = new Person1()  //不报错
function Person1(){}

使用 extends 扩展子类

exends 关键字在 类声明和类表达式中用于创建一个类作为另一个类的子类

class Animal {
    constructor(name){
        this.name = name;
    }

speak(){
    console.log(`${this.name} makes a noise`);
}
}


class Dog extends Animal {
    constructor(name){
    //如果子类定义了构造函数,那么它必须先调用`super` 才能使用 `this`
        super(name)
    }

speak(){
    console.log(`${this.name} barks.`)
}
}

var d = new Dog('Mitzie')
d.speak()  //Mitzie barks.

模块化(Module

在日常开发中我们常用到导入(import)、导出(export),其实这个ES6新增的模块加载

ES6 之前社区制定了一些模块加载方案,主要 Common JSAMD ,前者用于服务器,后者用于浏览器

CommonJS主要用在node开发上,每个文件就是一个模块,每个文件就是一个模块,每个文件都有自己的一个作用域。通过module.exports暴露public成员。通过require引入模块依赖

let x = 1;
let addx = function (value) {
retuen value + x
}
module.export.x = x
module.export.addx = addx

//使用 `require` 引入
var example = require('暴露文件路径')

ES6 module 和 Common JS 以及 AMD的比较

CommonJS 和 AMD 模块 , 采用的是 “运行时加载” , 是将暴露的变量、方法等构成一个对象,然后再从这个而对象上读取对应的方法。也就是说会将所有的方法都进行加载。

ES6模块 ,采用的是 “编译时加载”或者静态加载。即只加载需要的方法,其他的方法不加载。

ES6的模块自动采取严格模式,不管你有没有在模块头部加上‘use strict’

严格模式主要有以下限制 :

  • 变量必须声明后再使用
  • 函数参数不能有同名属性,否则报错
  • 不能使用with 语句
  • 不能对只读属性赋值,否则报错
  • 不能使用 arguments.callee
  • 不能使用 arguments.calles
  • 禁止this 指向全局对象
  • 不能使用前缀0表示八进制数,否则报错
  • 不能删除变量delete prop,会报错,只能删除属性delete global[prop]
  • eval不会在它的外层作用域引入变量
  • eval和arguments不能被重新赋值
  • arguments不会自动反映函数参数的变化
const lastName = "Jone"
export {lastName}


import {lastName} from '路径'

Proxy

proxy :用于创建一个对象的代理,从而实现基本操作的拦截和自定义(如属性查找、赋值、枚举、函数调用等)

var proxy = new Proxy(target,hander)

target 表示所要拦截的目标对象(任何类型的对象,包括原生数组、函数、甚至另一个代理)

handler 通常以函数作为属性的对象,各属性的函数分别定义了在执行各种操作时的代理行为

参数 handler 属性:

  • get(target,propKey,receiver):拦截对象属性的读取
  • set(target,propKey,value,receiver):拦截对象属性的设置
  • has(target,propKey) : 拦截 propKey in proxy 的操作,返回一个布尔值
  • deleteProperty(target,propKey) :拦截 delete proxy[propKey]的操作,返回一个布尔值
  • ownKeys(target):拦截 Object.keys(proxy) 、 for...in 等循环,返回一个数组
  • getOwnProertyDescriptor(target,proKey):拦截 Object.getOwnProertyDescriptor(target,proKey) ,返回属性的描述对象
  • defineProperty(target,propKey,propDesc):拦截 Object.defineProperty(target,propKey,propDesc)返回一个布尔值
  • preventExtensions(target):拦截 Object.preventExtensions(proxy) ,返回一个布尔值
  • getPrototypeOf(target) :拦截 Object.getPrototypeOf(proxy) ,返回一个对象
  • isExtensible(traget):拦截 Object.isExtensible(proxy) ,返回一个布尔值
  • setPrototypeOf(target,proto):拦截 Object.setPrototypeOf(target,proto) ,返回一个布尔值
  • apply(target,object,args):拦截proxy实例作为函数回调的操作
  • construct(target,args):拦截proxy实例作为函数回调的操作

看到proxy ,有没有跟 Object.defineProperty 非常相似呀,其中 get 和 set 属性都是一样的

Object.defineProperty的作用:直接在一个对象上新增一个属性、修改和删除已有的属性值,并返回这个对象 ,同时proxy也具备这些作用,而且性能更好。

它们最大的区别就是,拦截修改的对象不同,先看Object.defineProperty的参数(object,propName,descriptor)

  • object对象 =>对哪个对象进行操作
  • propName属性名 =>要操作对象里的哪个属性
  • descriptor属性描述 => 加的这属性有什么特性(属性的值,可以是数字、字符串和方法等)
var user ={ name:"嘿嘿" } 
var count = 12; //定义一个age 获取值时返回定义好的变量count 
Object.defineProperty(user,"age",{
//value:18
get:function(){ return count;
}
}) 
console.log(user.age);//12

proxy 拦截的是整个对象,它的参数只有两个(target,handler)

在使用 proxy之前先介绍一下 relect

ES6 中操作对象而提供的新 API ,若需要在 Proxy内部调用对象的默认行为,建议使用 reflect,它的特性:

  • 只要proxy对象具有的代理方法(handler),reflect 对象全部都有,以静态方法的形式存在
  • 修改某些 object方法的返回结果,让其变得更合理(定义不存在属性行为的时候不报错而是返回false)
  • objet 操作都变成函数行为

**get()**方法

let person = {
    name:"huahua"
}
let proxy = new Proxy(person,{
    get:function(target,propKey,receiver){
        return Reflect.get(target,propKey,receiver)
    }
})

console.log(proxy.name) //huahua

get还能对数组的增删改查进行拦截

function createArray(...elements) { 
let handler = { 
get(target, propKey, receiver) {
let index = Number(propKey);
if (index < 0) {
propKey = String(target.length + index);
} 
return Reflect.get(target, propKey, receiver);
}
};
let target = [];
target.push(...elements);
return new Proxy(target, handler);
} 
let arr = createArray('a', 'b', 'c');
arr[-1] // c

set()

const obj = {name:"张三",age:18};
 const proxy = new Proxy (obj,{
    get(target,prop){
        if(prop in target){
            return  Reflect.get(target,prop);
        }else{
            console.error("字段不存在")
            return undefined
        }
    },
    set(target,propKey,value,receiver){
        if(propKey === "age"){
            if(typeof value === "number"){
                return Reflect.set(target,propKey,value,receiver);
            }else{
                console.log("年龄只能输入正整数");
                return false;
            }
        }else{
            return false
        }
    }
 })
 proxy.age = 20
 console.log(proxy.age)

proxy.age = "20"
 console.log(proxy.age)
 console.log(proxy.tar)
// 20
//年龄只能输入正整数
//20
//字段不存在
//undefined

使用场景:

  • 拦截和监视外部对象的访问
  • 降低函数或类的复杂度
  • 在复杂操作前对操作进行校验或者对所需资源进行管理

使用proxy 保障数据类型的准确性

let data = {num:0}
data = new Proxy(data,{
   set(target,key,value,proxy){
    if(typeof value !== 'number'){
        console.error('属性只能是number类型')
    }
    return Reflect.set(target,key,value,proxy)
   }
})
data.num = 'foo' //属性只能是number类型