深入JS中的原型链和面向对象特性 - 继承(ES5)

152 阅读6分钟

面向对象有三大特性:封装、继承、多态

1)封装:将属性和方法封装到一个类中,可以称之为封装的过程;

2)继承:面向对象中非常重要的,不仅仅可以减少重复代码的数量,也是多态前提(纯面向对象中);

3)多态:不同的对象在执行时表现出不同的形态;

一、JavaScript原型链

1.1. 原型链

  • 从一个对象上获取属性,如果在当前对象中没有获取到就会去它的原型上面获取:
    // obj对象
    var obj = {
      name: 'yzh',
      age: 18
    };
    
    //第一个{}
    obj.__proto__ = { }
    
    //第二个{}
    obj.__proto__.__proto__ = { }
    
    //第三个{}
    obj.__proto__.__proto__.__proto__ = {
      address: '深圳市'
    }
    
    console.log(obj.address); //深圳市
    

1.2. 原型链尽头

  • 打印第三个对象原型__proto__属性
    console.log(obj.__proto__.__proto__.__proto__.__proto__); //[Object: null prototype] {}
    
  • [Object: null prototype] {}这个原型就是我们最顶层的原型了,从Object直接创建出来的对象的原型都是 [Object: null prototype] {}
    • 1)该对象有原型属性,但是它的原型属性已经指向的是null,也就是已经是顶层原型了;
    • 2)该对象上有很多默认的属性和方法;

原型链最顶层的原型对象就是Object的原型对象(Object是所有类的父类);

二、原型链实现继承

// 1. 父类:公共属性和方法;
function Person() {
  this.name = 'yzh'
  this.friends = [];
}

// 2.
Person.prototype.eating = function () {
  console.log(this.name + '吃东西!');
};

// 3. 子类:特有的属性和方法;
function Student() {
  this.son = 13456798465;
}

/**
 * 内存表现:
 *    创建了一个p对象,p对象的__proto__指向Person()的默认原型对象,
 * 把 p 对象赋值给Student.prototype。所以当我们在stu找不到属性时就会沿着原型链
 * 找到;
*/
// 4.创建父类对象,并且作为子类的原型对象
var p = new Person();
Student.prototype = p;

// 5.
Student.prototype.studying = function () {
  console.log(this.name + '在学习!');
};

var stu = new Student();
console.log(stu.son); //13456798465
stu.studying(); //yzh在学习!
console.log(stu.name); //yzh
stu.eating(); //yzh吃东西!

2.1. 原型链继承的弊端

  • 1)某些属性其实是保存在p对象上的,输出stu对象看不到这个属性;

    console.log(stu); //Person { son: 13456798465 }
    
  • 2)创建出来两个stu对象;

    var stu1 = new Student();
    var stu2 = new Student();
    
    //直接修改对象上的属性,是给本对象添加了一个新属性;
    stu1.name = "ace";
    console.log(stu1, stu1.name); //ace
    console.log(stu2, stu2.name); //yzh
    
    //获取引用,修改引用中的值,会互相影响;
    stu1.friends.push("luffly");
    console.log(stu1.friends) //[ 'luffly' ]
    console.log(stu2.friends) //[ 'luffly' ]
    

    如图:

    stu.png

  • 3)在前面实现类的过程中都没有传递参数;

三、借用构造函数实现继承

  • 为了解决原型链继承中存在的问题,开发人员提供了一种新的技术: constructor stealing:
    • steal是偷窃、剽窃的意思,但是这里可以翻译成借用;
  • 借用继承的做法:在子类型构造函数的内部调用父类型构造函数;
    • 因为函数可以在任意的时刻被调用;
    • 因此通过apply()和call()方法也可以在新创建的对象上执行构造函数;

constructor stealing有很多名称:借用构造函数、经典继承、伪造对象;

// 父类:公共属性和方法;
function Person(name, age, friends) {
  this.name = name;
  this.age = age;
  this.friends = friends;
};

Person.prototype.eating = function () {
  console.log(this.name + '吃东西!');
};

// 子类:特有的属性和方法;
function Student(name, age, friends, son) {
  Person.call(this, name, age, friends, son); //相当于借用Person()赋值这一操作
  this.son = son;
};

var p = new Person();
Student.prototype = p;

Student.prototype.studying = function () {
  console.log(this.name + '在学习!');
};

var stu = new Student("yzh", 18, ['long'], 180);

//原型链实现继承解决的弊端:
//第一个弊端:输出stu对象,继承的属性是看不到的;
console.log(stu); //Person { name: 'yzh', age: 18, friends: [ 'long' ], son: 180 }

//第二个弊端:创建出来两个stu对象;
var stu1 = new Student("stu1", 18, ['long'], 181);
var stu2 = new Student("stu2", 20, ['long'], 182);

// 直接修改对象上的属性,是给本对象添加了一个新属性;
stu1.name = "ace";
console.log(stu1, stu1.name); //ace
console.log(stu2, stu2.name); //yzh

