面向对象基础知识点汇总(3)—— 原型、继承与实现继承的几种方式

62 阅读10分钟

一起养成写作习惯!这是我参与「掘金日新计划 · 4 月更文挑战」的第5天,点击查看活动详情

九、类与实例

9-1、什么是类、什么是实例

使用new关键字调取People构造函数构造出了三个不同对象,我们可将People构造函数看作类,将构造出的三个不同对象看作三个实例:

  • 类好比“蓝图”,类只描述对象会拥有哪些属性和方法,但是并不具体指明属性的值
  • 比如狗是类,“史努比”是实例
  • 由类构造对象(实例)的过程我们可以称为“实例化”

十、原型

10-1、什么是prototype

任何函数都有prototype属性,prototype是英语“原型”的意思。

  • prototype属性值是个对象,它默认拥有constructor属性指回构造函数:
		function sum(a, b) {
			return a + b;
		}

		console.log(sum.prototype);
		console.log(typeof sum.prototype);
		console.log(sum.prototype.constructor === sum);

输出结果为:

可见,prototype属性值是个对象,它默认拥有constructor属性指回构造函数,用图形表示如下:

对于普通函数来说,prototype没有任何用处,但是对于构造函数来说prototype属性非常有用:

  • 构造函数的prototype属性是它实例的原型

注意,实例上的__proto__属性并不是标准的W3C属性,只是chrome浏览器方便学习者学习而创造出的隐匿属性,所以可以在chrome浏览器中访问到该属性。

现在,我们可以通过如下代码来证明它们之间的三角关系:

function People(name, age, sex) {
  this.name = name;
  this.age = age;
  this.sex = sex;
}
// 实例化
var zhangsan = new People('张三', 18, '男');
// 测试三角关系是否存在
console.log(zhangsan.__proto__ === People.prototype); // true

10-2、原型链查找

JavaScript规定,实例可以打点访问它的原型上的属性和方法,这被称为“原型链查找”

		function People(name, age, sex) {
			this.name = name;
			this.age = age;
			this.sex = sex;
		}
		// 往原型上添加nationality属性
		People.prototype.nationality = '中国';

		var zhangsan = new People('张三', 18, '男');

		console.log(zhangsan.nationality);      // 中国
		console.log(zhangsan);

可见,在张三的原型上添加nationality属性,使用张三打点可以访问到此属性,通过打印张三可看到:

实例的__proto__指向其原型,当使用实例打点访问其属性时,如果实例上没有那么会沿着原型链去其原型上找有无该属性

10-3、hasOwnProperty & in

  • hasOwnProperty方法可以检测对象是否真正“自己拥有”某属性或方法
  • in运算符只能检测某属性或方法是否能被对象访问,而不能检测出是否为自己的属性或方法
  function People(name, age, sex) {
    this.name = name;
    this.age = age;
    this.sex = sex;
  }
  // 往原型上添加nationality属性
  People.prototype.nationality = '中国';

  // 实例化
  var zhangsan = new People('张三', 12, '男');

  console.log(zhangsan.hasOwnProperty('name')); // true
  console.log(zhangsan.hasOwnProperty('age')); // true
  console.log(zhangsan.hasOwnProperty('sex')); // true
  console.log(zhangsan.hasOwnProperty('nationality')); // false

  console.log('name' in zhangsan); // true
  console.log('age' in zhangsan); // true
  console.log('sex' in zhangsan);// true
  console.log('nationality' in zhangsan); // true

10-4、在prototype上添加方法

在之前,我们是将方法直接添加到实例身上:

function People() {
  this.sayHello = function () {
  };
}
var xiaoming = new People();
var xiaohong = new People();
var xiaogang = new People();

console.log(xiaoming.sayHello === xiaohong.sayHello); // false

这种方式添加的方法的话,每个实例的方法函数都是内存中不同的函数,会造成内存的浪费

所以,我们可将方法写到prototype上来解决此问题:

