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,即不可改變與刪除,所以刪除成員就別想了
二、通過代理的方式
1 //居中填充 2 String.prototype.padCenter = 3 function(targetLength, padString = ' ') { 4 if (typeof targetLength != 'number') 5 throw new TypeError('Parameter "targetLength" ' + 6 'must be a number object.'); 7 if (typeof padString != 'string') { 8 if (padString === null) 9 padString = 'null'; 10 else 11 padString = padString.toString(); 12 } 13 let padStrWidth = padString.length; 14 if (padStrWidth == 0) return this; 15 let restWidth = targetLength - this.length; 16 if (restWidth <= 0) return this; 17 let leftWidth = Math.trunc(restWidth / 2); 18 let rightWidth = leftWidth + restWidth % 2; 19 if (padString.length == 1) { 20 return padString.repeat(leftWidth) + this + 21 padString.repeat(rightWidth); 22 } else { 23 if (leftWidth == 0) 24 return this + padString[0]; 25 else { 26 //leftPart 27 let leftRepeat = Math.trunc(leftWidth / padStrWidth); 28 let leftRest = leftWidth - leftRepeat * padStrWidth; 29 let leftStr = padString.repeat(leftRepeat) + 30 padString.substr(0, leftRest); 31 //rightPart 32 let rightRepeat = Math.trunc(rightWidth / padStrWidth); 33 let rightRest = rightWidth - rightRepeat * padStrWidth; 34 let rightStr = padString.repeat(rightRepeat) + 35 padString.substr(0, rightRest); 36 return leftStr + this + rightStr; 37 } 38 } 39 } 40 /*Proxy handler 可對應於 41 Reflect.apply() 42 Reflect.construct() 43 Reflect.defineProperty() 44 Reflect.deleteProperty() 45 Reflect.get() 46 Reflect.getOwnPropertyDescriptor() 47 Reflect.getPrototypeOf() 48 Reflect.has() 49 Reflect.isExtensible() 50 Reflect.ownKeys() 51 Reflect.preventExtensions() 52 Reflect.set() 53 Reflect.setPrototypeOf() 54 實現想要的代理 55 */ 56 class ES6ProxyTest { 57 //既然 Reflect.set(target, key, value[, receiver]) 有 58 //要設置的屬性的鍵和值,我們就可以通過 set 代理,在一個 59 //對象的定義的外部: 60 //1.通過鍵,攔截對某些屬性的寫入 61 //2.通過值,檢驗類型,攔截非法寫入 62 //3.通過鍵,重定向屬性的寫入,就像為屬性設置一些假名一樣 63 static SetProxy() { 64 let p = new Point(3, 8); 65 let pp = new Proxy(p, { 66 set:function(target, key, value, receiver){ 67 //Reflect.set(target, key, value[, receiver]) 68 //target : 用於接收屬性(被代理的對象)的對象 69 //key : 要寫入的屬性的鍵(字符串或Symbol類型) 70 //value : 要寫入的屬性新值 71 //receiver : 如果 target 對象的 key 屬性有 setter, 72 // receiver 則為 setter 調用時的 this 值。 73 //return : 返回一個 Boolean 值,表明操作的成敗 74 let success = Reflect.set(target, key, value, receiver); 75 if (success) { 76 //Console 在此不可用 77 Debug.Print('property '+ key +' on '+ 78 target + ' set to '+ value); 79 } 80 //必須返回操作成敗狀態,否則報錯 81 return success; 82 } 83 }); 84 pp.xxx = 13; 85 Console.log(p.xxx); 86 } 87 88 //既然 Reflect.get(target, key[, receiver]) 提供了要讀取 89 //的屬性的鍵,我們就可以通過 get 代理,在對象的定義的外部: 90 //1.通過鍵,攔截對某些屬性的讀取 91 //2.通過鍵,偽造一些不存在的屬性 92 //3.通過鍵,實現類同假名的屬性 93 static GetProxy() { 94 var obj = new Proxy({}, { 95 get: function (target, key, receiver) { 96 //Console 在此不可用 97 Debug.Print(`getting ${key}!`); 98 //Reflect.get(target, key[, receiver]) 99 //target : 被代理的對象 100 //key : 要讀取的屬性的鍵(字符串或Symbol類型) 101 //receiver : 如果 target 對象的 key 屬性有 getter, 102 // receiver 則為 getter 調用時的 this 值。 103 //return : 屬性的值。 104 return Reflect.get(target, key, receiver); 105 } 106 }); 107 obj.count = 1; 108 ++obj.count; 109 } 110 111 /*Reflect.apply(target, thisArg, argsList) 112 target : 被代理對象,請確保它是一個 Function 對象 113 thisArg : 函數調用時綁定的對象 114 argsList : 函數調用時傳入的實參列表,該參數應該是一個類數組的對象。 115 return : 調用函數返回的結果 116 通過這種代理: 117 1.檢驗調用時傳入的參數 118 2.阻止函數被調用 119 */ 120 static ApplyProxy() { 121 function sum (...values){ 122 return values.reduce((pre, cur) => pre + cur, 0); 123 } 124 let sumProxy = new Proxy(sum, { 125 apply : function(target, thisArg, argsList){ 126 argsList.forEach(arg => { 127 if(typeof arg !== "number") 128 throw new TypeError("所有參數必須是數字,親!"); 129 }); 130 return Reflect.apply(target, thisArg, argsList); 131 } 132 }); 133 134 try { 135 let r = sumProxy(3, 5, 'hello'); 136 Console.log(r); 137 } catch(e) { 138 Console.log(e.message); 139 } 140 Console.log(sumProxy(3, 8, 5)); 141 } 142 143 /*Reflect.construct(target, argsList[, newTarget]) 144 target : 被運行的目標構造函數 145 argsList : 類數組,目標構造函數調用時的參數。 146 newTarget : 可選,作為新創建對象的原型對象的 constructor 屬性, 147 參考 new.target 操作符,默認值為 target。 148 */ 149 static ConstructProxy() { 150 function sum (...values){ 151 return values.reduce((pre, cur) => pre + cur, 0); 152 } 153 let sumProxy = new Proxy(sum, { 154 construct:function(target, argsList){ 155 throw new TypeError("親,該函數不能通過 new 調用。"); 156 } 157 }); 158 159 try { 160 let x = new sumProxy(3, 5, 7); 161 } catch(e) { 162 Console.log(e.message); 163 } 164 } 165 166 //禁止向指定單元格區域寫入數據 167 static ForbidSetValue() { 168 let rng = new Range('B1:D3'); 169 rng.Value2 = 32; 170 171 let rngProxy = new Proxy(rng, { 172 set : function(target, key, value, receiver) { 173 if (key === 'Value2') { 174 throw new Error('無法設置屬性') 175 } else 176 return Reflect.set(target, key, value, receiver); 177 } 178 }); 179 180 try { 181 rngProxy.Value2 = 168; 182 } catch(e) { 183 Console.log(e.message); 184 } 185 Console.log(rngProxy.Text); 186 } 187 188 //運行所有測試用例 189 static RunAll() { 190 let members = Object.getOwnPropertyNames(ES6ProxyTest); 191 let notCall = ['length', 'prototype', 'name', 'RunAll']; 192 for (let member of members) { 193 if (!notCall.includes(member)) { 194 Console.log(member.padCenter(56, '-')); 195 eval(`ES6ProxyTest.${member}()`); 196 } 197 } 198 } 199 }
三、修改原型來漢化對象模型
1 /*借助原型對象漢化對象成員樣例*/ 2 class ChinesizationRangeTypeExample { 3 static Do() { 4 ActiveCell.__proto__.取地址 = function() { 5 return this.Address(...arguments); 6 }; 7 8 Object.defineProperty(ActiveCell.__proto__, '值', { 9 get() { return this.Value2; }, 10 set(value) { this.Value2 = value; }, 11 configurable : true, 12 enumerable : true 13 }); 14 15 ActiveCell.__proto__.取值 = function() { 16 return this.Value(); 17 } 18 } 19 20 static Undo() { 21 for (let name in ActiveCell.__proto__) 22 delete ActiveCell.__proto__[name]; 23 } 24 25 static CallGetterSetterOtherWay() { 26 let rng = new Range('A1:B3, C5:E4'); 27 rng.值 = 159; 28 29 let cell = rng.Cells.Item(1); 30 Console.log(cell.值); 31 32 let desc = Object.getOwnPropertyDescriptor( 33 ActiveCell.__proto__, '值'); 34 35 let setter = desc.set; 36 setter.call(cell/*第一個參數綁定 this*/, 357/*設置新值*/); 37 Console.log(cell.值); 38 39 //也可以通過 getter 來讀取值 40 let getter = desc.get; 41 Console.log(getter.call(cell/*綁定this*/)); 42 } 43 44 static CallMethodOtherWay() { 45 let cell = new Range('A1'); 46 let address = ActiveCell.__proto__.取地址 47 .call(cell/*第一個參數綁定 this*/); 48 Console.log(address); 49 } 50 51 static WorkonIt() { 52 let rng = new Range('B2'); 53 Console.log(rng.取地址()); 54 Console.log(rng.取地址(true, true, xlR1C1)); 55 rng.值 = 32; 56 Console.log(rng.值); 57 Console.log(rng.取值()); 58 } 59 60 static RunAll() { 61 //執行漢化 62 ChinesizationRangeTypeExample.Do(); 63 64 //測試漢化 65 ChinesizationRangeTypeExample.WorkonIt(); 66 67 //別樣調用屬性 68 ChinesizationRangeTypeExample.CallGetterSetterOtherWay(); 69 70 //別樣調用方法 71 ChinesizationRangeTypeExample.CallMethodOtherWay(); 72 73 //撤銷漢化 74 ChinesizationRangeTypeExample.Undo(); 75 } 76 }
其輸出如下:
$B$2 R2C2 32 32 357 $A$1