早在 2013 年 6 月,我就在 benefits of prototypal inheritance over classical 上回答了一个问题。从那以后,我花了很多时间思考继承,包括原型和经典,我写了大量关于prototype-classisomorphism的文章。
是的,原型继承的主要用途是模拟类。但是,它的用途远不止模拟类。例如,原型链与作用域链非常相似。
原型范围同构
JavaScript 中的原型和作用域有很多共同点。 JavaScript 中有三种常见的链类型:
-
原型链。
var foo = {};
var bar = Object.create(foo);
var baz = Object.create(bar);
// chain: baz -> bar -> foo -> Object.prototype -> null
-
作用域链。
function foo() {
function bar() {
function baz() {
// chain: baz -> bar -> foo -> global
}
}
}
-
方法链。
var chain = {
foo: function () {
return this;
},
bar: function () {
return this;
},
baz: function () {
return this;
}
};
chain.foo().bar().baz();
在这三个中,原型链和作用域链最相似。实际上,您可以使用 notorious with 语句将原型链附加到范围链。
function foo() {
var bar = {};
var baz = Object.create(bar);
with (baz) {
// chain: baz -> bar -> Object.prototype -> foo -> global
}
}
那么原型范围同构有什么用呢?一种直接用途是使用原型链对作用域链进行建模。这正是我为自己的编程语言 Bianca 所做的,我用 JavaScript 实现了它。
我首先定义了 Bianca 的全局范围,并在一个名为 global.js 的文件中填充了一堆有用的数学函数,如下所示:
var global = module.exports = Object.create(null);
global.abs = new Native(Math.abs);
global.acos = new Native(Math.acos);
global.asin = new Native(Math.asin);
global.atan = new Native(Math.atan);
global.ceil = new Native(Math.ceil);
global.cos = new Native(Math.cos);
global.exp = new Native(Math.exp);
global.floor = new Native(Math.floor);
global.log = new Native(Math.log);
global.max = new Native(Math.max);
global.min = new Native(Math.min);
global.pow = new Native(Math.pow);
global.round = new Native(Math.round);
global.sin = new Native(Math.sin);
global.sqrt = new Native(Math.sqrt);
global.tan = new Native(Math.tan);
global.max.rest = { type: "number" };
global.min.rest = { type: "number" };
global.sizeof = {
result: { type: "number" },
type: "function",
funct: sizeof,
params: [{
type: "array",
dimensions: []
}]
};
function Native(funct) {
this.funct = funct;
this.type = "function";
var length = funct.length;
var params = this.params = [];
this.result = { type: "number" };
while (length--) params.push({ type: "number" });
}
function sizeof(array) {
return array.length;
}
请注意,我使用Object.create(null) 创建了全局范围。我这样做是因为全局作用域没有任何父作用域。
之后,我为每个程序创建了一个单独的程序范围,其中包含程序的顶级定义。该代码存储在一个名为analyzer.js 的文件中,该文件太大而无法放入一个答案中。以下是文件的前三行:
var parse = require("./ast");
var global = require("./global");
var program = Object.create(global);
如您所见,全局范围是程序范围的父级。因此,program 继承自 global,使得范围变量查找与对象属性查找一样简单。这使得语言的运行时更加简单。
程序范围包含程序的顶级定义。例如,考虑以下存储在matrix.bianca 文件中的矩阵乘法程序:
col(a[3][3], b[3][3], i, j)
if (j >= 3) a
a[i][j] += b[i][j]
col(a, b, i, j + 1)
row(a[3][3], b[3][3], i)
if (i >= 3) a
a = col(a, b, i, 0)
row(a, b, i + 1)
add(a[3][3], b[3][3])
row(a, b, 0)
顶级定义为col、row 和add。这些函数中的每一个都有自己的函数范围,它继承自程序范围。代码可以在line 67 of analyzer.js找到:
scope = Object.create(program);
例如,add 的函数范围具有矩阵a 和b 的定义。
因此,除了类原型之外,对函数范围建模也很有用。
代数数据类型建模的原型
类并不是唯一可用的抽象类型。在函数式编程语言中,数据使用algebraic data types 建模。
代数数据类型的最佳示例是列表:
data List a = Nil | Cons a (List a)
这个数据定义仅仅意味着a的列表可以是一个空列表(即Nil),也可以是插入到a列表中的“a”类型的值(即Cons a (List a))。例如,以下都是列表:
Nil :: List a
Cons 1 Nil :: List Number
Cons 1 (Cons 2 Nil) :: List Number
Cons 1 (Cons 2 (Cons 3 Nil)) :: List Number
数据定义中的类型变量a 启用parametric polymorphism(即它允许列表保存任何类型的值)。例如,Nil 可以专门用于数字列表或布尔值列表,因为它具有 List a 类型,其中 a 可以是任何东西。
这让我们可以创建像length这样的参数函数:
length :: List a -> Number
length Nil = 0
length (Cons _ l) = 1 + length l
length 函数可用于查找任何列表的长度,而与它包含的值类型无关,因为length 函数根本不关心列表的值。
除了参数多态性之外,大多数函数式编程语言还具有某种形式的ad-hoc polymorphism。在 ad-hoc 多态中,根据多态变量的类型选择函数的一种特定实现。
例如,JavaScript 中的+ 运算符用于加法和字符串连接,具体取决于参数的类型。这是一种特殊的多态性。
同样,在函数式编程语言中,map 函数通常是重载的。例如,您可能对列表有不同的map 实现,对集合有不同的实现等。类型类是实现临时多态性的一种方式。比如Functor类型类提供了map函数:
class Functor f where
map :: (a -> b) -> f a -> f b
然后我们为不同的数据类型创建Functor 的特定实例:
instance Functor List where
map :: (a -> b) -> List a -> List b
map _ Nil = Nil
map f (Cons a l) = Cons (f a) (map f l)
JavaScript 中的原型允许我们对代数数据类型和临时多态性进行建模。例如,上面的代码可以一对一地翻译成 JavaScript,如下所示:
var list = Cons(1, Cons(2, Cons(3, Nil)));
alert("length: " + length(list));
function square(n) {
return n * n;
}
var result = list.map(square);
alert(JSON.stringify(result, null, 4));
<script>
// data List a = Nil | Cons a (List a)
function List(constructor) {
Object.defineProperty(this, "constructor", {
value: constructor || this
});
}
var Nil = new List;
function Cons(head, tail) {
var cons = new List(Cons);
cons.head = head;
cons.tail = tail;
return cons;
}
// parametric polymorphism
function length(a) {
switch (a.constructor) {
case Nil: return 0;
case Cons: return 1 + length(a.tail);
}
}
// ad-hoc polymorphism
List.prototype.map = function (f) {
switch (this.constructor) {
case Nil: return Nil;
case Cons: return Cons(f(this.head), this.tail.map(f));
}
};
</script>
虽然类也可用于对临时多态性进行建模,但所有重载函数都需要在一个地方定义。使用原型,您可以在任何地方定义它们。
结论
如您所见,原型用途广泛。是的,它们主要用于建模类。但是,它们可以用于许多其他事情。
原型可用于的其他一些事情:
-
使用结构共享创建persistent data structures。
结构共享的基本思想是,不是修改一个对象,而是创建一个继承自原始对象的新对象并进行任何您想要的修改。原型继承擅长于此。
正如其他人所提到的,原型是动态的。因此,您可以追溯添加新的原型方法,它们将自动在原型的所有实例上可用。
希望这会有所帮助。