js原型链及继承

1,原型链

JavaScript 只有一种结构:对象。每个实例对象( object )都有一个私有属性(称之为 proto)指向它的构造函数的原型对象(prototype )。该原型对象也有一个自己的原型对象( proto ) ,层层向上直到一个对象的原型对象为 null。根据定义,null 没有原型,并作为这个原型链中的最后一个环节。

几乎所有 JavaScript 中的对象都是位于原型链顶端的 Object的实例。

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
45
46
// 让我们从一个函数里创建一个对象o,它自身拥有属性a和b的:
let f = function () {
this.a = 1;
this.b = 2;
}
/* 这么写也一样
function f() {
this.a = 1;
this.b = 2;
}
*/
let o = new f(); // {a: 1, b: 2}

// 在f函数的原型上定义属性
f.prototype.b = 3;
f.prototype.c = 4;

// 不要在 f 函数的原型上直接定义 f.prototype = {b:3,c:4};这样会直接打破原型链
// o.[[Prototype]] 有属性 b 和 c
// (其实就是 o.__proto__ 或者 o.constructor.prototype)
// o.[[Prototype]].[[Prototype]] 是 Object.prototype.
// 最后o.[[Prototype]].[[Prototype]].[[Prototype]]是null
// 这就是原型链的末尾,即 null,
// 根据定义,null 就是没有 [[Prototype]]。

// 综上,整个原型链如下:

// {a:1, b:2} ---> {b:3, c:4} ---> Object.prototype---> null

console.log(o.a); // 1
// a是o的自身属性吗?是的,该属性的值为 1

console.log(o.b); // 2
// b是o的自身属性吗?是的,该属性的值为 2
// 原型上也有一个'b'属性,但是它不会被访问到。如果没有 初始化时候的this.b = 2,那么此时后的b就为改语句f.prototype.b = 3赋的值,为3
// 这种情况被称为"属性遮蔽 (property shadowing)"

console.log(o.c); // 4
// c是o的自身属性吗?不是,那看看它的原型上有没有
// c是o.[[Prototype]]的属性吗?是的,该属性的值为 4

console.log(o.d); // undefined
// d 是 o 的自身属性吗?不是,那看看它的原型上有没有
// d 是 o.[[Prototype]] 的属性吗?不是,那看看它的原型上有没有
// o.[[Prototype]].[[Prototype]] 为 null,停止搜索
// 找不到 d 属性,返回 undefined

2,属性查找逻辑

属性是自上向下依次查找属性,直到查找到结果停止。例子如下:

  1. 代码

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    function doSomething(){}
    doSomething.prototype.foo = "bar"; // add a property onto the prototype
    var doSomeInstancing = new doSomething();
    doSomeInstancing.prop = "some value"; // add a property onto the object
    console.log( doSomething.prototype );
    console.log("doSomeInstancing.prop: " + doSomeInstancing.prop);
    console.log("doSomeInstancing.foo: " + doSomeInstancing.foo);
    console.log("doSomething.prop: " + doSomething.prop);
    console.log("doSomething.foo: " + doSomething.foo);
    console.log("doSomething.prototype.prop: " + doSomething.prototype.prop);
    console.log("doSomething.prototype.foo: " + doSomething.prototype.foo);
  2. 在google浏览器其中获得结构

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    {
    prop: "some value",
    __proto__: {
    foo: "bar",
    constructor: ƒ doSomething(),
    __proto__: {
    constructor: ƒ Object(),
    hasOwnProperty: ƒ hasOwnProperty(),
    isPrototypeOf: ƒ isPrototypeOf(),
    propertyIsEnumerable: ƒ propertyIsEnumerable(),
    toLocaleString: ƒ toLocaleString(),
    toString: ƒ toString(),
    valueOf: ƒ valueOf()
    }
    }
    }
  3. 执行结果

    1
    2
    3
    4
    5
    6
    doSomeInstancing.prop:      some value
    doSomeInstancing.foo: bar
    doSomething.prop: undefined
    doSomething.foo: undefined
    doSomething.prototype.prop: undefined
    doSomething.prototype.foo: bar
  4. 分析结果

    当你访问doSomeInstancing 中的一个属性,浏览器首先会查看doSomeInstancing 中是否存在这个属性。

    如果 doSomeInstancing 不包含属性信息, 那么浏览器会在 doSomeInstancing 的 proto 中进行查找(同 doSomething.prototype). 如属性在 doSomeInstancing 的 proto 中查找到,则使用 doSomeInstancing 中 proto 的属性。

    否则,如果 doSomeInstancing 中 proto 不具有该属性,则检查doSomeInstancing 的 protoproto 是否具有该属性。默认情况下,任何函数的原型属性 proto 都是 window.Object.prototype. 因此, 通过doSomeInstancing 的 protoproto ( 同 doSomething.prototype 的 proto (同 Object.prototype)) 来查找要搜索的属性。

    如果属性不存在 doSomeInstancing 的 protoproto 中, 那么就会在doSomeInstancing 的 protoprotoproto 中查找。然而, 这里存在个问题:doSomeInstancing 的 protoprotoproto 其实不存在。因此,只有这样,在 proto 的整个原型链被查看之后,这里没有更多的 proto , 浏览器断言该属性不存在,并给出属性值为 undefined 的结论。

