JavaScript对象的继承【面试必备】

379 阅读6分钟

原型链

原型链是ECMAScript中重要的概念,理解原型链我们需要理解这三者之间的关系,即构造函数、构造函数的原型对象和调用构造函数生成的对象实例之间的关系;


如上图,我们可以看出一个构造函数有一个prototype属性指向它的原型对象,原型对象有一个constructor属性指向构造函数,构造函数的实例有一个__proto__属性指向原型对象;我们可以在控制台进行验证:


原型链的形成就是将一个对象构造函数的prototype赋值为需要继承对象的实例,所有对象最终继承Object;我们通过一个例子来具体说明一下:

function A () {};
A.prototype.sayhi = () => {
  // ...
}
function B () {};

B.prototype = new A();

var b = new B();
b.sayhi();

我们将A的实例赋值给B的原型B就将继承A原型上的所有属性和方法;因为所有对象都继承Object,所以A或者B可以使用Object提供的一些方法,如hasOwnProperty、toString等;我们可以画一个B继承A,A继承Object的关系图,如下:

由上,我们可以总结原型链继承的原理,让一个对象继承另一个对象的属性和方法,即让一个对象的原型作为另一个对象的实例,此时这个对象的原型就继承了另一个原型的属性和方法,当生成第一个对象实例时,这个实例对象就继承了其原型和另一个对象的属性和方法,举个栗子,我们要访问b对象的sayhi方法,现在b实例上找,没找到就在B.prototype上找,B.prototype是A的实例,A实例上也没有sayhi方法,就去A.prototype上找,找到了就返回或执行该方法,这种查找就是原型链查找,查找的这个链路就叫做原型链;b继承B.prototype,B.prototype继承A.prototype,A.prototype继承Object.prototype,这种继承就叫做原型链继承;

因为原型链的查找方式是依次网上查找,如果sayhi在b实例中找到了,就不会再往上查找了,这叫做属性屏蔽

这里需要注意,我们可以往B.prototype上加属性或者方法,如:

B.prototype.dd = () => {console.log('123')}b.dd();// 12B.prototype.constructor === A; // true

但是如果我们用字面量方式给B原型添加方法,就会断开继承:

B.prototype = {
  dd () {
    return "dd";
  }
}

B.prototype.constructor === A; // false
B.prototype.constructor === Object; // false

原型链跟上一篇讲的原型构造对象存在一样的缺点,即当对象继承的属性包括引用类型值时,此时所有的实例都会继承这个引用类型的属性,因此其中一个属性修改它那它的修改将会反射到所有的实例上,其次就是子类型实例不能向超类型构造函数传递参数。

借用构造函数

这种方法即在子类构造函数中调用超类型构造函数,通过call()或apply()来传递实例对象,如下:

function A () {
  this.friends: ['liu', 'chen', 'lin']
}

function B () {
  A.call(this);
}

var b1 = new B();
var b2 = new B();

b1.friends.push('zhang');
b2.friends; // "liu", "chen", "lin"

从上面代码我们可以看出b1的操作并不会影响b2的friends,这是因为在B中传递的是不同的实例对象,而且我们也可以向超类型构造函数传递参数,如:

function A (name) {
  this.name = name;
}

function B (age, name) {
  A.call(this, name);
  this.age = age;
}

var b = new B('leizi', 22);

b.name; // "leizi"
b.age; // 22

借用构造函数其实就是调用超类型构造函数在子类型函数里生成超类型中有的属性,它解决了原型链继承的这两个问题,但是我们定义方法只能在函数内部定义,而每一个实例实现相同功能的函数其实又是不同的函数,这就失去了函数复用的意义。

原型链构造函数组合式继承(组合继承)

融合了原型链继承和构造函数继承,如下:

function A (name) {
  this.name = name;
}

A.prototype.sayName = function () {
  alert(this.name);
}

function B (age, name) {
  A.call(this, name);
  this.age = age;
}