function People(name, age, sex) {
  this.name = name;
  this.age = age;
  this.sex = sex;
}
// 把方法要写到原型上
People.prototype.sayHello = function () {
  console.log('你好,我是' + this.name + '我今年' + this.age + '岁了');
}

var xiaoming = new People('小明', 12, '男');
var xiaohong = new People('小红', 11, '女');

console.log(xiaoming.sayHello === xiaohong.sayHello); // true

xiaoming.sayHello();
xiaohong.sayHello();

这样,每个实例上的sayHello方法实际是同一个:

10-5、原型链的终点

以上案例中,People的prototype对象并不是原型链的终点,也就是说People的prototype对象也有它自己的原型:

在JS中,内置了一个Object构造函数,它也拥有自己的prototype对象,Object的prototype对象就是原型链的终点(也就是说Object的prototype对象就没有它自己的原型了)

在Object的prototype上会有hasOwnProperty、toString方法等,这也就是为什么可以通过实例打点调取hasOwnProperty方法了(会沿着原型链去找该方法):

function People(name, age) {
  this.name = name;
  this.age = age;
}
var xiaoming = new People('小明', 12);

console.log(xiaoming.__proto__.__proto__ === Object.prototype);     // true
console.log(Object.prototype.__proto__); // null,Object.prototype是原型的终点

console.log(Object.prototype.hasOwnProperty('hasOwnProperty'));     // true
console.log(Object.prototype.hasOwnProperty('toString'));           // true

10-6、数组的原型链

我们再来看下数组的原型链:

十一、继承

11-1、什么是继承

比如有以下两个类: People类与Student类

People类拥有的属性和方法Student类都有,Student类还扩展了一些属性和方法。

Student“是一种”People,两个类之间是“is a kind of”关系,这就是继承关系:Student类继承自People类

  • 继承描述了两个类之间的“is a kind of”关系,比如学生“是一种”人,所以人类和学生之间就构成继承关系
  • People称为“父类”(或者“超类”、“基类”);Student称为“子类”(或“派生类”)
  • 子类丰富了父类,让类描述得更具体、更细化

11-2、实现继承的方式

实现继承的关键在于:子类必须拥有父类的全部属性和方法,同时子类还应该能自己定义持有的属性和方法

11-2-1、通过原型链实现继承

让子类的原型指向父类的实例

代码示例:

  // 父类,人类
  function People(name, age, sex) {
    this.name = name;
    this.age = age;
    this.sex = sex;
  }
  People.prototype.sayHello = function () {
    console.log('你好,我是' + this.name + '我今年' + this.age + '岁了');
  };
  People.prototype.sleep = function () {
    console.log(this.name + '开始睡觉,zzzzz');
  };

  // 子类,学生类
  function Student(name, age, sex, scholl, studentNumber) {
    this.name = name;
    this.age = age;
    this.sex = sex;
    this.scholl = scholl;
    this.studentNumber = studentNumber;
  }
  // 关键语句,实现继承,让子类的原型指向父类的实例
  Student.prototype = new People();

  Student.prototype.study = function () {
    console.log(this.name + '正在学习');
  }
  Student.prototype.exam = function () {
    console.log(this.name + '正在考试,加油!');
  }
  // 重写、复写(override)父类的sayHello
  Student.prototype.sayHello = function () {
    console.log('敬礼!我是' + this.name + '我今年' + this.age + '岁了');
  }

  // 实例化
  var hanmeimei = new Student('韩梅梅', 9, '女', '实验小学', 100556);

  hanmeimei.study();
  hanmeimei.sayHello();
  hanmeimei.sleep();

  var laozhang = new People('老张', 66, '男');
  laozhang.sayHello();

使用原型链实现继承往往存在如下问题

  • 1)如果父类的属性中有引用类型值,则这个属性会被所有子类的实例共享

    • 比如,父类中如果有个数组属性,由于子类实例可以打点访问到该父类属性,那么一个子类实例通过打点访问到该属性后,push或pop...操作了该数组,那么其它子类再次访问该属性时也会受到影响
  • 2)子类的构造函数中,往往需要重复定义很多父类定义过的属性(子类构造函数写的不够优雅)

