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 Testlet isItalic = rng.Font.Italic;Console.log('isItalic?:' + isItalic);rng.Italic = !isItalic;Console.log('isItalic?:' + rng.Italic);Console.log('isItalic?:' + rng.Font.Italic);//Size Property TestConsole.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,返回值用作属性值;如果不提供它,则为 undefinedset : 给属性提供 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 = falseresult = 'Text' in rng &&delete rng.Text;} catch { result = false; }Console.log('删除 Text 成功?:' + result);//4.删除对象上已经存在的方法try {//会失败,因为 configurable = falseresult = '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]1234512345Data PropertyisBold?:falseisBold?:trueisBold?:trueisItalic?:falseisItalic?:trueisItalic?:truefont size?:11new font size?:9new font size?:9^_^^_^---------------------ReplaceMember----------------------$B$2$B$3Deleting 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';elsepadString = 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 {//leftPartlet leftRepeat = Math.trunc(leftWidth / padStrWidth);let leftRest = leftWidth - leftRepeat * padStrWidth;let leftStr = padString.repeat(leftRepeat) +padString.substr(0, leftRest);//rightPartlet 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('无法设置属性')} elsereturn 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$2R2C23232357$A$1
