JS 语言的动态性,使我们能够修改对象乃至类型的成员,主要有两种方式:
- 对 proto/prototype 进行操作,修改原型对象;
- 使用代理 Proxy 对对象进行一次封装,返回包装后的代理对象给用户使用
一、通过修改原型对象
/*增删改 Excel 对象的成员样例*/
class ModifyRangeTypeExample {
static AddMember() {
let rng = new Range('B3');
//1.通过 __proto__ 对象增加成员,是可行的;
//2.添加方法时,不能建议使用箭头函数,因为在它内部
// this is undefined,除非你增加的方法不需要访问
// 实例本身
ActiveCell.__proto__.AddressR1C1 = function(){
return this.Address(false, false, xlR1C1);
}
Console.log(rng.AddressR1C1());
Console.log(rng.Address(false, false, xlR1C1));
//3.可以添加常字段形式的属性,每个实例访问到的此成
// 员将是一样的值
ActiveCell.__proto__.ABCDE = 12345;
Console.log(rng.ABCDE);
Console.log(ActiveCell.ABCDE);
//4.也可以添加带有 getter/setter 的非字段形式的属性
//4.1.通过 Object.create() 来实现 Data 属性
ActiveCell.__proto__ = Object.create(
ActiveCell.__proto__, {
Data : {
get() {
return this.Value();
},
set(value) {
this.Value2 = value;
},
configurable : true
}
});
Console.log(rng.Data);
rng.Data = 'Data Property';
Console.log(rng.Data);
//4.2.通过 Object.defineProperty() 来实现 Bold 属性
Object.defineProperty(ActiveCell.__proto__,
'Bold', {
get() {
return this.Font.Bold;
},
set(value) {
this.Font.Bold = value;
},
configurable : true
});
let isBold = rng.Font.Bold;
Console.log('isBold?:' + isBold);
rng.Bold = !isBold;
Console.log('isBold?:' + rng.Bold);
Console.log('isBold?:' + rng.Font.Bold);
//4.3.通过 Object.defineProperties 来实现
// Italic/Underline 属性
Object.defineProperties(ActiveCell.__proto__,
{
Italic : {
get() {
return this.Font.Italic;
},
set(value) {
this.Font.Italic = value;
},
configurable : true
},
Size : {
get() {
return this.Font.Size;
},
set(value) {
this.Font.Size = value;
},
configurable : true
}
});
//Italic Property Test
let isItalic = rng.Font.Italic;
Console.log('isItalic?:' + isItalic);
rng.Italic = !isItalic;
Console.log('isItalic?:' + rng.Italic);
Console.log('isItalic?:' + rng.Font.Italic);
//Size Property Test
Console.log('font size?:' + rng.Font.Size);
rng.Size = 9;
Console.log('new font size?:' + rng.Size);
Console.log('new font size?:' + rng.Font.Size);
//5.通过属性描述符来定义属性
/*描述符(https://segmentfault.com/a/1190000003882976):
有数据型描述符和存取型描述符。
两种类型的描述符都可以有 configurable 和 enumerable 描述符
configurable : 它是 true 时,表明属性可以被覆写和删除,
默认值是 false.
enumerable : 它是 true 时,属性可以被 for...in 枚举到;默认 false
数据型描述符,特有的是 value 和 writable 描述符:
value : 提供属性值
writable : 它是 true 时,属性值可变;默认值是 false
存取型描述符(上面 4.1-4.3 都是),特有的是 get 和 set 描述符
get : 给属性提供 getter,返回值用作属性值;如果不提供它,则为 undefined
set : 给属性提供 setter,用来修改属性值,有惟一参数;不提供为 undefined
*/
Object.defineProperty(ActiveCell.__proto__,
'Smile', {
value : '^_^',
writable : false,
enumerable : true,
configurable : true
});
Console.log(rng.Smile);
try {
//这句会报错
rng.Smile = '-_-';
} catch { }
Console.log(rng.Smile);
}
static ReplaceMember() {
//1.JS 是基于原型链的,你可以通过 __proto__ 或者
// prototype 访问原型,它也是个对象
//2.你可以为原型对象,添加成员来为属于它的所有实例
// 添加更多属性与操作(方法)
//3.对象被构造时固有的成员,你可以通过
// Object.getOwnPropertyNames() 方法来取得,且它
// 不包含通过 __proto__ 或 prototype 定义在原型上
// 的成员
//4.因为实例的固有成员的优先级,要高于附加在
// __proto__ 或 prototype 上的同名成员,所以虽然你可
// 以为原型对象加上同名的成员,但实例对象是无法直接访
// 问到它们的,所以想覆写固有成员,是做不到的
//4.1.在 __proto__ 上创建同名方法,以图覆写它:
ActiveCell.__proto__.Delete = function() {
Console.log('Deleting range ' + this.Address());
}
let rng = new Range('B2:C3');
for (let i = 1; i <= rng.Cells.Count; i++)
rng.Cells.Item(i).Value2 =
rng.Cells.Item(i).Address();
let sht = rng.Worksheet;
//由以下两个输出,你会了解到实例直接访问到
//的还是被构造时固有的 Delete() 方法
Console.log(sht.Range('B2').Value2);
rng.Cells.Item(1).Delete();
Console.log(sht.Range('B2').Value2);
//但是你仍可以通过如下方式,访问到同名方法
rng.__proto__.Delete.call(rng);
//4.2.在 __proto__ 上创建同名属性,以图覆写它:
Object.defineProperty(ActiveCell.__proto__, 'Text', {
get() {
return this.Address() + " : " + this.Value2.toString();
},
set(value) {
this.Value2 = value;
},
configurable : true,
enumerale : true
});
let cell = sht.Range('B2');
Console.log(cell.Text);
try {
//这句会报错,因为固有的 Text 属性是只读的
cell.Text = 321;
} catch { }
Console.log(cell.Text);
//你总可以通过属性的描述符对象拿到 getter/setter,如果提供了
let desc = Object.getOwnPropertyDescriptor(
ActiveCell.__proto__, 'Text');
let setter = desc.set;
setter.call(cell/*第一个参数绑定 this*/, 111/*设定新值*/);
let getter = desc.get;
Console.log(getter.call(cell/*第一个参数绑定 this*/));
}
static DeleteMember() {
//1.删除原型对象上一个不存在的成员
let result = 'AreYouOkey' in ActiveCell.__proto__ &&
delete ActiveCell.__proto__.AreYouOkey;
Console.log('删除 AreYouOkey 成功?:' + result);
//2.删除原型上已经存在的成员
ActiveCell.__proto__.YesItIs = 857;
result = 'YesItIs' in ActiveCell.__proto__ &&
delete ActiveCell.__proto__;
Console.log('删除 YesItIs 成功?:' + result);
let rng = new Range('B2');
//3.删除对象上已经存在的属性
try {
//会失败,因为 configurable = false
result = 'Text' in rng &&
delete rng.Text;
} catch { result = false; }
Console.log('删除 Text 成功?:' + result);
//4.删除对象上已经存在的方法
try {
//会失败,因为 configurable = false
result = 'Address' in rng &&
delete rng.Address;
} catch { result = false; }
Console.log('删除 Address() 成功?:' + result);
//5.对于对象固有属性,可以通过
// Object.getOwnPropertyDescriptor(obj, pptName) 方法
// 来查看成员的描述符,由 configurable 描述符来确定它是
// 否支持 delete 操作
//5.1.固有属性的描述符
let textDesc = Object.getOwnPropertyDescriptor(rng, 'Text');
Console.log(JSON.stringify(textDesc, undefined, 4));
//5.2.固有方法的描述符
let addressDesc = Object.getOwnPropertyDescriptor(rng, 'Address');
Console.log(JSON.stringify(addressDesc, undefined, 4));
}
static RunAll() {
let padCenter = (str) => {
let restWidth = 56 - str.length;
let left = Math.floor(restWidth / 2);
let right = left + restWidth % 2;
return '-'.repeat(left) + str + '-'.repeat(right);
}
Console.log(padCenter(ModifyRangeTypeExample.AddMember.name));
ModifyRangeTypeExample.AddMember();
Console.log(padCenter(ModifyRangeTypeExample.ReplaceMember.name));
ModifyRangeTypeExample.ReplaceMember();
Console.log(padCenter(ModifyRangeTypeExample.DeleteMember.name));
ModifyRangeTypeExample.DeleteMember();
ModifyRangeTypeExample.Clear();
}
//清除 RunAll() 调用后,创建的成员
static Clear() {
for (let name in ActiveCell.__proto__)
delete ActiveCell.__proto__[name];
}
}
在【立即窗口】里面输入 ModifyRangeTypeExample.RunAll();
然后回车,即可执行以上测试,其输出如下:
-----------------------AddMember------------------------
R[2]C[1]
R[2]C[1]
12345
12345
Data Property
isBold?:false
isBold?:true
isBold?:true
isItalic?:false
isItalic?:true
isItalic?:true
font size?:11
new font size?:9
new font size?:9
^_^
^_^
---------------------ReplaceMember----------------------
$B$2
$B$3
Deleting range $B$2:$C$3
$B$3
$B$3
$B$2 : 111
----------------------DeleteMember----------------------
删除 AreYouOkey 成功?:false
删除 YesItIs 成功?:true
删除 Text 成功?:false
删除 Address() 成功?:false
{
"value": "111",
"writable": false,
"enumerable": true,
"configurable": false
}
{
"writable": false,
"enumerable": true,
"configurable": false
}
综上:
- 我们总可以通过对象的 proto 访问到对象的原型,并在此原型对象上增添一些成员,这总是没问题的
- 因为对象被构造产生的成员的访问优先级,高于原型对象上增添的同名成员,所以实际上并不能覆写已有的成员
- 因为内置对象的成员大多数是 configurable == false,即不可改变与删除,所以删除成员就别想了
二、通过代理的方式
//居中填充
String.prototype.padCenter =
function(targetLength, padString = ' ') {
if (typeof targetLength != 'number')
throw new TypeError('Parameter "targetLength" ' +
'must be a number object.');
if (typeof padString != 'string') {
if (padString === null)
padString = 'null';
else
padString = padString.toString();
}
let padStrWidth = padString.length;
if (padStrWidth == 0) return this;
let restWidth = targetLength - this.length;
if (restWidth <= 0) return this;
let leftWidth = Math.trunc(restWidth / 2);
let rightWidth = leftWidth + restWidth % 2;
if (padString.length == 1) {
return padString.repeat(leftWidth) + this +
padString.repeat(rightWidth);
} else {
if (leftWidth == 0)
return this + padString[0];
else {
//leftPart
let leftRepeat = Math.trunc(leftWidth / padStrWidth);
let leftRest = leftWidth - leftRepeat * padStrWidth;
let leftStr = padString.repeat(leftRepeat) +
padString.substr(0, leftRest);
//rightPart
let rightRepeat = Math.trunc(rightWidth / padStrWidth);
let rightRest = rightWidth - rightRepeat * padStrWidth;
let rightStr = padString.repeat(rightRepeat) +
padString.substr(0, rightRest);
return leftStr + this + rightStr;
}
}
}
/*Proxy handler 可对应于
Reflect.apply()
Reflect.construct()
Reflect.defineProperty()
Reflect.deleteProperty()
Reflect.get()
Reflect.getOwnPropertyDescriptor()
Reflect.getPrototypeOf()
Reflect.has()
Reflect.isExtensible()
Reflect.ownKeys()
Reflect.preventExtensions()
Reflect.set()
Reflect.setPrototypeOf()
实现想要的代理
*/
class ES6ProxyTest {
//既然 Reflect.set(target, key, value[, receiver]) 有
//要设置的属性的键和值,我们就可以通过 set 代理,在一个
//对象的定义的外部:
//1.通过键,拦截对某些属性的写入
//2.通过值,检验类型,拦截非法写入
//3.通过键,重定向属性的写入,就像为属性设置一些假名一样
static SetProxy() {
let p = new Point(3, 8);
let pp = new Proxy(p, {
set:function(target, key, value, receiver){
//Reflect.set(target, key, value[, receiver])
//target : 用于接收属性(被代理的对象)的对象
//key : 要写入的属性的键(字符串或Symbol类型)
//value : 要写入的属性新值
//receiver : 如果 target 对象的 key 属性有 setter,
// receiver 则为 setter 调用时的 this 值。
//return : 返回一个 Boolean 值,表明操作的成败
let success = Reflect.set(target, key, value, receiver);
if (success) {
//Console 在此不可用
Debug.Print('property '+ key +' on '+
target + ' set to '+ value);
}
//必须返回操作成败状态,否则报错
return success;
}
});
pp.xxx = 13;
Console.log(p.xxx);
}
//既然 Reflect.get(target, key[, receiver]) 提供了要读取
//的属性的键,我们就可以通过 get 代理,在对象的定义的外部:
//1.通过键,拦截对某些属性的读取
//2.通过键,伪造一些不存在的属性
//3.通过键,实现类同假名的属性
static GetProxy() {
var obj = new Proxy({}, {
get: function (target, key, receiver) {
//Console 在此不可用
Debug.Print(`getting ${key}!`);
//Reflect.get(target, key[, receiver])
//target : 被代理的对象
//key : 要读取的属性的键(字符串或Symbol类型)
//receiver : 如果 target 对象的 key 属性有 getter,
// receiver 则为 getter 调用时的 this 值。
//return : 属性的值。
return Reflect.get(target, key, receiver);
}
});
obj.count = 1;
++obj.count;
}
/*Reflect.apply(target, thisArg, argsList)
target : 被代理对象,请确保它是一个 Function 对象
thisArg : 函数调用时绑定的对象
argsList : 函数调用时传入的实参列表,该参数应该是一个类数组的对象。
return : 调用函数返回的结果
通过这种代理:
1.检验调用时传入的参数
2.阻止函数被调用
*/
static ApplyProxy() {
function sum (...values){
return values.reduce((pre, cur) => pre + cur, 0);
}
let sumProxy = new Proxy(sum, {
apply : function(target, thisArg, argsList){
argsList.forEach(arg => {
if(typeof arg !== "number")
throw new TypeError("所有参数必须是数字,亲!");
});
return Reflect.apply(target, thisArg, argsList);
}
});
try {
let r = sumProxy(3, 5, 'hello');
Console.log(r);
} catch(e) {
Console.log(e.message);
}
Console.log(sumProxy(3, 8, 5));
}
/*Reflect.construct(target, argsList[, newTarget])
target : 被运行的目标构造函数
argsList : 类数组,目标构造函数调用时的参数。
newTarget : 可选,作为新创建对象的原型对象的 constructor 属性,
参考 new.target 操作符,默认值为 target。
*/
static ConstructProxy() {
function sum (...values){
return values.reduce((pre, cur) => pre + cur, 0);
}
let sumProxy = new Proxy(sum, {
construct:function(target, argsList){
throw new TypeError("亲,该函数不能通过 new 调用。");
}
});
try {
let x = new sumProxy(3, 5, 7);
} catch(e) {
Console.log(e.message);
}
}
//禁止向指定单元格区域写入数据
static ForbidSetValue() {
let rng = new Range('B1:D3');
rng.Value2 = 32;
let rngProxy = new Proxy(rng, {
set : function(target, key, value, receiver) {
if (key === 'Value2') {
throw new Error('无法设置属性')
} else
return Reflect.set(target, key, value, receiver);
}
});
try {
rngProxy.Value2 = 168;
} catch(e) {
Console.log(e.message);
}
Console.log(rngProxy.Text);
}
//运行所有测试用例
static RunAll() {
let members = Object.getOwnPropertyNames(ES6ProxyTest);
let notCall = ['length', 'prototype', 'name', 'RunAll'];
for (let member of members) {
if (!notCall.includes(member)) {
Console.log(member.padCenter(56, '-'));
eval(`ES6ProxyTest.${member}()`);
}
}
}
}
三、修改原型来汉化对象模型
/*借助原型对象汉化对象成员样例*/
class ChinesizationRangeTypeExample {
static Do() {
ActiveCell.__proto__.取地址 = function() {
return this.Address(...arguments);
};
Object.defineProperty(ActiveCell.__proto__, '值', {
get() { return this.Value2; },
set(value) { this.Value2 = value; },
configurable : true,
enumerable : true
});
ActiveCell.__proto__.取值 = function() {
return this.Value();
}
}
static Undo() {
for (let name in ActiveCell.__proto__)
delete ActiveCell.__proto__[name];
}
static CallGetterSetterOtherWay() {
let rng = new Range('A1:B3, C5:E4');
rng.值 = 159;
let cell = rng.Cells.Item(1);
Console.log(cell.值);
let desc = Object.getOwnPropertyDescriptor(
ActiveCell.__proto__, '值');
let setter = desc.set;
setter.call(cell/*第一个参数绑定 this*/, 357/*设置新值*/);
Console.log(cell.值);
//也可以通过 getter 来读取值
let getter = desc.get;
Console.log(getter.call(cell/*绑定this*/));
}
static CallMethodOtherWay() {
let cell = new Range('A1');
let address = ActiveCell.__proto__.取地址
.call(cell/*第一个参数绑定 this*/);
Console.log(address);
}
static WorkonIt() {
let rng = new Range('B2');
Console.log(rng.取地址());
Console.log(rng.取地址(true, true, xlR1C1));
rng.值 = 32;
Console.log(rng.值);
Console.log(rng.取值());
}
static RunAll() {
//执行汉化
ChinesizationRangeTypeExample.Do();
//测试汉化
ChinesizationRangeTypeExample.WorkonIt();
//别样调用属性
ChinesizationRangeTypeExample.CallGetterSetterOtherWay();
//别样调用方法
ChinesizationRangeTypeExample.CallMethodOtherWay();
//撤销汉化
ChinesizationRangeTypeExample.Undo();
}
}
其输出如下:
$B$2
R2C2
32
32
357
$A$1