11-2-2、借用构造函数

为了解决原型中包含引用类型值所带来的问题以及子类构造函数不优雅的问题,开发人员通常使用一种“借助构造函数”的技术(也称为“伪造对象”或“经典继承”)

其核心思想为:

  • 在子类构造函数的内部调用父类的构造函数,需要注意的是要使用call()绑定上下文
function People(name, sex, age) {
  this.name = name;
  this.sex = sex;
  this.age = age;
  this.arr = [33, 44, 55];
}
function Student(name, sex, age, school, sid) {
  People.call(this, name, sex, age);
  this.school = school;
  this.sid = sid;
}

var xiaoming = new Student('小明', '男', 12, '实验小学', 100666);
console.log(xiaoming);
xiaoming.arr.push(77);
console.log(xiaoming.arr);
console.log(xiaoming.hasOwnProperty('arr'));

var xiaohong = new Student('小红', '女', 11, '实验小学', 100667);
console.log(xiaohong.arr);
console.log(xiaohong.hasOwnProperty('arr'));

要注意在子类中调用父类构造函数时要通过call指定上下文为该子类对象,不然独立调用People的话this会指向window

这种方式实现继承后,属性会写在各子类自己的身上,在往小明的arr中push数字时,小红的arr不会受到影响

11-2-3、组合继承

将借用原型链和借用构造函数的技术组合到一起,叫做组合继承(也称伪经典继承),是JS中最常用的继承方式

// 父类
function People(name, sex, age) {
  this.name = name;
  this.sex = sex;
  this.age = age;
}
People.prototype.sayHello = function () {
  console.log('你好,我是' + this.name + '今年' + this.age + '岁了');
}
People.prototype.sleep = function () {
  console.log(this.name + '正在睡觉');
}

// 子类
function Student(name, sex, age, school, sid) {
  // 借助构造函数
  People.call(this, name, sex, age);
  this.school = school;
  this.sid = sid;
}
// 实现继承,借助原型链
Student.prototype = new People();
Student.prototype.exam = function() {
  console.log(this.name + '正在考试');
};
Student.prototype.sayHello = function() {
  console.log('敬礼!你好,我是' + this.name + '今年' + this.age + '岁了,我是' + this.school + '学校的学生');
};

var xiaoming = new Student('小明', '男', 12, '实验小学', 100666);
xiaoming.sayHello();
xiaoming.sleep();
xiaoming.exam();

但是这种方式最大的问题就是,无论什么情况下,都会调用两次父类的构造函数:一次是在创建子类原型的时候( Student.prototype = new People() )、一次是在子类构造函数的内容调用(People.call(this, name, sex, age) )

11-2-4、原型式继承

首先,我们先来认识一下Object.create()函数:

  • IE9+开始支持Object.create()方法,可以根据指定的对象为原型创建出新对象
  • 如:var obj2 = Object.create(obj1); 即:表示以obj1为原型创建出obj2对象,也就是说obj2的__proto__指向obj1
  • 在之前我们要实现这种方式,需要借助构造函数,通过Object.create()的话就不需要借助构造函数了
  • Object.create()方法也可以接受第二个参数,第二个参数可以用来指定要补充的属性或要覆盖的属性
var obj1 = {
  a: 33,
  b: 45,
  c: 12,
  test: function() {
      console.log(this.a + this.b);
  }
};

var obj2 = Object.create(obj1, {
  d: {
      value: 99
  }, 
  a: {
      value: 2
  }
});


console.log(obj2.__proto__ === obj1);       // true
console.log(obj2.a); // 2
console.log(obj2.b);
console.log(obj2.c);
console.log(obj2.d);

obj2.test();

所以,在没有必要“兴师动众”地创建构造函数,而只是想让新对象与现有对象“类似”的情况下,使用Object.create()即可胜任,成为原型式继承

11-2-4-1、Object.create()的兼容写法

