除了模仿经典类系统之外,JavaScript原型系统还能做些什么?

gsk*_*lee 65 javascript prototype prototype-programming

原型系统看起来比传统的类系统更灵活,但人们似乎对所谓的"最佳实践"感到满意,它模仿了传统的类系统:

function foo() {
  // define instance properties here
}

foo.prototype.method = //define instance method here

new foo()
Run Code Online (Sandbox Code Playgroud)

原型系统必须具备其他所有灵活性.

在模仿课程之外是否有用于原型系统的用途?什么样的东西原型可以做哪些类不能,或者没有?

Ber*_*rgi 46

原型系统通过标准对象实现继承,提供了一种迷人的元编程模型.当然,这主要用于表达已建立的简单实例类概念,但没有类作为语言级不可变结构,需要特定语法来创建它们.通过使用普通对象,您可以对对象执行所有操作(并且您可以执行所有操作),现在您可以对"类"执行操作 - 这是您所说的灵活性.

然后,仅使用JavaScript的给定对象变异功能,以编程方式使用这种灵活性来扩展和更改类:

  • mixin和traits用于多重继承
  • 在实例化从其继承的对象之后,可以修改原型
  • 高阶函数和方法装饰器可以在原型的创建中轻松使用

当然,原型模型本身比仅实现类更强大.这些特性很少使用,因为类概念非常有用且广泛,因此原型继承的实际功能并不为人所知(并且在JS引擎中没有很好地优化: - /)


Aad*_*hah 32

早在2013年6月,我回答了一个关于原型继承优于经典原型的问题.从那以后,我花了很多时间思考继承,包括原型和经典,并且我写了很多关于原型 - 类同 构的文章.

是的,原型继承的主要用途是模拟类.但是,它可以用于更多,而不仅仅是模拟类.例如,原型链与范围链非常相似.

原型范围同构也是如此

JavaScript中的原型和范围有很多共同之处.JavaScript中有三种常见的链类型:

  1. 原型链.

    var foo = {};
    var bar = Object.create(foo);
    var baz = Object.create(bar);
    
    // chain: baz -> bar -> foo -> Object.prototype -> null
    
    Run Code Online (Sandbox Code Playgroud)
  2. 范围链.

    function foo() {
        function bar() {
            function baz() {
                // chain: baz -> bar -> foo -> global
            }
        }
    }
    
    Run Code Online (Sandbox Code Playgroud)
  3. 方法链.

    var chain = {
        foo: function () {
            return this;
        },
        bar: function () {
            return this;
        },
        baz: function () {
            return this;
        }
    };
    
    chain.foo().bar().baz();
    
    Run Code Online (Sandbox Code Playgroud)

在这三个中,原型链和范围链是最相似的.实际上,您可以使用臭名昭着的 with语句将原型链附加到范围链.

function foo() {
    var bar = {};
    var baz = Object.create(bar);

    with (baz) {
        // chain: baz -> bar -> Object.prototype -> foo -> global
    }
}
Run Code Online (Sandbox Code Playgroud)

那么原型范围同构的用途是什么?一个直接用途是使用原型链对范围链进行建模.这正是我为自己的编程语言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;
}
Run Code Online (Sandbox Code Playgroud)

请注意,我使用了创建全局范围Object.create(null).我这样做是因为全局范围没有任何父范围.

之后,对于每个程序,我创建了一个单独的程序范围,其中包含程序的顶级定义.代码存储在名为analyzer.js的文件中,该文件太大而无法放入一个答案中.以下是该文件的前三行:

var parse = require("./ast");
var global = require("./global");
var program = Object.create(global);
Run Code Online (Sandbox Code Playgroud)

如您所见,全局范围是程序范围的父级.因此,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)
Run Code Online (Sandbox Code Playgroud)

顶级定义是col,rowadd.这些函数中的每一个都有自己的函数作用域,它继承自程序范围.可以在analyzer.js的第67行找到该代码:

scope = Object.create(program);
Run Code Online (Sandbox Code Playgroud)

例如,函数范围add具有矩阵ab.的定义.

因此,除了类之外,原型对于函数范围的建模也很有用.

用于建模代数数据类型的原型

类不是唯一可用的抽象类型.在函数式编程语言中,使用代数数据类型对数据进行建模.

代数数据类型的最佳示例是列表:

data List a = Nil | Cons a (List a)
Run Code Online (Sandbox Code Playgroud)

该数据定义仅仅意味着a的列表可以是空列表(即Nil),或者是插入到列表中的类型"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
Run Code Online (Sandbox Code Playgroud)

a数据定义中的类型变量启用参数多态(即它允许列表保存任何类型的值).例如,Nil可能是专门为数字列表或布尔值的列表,因为它的类型是List a在那里a可以是任何东西.

这允许我们创建参数函数,如length:

length :: List a -> Number
length Nil        = 0
length (Cons _ l) = 1 + length l
Run Code Online (Sandbox Code Playgroud)

length函数可用于查找任何列表的长度,而不管其包含的值的类型,因为该length函数根本不关心列表的值.

除参数多态性外,大多数函数式编程语言也具有某种形式的ad-hoc多态性.在ad-hoc多态性中,根据多态变量的类型选择函数的一个特定实现.

例如,+JavaScript中的运算符用于加法和字符串连接,具体取决于参数的类型.这是ad-hoc多态的一种形式.

类似地,在函数式编程语言中,map函数通常被重载.例如,您可能有不同的map列表实现,集合的不同实现等.类型类是实现ad-hoc多态的一种方法.例如,Functor类类提供了以下map功能:

class Functor f where
    map :: (a -> b) -> f a -> f b
Run Code Online (Sandbox Code Playgroud)

然后,我们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)
Run Code Online (Sandbox Code Playgroud)

JavaScript中的原型允许我们对代数数据类型和ad-hoc多态进行建模.例如,上面的代码可以一对一翻译成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));
Run Code Online (Sandbox Code Playgroud)
<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>
Run Code Online (Sandbox Code Playgroud)

虽然类也可用于建模ad-hoc多态,但所有重载函数都需要在一个地方定义.使用原型,您可以在任何地方定义它们.

结论

如您所见,原型非常通用.是的,它们主要用于模拟类.但是,它们可以用于许多其他事情.

原型的一些其他东西可用于:

  1. 使用结构共享创建持久数据结构.

    结构共享的基本思想是,不是修改对象,而是创建一个从原始对象继承并进行所需修改的新对象.原型继承擅长于此.

  2. 正如其他人所提到的,原型是动态的.因此,您可以追溯添加新的原型方法,它们将在原型的所有实例上自动提供.

希望这可以帮助.


the*_*sti 11

我认为原型继承系统允许更加动态地添加方法/属性.

您可以轻松扩展其他人编写的类,例如所有jQuery插件,您还可以轻松添加到本机类,将实用程序函数添加到字符串,数组以及任何内容.

例:

// I can just add whatever I want to anything I want, whenever I want
String.prototype.first = function(){ return this[0]; };

'Hello'.first() // == 'H'
Run Code Online (Sandbox Code Playgroud)

您还可以从其他类复制方法,

function myString(){
  this[0] = '42';
}
myString.prototype = String.prototype;

foo = new myString();
foo.first() // == '42'
Run Code Online (Sandbox Code Playgroud)

它还意味着您可以对象从其继承之后扩展原型,但是将应用这些更改.

而且,就个人而言,我发现原型非常方便和简单,在对象中放置方法对我来说真的很吸引人;)