Skip to article frontmatterSkip to article content

但对一些应用而言, modular model能力稍显不够. 我们需要更强的模型.

Object和Module的不同

比起module, object不仅能够承载状态变量, 还能够实例化, 也就是说可以存在多个object, 它们彼此独立, 虽然API相同, 但拥有不同的状态.

对象之所以能力更强, 是因为它可以模拟各种其他实体.

无外乎很多语言把一切皆对象作为自己的设计哲学之一.

无外乎很多语言把一切皆对象作为自己的设计哲学之一.

对象模式风靡后, 有两种不同风格的模型, 一种是Object based programming, 一种是Object oriented programming. 有相当一部份语言, 选择了object based model来构建自己的对象体系.

对象的本质: 如何在任何语言中创造对象

基于对象编程是一种理念, 在不支持对象的语言中, 也可以自己实现对象.

只要语言中有map, 函数是第一类公民, 并支持闭包, 我们就可以造出对象, 譬如

hand_made_object.py
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
def create_obj():
    value = 1
    id_ = 2
    greeting = "hello"
    
    def set_value(new_value):
        nonlocal value
        value = new_value
        ...

    def show():
        print(f"{id_}:{value}")

    return {
        "set_value": set_value,
        "show": show,
        "greeting": greeting
    }

obj = create_obj()

obj["show"]()
obj["set_value"](10)
obj["show"]()
print(obj["greeting"])

我们把公有数据放在map中, 把私有数据藏在函数闭包中, 只能通过对象暴露的方法进行访问. 每个对象为一个独立的map, 对象上可以访问的方法即为公有方法, 在闭包中未暴露的方法即为私有方法. 通过这种方式, 我们可以在任意语言中定义对象.

上面例子中, 对象的主体是一个map(一个key-value容器). map是动态的ADT, 我们也可以使用静态的数据结构(譬如结构体)实现同样的功能.

从函数到方法

对象实例中的函数和对象外的普通函数有些微区别.

对象+函数
把函数放入对象中
将函数改为方法

如果我们有一个对象实例book, 以及一个函数read(Book)->string, 该函数读取book对象中的数据, 并返回字符串.

如果read是一个普通函数和Book对象分离,

调用时我们需要单独获取readbook, 并调用read(book)

然而不同语言中, 方法和对象的绑定也有不同的风格.

在大部份语言中, 方法和对象是静态绑定的, 即无论方法被如何传递, 它的self / this始终指向原对象.

但在一部份语言中, 譬如javascript, 方法和对象的绑定是在调用时进行的, 调用方式不同, 绑定的对象也不同, 这种风格被称为动态绑定.

静态绑定
动态绑定
static_binding.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
class T:
    def __init__(self, val):
        self.val = val

    def show(self):
        print(self.val)


t = T(10)
method = t.show  # 将t.show方法转存到method中

# 调用method等同于调用t.show
# 换句话说method函数依然绑定了t作为上下文!
method()  # 10   

t.val = 5
method()  # 5

无论t.show被转存到哪里, 函数始终绑定了对象t作为上下文

如何构造对象

OOP和Object based model最大的区别在于对象的创建上. 在objects based model中, 我们没有类去创造对象, 我们只能通过其他操作来创建对象. 大体上有5种创建对象的方式.

create_obj.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
// 字面量声明对象
hp = {
  name: "Harry Potter",
  occupation: "wizard",
  level: 5,
  speak: function() { ... }
}

// 使用函数直接组装对象
function person1(name, occupation, level) {
  return {
    name: name,
    occupation: occupation,
    level: level,
    speak: function() { ... }
  }
}
tom = person1("Tom Riddle", "wizard", 100);

// 定义初始化函数, 初始化一个空对象
function person2(name, occupation, level) {
  this.name = name;
  this.occupation = occupation;
  this.level = level;
  this.speak = function() {
    ...
  };
}
newton = new person2("Isac Newton", "scientist", 1000);

// 直接深度复制一个对象, 但无法复制函数, 无法处理循环引用!
function clone(obj) {
  return JSON.parse(JSON.stringify(obj))
}
newton_clone = clone(newton)

// 设置一个对象为原形. 通过这种方式去创建对象. 
newton_derivative = {}
Object.setPrototypeOf(newton_derivative, newton)
// same as Object.create(newton)

最后一种构造对象的方式--通过链接原型构造对象--是object based model的精髓.

不同于深度复制数据来构造新对象, 我们可以创造一个新对象去引用原有对象. 如果访问到本对象中没有的属性, 就去这个被引用的对象中去找, 我们把这个被引用的对象称为原型.

不同于深度复制数据来构造新对象, 我们可以创造一个新对象去引用原有对象. 如果访问到本对象中没有的属性, 就去这个被引用的对象中去找, 我们把这个被引用的对象称为原型.

原型是对象的一个特殊属性, 每个对象都拥有原型. 在其他语言中原型有其他的名字, 譬如在lua中它被叫做meta-table.

原型链的实践

既然每个对象都有原型, 那么作为原型的对象也有其原型. 如此一来就有了原型链一说. 一般而言, 原形链用来表示一个对象从具体到抽象的整个链条.

譬如这里从wizard到Object由原型链穿起来. 每一个对象都有自己的属性和方法, 而wizard能够访问所有的属性和方法. 譬如wizard.eat(), wizard.del().

譬如这里从wizard到Object由原型链穿起来. 每一个对象都有自己的属性和方法, 而wizard能够访问所有的属性和方法. 譬如wizard.eat(), wizard.del().

Object based model和OOP的主要区别就在于原型链. 原型链带来一种特殊的应用模式.

从原型创建对象

OOP中一个class创建对象. object-based model则可以从原型创建对象, 这里原型的作用就像是对象模板. 我们只需要创建一个空对象, 其原型指向模版对象即可. 新的属性和方法只需要增加在空对象中, 同名属性和方法会自动覆盖原型中的属性和方法.

譬如这里Harry Potter是这样一个对象
{level: 10, scar: "⚡", id:0, .proto:wizard}

譬如这里Harry Potter是这样一个对象

模仿类的继承

既然原型作为对象模版, 模仿了类的作用, 它应该也有办法模仿继承.

这里, wizard对象就是human对象所谓的“子类型“.

这里, wizard对象就是human对象所谓的“子类型“.

1
2
3
4
5
6
7
8
9
10
const IssacNewton = {
  iq: 200,
  .proto: human
}

const wizard = {
  MP: 0,
  cast_spell: function(){...},
  .proto: human
}

有趣的是, 以human作为原型, 我们可以创建Issac Newton这个"实例", 同样的以human作为原型, 我们可以创建wizard这个"子类型". 他们有同样的形式.

也就是说原型机制用同样的方式表示对象和子类型, 它统一了创建实例和继承, 这跟OOP是非常不同的.

黑魔法: 运行时修改原型

既然原型是对象, 我们就可以在运行时修改它.

譬如我们可以在运行时把human的原型指向immortal. 这样所有人就突然丢失了eat()和drink(). 我们也可以在thing上增加更多的通用属性和方法.

譬如我们可以在运行时把human的原型指向immortal. 这样所有人就突然丢失了eat()和drink(). 我们也可以在thing上增加更多的通用属性和方法.

只需一个简单的改动, 所有“被原型”对象的行为都会随之改变.

这种机制极为强大, 甚至可视为元编程的一种实践. 使用得当时, 确实能够大幅减少代码量, 但代价是使代码变得难以理解, 因此需谨慎使用.