3,性能

  1. 分析

    在原型链上查找属性比较耗时,对性能有副作用,这在性能要求苛刻的情况下很重要。另外,试图访问不存在的属性时会遍历整个原型链。

    遍历对象的属性时,原型链上的每个可枚举属性都会被枚举出来。要检查对象是否具有自己定义的属性,而不是其原型链上的某个属性,则必须使用所有对象从 Object.prototype 继承的 hasOwnProperty 方法。

  2. 例子

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    console.log(g.hasOwnProperty('vertices'));
    // true

    console.log(g.hasOwnProperty('nope'));
    // false

    console.log(g.hasOwnProperty('addVertex'));
    // false

    console.log(g.__proto__.hasOwnProperty('addVertex'));
    // true
  3. 备注

    1,hasOwnProperty 是 JavaScript 中唯一一个处理属性并且不会遍历原型链的方法。(译者注:原文如此。另一种这样的方法:Object.keys())

    2,检查属性是否为 undefined 是不能够检查其是否存在的。该属性可能已存在,但其值恰好被设置成了 undefined。

4,创建对象

  1. 语法结构创建

    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
    var o = {a: 1};

    // o 这个对象继承了 Object.prototype 上面的所有属性
    // o 自身没有名为 hasOwnProperty 的属性
    // hasOwnProperty 是 Object.prototype 的属性
    // 因此 o 继承了 Object.prototype 的 hasOwnProperty
    // Object.prototype 的原型为 null
    // 原型链如下:
    // o ---> Object.prototype ---> null

    var a = ["yo", "whadup", "?"];

    // 数组都继承于 Array.prototype
    // (Array.prototype 中包含 indexOf, forEach 等方法)
    // 原型链如下:
    // a ---> Array.prototype ---> Object.prototype ---> null

    function f(){
    return 2;
    }

    // 函数都继承于 Function.prototype
    // (Function.prototype 中包含 call, bind等方法)
    // 原型链如下:
    // f ---> Function.prototype ---> Object.prototype ---> null
  2. 构造器创建对象

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    function Graph() {
    this.vertices = [];
    this.edges = [];
    }

    Graph.prototype = {
    addVertex: function(v){
    this.vertices.push(v);
    }
    };

    var g = new Graph();
    // g 是生成的对象,他的自身属性有 'vertices' 和 'edges'。
    // 在 g 被实例化时,g.[[Prototype]] 指向了 Graph.prototype。
  3. Object.create创建对象

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    var a = {a: 1}; 
    // a ---> Object.prototype ---> null

    var b = Object.create(a);
    // b ---> a ---> Object.prototype ---> null
    console.log(b.a); // 1 (继承而来)

    var c = Object.create(b);
    // c ---> b ---> a ---> Object.prototype ---> null

    var d = Object.create(null);
    // d ---> null
    console.log(d.hasOwnProperty); // undefined, 因为d没有继承Object.prototype
  4. Class关键字创建对象

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    "use strict";

    class Polygon {
    constructor(height, width) {
    this.height = height;
    this.width = width;
    }
    }

    class Square extends Polygon {
    constructor(sideLength) {
    super(sideLength, sideLength);
    }
    get area() {
    return this.height * this.width;
    }
    set sideLength(newLength) {
    this.height = newLength;
    this.width = newLength;
    }
    }

    var square = new Square(2);

