原型链与继承
构造函数
构造函数模式的目的就是为了创建一个自定义类,并且创建这个类的实例。构造函数模式中拥有了类和实例的概念,并且实例和实例之间是相互独立的,即实例识别。
构造函数就是一个普通的函数,创建方式和普通函数没有区别,不同的是构造函数习惯上首字母大写。另外就是调用方式的不同,普通函数是直接调用,而构造函数需要使用 new 关键字来调用。
function Person(name, age, gender) {
this.name = name
this.age = age
this.gender = gender
this.sayName = function () {
alert(this.name)
}
}
var per = new Person('孙悟空', 18, '男')
function Dog(name, age, gender) {
this.name = name
this.age = age
this.gender = gender
}
var dog = new Dog('旺财', 4, '雄')
console.log(per) //当我们直接在页面中打印一个对象时,事件上是输出的对象的toString()方法的返回值
console.log(dog)
每创建一个 Person 构造函数,在 Person 构造函数中,为每一个对象都添加了一个 sayName 方法,也就是说构造函数每执行一次就会创建一个新的 sayName 方法。这样就导致了构造函数执行一次就会创建一个新的方法,执行 10000 次就会创建 10000 个新的方法,而 10000 个方法都是一摸一样的,为什么不把这个方法单独放到一个地方,并让所有的实例都可以访问到呢?这就需要原型(prototype)
原型
在 JavaScript 中,每当定义一个函数数据类型(普通函数、类)时候,都会天生自带一个 prototype 属性,这个属性指向函数的原型对象,并且这个属性是一个对象数据类型的值。

原型对象就相当于一个公共的区域,所有同一个类的实例都可以访问到这个原型对象,我们可以将对象中共有的内容,统一设置到原型对象中。
原型链
proto和 constructor
每一个对象数据类型(普通的对象、实例、prototype......)也天生自带一个属性proto,属性值是当前实例所属类的原型(prototype)。原型对象中有一个属性 constructor, 它指向函数对象。
function Person() {}
var person = new Person()
console.log(person.__proto__ === Person.prototype) //true
console.log(Person.prototype.constructor === Person) //true
//顺便学习一个ES5的方法,可以获得对象的原型
console.log(Object.getPrototypeOf(person) === Person.prototype) // true

何为原型链
在 JavaScript 中万物都是对象,对象和对象之间也有关系,并不是孤立存在的。对象之间的继承关系,在 JavaScript 中是通过 prototype 对象指向父类对象,直到指向 Object 对象为止,这样就形成了一个原型指向的链条,专业术语称之为原型链。
举例说明:person → Person → Object ,普通人继承人类,人类继承对象类
当我们访问对象的一个属性或方法时,它会先在对象自身中寻找,如果有则直接使用,如果没有则会去原型对象中寻找,如果找到则直接使用。如果没有则去原型的原型中寻找,直到找到 Object 对象的原型,Object 对象的原型没有原型,如果在 Object 原型中依然没有找到,则返回 undefined。
我们可以使用对象的 hasOwnProperty()来检查对象自身中是否含有该属性;使用 in 检查对象中是否含有某个属性时,如果对象中没有但是原型中有,也会返回 true
function Person() {}
Person.prototype.a = 123
Person.prototype.sayHello = function () {
alert('hello')
}
var person = new Person()
console.log(person.a) //123
console.log(person.hasOwnProperty('a')) //false
console.log('a' in person) //true
person 实例中没有 a 这个属性,从 person 对象中找不到 a 属性就会从 person 的原型也就是 person.__proto__
,也就是 Person.prototype 中查找,很幸运地得到 a 的值为 123。那假如 person.__proto__
中也没有该属性,又该如何查找?
当读取实例的属性时,如果找不到,就会查找与对象关联的原型中的属性,如果还查不到,就去找原型的原型,一直找到最顶层 Object 为止。Object 是 JS 中所有对象数据类型的基类(最顶层的类)在 Object.prototype 上没有proto这个属性。
console.log(Object.prototype.__proto__ === null) // true

