Skip to content

js原型、原型链和继承

在谈原型之前首先先介绍一下创建对象的几个方法

创建对象的几个方法

  • Object构造函数,即new Object()
  • 对象字面量,即var o = { // 定义属性或方法 }

这两种有一个明显的缺点,当使用同一个接口创建很多对象的时候,会产生大量重复的代码。

  • 工厂模式
javascript
function create(name) {
	var o = new Object();
	o.name = name;
	o.sayHi = function() {
		console.log(thi.name);
	}
	return o;
}
var person1 = create('kim');

缺点:没有解决对象识别的问题(即怎样知道一个对象的类型,instanceof

  • 构造函数模式
javascript
function Person(name, age) {
  this.name = name;
  this.age = age;
  this.sayHi = function () {
    console.log(`Hi, My name is ${ this.name }`);
  }
}

let person1 = new Person('kim', 22);
let person2 = new Person('Lee', 21);
console.log(person1 instanceof Object); // true
console.log(person1 instanceof Person); // true
console.log(person2 instanceof Object); // true
console.log(person2 instanceof Person); // true

缺点:每个方法都要在每个实例上重新创建一遍。this.sayHi = function () {} 等价于 this.sayHi = new Function();注意:构造函数以大写字母开头,而且必须使用new操作符实例对象。 调用构造函数会经历一下4个步骤

  1. 创建一个新对象;
  2. 将构造函数的作用域赋给新对象(因此this就指向了这个新对象);
  3. 执行构造函数中的代码(为这个新对象添加属性)
  4. 返回新对象。

构造函数胜于工厂函数的地方是:创建自定义构建函数都有一个constructor(构造函数)属性,用来标识对象类型,而且将它的实例标识为一种特定的类型,即可以通过instanceof来检查对象类型。

如上例子中js原型

构造函数和普通函数的区别:前者是new出来的,如果不是new出来的则区别,当然要注意函数内部的this。

原型模式(重要)

解析

为什么会用小篇幅度来讲创建对象的几个方法,因为每个解决方案出来都是因为解决其缺点出来的。或者说某个东西的优点就是解决某个东西的缺点。

因为构造函数的缺点是每个实例重复创建方法,那原型就来解决这个问题。 创建的每个函数都会有一个prototype(原型)属性,这个属性是一个指针,指向一个对象,而这个对象的用途是包含可以有特定类型的所有实例共享的属性和方法。简单来说就是,原型上包含的属性和方法都会被所有对象实例共享。

javascript
function Person() {
  }

  Person.prototype.name = 'kim';
  Person.prototype.sayHi = function () {
    console.log(this.name);
  }

  let person1 = new Person();
  person1.sayHi(); // kim

解析(重要,对理解原型链和继承有关键作用!!!): 每创建一个对象的时候同时创建它的prototype属性,指向函数的原型对象,且所有原型对象都会自动获取constructor属性,这个属性是一个执行prototype属性所在函数的指针(如上例子Person.prototype.constructor指向Person)。 这个连接([prototype])存在于实例于构造函数的原型对象之间,而不是存在于实例与构造函数之间。

js原型

注意点

  1. 查找对象属性时,会先从实例找,再到原型找。(也就是说如果实例上有与原型的同名属性或方法会屏蔽掉原型上的)。
  2. 不能通过对象实例重写原型中的值。

附加

  • Object.hasOwnProperty() 判断给定属性是否存在于对象实例中,是则true,反之false
  • in操作符,在单独使用时,只要通过对象能够访问到属性时就返回true,反之false,如:'name' in person1,一般这两个方法一起使用,用来判断属性是实例上的还是原型上的。

添加原型属性和方法的两种方法

  1. 每添加一个属性和方法就敲一遍Person.prototype
  2. 用一个包含所有属性和方法的对象字面量重写整个原型对象。

如:

javascript
function Person() {}
Person.prototype = {
	name: 'kim',
	sayHi: function () {
		console.log(this.name);
	}
}

注意第二种方法本质上完全重写了默认的prototype对象,因此constructor属性也就变成了新对象的constructor属性(指向Object构造函数),不再指向Person函数。

一定要弄懂两者的区别,对理解继承有帮助!!! 第一种是自动生成的(原型解析有说到),第二种是重写。 例子: 第一种方式:

javascript
function Person() {
  }

  let person1 = new Person();
  Person.prototype.sayHi = function () {
    console.log('hi');
  }

  person1.sayHi(); // hi

第二种方式:

javascript
function Person() {
  }

  let person1 = new Person();
  Person.prototype = {
    sayHi: function () {
      console.log('hi');
    }
  }

  person1.sayHi(); // error

解析区别:因为对原型对象的修改都能够立即从实例上反映出来,即使在实例后修改也如此。原因是实例于原型之间是松散连接关系。为什么第二种会报错呢?因为调用构造函数时会为实例添加一个指向原来最初原型的[[prototype]]指针,但后来被重写切断构造函数与最初原型之间的联系。 要记住,实例中的指针仅指向原型,而不指向构造函数

第二种修改的过程内幕

js原型js原型

原型的缺点

原型的缺点也是因为它的共享特性导致的,最为明显的就是原型上引用类型值的属性,因为所有实例取该属性值都是同一个(同一个指针,因为是引用类型)。

javascript
function Person() {
  }

  Person.prototype.friends = ['aa', 'bb'];

  let person1 = new Person();
  person1.friends.push('cc');
  console.log(person1.friends); // "aa,bb,cc"

  let person2 = new Person();
  console.log(person2.friends); // "aa,bb,cc"

组合使用构造函数模式和原型模式

构造函数模式用于实例属性,而原型模式用于定义方法和共享属性。这个最广泛使用的一种模式。

javascript
function Person(name) {
    this.name = name
  }

  Person.prototype.sayHi = function () {
    console.log(this.name);
  }

  let person1 = new Person('kim');
  person1.sayHi(); // kim

继承

实现继承主要依靠原型链来实现的。 基本思想是利用原型让一个引用类型继承另一个引用类型的属性和方法。

原型链(重要)

说继承之前,必须说一说原型链。 上文提到过构造函数、原型和实例的关系,这里就简单回顾一下,每个构造函数都有一个原型对象,原型对象都包含一个指向构造函数的指针,而实例都包含一个指向原型对象的内部指针

那么我们把原型对象将包含一个指向另一个原型对象的指针,相应地,另一个原型中也包含着一个指向另一个构造函数的指针,假如另一个原型又是另一个类型的实例,如果关系依旧成立,则就构成实例与原型的链条,也就是原型链的基本概念

不理解?无所谓,直接上例子:

javascript
function Animal() {
    this.run = true;
  }

  Animal.prototype.getRun = function () {
    return this.run;
  }

  function Dog() {
    this.eat = 'meat';
  }
  // 继承了Animal
  Dog.prototype = new Animal();

  Dog.prototype.getEat = function () {
    return this.eat;
  }

  let dog = new Dog();
  console.log(dog.getRun()); // true

解析:首先我们声明了一个超类Animal(js中没有类的概念,只能模拟类,在其他语言中叫父类或者超类),超类上有一个原型方法getRun,然后我们再声明了一个基类Dog(其他语言中叫子类或者基类),接着是关键的一步,我们没有使用Dog的默认原型,而是把基类的原型指针重写,指向了Animal的实例,实例上有一个指向原型对象的指针,即Dog的原型指向了Animal的原型,这不怯怯好符合原型链的概念,实现了继承吗?然后Dog原型上又有一个方法getEat实现的本质是重写原型对象,代之以一个新类型的实例

解析图

dog→Dog Prototype(new Animal())→Animal Prototype

js原型

js原型

有人就会发现Dog.prototype什么会有Animal的实例属性和方法? 因为Dog.prototype重写为Animal的实例,理所当然的Animal上的实例属性和方法也会被添加到Dog.prototype上共享

注意

  • 继承后的基类的constructor会指向最终那个超类的构造函数。如上例Dog.prototype.constructor指向的是Animal
  • 添加超类不存在的方法或覆盖超类方法一定要在替换原型的语句之后
  • 通过原型链继承后,不能再使用对象字面量为基类添加原型方法或属性。因为上文也说到过对象字面量添加原型方法的本质是重写prototype对象,而原型链继承也是重写prototype对象,所以会造成再次重写原型链。

原型链的缺点

  1. 因为原型链是有原型组成,当然也会有同一缺点,就是引用类型值的问题,因为超类的实例属性会成为子类的原型属性,会被共享,当使用原型上的引用属性时,使用的指针都是同一个(引用类型,栈保存指针,堆保存值)。
  2. 在穿件子类型实例时无法向父类构造函数中传递参数

借用构造函数

这种方法也叫伪造对象或经典继承。 基本思路:在子类型构造函数的内部调用超类型构造函数。主要通过使用apply()和call()。 还不了解apply和call方法的先传送→MDN

javascript
function Father(surname) {
    this.surname = surname;
    this.hobby = ['basketball'];
  }

  function Son(surname, name) {
  	// 继承了SuperType,同时还传递了参数
    Father.call(this, surname); // 使用apply和call都可以,只是用法传不一样
    // 实例属性
    this.name = name;
  }

  let son1 = new Son('lee', 'kim');
  son1.hobby.push('run');
  console.log(son1.hobby); // "basketball,run"
  console.log(son1.surname, son1.name); // "lee kim"

  let son2 = new Son('lee', 'kami');
  console.log(son2.hobby); // "basketball"
  console.log(son2.surname, son2.name); // "lee kami"

js原型

注意:这种与原型链继承不一样,只是单纯在基类构造函数调用超类构造函数,达到继承的目的,但并不是原型上的修改(依旧只有Son→Son prototype),所有Son prototype指向并没有修改,依旧是默认自动创建的对象。因此超类的原型中定义的方法,对子类是不可见的。简单来说Son构造函数中的Father属性,只是个副本。

这种方法解决了原型链继承的两个缺点,即可以传参数,引用类型也不会相互影响了。 但既然叫构造函数,那么就有构造函数的缺点,方法都在构造函数中定义,因此函数复用就无从谈起。而且父类原型方法子类不可见

这也是为什么我一开始先提一下创建对象的几个方法,复杂的东西都是由简单的东西一点点组成,只有了解透彻,当组合起来使用的时候,就更好的分析了。

组合继承

将原型链和借用构造函数的技术组合一起,发挥两者之长的模式,也是js目前最常用的继承模式。 思路:使用原型链实现对原型属性和方法的继承,而通过借用构造函数来实现对实例属性的继承

javascript
function Father(surname) {
    this.surname = surname;
    this.hobby = ['basketball'];
  }

  Father.prototype.sayHi = function () {
    console.log('Hi, son');
  }

  function Son(surname, name) {
  	// 继承属性
    Father.call(this, surname);
    this.name = name;
  }
  // 继承方法
  Son.prototype = new Father();
  Son.prototype.constructor = Son;
  Son.prototype.sayName = function () {
    console.log(this.name);
  }

  let son1 = new Son('lee', 'kim');
  son1.hobby.push('run');
  console.log(son1.hobby); // "basketball, run"
  son1.sayHi(); // "Hi, son"
  son1.sayName(); // "lee kim"

  let son2 = new Son('lee', 'kami');
  console.log(son2.hobby); // "basketball"
  son2.sayHi(); // "Hi, son"
  son2.sayName(); // "lee kami"

js原型

这里就不再带着一步步分析了,就是两者结合只要弄懂原型链和借用构造函数,这个也会看懂了。如果还有哪里不懂就多看几遍例子。

寄生组合式继承

目前普遍认为是引用类型最理想的继承范式。

组合式继承最大的问题是在什么情况下,都会调用两次超类构造函数,一次在创建子类型原型的时候(Son.prototype = new Father()),另一次是在子类型构造函数内部(Father.call(this, surname))。

细心的就会发现在组合式继承的图里发现了,我在图里也用两个黄色的框框了起来。但为什么属性不会相互影响呢?因为实例属性和方法屏蔽了与之同名的原型方法,上文也提到过查找属性或方法时,是按原型链往上查找的。

基本思路是:不必为了指定子类型的原型而调用超类型的构造函数,所需要的无非就是超类型原型的一个副本而已。 其实也是组合式继承稍微修改了一点而已,就不一一解释了,注释也写清楚了,直接上代码↓

javascript
// ES6中新增Object.create()就是相当于copyPrototype()
function copyPrototype(o) {
    function F() {
    }

    F.prototype = o;
    return new F();
  }
  // subType 子类构造函数, superType 父类构造函数
  function initPrototype(subType, superType) {
    let prototype = copyPrototype(superType.prototype); // 创建超类型原型的一个副本
    // let prototype = Object.create(superType.prototype); // 与上面一样
    prototype.constructor = subType; // 添加构造器属性,弥补重新原型失去的默认constructor属性
    subType.prototype = prototype; // 将副本赋值给子类型原型
  }

  function Father(surname) {
    this.surname = surname;
    this.hobby = ['basketball'];
  }

  Father.prototype.sayHi = function () {
    console.log('Hi, son');
  }

  function Son(surname, name) {
    Father.call(this, surname);
    this.name = name;
  }

  initPrototype(Son, Father);
  Son.prototype.sayName = function () {
    console.log(this.name);
  }

  let son1 = new Son('lee', 'kim');
  son1.hobby.push('run');
  console.log(son1.hobby);
  son1.sayHi();
  son1.sayName();
  console.log(son1);

js原型

js原型

Released under the MIT License.