javascript语言本身不支持面向对象,ES2015中增加了class关键字,却不过是prototype语法糖而已,本质上prototype形式的面向对象只能算是一种“模拟”,这其中很重要的原因之一是js从来没有一套完美的深拷贝方案,子类只能借助原型链获取父类方法的引用,这不能算是严格意义的继承,当然也就算不上面向对象。
和js一样,lua的面向对象需要通过table来模拟,有些行为很像js中的原型,比如下面的例子。
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| Animal = {name = "Animal"} Animal.__index= Animal function () local re = {} setmetatable(re, self) return re end function Animal:GetName() return self.name end x = Animal:new()
|
在lua中,元表是个很有意思的存在。上面Animal是实例出的对象x的元表,元表在某种意义上相当于js中的构造函数,而__index
则类似prototype
(这里__index
我设置为指向自身)。
1 2 3 4 5
| print(x.name) print(x:GetName()) Animal.name = "Animal2" print(x.name) print(x:GetName())
|
如果在x中不存在name键,对x.name
的访问实际上会从x的元表中的__index
键寻找,如果仍然找不到,则会在Animal的元表中继续找,若既没有元表也找不到该键,返回nil,这和js原型链如出一辙。
借助元表,我们很容易模拟面向对象中的继承和多态,比如我们来实现一个继承自Animal的Dog类。
1 2 3 4 5 6 7 8 9
| Dog = {} setmetatable(Dog, Animal) function Dog:new() local re = {} setmetatable(re, self) self.__index = self return re end
|
效果如下。
1 2 3 4 5 6 7
| y = Dog:new() print(y:GetName()) Animal.name = "Hello" print(y.GetName()) Dog.name = "Dog" print(y.GetName())
|
实际上,lua中的元表比js中的原型机制强大的多。
上述的例子是Lua中最常见的实现OO的方法,除了关键的setmetatable
函数,__index
键也很重要,它不仅可以是另一个table的引用,也可以是一个函数,当实例对象试着从__index
寻找时便会调用这个函数,可以想象,这为多重继承的实现提供了可能,而js做不到这一点(参考)。
举个简单的例子。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57
| A = {foo1 = 123, name = "A"} A.__index = A B = {foo2 = 456, name = "B"} B.__index = B function A:new() local re = {} setmetatable(re, self) return re end function A:GetName() print(self.name) end function B:new() local re = {} setmetatable(re, self) return re end function B:GetName() print(self.name) end function B:MethodOnB() print("method on B") end C = {} function C:new() local childA = A:new() local childB = B:new() local re = {} setmetatable(re, { __index = function (table, key) if childA[key] then return childA[key] elseif childB[key] then return childB[key] else return "not found" end end }) return re end x = C:new() print(x.foo1) print(x.foo2) x:GetName() x:MethodOnB()
|
C类继承了来自A和B的方法。
注意到上面的例子中A和B都拥有GetName方法,我们可以进一步假设A和B都继承自另外一个对象,而他们各自的GetName方法其实都继承自这个对象,这就产生了经典的钻石问题(也叫菱形继承问题),即:C继承到的GetName方法到底来自A还是B?一些原生支持面向对象和多重继承的语言为了解决钻石问题,往往会采用特定的遍历算法,如Python采用的是从左到右广度优先原则,使用的是名叫“C3”的算法。而在上面这里例子里我只是简单的指定了先从A中寻找,再从B中寻找,所以C继承了A的GetName方法。
在lua中,元表除了用来模拟面向对象,还有一些不可思议的作用:自定义table间运算的行为。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| meta = {__add = function(A, B) local re = {} for _, val in ipairs(A) do table.insert(re, val) end for _, val in ipairs(B) do table.insert(re, val) end return re end } a = setmetatable({1, 2, 3}, meta) b = setmetatable({4, 5, 6}, meta) c = a + b
|
除了__add
,元表上可自定义的运算行为包括如下。
1 2 3 4 5 6 7 8 9 10
| __add 对应的运算符 '+'. __sub 对应的运算符 '-'. __mul 对应的运算符 '*'. __div 对应的运算符 '/'. __mod 对应的运算符 '%'. __unm 对应的运算符 '-'. __concat 对应的运算符 '..'. __eq 对应的运算符 '=='. __lt 对应的运算符 '<'. __le 对应的运算符 '<='.
|
而除了运算,元表甚至可以让table像函数一样调用,使用__call
。
1 2 3 4 5
| a = setmetatable({}, {__call = function(mytable, params) print("123"..params) end}) a(456) -- 123456
|
所有上述提到的在元表上以__
开头的方法统称为元方法。
元表有这么多有意思的设计,也难怪lua程序员说js中的原型只能算实现了元表功能的十分之一。
话说回来,在lua中使用面向对象和在js中的感觉差不多,过去基于prototype模拟OO,很多人有不同的实现,如今js在语法层面统一了写法,而在lua中仍然有很多人尝试对上面这些例子的写法进行封装,试图让代码更容易维护和扩展,这样的折腾其实没什么意义,因为面向对象本身就不易维护。以小而精致著称的lua也不太可能提供语言层面支持,毕竟连社区都没几个,也没看到有人表达这样的诉求,函数式语言就写函数式,多好。