B.prototype = new A();
var b = new B('leizi', 22);

此时b实例就可以调用A原型上的sayName()方法了;这个方法看似很好,融合了原型链和构造函数继承的优点,但是,这个方法它调用了两次A构造函数,一次是new A()生成A的实例,一次是在B内部A.call()调用A;

原型式继承

这种方法即直接把一个对象赋值给构造函数的原型对象,使其实例对象能够访问到o中的属性和方法从而实现继承,原型式继承就是将这种方法封装起来,如下:

function object (o) {
  function F() {};
  F.prototype = o;

  return new F();
}

var o = {
  name = "chenlei",
  age = 22;
};

var f = new F();

f.name; //"chenlei"
f.age; // 22

    ECMAScript新增Object.create()方法来实现原型式继承,它接收两个参数,第一个为需要继承的基对象,第二个是要额外或者修改的参数,第二个参数格式与Object.defineProperties()方法的第二个参数格式一样,即:

    var o = {
      name = "chenlei",
      age = 22;
    };
    
    var f = Object.create(o, {
      name: {
        value: "leizi"
      }
    });

    这种方式的继承跟原型链继承一样存在引用类型值时修改将会反射到所有继承的实例中。

    ES6 class类的继承

    extends

    ES6为了更接近其他OO语言,采用extends关键字来实现类的继承:

    class A {};
    class B extends A {};

      在ES5中我们知道不能给内置对象添加自定义方法,所以我们采用寄生式构造函数的方式来创建一个对象,它拥有自定义的方法和内置对象的所有方法,但在ES6的Class中我们可以直接让子类extends内置对象,如我们之前的例子:

      function MyArray () {
        var arr = new Array();
        arr.push.apply(arr, arguments);
        arr.toPipedString = function () {
          return this.join('|');
        };
        return arr;
      };
      
      var myarr = new MyArray('red', 'green', 'black');
      
      myarr.toPipedString(); // "red|green|black"

        这个例子是构造一个有特殊方法的数组,因为我们不能直接在Array上新增方法,我们用Class来改写:

        class MyArray extends Array {
           constructor () {
             super();
             this.arr = [...arguments];
           }
        
           toPipedString () {
             return this.arr.join('|');
           }
         }
        
         var myarr = new MyArray('red', 'green', 'black');
        
        myarr.toPipedString(); // "red|green|black

        super

        在class类中,super是指超类型,它有两种使用方式,如下:

        class A {
          constructor (x, y) {
            this.x = x;
            this.y = y;
          }
        
          add () {
            console.log(this.x + this.y); 
          }  
        }
        
        class B extends A {
          constructor (x, y, z) {
            super(x, y);
            this.z = z;
            this.p = super.add();
          }
        }
        
        var b = new B(10, 12);
        b.x; // 10
        b.y; // 12
        b.p; // 22

        如上,当super作为构造函数时指向的是类A的constructor,当super作为对象时,在普通方法中使用则是指向父类的原型,当在静态方法中使用时指向父类,即上面的super.add()实际上等于A.prototype.add()

        ES6规定必须在子类的constructor中用一次super,且内部的this使用必须在super之后,比如这样将报错:

        class B extends A {
          constructor (x, y, z) {
           this.z = z;  //TypeError
           super(x, y);
            this.p = super.add();
          }
        }

          这是因为Class是先创建父类实例对象this,然后子类的构造函数再修改this,子类自身是没有this对象的,所以先于super()使用this会报错;ES5的继承机制是先创建子类型的this对象,再将父类属性和方法添加到子类型的this上;

          判断一个类是否是另一个类的子类可以用Object.getPrototypeOf()方法:

          Object.getPrototypeOf(B) === A; // true

          在class中有__proto__和prototype两条继承线,子类__proto__指向父类,prototype的__proto__指向父类的prototype:

          B.__proto__ === A; //true
          B.prototype.__proto__ === A.prototype; // true