JavaScript - 原型链和继承

# 原型对象和原型链

JavaScript 常被描述为一种基于原型的语言 (prototype-based language)——每个对象拥有一个原型对象 (prptotype),对象以其原型为模板、从原型继承方法和属性。原型对象也可能拥有原型,并从中继承方法和属性,一层一层、以此类推。这种关系常被称为原型链 (prototype chain),它解释了为何一个对象会拥有定义在其他对象中的属性和方法. 这种继承模型提供了一个强大而可扩展的功能系统。

准确地说,这些属性和方法定义在Object的构造器函数(constructor functions)之上的prototype属性上,而非对象实例本身。

在传统的 OOP 中,首先定义“类”,此后创建对象实例时,类中定义的所有属性和方法都被复制到实例中。在 JavaScript 中并不如此复制——而是在对象实例和它的构造器之间建立一个链接(它是__proto__属性,是从构造函数的prototype属性派生的),之后通过上溯原型链,在构造器中找到这些属性和方法。

# 几种创建对象的方式及其原型链:

  • 语法结构创建

     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
    
  • 构建器创建

     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.
    
  • Object.create() 创建

    ECMAScript 5 中引入了一个新方法:Object.create()。可以调用这个方法来创建一个新对象。新对象的原型就是调用 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
    

# __proto__, prptotype, constructor, new 和实例 对象之间的关系:

7ed42f5cly1fqguw4y1zej20ge0e8wes.jpg

例子:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
function A(name, age) {
  this.age = age;
  this.name = name;
}

A.fullname = function (obj) {
  return `fullname: ${obj.name}  ${obj.age}`;
}

A.prototype.info = function () {
  return `name: ${this.name}  age: ${this.age}`;
}

var aaa = new A('aa', '23');
console.log(aaa);
console.log(aaa.info())
console.log(A.fullname(aaa))

aaa.__proto__ === A.prptotype === Object.getPrototypeOf(aaa)

注:

  1. 只有 prototype 对象里面的属性和方法会被继承, 像A.fullname 并不会被继承, 像 Object.is(), Object.keys() 方法等.
  2. 通过 prototype 对象可以给原有的对象动态添加 属性和方法, 以达到扩展的目的, 但是 Object.prototype 尽量不做修改. 扩展内置原型的唯一理由是支持JavaScript 引擎的新特性,如Array.forEach;
  3. 但是 prototype 定义属性当属性不是很灵活, 当直接使用 this 的时候还会出现问题, 一般在构造函数里面定义属性, 在 prototype 中定义方法.
  4. 因为原型链的机制, 查找属性和方法时会一直遍历对象的原型对象及原型对象的原型对象, 直到找到对应的方法属性为止, 或者 到 null 抛出异常, 而在原型链上查找属性是很耗时的操作, 原型链太长会导致性能下降.

# 继承

ES6 之前的写法:

B 继承自 A:

 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
  function B(name, age, gender) {
    A.call(this, name, age);
    this.gender = gender;
  }
  
  // B.prototype = Object.create(A.prototype, {
  //   info: {
  //     value: function () {return `name: ${this.name}  age: ${this.age} ... `},
  //     enumerable: true, 
  //     configurable: true, 
  //     writable: true 
  //   }, // 重写
  //   infoB: { 
  //     value: function () {
  //       return `name: ${this.name}  age: ${this.age} gender: ${this.gender}`;
  //     },
  //     enumerable: true, 
  //     configurable: true, 
  //     writable: true 
  //   }
  // })
  B.prototype = Object.create(A.prototype)
  B.prototype.infoB = function (){
    return `name: ${this.name}  age: ${this.age} gender: ${this.gender}`;
  }
  B.prototype.info = function (){   // 重写              
    const p = A.prototype.info.apply(this);
    return p + ' gender: ahu ' + this.gender;
  }
  B.prototype.constructor = B;

注: B 并没有 fullname 方法, 通过重新设置 B.prototype.info() 以达到重写 A,prototype,info() 的目的;

ES6 之后的写法:

 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
  class A {
    constructor (name, age) {
      this.name = name;
      this.age = age;
      this.info = function () {
        return `name: ${this.name}  age: ${this.age}`;
      }
    }
  
    static fullname(obj) {
      return `fullname: ${obj.name}  ${obj.age}`;
    }
  }
  
  class B extends A {
    constructor(name, age, gender) {
      super(name, age);
      this.gender = gender;
      this.info = function () {
        return `name: ${this.name}  age: ${this.age} ...`;
      }
      this.infoB = function () {
        return `name: ${this.name}  age: ${this.age} gender: ${this.gender}`;
      }
    }
  }
  
Licensed under CC BY-NC-SA 4.0