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模式,包括动态链的职责链,哦,当然还有原型模式。
最后一个的一个很好的例子是具有默认值的选项对象。每个人都用它来创造它们
1var myOptions = extend({}, defaultOptions, optionArgument);但更有活力的方法是使用
1var 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(); |
在这三个中,原型链和范围链是最相似的。实际上,您可以使用臭名昭着的
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; } |
请注意,我使用
之后,对于每个程序,我创建了一个单独的程序范围,其中包含程序的顶级定义。代码存储在名为analyzer.js的文件中,该文件太大而无法放入一个答案中。以下是该文件的前三行:
1 2 3 | var parse = require("./ast"); var global = require("./global"); var program = Object.create(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) |
顶级定义是
1 | scope = Object.create(program); |
例如,
因此,除了类之外,原型对于函数范围的建模也很有用。
用于建模代数数据类型的原型
类不是唯一可用的抽象类型。在函数式编程语言中,使用代数数据类型对数据进行建模。
代数数据类型的最佳示例是列表:
1 | data List a = Nil | 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 |
数据定义中的类型变量
这允许我们创建像
1 2 3 | length :: List a -> Number length Nil = 0 length (Cons _ l) = 1 + length l |
除参数多态性外,大多数函数式编程语言也具有某种形式的ad-hoc多态性。在ad-hoc多态性中,根据多态变量的类型选择函数的一个特定实现。
例如,JavaScript中的
类似地,在函数式编程语言中,
1 2 | class Functor f where map :: (a -> b) -> f a -> f b |
然后,我们为不同的数据类型创建
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多态,但所有重载函数都需要在一个地方定义。使用原型,您可以在任何地方定义它们。
结论
如您所见,原型非常通用。是的,它们主要用于模拟类。但是,它们可以用于许多其他事情。
原型的一些其他东西可用于:
使用结构共享创建持久数据结构。
结构共享的基本思想是,不是修改对象,而是创建一个从原始对象继承并进行所需修改的新对象。原型继承擅长于此。
正如其他人所提到的,原型是动态的。 因此,您可以追溯添加新的原型方法,它们将在原型的所有实例上自动提供。
希望这可以帮助。
好。
我认为原型继承系统允许更加动态地添加方法/属性。
您可以轻松扩展其他人编写的类,例如所有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中使用原型系统。