【问题标题】:What can the JavaScript prototype system do beyond mimicking a classical class system?除了模仿经典的类系统,JavaScript 原型系统还能做什么?
【发布时间】:2015-05-01 05:28:21
【问题描述】:

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

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

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

new foo()

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

除了模仿类之外,原型系统还有其他用途吗?原型可以做哪些类不能做的事情,或者根本没有?

【问题讨论】:

标签: javascript prototype prototype-programming


【解决方案1】:

原型系统通过标准对象实现继承,提供了​​一个迷人的metaprogramming 模型。当然,这主要用于表达实例类的既定和简单概念,但没有类作为语言级别的不可变结构,需要特定的语法来创建它们。通过使用普通对象,你可以对对象做的所有事情(你可以做任何事情)现在可以对“类”做 - 这就是你所说的灵活性。

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

  • 用于多重继承的混合和特征
  • 可以在从原型继承的对象实例化后修改原型
  • 高阶函数和方法装饰器可轻松用于创建原型

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

  • 切换现有对象的原型可用于显着改变它们的行为。 (ES6 Reflect.setPrototypeOf 提供全面支持)

  • 一些软件工程模式可以直接用对象来实现。示例是带有属性的flyweight pattern,包括动态链的chain of responsibilities,哦,当然还有prototype pattern

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

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

    但更动态的方法是使用

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

【讨论】:

  • 使用更动态的方法扩展myOptions 有什么优势吗?我的意思是,通常配置对象在整个函数调用中保持不变。
  • @Kay:它会更小(内存更少)并且应该更快地创建,尤其是对于大型默认对象。此外,对默认值的更改会自动传播
【解决方案2】:

早在 2013 年 6 月,我就在 benefits of prototypal inheritance over classical 上回答了一个问题。从那以后,我花了很多时间思考继承,包括原型和经典,我写了大量关于prototype-classisomorphism的文章。

是的,原型继承的主要用途是模拟类。但是,它的用途远不止模拟类。例如,原型链与作用域链非常相似。

原型范围同构

JavaScript 中的原型和作用域有很多共同点。 JavaScript 中有三种常见的链类型:

  1. 原型链。

    var foo = {};
    var bar = Object.create(foo);
    var baz = Object.create(bar);
    
    // chain: baz -> bar -> foo -> Object.prototype -> null
    
  2. 作用域链。

    function foo() {
        function bar() {
            function baz() {
                // chain: baz -> bar -> foo -> global
            }
        }
    }
    
  3. 方法链。

    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)

顶级定义为colrowadd。这些函数中的每一个都有自己的函数范围,它继承自程序范围。代码可以在line 67 of analyzer.js找到:

scope = Object.create(program);

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

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

代数数据类型建模的原型

类并不是唯一可用的抽象类型。在函数式编程语言中,数据使用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>

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

结论

如您所见,原型用途广泛。是的,它们主要用于建模类。但是,它们可以用于许多其他事情。

原型可用于的其他一些事情:

  1. 使用结构共享创建persistent data structures

    结构共享的基本思想是,不是修改一个对象,而是创建一个继承自原始对象的新对象并进行任何您想要的修改。原型继承擅长于此。

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

希望这会有所帮助。

【讨论】:

  • 一个(太)长但有趣的阅读 :-) 但是,通过解释原型类同构,它错过了问题的重点 imo - OP 已经知道这些是如何工作的,他想知道除此之外还有什么。您提到的唯一功能是原型可用于实现范围链(非常有趣的示例),并且它们允许在您想要的任何地方添加方法(这似乎是实现 ADT 所必需的)。
  • 现在开始阅读有关持久数据结构的文章。我想知道如何在不泄露旧数据的情况下使用原型来实现它们。
  • 你是对的。我将通过删除原型类同构来缩短答案的长度。
  • 哦,谢谢!但是,我现在已经阅读了有关持久向量的那些文章,并且我认为原型与此无关。结构共享并不意味着对象相互继承。你能提供(链接)一个示例实现吗?
【解决方案3】:

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

您可以轻松地扩展其他人编写的类,例如所有的 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'

你也可以复制其他类的方法,

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

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

这也意味着你可以一个对象从它继承之后扩展一个原型,但是这些更改将被应用。

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

【讨论】:

    【解决方案4】:

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

    在原型中,你可以给别人写的函数添加属性。

    例如

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

    在继承中使用:

    您可以通过使用原型属性来使用继承。 Here 是您可以在 JavaScript 中使用继承的方法。

    在传统的类系统中,类一旦定义就不能修改。但是你可以在带有原型系统的 JavaScript 中做。

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 2012-09-15
      • 2016-07-15
      • 2011-02-16
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多