前文《防止浏览器控制台修改网页数据与函数的方法》给出了几个解决方法,需要对对象的每个属性或函数使用Object.defineProperty()配置其元特性configurable、enumerable、writable等。显然,这些操作缺乏灵活、不够实用、不能通用。
1、javascript对象的元特性
javascript对象的成员(数据或函数)均有一组元特性,约束成员的是否可配置(configurable)、可枚举(enumerable)、可改写(writable),以及提供value值和getter/setter读写器。一旦configurable为false,那么不能重定义其他的特性(writable除外,仍然可以由true改为false)。如果configurable为true,即使wrtiable为false,仍然可以通过Object.defineProperty()或Reflect.defineProperty()函数修改其value(此时不能通过类似obj.val = 1的方式修改其值)。
定义或修改元特性的语法如下:
- Object.defineProperty(obj, propName, {元特性}, value)
- Reflect.defineProperty(obj, propName, {元特性}, value)
Object与Reflect两个同名方法的区别在于:前者定义成功返回对象、失败抛出异常;后者成功返回true、失败返回false。另一个区别是,Reflect支持的最低版本是ES6(与Proxy一样)。
2、保护window和对象(以String.prototype为例)上新增的属性和函数
ES6中的Proxy类和Reflect类提供了通用、一致的便捷方法,只需要给被保护的对象(如window对象、String.prototype原型、String构造函数等)关联一个代理对象,在代理对象上新增属性或函数,就可以自动配置其元特性{configurable: false, enumerable: true, writable: false},从而保护数据(属性值)或函数被删除或修改。
下面代码是一个关联window对象和String.prototype原型的两个代理类,分别增加其函数sayOk(),然后修改函数定义,结果是:修改后的函数无效。
<!DOCTYPE html>
<script>
/* 定义代理处理器handler,同时也是window的一个不可修改的函数 */
/* Reflect与Object的defineProperty调用区别,后者可能抛出TypeError */
Object.defineProperty(window, "handler", {
configurable: false, enumerable: true, writable: false, value: {
set(target, prop, propValue, receiver) {
Object.defineProperty(target, prop, {configurable: false, enumerable: true, writable: false, value: propValue});
}
}
});
const WindowProxy = new Proxy(window, handler); /* window 的代理 */
const StringProtoProxy = new Proxy(String.prototype, handler); /* String原型的代理 */
WindowProxy.sayOk = function(){console.log("win ok.");}; /* 使用代理新增函数 */
StringProtoProxy.sayOk = function(){console.log("str ok.")}; /* 使用String原型代理新增函数 */
sayOk(); /* 输出win ok.,新增window的函数成功 */
"ok".sayOk(); /* 输出str ok.,新增String原型的函数成功 */
window.sayOk = function(){console.log("win no.");}; /* 在window上重定义函数 */
String.prototype.sayOk = function(){console.log("win no.");}; /* 在String原型上重定义函数 */
sayOk(); /* 仍然输出win ok.,在window上重定义函数无效 */
"ok".sayOk(); /* 任然输出str ok.,在String原型上重定义函数无效 */
try{
WindowProxy.sayOk = function(){console.log("win no.");}; /* 在window代理上重定义函数报异常 */
StringProtoProxy.sayOk = function(){console.log("str no.");}; /* 在String原型代理上重定义函数报异常 */
} catch(err){
console.log("重定义函数异常: " + err); /* 输出异常消息,不能重定义 */
}
sayNo = function(){console.log("win no.");}; /* 在window对象上新增函数 */
String.prototype.sayNo = function(){console.log("str no.");}; /* 在String原型上新增函数 */
sayNo(); /* 输出 win no.,在window上新增函数成功 */
"ok".sayNo(); /* 输出 str no.,在String原型上新增函数成功 */
sayNo = function(){console.log("win no no.");} /* 在window对象上重定义函数 */
String.prototype.sayNo = function(){console.log("str no no.");}; /* 在String原型上重定义函数 */
sayNo(); /* 输出 win no no.,在window上重定义函数成功 */
"ok".sayNo(); /* 输出 str no no.,在String原型上重定义函数成功 */
</script>
</html>
上文的技术要点是:创建一个通用的处理器handler,并关联到一个代理Proxy对象。然后,在代理对象上添加函数,这些新增的函数将自动配置成{configurable: false, enumerable: true, writable: false}元特性,使得它们不能被删除或修改。同样,新增数据属性结果相同。
3、保护网页<input>元素的value值
前文《防止浏览器控制台修改网页数据与函数的方法》提出方法时:获取该值后另存为window对象的一个不可修改的数据属性,可以有效保护该数据被删除或修改,但使用起来不够直接明了。
下面代码可以直接保护<input>元素的value属性,使得该值正常读取、不能被删除或修改(代码中还给出了jQuery读写的示例)。
<!DOCTYPE html>
<head>
<script type="text/javascript" src="./jquery/jquery-1.12.4.min.js"></script>
</head>
<body>
输入文本1:<input id="tb1" type="text" value = "1" /> <br/>
输入文本2:<input id="tb2" type="text" value = "2" />
</body>
<script>
/* 1) value是原型的,该语句等价新建input对象的自有属性value */
/* 2) 建议锁住defaultValue,在程序中使用defaultValue代替value */
lockInputValue = function(id){
let e = document.getElementById(id);
if(!e || e instanceof HTMLInputElement === false) throw new Error("the param is not valid.");
Object.defineProperty(e, "value", {configurable: false, enumerable: true, writable: false, value: e.value});
}
let tb1 = document.getElementById("tb1");
console.log(tb1.value); // 输出1;
lockInputValue("tb1"); // 锁住tb的value属性
tb1.value = "111"; // 在UI界面上输入111,结果一样
console.log(tb1.value); // 仍然输出1,修改值无效
$('#tb1').val("111"); // jQuery修改value值
console.log($("#tb1").val()); // 仍然输出1,修改值无效
</script>
</html>
因为是保护已经存在的若干对象,不需要使用代理Proxy,直接使用Object.defineProperty()即可。
需要特别指出,不同于配置一般对象的元特性,Object.defineProperty()设置value的元特性时,还需要给出value值。原因是,value不是HTMLInputElement元素的自有属性,而是其原型上继承来的属性,而Object.defineProperty()只能配置对象自有属性的元特性。事实上,上述代码在配置value元特性时,等价新建了一个<input>对象的自有value属性(该属性隐藏了原型的value属性,相当于派生类隐藏了基类属性),在配置其元特性时需要给出并保留原value的值。