由于IE9+支持Object.create(),那么如何在低版本浏览器中实现Object.create()呢?

// 道格拉斯·克罗克福德写的一个函数,非常巧妙,面试常考
// 函数的功能就是以o为原型,创建新对象
function object(o) {
  // 创建一个临时构造函数
  function F() {}
  // 让这个临时构造函数的prototype指向o,这样一来它new出来的对象,__proto__指向了o
  F.prototype = o;
  // 返回F的实例
  return new F();
}

var obj1 = {
  a: 23,
  b: 5
};
var obj2 = object(obj1);

console.log(obj2.__proto__ === obj1);
console.log(obj2.a);
console.log(obj2.b);

11-2-5、寄生式继承

寄生式继承即:编写一个函数,它接收一个参数o,返回以o为原型的新对象p,同时给p上添加预置的新方法

var o1 = {
  name: '小明',
  age: 12,
  sex: '男'
};

var o2 = {
  name: '小红',
  age: 11,
  sex: '女'
};

// 寄生式继承
function f(o) {
  // 以o为原型创建出新对象
  var p = Object.create(o);
  // 补充方法
  p.sayHello = function () {
      console.log('你好,我是' + this.name + '今年' + this.age + '岁了');
  }
  p.sleep = function () {
      console.log(this.name + '正在睡觉');
  }

  return p;
}

var p1 = f(o1);
p1.sayHello();

var p2 = f(o2);
p2.sayHello();

console.log(p1.sayHello == p2.sayHello); // false

寄生式继承就是编写一个函数,它可以“增强对象”,只要把对象传入这个函数,这个函数将以此对象为“基础”创建出新对象,并为新对象赋予新的预置方法,在主要考虑对象而不是自定义类型和构造函数的情况下,寄生式继承也是一种有用的模式;但是使用寄生式继承来为对象添加函数,由于不能将函数复用而降低效率,即“方法没有写到prototype上”

11-2-6、寄生组合式继承

寄生组合式继承即:借用构造函数来继承属性,通过原型链的混成形式来继承方法。

背后的原理思路为:不必为了指定子类的原型而调用父类的构造函数,我们所需要的无非就是父类原型的一个副本而已。本质上,就是使用寄生式继承来继承父类的原型,然后再将结果指定给子类的原型即可。

// 这个函数接收两个参数,subType是子类的构造函数,superType是父类的构造函数
function inheritPrototype(subType, superType) {
  var prototype = Object.create(superType.prototype);
  subType.prototype = prototype;
}

// 父类
function People(name, sex, age) {
  this.name = name;
  this.sex = sex;
  this.age = age;
}
People.prototype.sayHello = function () {
  console.log('你好,我是' + this.name + '今年' + this.age + '岁了');
}
People.prototype.sleep = function () {
  console.log(this.name + '正在睡觉');
}

// 子类
function Student(name, sex, age, school, sid) {
  // 借助构造函数
  People.call(this, name, sex, age);
  this.school = school;
  this.sid = sid;
}
// 调用我们自己编写的inheritPrototype函数,这个函数可以让Student类的prototype指向“以People.prototype为原型的一个新对象”
inheritPrototype(Student, People);
Student.prototype.exam = function() {
  console.log(this.name + '正在考试');
};
Student.prototype.sayHello = function() {
  console.log('敬礼!你好,我是' + this.name + '今年' + this.age + '岁了,我是' + this.school + '学校的学生');
};

var xiaoming = new Student('小明', '男', 12, '实验小学', 100666);
xiaoming.sleep();
xiaoming.exam();
xiaoming.sayHello();

11-3、instanceof运算符

用来检测某对象是不是某个类的实例

  • 比如:xiaoming instanceof Student
  • 底层机理:检查Student.prototype属性是否在xiaoming的原型链上(多少层都行,只要在就行)
function People() {

}
function Student() {

}
Student.prototype = new People();
var xiaoming = new Student();

// 测试instanceof
console.log(xiaoming instanceof Student); // true
console.log(xiaoming instanceof People); // true