JavaScript
中一旦被定义就无法再被修改的变量,称之为常量。
ES6中通过const
定义常量,常量通常用大写字母定义,多个单词之间用_分隔。
const
定义常量后,如果修改常量会报错:
const PI = Math.PI;
PI = 100;
这是const
定义常量的特点之一。
但当我们使用const
定义常量,而赋值的是一个引用类型值,再修改常量可不一定报错了!!!
const SKILLS = {
CSS: 1 ,
JS: 2,
HTML: 3
WEB_GL: 4
}
SKILLS.CSS = 2
const SKILLS_MAP = new Map([['CSS',1],['JS',2]])
SKILLS_MAP.set('HTML',3)
上述示例代码,即使进行了修改,它也不会出现任何报错。
回到真实企业项目,团队开发团队中每一位成员技术水平肯定是有区别的。
当定义一个全局常量const SKILLS = {CSS: 1 ,JS: 2,HTML: 3,WEB_GL: 4}
,如果团队成员分别对SKILLS
进行增删改查,由于不会报错极可能不小心造成SKILLS
值被修改,影响到其他依赖SKILLS
常量的功能模块。
const.js
export const SKILLS = {CSS: 1 ,JS: 2,HTML: 3,WEB_GL: 4}
成员a:
xxx.js
import {SKILLS} from "const.js"
SKILLS.delete('CSS')
SKILLS.set("TS",1)
//...
成员b
yyy.js
import {SKILLS} from "const.js"
const css = SKILLS.CSS
//...
如上述,成员a在xxx.js
文件中删除SKILLS
中属性CSS
,而成员b在yyy.js
文件中读取SKILLS
中属性CSS
。如果xxx.js
优先执行,yyy.js
文件中SKILLS.CSS
肯定为undefined
,将会影响yyy.js
文件后续的逻辑。
项目、团队越大,类似问题出现的机率越高,Bug防不胜防。
这时候,常量修改报错就非常重要了!!!。
本文目的就是记录让其报错的方法。
对象、数组只读
在Deep freeze object - 30 seconds of code中已经有示例,关键是Object.freeze()
方法,它可以冻结一个对象。一个被冻结的对象再也不能被修改,由此达到对象、数组只读的目的。
对象、数组进行遍历,判断值类型,递归手段进行深层冻结:
const getType = (value) => {
const reg = /^\[object\s(\w+)\]$/;
const type = toString.call(value);
return reg.exec(type)[1]
}
const or = (param1, param2) => param1 || param2;
const isObject = (value) => {
const type = getType(value);
return type === 'Object'
}
const isArray = (value) => {
return Array.isArray(value)
}
const isFunc = (callBack) => {
let type = typeof callBack
return type === 'function'
}
const each = (origin, callBack) => {
const isArr = isArray(origin)
if (!or(isObject(origin), isArr)) return;
if (!isFunc(callBack)) return
const keys = Object.keys(origin);
for (let i = 0; i < keys.length; i++) {
const key = keys[i];
callBack(isArr ? i : key, origin[key], origin);
}
}
const types = [getType([]), getType({})]
function deepFreeze(obj) {
const type = getType(obj)
if (!types.includes(type)) return obj
if (or(isObject(obj), isArray(obj))) {
each(obj, (key, value) => {
if (typeof value === "object") {
obj[key] = deepFreeze(value)
}
})
return Object.freeze(obj)
}
}
测试:
const obj = deepFreeze({ name: 'deepFreeze', age: 20 })
const arr = deepFreeze([1,2])
delete obj.name
arr[1] = 'deep'
console.log(obj);//{ name: 'deepFreeze', age: 20 }
console.log(arr);//[1,2]
delete obj.name
删除对象属性;arr[1] = 'deep'
重新赋值;它们均不会修改原数据,但也没有报错。
我要它报错,它居然没有给我报错,为啥呢?🥶
官网MDN给出了答案:
严格模式下,才会进行报错。那我们开启严格模式,文件开头添加'use strict'
'use strict'
//...
这个时候再次执行就会出现我们想看到的报错结果了。
Set、Map只读
Object.freeze()
对数据、对象可以冻结,但却无法阻止Set、Map
类型的增删改。
const SKILLS_MAP = Object.freeze(new Map())
SKILLS_MAP.set(1,2)
Map.set()
是一个方法。
如果我们在Map.set
时报错,或重新赋值覆盖原有Map.set()
方法逻辑Map.set = function (){throw new Error('..')}
这样即可满足我们的目的。
又有一个问题是我们怎么知道Map.set
何时何地会被调用呢?
有人也许会提出使用Map
原型:
Map.prototype.set = ()=>{throw new Error('')}
可是修改原型,将导致正常的Map
对象不能修改,明显不能满足我们的需求。
const m = Map()
m.set(1,2);//报错
其实,我们可以参考Vue3响应性原理,对Map、Set
数据源进行Proxy代理捕获。
//...
const isSet = (obj) => getType(obj)==='Set'
const isMap = (obj) => getType(obj)==='Map'
const keys = ['add', 'delete', 'set']
//...
首先定义一个keys
,用于存储Set、Map
中会修改源数据的方法名。
//...
function deepFreeze(obj) {
const type = getType(obj)
if (!types.includes(type)) return obj
if (or(isObject(obj), isArray(obj))) {
each(obj, (key, value) => {
if (typeof value === "object") {
obj[key] = deepFreeze(value)
}
})
return Object.freeze(obj)
}
//新增代码
if (or(isMap(obj), isSet(obj))) {
//遍历Set、Map值,深层处理
Array.from(obj.values()).forEach(value => {
deepFreeze(value)
})
return new Proxy(obj, {
get(target, key) {
if (keys.includes(key)) {
throw new Error('只读,不允许修改!')
}
return isFunc(target[key]) ? target[key].bind(target) : target[key]
},
set() {
throw new Error('只读,不允许修改或添加属性!')
}
})
}
}
if (keys.includes(key)) {throw new Error('只读,不允许修改!')}
当key为add
, delete
, set
之一时,我们需要进行处理,阻止它执行;这里是Map.set
就会抛出错误,当然也可以返回一个函数:
//...
if (keys.includes(key)) {
return ()=>{
throw new Error('只读,不允许修改!')
}
}
//...
抛出错误的时机是Map.set()
调用。
//...
return isFunc(target[key]) ? target[key].bind(target) : target[key]
//...
像Map.get,Map.size、Map.forEach...
等属性或方法,它们并不会改变源数据,所以我们不做任何处理,该咋样就咋样,注意this
指向即可。
至此,一个创建对象、数组、Set、Map只读的方法完成了。
完整代码链接:deepFreeze.ts
最后
原创不易!如果我的文章对你有帮助,你的👍就是对我的最大支持^_^。