5,扩展原型链方法

  1. 下面列举四种用于拓展原型链的方法,以及他们的优势和缺陷。下列四个例子都创建了完全相同的 inst 对象(所以在控制台上的输出也是一致的),为了举例,唯一的区别是他们的创建方法不同。

    1. New-initialization

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      function foo(){}
      foo.prototype = {
      foo_prop: "foo val"
      };

      var proto = new foo;
      proto.bar_prop = "bar val";

      function bar(){}
      bar.prototype = proto;

      var inst = new bar;
      console.log(inst.foo_prop); //foo val
      console.log(inst.bar_prop); //bar val
      //此时inst 是一个object
    2. Object.create

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      function foo(){}
      foo.prototype = {
      foo_prop: "foo val"
      };
      var proto = Object.create(foo.prototype);
      proto.bar_prop = "bar val";

      function bar(){}
      bar.prototype = proto;

      var inst = new bar;
      console.log(inst.foo_prop);
      console.log(inst.bar_prop);
    3. Object.setPrototypeOf

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      function foo(){}
      foo.prototype = {
      foo_prop: "foo val"
      };

      var proto = {
      bar_prop: "bar val"
      };
      Object.setPrototypeOf(proto, foo.prototype);

      function bar(){}
      bar.prototype = proto;

      var inst = new bar;
      console.log(inst.foo_prop);
      console.log(inst.bar_prop);
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      function foo(){}
      foo.prototype = {
      foo_prop: "foo val"
      };

      var proto;
      proto=Object.setPrototypeOf(
      { bar_prop: "bar val" },
      foo.prototype
      );

      function bar(){}
      bar.prototype = proto;
      var inst = new bar;
      console.log(inst.foo_prop);
      console.log(inst.bar_prop)
    4. prop

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      function foo(){}
      foo.prototype = {
      foo_prop: "foo val"
      };
      function bar(){}
      var proto = {
      bar_prop: "bar val",
      __proto__: foo.prototype
      };
      bar.prototype = proto;
      var inst = new bar;
      console.log(inst.foo_prop);
      console.log(inst.bar_prop);
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      var inst = {
      __proto__: {
      bar_prop: "bar val",
      __proto__: {
      foo_prop: "foo val",
      __proto__: Object.prototype
      }
      }
      };
      console.log(inst.foo_prop);
      console.log(inst.bar_prop)
  2. 优缺点分析

    名称 优势 缺陷
    New-initialization 支持目前以及所有可想象到的浏览器(IE5.5都可以使用)。 这种方法非常快,非常符合标准,并且充分利用JIT优化。 为使用此方法,必须对相关函数初始化。 在初始化过程中,构造函数可以存储每个对象必须生成的唯一信息。但是,这种唯一信息只生成一次,可能会带来潜在的问题。此外,构造函数的初始化,可能会将不需要的方法放在对象上。然而,如果你只在自己的代码中使用,你也清楚(或有通过注释等写明)各段代码在做什么,这些在大体上都不是问题(事实上,通常是有益处的)。
    Object.create 支持当前所有非微软版本或者 IE9 以上版本的浏览器。允许一次性地直接设置 __proto__ 属性,以便浏览器能更好地优化对象。同时允许通过 Object.create(null)来创建一个没有原型的对象。 不支持 IE8 以下的版本。然而,随着微软不再对系统中运行的旧版本浏览器提供支持,这将不是在大多数应用中的主要问题。 另外,这个慢对象初始化在使用第二个参数的时候有可能成为一个性能黑洞,因为每个对象的描述符属性都有自己的描述对象。当以对象的格式处理成百上千的对象描述的时候,可能会造成严重的性能问题。
    Object.setPrototypeOf 支持所有现代浏览器和微软IE9+浏览器。允许动态操作对象的原型,甚至能强制给通过 Object.create(null)创建出来的没有原型的对象添加一个原型。 这个方式表现并不好,应该被弃用。如果你在生产环境中使用这个方法,那么快速运行 Javascript 就是不可能的,因为许多浏览器优化了原型,尝试在调用实例之前猜测方法在内存中的位置,但是动态设置原型干扰了所有的优化,甚至可能使浏览器为了运行成功,使用完全未经优化的代码进行重编译。 不支持 IE8 及以下的浏览器版本。
    proto 支持所有现代非微软版本以及 IE11 以上版本的浏览器。将 __proto__ 设置为非对象的值会静默失败,并不会抛出错误。 应该完全将其抛弃因为这个行为完全不具备性能可言。 如果你在生产环境中使用这个方法,那么快速运行 Javascript 就是不可能的,因为许多浏览器优化了原型,尝试在调用实例之前猜测方法在内存中的位置,但是动态设置原型干扰了所有的优化,甚至可能使浏览器为了运行成功,使用完全未经优化的代码进行重编译。不支持 IE10 及以下的浏览器版本。

备注,参考:https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Inheritance_and_the_prototype_chain