继承
1.原型链继承
原型链继承涉及的是构造函数、原型、实例化对象,这三者之前存在一定的关系:
- 每一个构造函数都有一个原型对象(prototype),
- 原型对象中包含一个指向构造函数的指针(constructor),指向对应的构造函数
- 实例中包含一个原型对象的指针([[Prototype]],这个内部属性无法直接访问,在 FF,Chrome 等浏览器可以通过proto得到)
function Person() {
this.name = '张三'
this.arr = [1, 2, 3]
}
function Teacher() {
this.teach = 'chinese'
}
Teacher.prototype = new Person()
console.log(new Teacher())
如果构造函数创建多个对象,属性被多个对象共享,如果属性是引用类型,那个修改一个对象的属性,会导致其他对象的这个属性也会被修改(如果是直接赋值对象的属性,则不会触发其他对象上该属性的修改)
//原型链继承
function Person() {
this.name = '张三'
this.arr = [1, 2, 3]
}
function Teacher() {
this.teach = 'chinese'
}
Teacher.prototype = new Person()
// console.log(new Teacher())
//原型继承缺点:构造函数创建多个对象,对象的属性如果为引用类型,修改了该属性,其他对象的属性也会改变
let p1 = new Teacher()
// console.log(p1) // {name:"张三",arr:[1,2,3]}
let p2 = new Teacher()
p2.arr.push(4)
console.log(p1.arr) // [1,2,3,4]
console.log(p2.arr) // [1,2,3,4]
修改了 p1 的 arr 属性,为什么导致了 p2 的 arr 属性的改变? 因为两个实例使用的是同一个原型对象(Teacher.prototype),它们的内存空间是共享的,当一个发生变化的时候,另外一个也随之进行了变化
缺点
- 原型对象如果有引用类型的属性,会被所有的示例对象所共享,容易引起数据修改混乱
- 创建实例化对象的时候,不能向父类构造函数不能传递参数
2.构造函数继承(借助 call)
function Person() {
this.name = '张三'
this.arr = [1, 2, 3]
}
Person.prototype.eating = function () {
console.log('吃东西')
}
function Student() {
Person.call(this)
this.study = { subject: 'chinese' }
}
let s1 = new Student()
let s2 = new Student()
s1.study.subject = 'english'
console.log(s1.study)
console.log(s2.study)
s1.eating()
这种方式实现了不同实例间数据的隔离,子类虽然能够拿到父类的属性值,解决了第一种继承方式的弊端,但问题是,父类原型对象中一旦存在父类之前自己定义的方法,那么子类将无法继承这些方法。
缺点
- 无法使用父类原型链上的方法
3.组合继承(前两种组合)
function Person() {
this.name = '张三'
this.arr = [1, 2, 3]
}
Person.prototype.eating = function () {
console.log('吃东西')
}
function Student() {
//第二次调用Person()
Person.call(this)
this.study = { subject: 'chinese' }
}
//第一次调用Person()
Student.prototype = new Person()
console.log(Student.prototype.constructor) //Person
//需要手动指定constructor为指向自己的构造函数,不然Student.prototype.constructor就为Person了,这样是错误的
Student.prototype.constructor = Student
console.log(Student.prototype.constructor)
let s1 = new Student()
let s2 = new Student()
s1.study.subject = 'english'
console.log(s1.study)
console.log(s1.name)
console.log(s2.study)
console.log(s1)
s1.eating()
但是这里又增加了一个新问题:通过注释我们可以看到 Person 执行了两次,第一次是改变 Student 的 prototype 的时候,第二次是通过 call 方法调用 Person 的时候,那么 Person 多构造一次就多进行了一次性能开销,这显然也是不太好的
缺点
- 构造函数会被调用两次(一次在创建子类型原型对象的时候, 一次在创建子类型实例的时候. ),造成了资源的浪费
- 父类型中的属性会有两份: 一份在原型对象中, 一份在子类型实例中
4.原型式继承
原型式继承是使用 ES5 里面的 Object.create 方法来实现继承.
Object.create 这个方法接收两个参数:一是用作新对象原型的对象、二是为新对象定义额外属性的对象(可选参数)。
let people = {
name: '拉拉',
friends: ['丁丁', '迪西', '小波'],
getName: function () {
return this.name
},
}
let p1 = Object.create(people, {
property1: {
value: true,
writable: true,
configurable: true,
enumerable: true,
},
})
let p2 = Object.create(people)
p1.name = 'jerry'
console.log(p1.name) //jerry
console.log(p1.getName()) //jerry
console.log(p2.name) //拉拉
p1.friends.push('jerry')
console.log(p1.friends) // ['丁丁', '迪西', '小波', 'jerry']
console.log(p2.friends) //['丁丁', '迪西', '小波', 'jerry']
通过上面代码,可以看到 Object.create 可以实现对象的继承,除了可以继承 people 的属性外,还可以使用继承的对象的方法(getName),但是也会出现引用数据类型“共享”的问题:多个实例的引用类型属性指向相同的内存,存在篡改的可能
缺点
- 实例化对象数据共享问题
5.寄生式继承
使用原型式继承可以获得目标对象的浅拷贝,利用浅拷贝的能力进行一些方法的增加,这样的继承方式叫做寄生式继承,缺点其实和原型式继承一样,优点是在父类的继承上增加了更多的方法。
let people = {
name: '拉拉',
friends: ['丁丁', '迪西', '小波'],
getName: function () {
return this.name
},
}
function clone(obj) {
//第二次调用people
let cloneObj = Object.create(obj)
cloneObj.getFriends = function () {
return this.friends
}
return cloneObj
}
//第一次调用people
let p1 = clone(people)
let p2 = Object.create(people)
p1.name = 'jerry'
console.log(p1.name) //jerry
console.log(p1.getName()) //jerry
console.log(p1.getFriends()) //['丁丁', '迪西', '小波']
console.log(p2.name) //拉拉
p1.friends.push('jerry')
console.log(p1.friends) // ['丁丁', '迪西', '小波', 'jerry']
console.log(p2.friends) //['丁丁', '迪西', '小波', 'jerry']
在上面代码上,就会发现 p1 可以直接使用 getFriends 方法
缺点
- 引用数据类型“共享”的问题:p1.friends 改变导致 p2.friends 改变
6.寄生组合式继承--最优
结合第四种中提及的继承方式( Object.create 实现),解决普通对象的继承问题的 Object.create 方法,把前面的继承有点结合,得出了寄生组合式的继承方式,这也是所有继承方式里面相对最优的继承方式
function Person() {
this.name = '张三'
this.arr = [1, 2, 3]
}
Person.prototype.eating = function () {
console.log('吃东西')
}
function Student() {
Person.call(this) //第一次执行Person()进行构造
this.study = { subject: 'chinese' }
}
Student.prototype.play = function () {
console.log('玩游戏')
}
function clone(child, parent) {
//这里改用 Object.create 就可以减少组合继承中多进行一次构造的过程
child.prototype = Object.create(parent.prototype)
child.prototype.constructor = child
}
clone(Student, Person)
let s1 = new Student()
let s2 = new Student()
console.log(s1.name)
console.log(s1.eating())
s1.arr.push(4)
console.log(s1.arr)
console.log(s2.arr)
解决了多次调用构造函数构造的问题,也解决了引用数据“共享的问题”
7.ES6 中 extends 实现继承
class People {
//constructor 是类的构造函数,他是方法,在new实例化的时候,会自动调用这个函数,所以在其中定义属性或者方法要写this;
constructor(name, age) {
this.name = name
this.age = 18
}
// People.prototype.getName = function(){} 简写为以下方式
getName() {
return this.name
}
}
class Student extends People {
constructor(name, age, num) {
super(name, age)
// 子类中存在构造函数,则需要在使用“this”之前首先调用 super(),因为子类本身并没有自己的this对象,他必须从父类继承;如果不使用construct,会自动调用父类的construct
this.num = num
}
// 对父类方法的重写,以及复用父类方法的逻辑,必须使用super
eat() {
let stuName = super.getName()
return stuName + '在吃饭'
}
}
let s1 = new Student('拉拉', 2)
console.log(s1.getName()) //拉拉
console.log(s1.eat()) //拉拉在吃饭
ES6 中 extends 其实就是一个语法糖,实现的继承方式其实是寄生组合式继承,以下是 babel 转换的代码
function _possibleConstructorReturn(self, call) {
// ...
return call && (typeof call === 'object' || typeof call === 'function')
? call
: self
}
function _inherits(subClass, superClass) {
// 这里可以看到
subClass.prototype = Object.create(superClass && superClass.prototype, {
constructor: {
value: subClass,
enumerable: false,
writable: true,
configurable: true,
},
})
if (superClass)
Object.setPrototypeOf
? Object.setPrototypeOf(subClass, superClass)
: (subClass.__proto__ = superClass)
}
var Parent = function Parent() {
// 验证是否是 Parent 构造出来的 this
_classCallCheck(this, Parent)
}
var Child = (function (_Parent) {
_inherits(Child, _Parent)
function Child() {
_classCallCheck(this, Child)
return _possibleConstructorReturn(
this,
(Child.__proto__ || Object.getPrototypeOf(Child)).apply(
this,
arguments
)
)
}
return Child
})(Parent)
总结
