使用元表可以自定义对象的默认行为、操作符。
一个对象可以用另一个 table 对象(元表)来定义一些元方法(metamethods)。用来定义元方法的表称为元表(metatable)。元表(metatable)允许我们改变对象的行为。
元表(metatable)中的函数称为元方法,通常用来重定义运算符。 例如对两个表(table)进行相加时,它会检查两个表是否有一个表有元表(metatable),并且检查元表(metatable)是否有_add
函数。如果找到则调用这个 _add
函数去计算结果。
虽然理论上所有对象都可以指定元表,但一般这样做是无意义的。
元表主要应用于 table 对象。
在表构造器中使用 @
操作符就可以将 @
操作符后面的表对象指定为元表。
示例:
//定义一个元表
_metaTable = {
//添加元方法
_get = function(k) {
owner[[k]] = k + " 通过元方法创建的值"
return owner[[k]] //不调用元方法
};
}
var tab = {
//使用 @ 操作符设置元表
@_metaTable
}
也可以在 @
操作符后面用字面量直接构造元表。
示例如下:
var tab = {
//使用 @ 操作符设置元表
@{
//添加元方法
_get = function(k) {
owner[[k]] = k + " 通过元方法创建的值"
return owner[[k]] //不调用元方法
};
}
}
在表对象后面加上 @
操作符可以获取或设置元表。
示例:
//设置元表
tab@ = {};
//获取元表
var meta = tab@;
//获取元方法
var f = tab@._get;
元表中的字段通常会使用下划线 _
作为首字符以声明为只读成员 以禁止随意改动对象的行为。
元表中的方法我们称之为"元方法",元表中的属性我们称之为"元属性",但很多时候同名的"元方法"与"元属性"可以相互替代,一些"元属性"允许定义函数并自动获取返回值,而一些"元方法"也允许直接指定一个固定的值而不是定义一个函数,所以 aardio 并不严格区分这两个术语。
对象指定元表以后,默认会锁定该元表,不能移除锁定的元表,也不能用其他的元表替换锁定元表。
只有在元表中将 _float
属性指定为 true 时,元表就不会锁定并且可以随时被移除或替换。也就是说只有元表自己才能决定它自己是不是可以被移除替换。
示例:
//创建非锁定的元表
tab@ = { _float = true };
//替换元表成功
tab@ = {};
//不能替换默认锁定的元表
tab@ = {};
请参考:运算符、表达式 - 运算符重载
元表中的属性、方法列表:
元属性/元方法 | 说明 |
---|---|
_weak |
弱引用不会增加引用计数、不会阻止垃圾回收器删除对象。 赋值为 "kv" 表示弱引用键、值。 赋值为 "v" 表示弱引用值。 赋值为 "k" 表示弱引用键。 |
_type |
自定义类型,如果值为"object",指明该对象为JSON兼容的对象,如果值为"array",指明该对象为JSON的数组。 |
_readonly |
如果显式指定此元属性为 false,则该表可以重写属性名首字符为下划线的字段( 禁用只读成员保护 )。_readonly 的值只能为 false 或 null 。设置 _readonly 为任何非null值都会被强制转换为 false, 只有不指定此属性的值(即保持 null 值)才能启用只读成员保护(所有名字以下划线开头的属性禁止修改非null值)。global对象无论元属性 _readonly 怎么设置都会被忽略,只读成员保护总是启用状态。# 关于表的只读成员 |
_defined |
用于返回对象的预定义已排序键名,被用于table.eachName等函数。 |
_keys |
可用于table.keys等函数动态获取对象的键名列表(例如动态生成键值对的外部JS对象可使用这个元方法返回成员名字列表)。 |
_startIndex |
用于table.eachIndex等函数动态指定数组的开始下标。 |
_get = function(k,ownerCall) {<br> <br> } |
成员(属性)操作符 . 索引(下标)操作符 [] 如果用 属性(.)操作符或下标操作符([])读取表中不存在的键会触发 _get 元方法并返回值。注意:使用 owner[[member]] 形式的直接下标以及 namespace 语句打开新的名字空间不会触发元方法。with 语句打开新的名字空间则会触发元方法。在 aardio 中.NET 名字空间 依赖元方法自动导入下级名字空间的 ,所以 namespace 只能用于打开已导入的 .NET 名空字间(否则应改用 with 语句)。_get 不但可以是一个函数,也可以指定一个 table 对象(找不到成员就到 _get 指定的 table 里找)。如果是 _get 元方法是一个函数,则调用参数 k 为键名。使用 owner[member] 形式的下标操作符触发 owner 对象的 _get 元方法时 ownerCall 参数的值为 null 。使用 owner.method() 形式的成员函数触发 owner 对象的 _get 元方法时 ownerCall 参数的值为 true 。ownerCall 参数如果为 true 则应当返回一个函数对象。 其他方式访问表的成员触发对象的 _get 元方法时 ownerCall 参数的值为 false。 |
_set = function(k,v,ownerAttr) { } |
成员(属性)操作符 . 索引(下标)操作符 [] 如果用 . 或 [] 操作符给表中不存在的键赋值会触发 _set 元方法。注意:使用 owner[[member]] 形式的直接下标赋值不会触发元方法。_set 元方法的 k 参数为新的键,v 参数为新的值 。在赋值语句中使用 owner[member] = value 形式的下标操作符赋值触发 owner 对象的 _set 元方法时 ownerAttr 参数的值为 false,其他任何方式触发对象的 _set 元方法时 ownerAttr 参数的值为 true 。使用形如 { ["key"] = value} 的格式在表内使用下标定义键名时如果触发 _set 元方法 ownerAttr 的值仍然为 true。只有 owner[member] = value 的形式 ownerAttr 参数才会为 false,也就是说下标操作符前面必须有一个 owner 对象。 |
_tostring = function(...) { } |
除布尔值、字符串 以外的对象作为参数 @1 调用 tostring 函数可触发该对象的 _tostring 元方法,调用 tostring 的第二个参数开始的所有参数会作为 _tostring 的调用参数。 |
_tonumber = function() { } |
除数值、指针、布尔值、字符串 以外的对象作为参数 @1 调用 tonumber() 会触发会调用该对象的 _tonumber 元方法转换为数值 。 |
_json |
表对象可使用此元方法自定义 web.json.stringify() 处理该对象时返回的数据。 此元方法的回调参数 owner 为当前表对象。如果此元方法返回一个值则 web.json.stringify() 继续转换返回对象为 JSON。 如果此元方法的第二个返回值为 true,并且第一个返回值为字符串,则 web.json.stringify() 将第一个返回值直接放入 JSON 并不作任何转换。 |
_eq = function(b) { } |
相等运算、不等运算符调用此元方法并取反 比较的两个对象必须指向相同的元方法(即 a@. _eq === b@._eq ),否则默认规则进行比较. |
_le = function(b) { } |
小于等于、大于等于运算符 比较的两个对象必须指向相同的元方法(即 a@._le === b@._le )当调用 a <= b 时, a 为元方法的 owner 对象(左参数)当调用 a >= b 时, b 为元方法的 owner 对象(右参数作为左参数) |
_lt = function(b) { } |
小于运算、大于运算符 比较的两个对象必须指向相同的元方法(即 a@._lt === b@._lt )当调用 a < b 时, a为元方法的owner对象(左参数)当调用 a > b 时, b 为元方法的 owner 对象(右参数作为左参数) |
_add = function(b) { } |
加运算符 调用元方法时,始终取左操作数作为元方法的owner参数. 无论左操作数或右操作参数定义了此元方法,都可以触发自定义的运算. 当左右操作数定义了不同的元方法,调用左操作数的元方法. |
_sub = function(b) { } |
减运算符 调用元方法时,始终取左操作数作为元方法的owner参数. 无论左操作数或右操作参数定义了此元方法,都可以触发自定义的运算. 当左右操作数定义了不同的元方法,调用左操作数的元方法. |
_mul = function(b) { } |
乘运算符 调用元方法时,始终取左操作数作为元方法的owner参数. 无论左操作数或右操作参数定义了此元方法,都可以触发自定义的运算. 当左右操作数定义了不同的元方法,调用左操作数的元方法. |
_div = function(b) { } |
除运算符 调用元方法时,始终取左操作数作为元方法的owner参数. 无论左操作数或右操作参数定义了此元方法,都可以触发自定义的运算. 当左右操作数定义了不同的元方法,调用左操作数的元方法. |
_lshift = function(b) { } |
左移运算符 << 调用元方法时,始终取左操作数作为元方法的owner参数. 无论左操作数或右操作参数定义了此元方法,都可以触发自定义的运算. 当左右操作数定义了不同的元方法,调用左操作数的元方法. |
_rshift = function(b) { } |
右移运算符 >> 调用元方法时,始终取左操作数作为元方法的owner参数. 无论左操作数或右操作参数定义了此元方法,都可以触发自定义的运算. 当左右操作数定义了不同的元方法,调用左操作数的元方法. |
_mod = function(b) { } |
模运算符 调用元方法时,始终取左操作数作为元方法的owner参数. 无论左操作数或右操作参数定义了此元方法,都可以触发自定义的运算. 当左右操作数定义了不同的元方法,调用左操作数的元方法. |
_pow = function(b) { } |
幂运算符 调用元方法时,始终取左操作数作为元方法的 owner 参数. 无论左操作数或右操作参数定义了此元方法,都可以触发自定义的运算. 当左右操作数定义了不同的元方法,调用左操作数的元方法. |
_unm = function() { } |
取负运算符,owner 参数为当前操作数 |
_len = function() { } |
取长运算符 # 。owner 参数为当前操作数。注意 table,string,null 这三种类型不能重载此操作符。 |
_concat = function(b) { } |
连接运算符 调用元方法时,始终取左操作数作为元方法的 owner 参数. 无论左操作数或右操作参数定义了此元方法,都可以触发自定义的运算. 当左右操作数定义了不同的元方法,调用左操作数的元方法. |
_call = function(...) { } |
函数调用,owner 参数为当前操作数,其他参数为调用函数的参数。 元方法还可以用于指定 COM 默认调用方法( DISPID 为: DISPID_VALUE ), DISPID_VALUE 调用会优先使用 _call 元方法执行 COM 默认调用,如果aardio 表对象、CDATA 对象未指定 _call 元方法, 则通过 _item 元方法获取默认调用表(以带参数方式读写该表属性)。ActiveX 控件类的 COM 默认调用必须在 ODL 中指定,在 aardio.idl 中定义的 IDispatchExecutable 已指定 COM 默认调用属性为 Item。 例如: VB 或 VBA 写默认属性 comObj("项目名") = 值; 读默认属性(或调用默认方法) v = comObj("项目名"); aardio 中类似的是函数调用语法: comObj("项目名") 或 comObj("项目名","项目值") aardio 使用函数读写默认值语法: comObj.setValue( value ); value = comObj.getValue(); aardio 使用属性读写默认值语法: comObj.Value = value; value = comObj.Value; aardio 使用属性读写带参数值语法; comObj.getItem("项目名"); comObj.setItem("项目名"); 这些写法主要用于操作 COM 函数,在原生 aardio 中并不常见。原生 aardio 代码用下标语法更方便。 |
_item |
_item 必须返回一个表对象,可直接指定一个表或返回表的函数。 aardio 表对象、cdata对象在 COM 接口中自动或调用 com.ImplInterface() 转换为 IDispatch 接口对象后支持 _item 元方法。 _item 元方法用于在 COM 接口中返回支持枚举接口( IEnumVARIANT )的表对象。 如果未指定该元方法,则 IEnumVARIANT 接口默认枚举表对象自身。 _item 元方法还可以用于指定 COM 默认调用表(COM 默认调用的 DISPID 为: DISPID_VALUE ), DISPID_VALUE 调用会优先使用 _call 元方法执行 COM 默认调用,如果aardio 表对象、CDATA 对象未指定 _call 元方法, 则通过 _item 元方法获取默认调用表(以带参数方式读写该表属性)。 如果表对象未指定 _item 元方法,则 IEnumVARIANT 默认枚举表自身。 而 DISPID_VALUE 调用也会默认读写表自身。 com.activeX 创建的 ActiveX 控件类接口的规则略有不同, ActiveX 控件类也只支持 _enum 指定 IEnumVARIANT 接口操作的枚举表。 但 ActiveX 控件类的 COM 默认调用必须在 ODL 中指定,在 aardio.idl 中定义的 IDispatchExecutable 已指定 COM 默认调用属性为 Item 。 但通过 ActiveX 控件类接口返回的其他表对象、cdata 对象则仍然支持匿名的 COM 默认调用。 |
_toComObject |
用于自定义一个表对象如何转换为 COM 对象,可定义为函数,也可以直接定义为对象。 |
_gc |
用于触发析构函数。只能通过 table.gc 或 gcdata 使用这个元方法。 |
重载所有操作符的写法基本都类似,下面我们以成员操作符为例演示并说明基本用法。
如果访问表中不存在的属性会调用 _get
元方法,如果修改表中不存在的属性调用 _set
元方法。
示例:
import console;
//创建表
var tab = {};
//创建元表
tab@ = {
//拦截获取成员的操作
_get = function(k) {
console.log(k+"被读了")
return k + "目前没有值";
}
//拦截写入成员的操作
_set = function (k,v) {
console.log(k+"被修改值为"+v)
owner[[k]]=v; //删除这句代码就创建了一个只读表
}
};
//读取成员,显示 "x被读了"
var c = tab.x;
//写入成员,显示 "y被修改值为19"
tab.y = 19;
console.pause()
要点:
_get
元方法中不需要再去重复读取 owner 对象的同名成员,因为只有读取不存在的成员时才会触发 _get
元方法。如果要拦截所有成员,那么更好的方法是用另一个表保存实际的数据。如果要改名读取自身的其他成员,要使用直接下标操作符 [[]]
以避免重复触发 _get
元方法,例如:
aardio
tab@ = {
_get = function(k) {
if(type.isString(k)) {
return owner[["real_" + k]];
}
}
};
_set
元方法,如果要将新值写入 owner 对象自身,同样要使用直接下标操作符 [[]]
以避免重复触发元方法。参考: 成员操作符、直接下标
aardio 里有一个在标准库中被大量使用的属性元表(util.metaProperty)主要就是基于上面的原理。
我们也可以用一个拥有元表的代理表接管对另一个真实对象的访问,以扩展、修改、封装真实源对象的方法与属性。
这样做有几个好处:
示例:
import console;
//为参数 @tab 指定的表创建一个代理表
function createProxy(tab) {
//保存被代理的数据表
var real = tab;
/*
创建一个代理表。
你要访问真正的表(real)?
先问过我(proxy)吧,我是他的经纪人!!!
*/
var proxy = {};
//创建元表
proxy@ = {
//拦截获取成员的操作
_get = function(k) {
console.log(k+"被读了")
return real[k];
}
//拦截写入成员的操作
_set = function (k,v) {
console.log(k+"被修改值为"+v)
real[k]=v; //删除这句代码就创建了一个只读表
}
};
return proxy;
}
//创建表对象
var tab = {x=12;y=15};
//创建一个代理表,以管理对 tab 的存取访问
var proxy = createProxy(tab);
//显示 "x被读了"
var c = proxy.x;
//显示 "y被修改值为19"
proxy.y = 19;
console.pause();
这里用到了函数闭包 保存 real,proxy 这些表对象。
如果是写库文件,通常会用类( class )来做类似的事。因为 class 本身也可以作为名字空间使用,写库比较方便。参考: class