1. 什么是原型?

    1. 最成功的流派是用“类”的方式来描述对象,诸如c++、Java等。这种叫做基于类的编程语言。
    2. 还有一种是基于原型的编程语言:利用原型来描述对象。JavaScript就是代表。
    3. “基于类”类型的语言中,总是现有类,再从类去实例化一个对象。类鱼类之间又可能形成继承、组合等关系。类又往往与语言的类型系统整合,形成一定编译时的能力。
    4. “基于原型”更提倡程序员去关注一系列对象实例的行为,而后才关心如何将这些对象,划分到最近的使用方式相似的原型对象,而不是将他们分成类。
    5. 基于原型和基于类都能够满足基本的复用抽象需求,但是适用场景不太相同。

      比如专业人士喜欢在看到老虎的时候,用猫科豹属豹亚种来描述,但是在不那么正式的场合,“大猫”可能更接近直观的感受。 (比起老虎来,美洲狮在历史上相当长时间都被划分为猫科猫属,所以性格跟猫更相似,亲近人)。

    1. 原型系统的“复制操作”有两种实现思路:
      • 一个是并不真的去复制一个原型对象,二十使得新对象持有一个原型的引用;
      • 另一个是切实地复制对象,从此两个对象再无关联。

    历史上的基于原型地语言因此产生了两个流派,JavaScript选择了第一种。

    2. JavaScript的原型

    1. 抛开JavaScript用于模拟Java类的复杂语法(new、Function Object、prototype属性等),原型系统相当简单,可以用两条概括:
      • 如果所有对象都有私有字段[[prototype]],就是对象的原型;
      • 读一个属性,如果对象本身没有,则会继续访问对象的原型,直到原型为空或者找到为止。

    历来ES的版本没有很大改变,但是ES6以来,JavaScript提供了一系列内置函数,以便更直接的访问操纵原型。分别为:

    • Object.create 根据指定的原型创建新对象,原型可以试null;
    • Object.getPrototypeOf 获得一个对象的原型;
    • Object.setPrototypeOf 设置一个对象的原型。

    利用这三个方法,完全可以抛开类,利用原型来实现抽象和符用。如下面代码展示了用原型来抽象猫和虎的例子:

    1. var cat = {
    2. say(){
    3. console.log("meow");
    4. },
    5. jump(){
    6. console.log("jump");
    7. }
    8. }
    9. var tiger = Object.create(cat, {
    10. say:{
    11. writable: true,
    12. configurable: true,
    13. enumerable: true,
    14. value: function () {
    15. console.log("roar!");
    16. }
    17. }
    18. })
    19. var anotherCat = Object.create(cat);
    20. anotherCat.say(); // meow~
    21. var anotherTiger = Object.create(tiger);
    22. anotherTiger.say(); // roar!

    这段代码创建了“猫”对象,又根据猫做了一些修改创建了虎。之后完全可以用Object.create创建另外的猫和虎对象,我们可以通过“原始猫对象”和“原始虎对象”来控制所有猫虎的行为。
    考虑到new 和 prototype属性等基础设施今天仍然有效,而且被很多代码使用,学习这些知识也有助于我们理解运行时的原型工作原理,下面追溯一下早年的JavaScript的原型和类。

    3. 早期版本的类与原型

    1. 早期版本中,“类”的定一是一个私有属性[[class]],语言标准为内置类型诸如Number、String、Date等制定了[[class]]属性,以表时他们的类。语言使用者唯一可以访问[[class]]属性的方式是Object.prototype.toString
      一下代码展示了所有具有内置class属性的对象:
      1. var o = new Object;
      2. var n = new Number;
      3. var s = new String;
      4. var b = new Boolean;
      5. var d = new Date;
      6. var arg = function () { return arguments }();
      7. var r = new RegExp;
      8. var f = new Function;
      9. var arr = new Array;
      10. var e = new Error;
      11. console.log([o, n, s, b ,d ,arg, r, f, arr, e].map(v => Object.prototype.toString.call(v)))
      12. //0: "[object Object]"
      13. //1: "[object Number]"
      14. //2: "[object String]"
      15. //3: "[object Boolean]"
      16. //4: "[object Date]"
      17. //5: "[object Arguments]"
      18. //6: "[object RegExp]"
      19. //7: "[object Function]"
      20. //8: "[object Array]"
      21. //9: "[object Error]"


    因此在ES3和之前的版本,js中类的概念相当弱,仅仅是运行时的一个字符串属性。

    1. 在ES5开始,[[class]]私有属性被Symbol.toStringTag代替,Object.prototype.toString的意义从命名上不再跟class相关。甚至可以自定义Object.prototype.toString的行为,一下代码展示了使用Symbol.toStringTag来自定义Object.prototype.toString的行为:
      1. var o = { [Symbol.toStringTag]: "MyObject" }
      2. console.log(o + ""); // [object MyObject]


    这里创建了一个新对象,并且给他唯一的一个属性Symbol.toStringTag,我们用字符串加法出发了Object.prototype.toString的调用,发现这个属性最终对Object.prototype.toString的结果产生了影响。
    但是,考虑到JavaScript语法中跟Java相似的部分,我们对类的讨论不能用“new运算是针对构造器对象,而不是类”来逃避。
    所以,我们仍然要把new理解成JavaScript面向对象的一部分,接下来说一下new 操作具体做了那些事情。

    1. new运算接受一个构造器和一组调用参数,实际上做了以下几件事:
      • 以构造器的prototype属性(注意与私有字段[[prototype]]的区分)为原型,创建新对象;
      • 将this和调用参数传给构造器,执行;
      • 如果构造器返回的是对象,则返回,否则返回第一部创建的对象。

    new这样的行为,试图让函数对象在语法上跟类变得相似,但是,它客观提供了两种方式:一是在构造器中添加属性,二是在构造器的prototype属性上添加属性。
    下面展示了用构造器模拟类的两种方法:

    1. function c1() {
    2. this.p1 = 1;
    3. this.p2 = function() {
    4. console.log(this.p1);
    5. }
    6. }
    7. var o1 = new c1;
    8. o1.p2();
    9. function c2() {
    10. }
    11. c2.prototype.p1 = 1;
    12. c2.prototype.p2 = function(){
    13. console.log(this.p1);
    14. }
    15. var o2 = new c2;
    16. o2.p2();

    第一种方法是直接在构造器中修改this,给this添加属性。
    第二种方法是修改构造器的prototype属性指向的对象,他是从这个构造器构造出来的所有对象的原型。
    没有Object.createObject.setPrototypeOf的早期版本中,new运算是唯一一个可以指定[[prototype]]的方法(当时mozilla提供了私有属性proto,但多数环境不支持),所以有人试图用它来替代后来的Object.create,我们甚至可以用它来实现一个Object.create的不完整的pollyfill,如下:

    1. Object.create = function(prototype){
    2. var cls = function(){}
    3. cls.prototype = prototype;
    4. return new cls;
    5. }

    它创建了一个空函数作为类,并把传入的原型挂在了它的prototype,最后创建了一个它的实例,根据new的行为,这将产生一个以传入的第一个参数为原型的对象。(他无法与原生Object.create一致:不支持第二个参数、不支持null作为原型,所以在今天意义已经不大)。

    4. ES6中的类

    1. 好在ES6加入了新特性class,new跟function搭配的怪异行为可以退休了(虽然运行时没有改变),在任何场景,都推荐使用ES6的语法来定义类,而令function回归原本的函数语义。下面看一下ES6中的类。
    2. ES6中引入了class关键字,并且在标准中删除了所有[[class]]相关的私有属性描述,类的概念正式从属性升级成语言的基础设施,从此,基于类的编程方式成为了JavaScript的官方编程范式。
      如下:
      1. class Rectangle {
      2. constructor(height, width) {
      3. this.height = height;
      4. this.width = width;
      5. }
      6. // Getter
      7. get area() {
      8. return this.calcArea();
      9. }
      10. // Method
      11. calcArea(){
      12. return this.height * this.width;
      13. }
      14. }


    在现有的类语法中,getter/setter和method是兼容性最好的。
    我们通过get/set关键字来创建getter,通过括号和大括号来创建方法,数据型成员最好卸载构造器里面。
    类的写法实际上也是由原型运行时来承载的,逻辑上JavaScript认为每个类是有共同原型的一组对象,类中定义的方法和属性则会被写在原型对象之上。
    最重要的是,类提供了继承能里。如下:

    1. class Animal {
    2. constructor(name) {
    3. this.name = name;
    4. }
    5. speak(){
    6. console.log(this.name + ' makes a noise');
    7. }
    8. }
    9. class Dog extends Animal {
    10. constructor(name) {
    11. super(name);
    12. // call the super class constructor and pass in the name parameter
    13. }
    14. speak(){
    15. console.log(this.name + 'barks.');
    16. }
    17. }
    18. let pp = new Dog('Mitzie');
    19. pp.speak(); // Mitzie barks.


    上面创造了Animal类,并通过extends让Dog继承了它,最终调用了子类的speak获取了父类的name。
    比起早期的原型模拟,使用extends关键字自动设置了constructor,并且会自动调用父类的构造函数,这是一种更少坑的设计。
    更激进的观点认为:class关键字和箭头运算符完全可以替代旧的function关键字,它更明确地区分了定义函数和定义类两种意图,这是有一定道理的。

    总结
    新的ES中,不用模拟类了,而原型体系同时作为一种编程范式和运行时机制存在。

    1. 我们可以自由选择原型或者类作为代码的抽象风格,但无论选择哪种,理解运行时的原型系统都很必要。