// 获取引用,修改引用中的值,会互相影响;
stu1.friends.push("luffly");
console.log(stu1.friends) //[ 'long', 'luffly' ]
console.log(stu2.friends) //[ 'long' ]

3.1. 借用构造函数的弊端

  • 1)无论在什么情况下,都会调用两次父类构造函数;
    • 一次在创建子类原型的时候;
    • 另一次在子类构造函数内部(也就是每次创建子类实例的时候);
  • 2)stu的原型对象上会多一些属性,但是这些属性是没有存在的必要; setal.png

3.2. 解决方法

四、原型式继承函数

  • 继承的目的:重复利用另外一个对象的属性和方法;
  • 最终的目的:info对象的原型指向了obj对象;
    var obj = {
      name: "yzh",
      age: 18
    };
    
    //1.
    function createObject1(o) {
      var newObj = {};
      Object.setPrototypeOf(newObj, o);
      return newObj
    };
    // var info = createObject1(obj);
    
    //2.
    function createObject2(o) {
      function Fn() {};
      Fn.prototype = o;
      var newObj = new Fn();
      return newObj;
    };
    // var info = createObject2(obj);
    
    //3.
    var info = Object.create(obj);
    
    console.log(info);  //{}
    console.log(info.__proto__);  //{ name: 'yzh', age: 18 }
    

Object.setPrototypeOf()  方法设置一个指定的对象的原型(即,内部 [[Prototype]] 属性) 到另一个对象;

#语法:Object.setPrototypeOf(obj, prototype);

#参数:obj要设置其原型的对象;prototype该对象的新原型(一个对象或null);

Object.create()  方法用于创建一个新对象,使用现有的对象来作为新创建对象的原型(prototype);

#语法:Object.create(proto);

#参数:proto新创建对象的原型对象;

五、寄生式继承函数

  • 寄生式(Parasitic)继承是与原型式继承紧密相关的一种思想;
  • 寄生式继承的思路是结合 原型类继承 和 工厂模式 的一种方式;
    • 即创建一个封装继承过程的函数, 该函数在内部以某种方式来增强对象,最后再将这个对象返回;
    var personObj = {
      running: function() {
        console.log('running!');
      }
    };
    
    function createStudent(name) {
      var stu = Object.create(personObj);
    
      stu.name = name;
      stu.studying= function() {
        console.log('studying!');
      };
    
      return stu;
    };
    
    var obj2 = createStudent('ace');
    console.log(obj2); //{ name: 'ace', studying: [Function (anonymous)] }
    console.log(obj2.__proto__); //{ running: [Function: running] }
    obj2.running(); //running!
    console.log(obj2.name); //ace
    
    // var obj3 = createStudent('sabo');
    // obj3.running();
    
    // var obj4 = createStudent('luffy');
    // obj4.running();
    

六、寄生组合式继承

  • 事实上,现在可以利用寄生式继承将 借用构造函数继承 的两个问题解决掉;
    • 当我们在子类型的构造函数中调用父类型.call(this, 参数)这个函数的时候,就会将父类型中的属性和方法复制一份到了子类型中. 所以父类型本身里面的内容,我们不再需要;
    • 获取到一份父类型的原型对象中的属性和方法,不能直接让子类型的原型对象 = 父类型的原型对象;
  • 因为直接让子类型的原型对象 = 父类型的原型对象,会造成以后修改了子类型原型对象的某个引用类型的时候, 父类型原型对象的引用类型也会被修改,所以我们使用前面的寄生式思想就可以了;
    • 即:Student.prototype = Object.create(Person.prototype)
    function inheritPrototype(SubType, SuperType) {
      SubType.prototype = Object.create(SuperType.prototype);
    
      //因为打印的是Person类型,所以我们还是要用属性描述符改变constructor;
      Object.defineProperty(SubType.prototype, "constructor", {
        enumerable: false,
        configurable: true,
        writable: true,
        value: SubType
      });
    };
    
    function Person(name, age, friends) {
      this.name = name;
      this.age = age;
      this.friends = friends;
    };
    Person.prototype.running = function() {
      console.log('在跑步!');
    };
    Person.prototype.eating = function() {
      console.log('在吃东西!');
    };
    
    function Student(name, age, friends, son) {
      Person.call(this, name, age, friends);
    
      this.son = son;
    };
    
    inheritPrototype(Student, Person);
    // Student.prototype = Object.create(Person.prototype);
    // Student.prototype.constructor = Student; 
    
    Student.prototype.studying = function() {
      console.log('在学习!');
    };
    
    var stu = new Student('yzh', 18, ['ace'], 1804515);
    /*
     * 改变之前:Person { name: 'yzh', age: 18, friends: [ 'ace' ], son: 1804515 } 
     * 改变之后:Student { name: 'yzh', age: 18, friends: [ 'ace' ], son: 1804515 }
    */
    console.log(stu);
    console.log(stu.name, stu.age);
    stu.studying();
    stu.running();
    stu.eating();
    

最后

继承可以帮助我们将重复的代码和逻辑抽取到父类中,子类只需要直接继承过来使用即可;