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

What can the JavaScript prototype system do beyond mimicking a classical class system?

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

1
2
3
4
5
6
7
function foo() {
  // define instance properties here
}

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

new foo()

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

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


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

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

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

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

  • 切换现有对象的原型可以用来显着改变他们的行为。 (完全支持ES6 Reflect.setPrototypeOf)
  • 一些软件工程模式可以直接用对象实现。例如具有属性的flyweight模式,包括动态链的职责链,哦,当然还有原型模式。

    最后一个的一个很好的例子是具有默认值的选项对象。每个人都用它来创造它们

    1
    var myOptions = extend({}, defaultOptions, optionArgument);

    但更有活力的方法是使用

    1
    var myOptions = extend(Object.create(defaultOptions), optionArgument);


早在2013年6月,我回答了一个关于原型继承优于经典原型的问题。从那以后,我花了很多时间思考继承,原型和经典,我写了很多关于原型级同构的内容。

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

原型范围同构也是如此

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

  • 原型链。

    1
    2
    3
    4
    5
    var foo = {};
    var bar = Object.create(foo);
    var baz = Object.create(bar);

    // chain: baz -> bar -> foo -> Object.prototype -> null
  • 范围链。

    1
    2
    3
    4
    5
    6
    7
    function foo() {
        function bar() {
            function baz() {
                // chain: baz -> bar -> foo -> global
            }
        }
    }
  • 方法链。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    var chain = {
        foo: function () {
            return this;
        },
        bar: function () {
            return this;
        },
        baz: function () {
            return this;
        }
    };

    chain.foo().bar().baz();
  • 在这三个中,原型链和范围链是最相似的。实际上,您可以使用臭名昭着的with语句将原型链附加到作用域链。

    1
    2
    3
    4
    5
    6
    7
    8
    function foo() {
        var bar = {};
        var baz = Object.create(bar);

        with (baz) {
            // chain: baz -> bar -> Object.prototype -> foo -> global
        }
    }

    那么原型范围同构的用途是什么?一个直接用途是使用原型链对范围链进行建模。这正是我为自己的编程语言Bianca所做的,我用JavaScript实现了它。

    我首先定义了Bianca的全局范围,在一个名为global.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
    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的文件中,该文件太大而无法放入一个答案中。以下是该文件的前三行:

    1
    2
    3
    var parse = require("./ast");
    var global = require("./global");
    var program = Object.create(global);

    如您所见,全局范围是程序范围的父级。因此,program继承自global,使范围变量查找与对象属性查找一样简单。这使得语言的运行时间更加简单。

    程序范围包含程序的顶级定义。例如,考虑以下矩阵乘法程序,该程序存储在matrix.bianca文件中:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    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)

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

    1
    scope = Object.create(program);

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

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

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

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

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

    1
    data List a = Nil | Cons a (List a)

    这个数据定义只是意味着一个列表可以是一个空列表(即Nil),也可以是一个类型为"a&rdquo"的值。插入到a的列表中(即Cons a (List a))。例如,以下是所有列表:

    1
    2
    3
    4
    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启用参数多态(即,它允许列表保存任何类型的值)。例如,Nil可以专门用于数字列表或布尔列表,因为它具有类型List a,其中a可以是任何内容。

    这允许我们创建像length这样的参数函数:

    1
    2
    3
    length :: List a -> Number
    length Nil        = 0
    length (Cons _ l) = 1 + length l

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

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

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

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

    1
    2
    class Functor f where
        map :: (a -> b) -> f a -> f b

    然后,我们为不同的数据类型创建Functor的特定实例:

    1
    2
    3
    4
    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中的原型允许我们对代数数据类型和ad-hoc多态进行建模。例如,上面的代码可以一对一翻译成JavaScript,如下所示:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    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));
    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
    // 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));
        }
    };

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

    结论

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

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

  • 使用结构共享创建持久数据结构。

  • 了解Clojure的持久向量,pt。 1
  • 了解Clojure的持久向量,pt。 2
  • 了解Clojure的持久向量,pt。 3
  • 结构共享的基本思想是,不是修改对象,而是创建一个从原始对象继承并进行所需修改的新对象。原型继承擅长于此。

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

  • 希望这可以帮助。

    好。


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

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

    例:

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

    'Hello'.first() // == 'H'

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

    1
    2
    3
    4
    5
    6
    7
    function myString(){
      this[0] = '42';
    }
    myString.prototype = String.prototype;

    foo = new myString();
    foo.first() // == '42'

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

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


    在JavaScript中,没有类的概念。这里的一切都是对象。 JavaScript中的所有对象都来自Object。当我们以面向对象的方式开发应用程序时,prototype属性有助于继承。原型中有更多的功能,而传统的面向对象结构中有类。

    在原型中,您可以向其他人编写的函数添加属性。

    对于前者

    1
    2
    3
    Array.prototype.print=function(){
      console.log(this);
    }

    用于继承:

    您可以使用prototype属性来继承。以下是如何使用JavaScript继承。

    在传统的Class系统中,一旦定义了类,就无法修改。但是你可以在JavaScript中使用原型系统。