定义一个常量,它真的不变吗?

999 阅读4分钟

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

最后

原创不易!如果我的文章对你有帮助,你的👍就是对我的最大支持^_^。