混合对象“类”

8/20/2020 js

在研究类的具体机制之前,我们首先会介绍面向类的设计模式:实例化(instantiation)继承(inheritance)(相对)多态(polymorphism)

# 类理论

类的另一个核心概念是多态,这个概念是说父类的通用行为可以被子类用更特殊的行为重写。实际上,相对多态性允许我们从重写行为中引用基础行为。

# “类”设计模式

你可能从来没把类作为设计模式来看待,讨论得最多的是面向对象设计模式,比如迭代器 模式、观察者模式、工厂模式、单例模式,等等。从这个角度来说,我们似乎是在(低级) 面向对象类的基础上实现了所有(高级)设计模式,似乎面向对象是优秀代码的基础。

当然,如果你有函数式编程(比如 Monad)的经验就会知道类也是非常常用的一种设计模式。但是对于其他人来说,这可能是第一次知道类并不是必须的编程基础,而是一种可选的代码抽象。

# JavaScript中的“类”

JavaScript“类”库试图掩盖这个现实,但是你迟早会面对它:其他语言中的类和 JavaScript 中的“类”并不一样。

# 类的机制

Stack 类仅仅是一个抽象的表示,它描述了所有“栈”需要做的 事,但是它本身并不是一个“栈”。你必须先实例化 Stack 类然后才能对它进行操作。

# 构造函数

类实例是由一个特殊的类方法构造的,这个方法名通常和类名相同,被称为构造函数。这个方法的任务就是初始化实例需要的所有信息(状态)。

举例来说,思考下面这个关于类的伪代码(编造出来的语法):

class CoolGuy {
    specialTrick = nothing
    CoolGuy( trick ) {
        specialTrick = trick
    }
    showOff() {
        output( "Here's my trick: ", specialTrick )
    }
}
// 我们可以调用类构造函数来生成一个 CoolGuy 实例:

Joe = new CoolGuy( "jumping rope" )
Joe.showOff() // 这是我的绝技:跳绳
1
2
3
4
5
6
7
8
9
10
11
12
13

注意,CoolGuy 类有一个 CoolGuy() 构造函数,执行 new CoolGuy()时实际上就是调用它。构造函数会返回一个对象(也就是类的一个实例),之后我们可以在这个对象上调用 showOff() 方法,来输出指定 CoolGuy 的特长。

# 类的继承

首先回顾一下本章前面部分提出的 Vehicle 和 Car 类。思考下面关于类继承的伪代码:

class Vehicle {
    engines = 1
    ignition() {
        output( "Turning on my engine." );
    }
    drive() {
        ignition();
        output( "Steering and moving forward!" )
    }
}

class Car inherits Vehicle { 
    wheels = 4
    drive() {
        inherited:drive()
        output( "Rolling on all ", wheels, " wheels!" )
    } 
}

class SpeedBoat inherits Vehicle { 
    engines = 2
    ignition() {
        output( "Turning on my ", engines, " engines." )
    }
    pilot() {
        inherited:drive()
        output( "Speeding through the water with ease!" )
    }
}
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

Car 和 SpeedBoat。它们都从 Vehicle 继承了通用 的特性并根据自身类别修改了某些特性。汽车需要四个轮子,快艇需要两个发动机,因此 它必须启动两个发动机的点火装置。

# 多态

Car 重写了继承自父类的 drive() 方法,但是之后 Car 调用了 inherited:drive() 方法, 这表明 Car 可以引用继承来的原始 drive() 方法。快艇的 pilot() 方法同样引用了原始 drive() 方法。

这个技术被称为多态或者虚拟多态。在本例中,更恰当的说法是相对多态。

# 混入

由于在其他语言中类表现出来的都是复制行为,因此 JavaScript 开发者也想出了一个方法来 模拟类的复制行为,这个方法就是混入。接下来我们会看到两种类型的混入:显式隐式

首先我们来回顾一下之前提到的 Vehicle 和 Car。由于 JavaScript 不会自动实现 Vehicle 到 Car 的复制行为,所以我们需要手动实现复制功能。这个功能在许多库和框架中被称为 extend(..),但是为了方便理解我们称之为 mixin(..)

// copy 没有的进入到 targetObj
function mixin( sourceObj, targetObj ) {
    for (var key in sourceObj) {
    // 只会在不存在的情况下复制 
        if (!(key in targetObj)) {
            targetObj[key] = sourceObj[key];
        }
    }
    return targetObj; 
}
var Vehicle = { 
    engines: 1,
    ignition: function() {
        console.log( "Turning on my engine." );
    },
    drive: function() { 
        this.ignition();
        console.log( "Steering and moving forward!" );
    }
};

// 生成新的Car
var Car = mixin( Vehicle, 
    { 
        wheels: 4,
        drive: function() { 
            Vehicle.drive.call( this ); 
            console.log( "Rolling on all " + this.wheels + " wheels!");
        } 
    }
);
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

有一点需要注意,我们处理的已经不再是类了,因为在 JavaScript 中不存在类,Vehicle 和 Car 都是对象,供我们分别进行复制和粘贴。

Car 已经有了 drive 属性(函数),所以这个属性引用并没有被 mixin 重写,从而保留了 Car 中定义的同名属性,实现了“子类”对“父类”属性的重写(参见 mixin(..) 例子中 的 if 语句)

# 隐式混入

var Something = { 
    cool: function() {
        this.greeting = "Hello World";
        this.count = this.count ? this.count + 1 : 1; 
    }
};

Something.cool();
Something.greeting; // "Hello World"
Something.count; // 1

var Another = {
    cool: function() {
        // 隐式把 Something 混入 Another
        Something.cool.call( this );
    }
};

Another.cool();
Another.greeting; // "Hello World" 
Another.count; // 1(count 不是共享状态)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22

虽然这类技术利用了 this 的重新绑定功能,但是 Something.cool.call( this ) 仍然无法 变成相对(而且更灵活的)引用,所以使用时千万要小心。通常来说,尽量避免使用这样的结构,以保证代码的整洁和可维护性。

# 总结

类是一种设计模式。许多语言提供了对于面向类软件设计的原生语法。JavaScript 也有类似的语法,但是和其他语言中的类完全不同。

类意味着复制。

传统的类被实例化时,它的行为会被复制到实例中。类被继承时,行为也会被复制到子类中

多态(在继承链的不同层次名称相同但是功能不同的函数)看起来似乎是从子类引用父类但是本质上引用的其实是复制的结果

此外,显式混入实际上无法完全模拟类的复制行为,**因为对象(和函数!别忘了函数也是对象)只能复制引用,无法复制被引用的对象或者函数本身。**忽视这一点会导致许多 问题。

上次更新: 2/15/2025, 2:29:28 PM