WPS JSA 宏編程(JS):6.修改 Excel 對象成員


JS 語言的動態性,使我們能夠修改對象乃至類型的成員,主要有兩種方式:

  1. 對 __proto__/prototype 進行操作,修改原型對象;
  2. 使用代理 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
}

綜上:

  1. 我們總可以通過對象的 _ proto_ 訪問到對象的原型,並在此原型對象上增添一些成員,這總是沒問題的
  2. 因為對象被構造產生的成員的訪問優先級,高於原型對象上增添的同名成員,所以實際上並不能覆寫已有的成員
  3. 因為內置對象的成員大多數是 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

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM