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

 

posted @ 2021-08-26 15:23  nutix  阅读(4619)  评论(0编辑  收藏  举报