前置
在JavaScript中,对象分为2种,一种是普通对象,另外一种是函数对象。
所有的对象都有__proto__
属性,但是只有函数对象有prototype
属性。函数对象,其实就是 JavaScript 的用函数来模拟的类实现
什么是原型
原型是js为所有函数所创建的一个对象类型的属性,原型当中的属性和方法被所有通过这个函数所创建的对象共享。
原型是一个对象,在原型中通常拥有两个属性:
(1)构造器constructor:该属性指向了这个类本身
(2)原型指向__proto__:该属性指向原型本身,提供给通过类创建的对象使用。
作用:原型用来创建类的公有属性和公有方法,为创建对象服务。
优点:节约内存空间,不必为每一个对象都分配公有属性和公有方法的内存。
缺点:原型中不能保存数组这类引用类型的数据,因为地址传递的问题会导致出现修改的连锁变化。
原型链
原型链中就是实例对象和原型对象之间的链接。实例对象自己会有一个指针(proto)指向他的构造函数的原型对象!这样构造函数和实例对象之间就通过( proto )连接在一起形成了一条链子。
__proto__的路径
就叫原型链。Object.prototype
也有__proto__,指向null,是原型链的终点
作用:为了实现继承,简化代码,实现代码可重用;只要是这个链条上的内容,都可以被访问和使用到。
而String、Array、Number、Function、Object都是 function。
Function.__proto__ === Function.prototype//true
所有 Function 的实例都是函数对象,其他的均为普通对象,其中包括 Function 实例的实例.
所有的构造函数都可以通过原型链找到 Function.prototype
,并且 functionFunction()
本质也是一个函数,为了不产生混乱就将 functionFunction()
的 __proto__
联系到了 Function.prototype
上。
prototype 显示原型,构造函数的原型对象
proto 隐式原型,是实例对象指向原型对象的指针
构造函数的prototype和其实例的__proto__指向同一个地方,即原型对象。
函数:使用new Function
来声明,Function
也是一个构造函数。
对象:构造函数创建对象;字面量创建对象;new object创建对象;Object.create创建对象
- Person.prototype,它是
构造函数Person
的原型对象 - Function.prototype,他是
构造函数Function
的原型对象
原型对象,可以知道其实这两个本质都是对象
, 本质肯定都是通过new Object()
来创建的。
那就说明Person.prototype 和 Function.prototype
都是构造函数Object
的实例。也就说明了Person.prototype 和 Function.prototype
他们两的__proto__
都指向Object.prototype
。
ES5中的继承
原型链继承 将父类的实例作为子类的原型
function SuperClass() {
this.superValue = true;
}
SuperClass.prototype.getSuperValue = function() {
return this.superValue;
}
function SubClass() {
this.subValue = false;
}
SubClass.prototype = new SuperClass();
SubClass.prototype.getSubValue = function() {
return this.subValue;
}
var instance = new SubClass();
console.log(instance instanceof SuperClass)//true
console.log(instance instanceof SubClass)//true
console.log(SubClass instanceof SuperClass)//false
缺点:
由于子类通过其原型prototype对父类实例化,继承了父类,所以说父类中如果共有属性是引用类型,就会在子类中被所有的实例所共享
,因此一个子类的实例更改子类原型从父类构造函数中继承的共有属性就会直接影响到其他的子类。
由于子类实现的继承是靠其原型prototype对父类进行实例化实现的,因此在创建父类的时候,是无法向父类传递参数
的。因而在实例化父类的时候也无法对父类构造函数内的属性进行初始化。
构造函数继承
原型链继承是让所有子类实例都访问同一个原型对象,导致原型上的属性共享,那每次new一个子类的时候都让它拥有一个新的原型对象,所以让子类的构造函数通过call的方法,借用父类构造函数的代码
function SuperClass(id) {
this.books = ['js','css'];
this.id = id;
}
SuperClass.prototype.showBooks = function() {
console.log(this.books);
}
function SubClass(id) {
//继承父类
SuperClass.call(this,id);
}
//创建第一个子类实例
var instance1 = new SubClass(10);
//创建第二个子类实例
var instance2 = new SubClass(11);
instance1.books.push('html');
console.log(instance1)
console.log(instance2)
instance1.showBooks();//TypeError
由于父类中给this绑定属性,因此子类自然也就继承父类的共有属性。由于这种类型的继承没有涉及到原型prototype
,所以父类的原型方法自然不会被子类继承,而如果想被子类继承,就必须放到构造函数中.
缺点:不能继承原型上的属性和方法。
组合式继承
function SuperClass(name) {
this.name = name;
this.books = ['Js','CSS'];
}
SuperClass.prototype.getBooks = function() {
console.log(this.books);
}
function SubClass(name,time) {
SuperClass.call(this,name);
this.time = time;
}
SubClass.prototype = new SuperClass();
SubClass.prototype.getTime = function() {
console.log(this.time);
}
父类的构造函数执行两遍。一次是在设置子类实例的的原型时;一次在创建子类实例时。
原型式继承
复制传入的对象到创建对象的原型上实现继承
function createObj(o) {
//声明一个过渡对象
function F() { }
//过渡对象的原型继承父对象
F.prototype = o;
//返回过渡对象的实例,该对象的原型继承了父对象
return new F();
}
var person = {
name : 'arzh',
body : ['foot','hand']
}
var person1 = createObj(person)
var person2 = createObj(person)
console.log(person1) //arzh
person1.body.push('head')
console.log(person2) //[ 'foot', 'hand', 'head' ]
就是 ES5 Object.create
的模拟实现,将传入的对象作为创建的对象的原型。
Object.create()创建一个新对象,使用现有对象作为新创建对象的原型
缺点同原型链继承一样,每个实例对引用类型的属性都会被其他实例共享;且不能传参
原型式继承非常适合不需要单独创建构造函数,但仍然需要在对象间共享信息的场合。
寄生式继承
使用Object.create来代替上述createObj的实现,原理基本上是一样的。寄生式继承其实就是在createObj的内部以某种形式来增强对象(这里的增强可以理解为添加对象的方法),最后返回增强之后的对象。
function createEnhanceObj(o) {
//代替原型式继承的createObj
var clone = Object.create(o)
clone.getName = function () {
console.log('arzh')
}
return clone;
}
寄生式继承就是对原型继承的拓展,一个二次封装的过程
通过createEnhanceObj
就可以在创建对象的时候,把对象方法也通过此种方式继承。
缺点: 同借用构造函数一样,无法复用父类函数,每次创建对象都会创建一遍方法。
寄生组合式继承
组合式继承的问题是子类不是父类的实例,子类的原型是父类的实例。
基本思路是使用寄生式继承来继承父类的原型对象,然后将返回的新对象赋值给子类的原型对象。
在组合式继承中,在子类的构造函数调用了一次父类的构造函数,但是在子类的构造函数外,由于要给子类的原型赋值,再次调用了父类的构造函数,造成资源的浪费。
function inheritPrototype(subClass,superClass) {
var p = Object.create(superClass.prototype); // 获取父类原型对象副本
// 将获取的副本的constructor指向子类,解决由于重写原型导致constructor丢失问题
p.constructor = subClass;
subClass.prototype = p; // 将子类的原型对象指向副本原型对象
}
需要继承的仅仅是父类的原型,不用去调用父类的构造函数。
换句话说,在构造函数继承中,我们已经调用了父类的构造函数。因此我们需要的就是父类的原型对象的一个副本,而这个副本我们可以通过原型继承拿到,但是这么直接赋值给子类会有问题,因为对父类原型对象复制得到的复制对象p中的constructor
属性指向的不是subClass
子类对象,因此在寄生式继承中要对复制对象p做一次增强,修复起constructor
属性指向性不正确的问题,最后将得到的复制对象p赋值给子类原型,这样子类的原型就继承了父类的原型并且没有执行父类的构造函数。
ES6继承
class User {
constructor(name, age) {
this.name = name
this.age = age
}
grow(years) {
this.age += years
console.log(`${this.name} is now ${this.age}`)
}
}
class Admin extends User {
constructor(name, age, address) {
super(name, age) //to call a parent constructor
this.address = address
}
grow(years) {
super.grow(years) // to call a parent method
console.log(`he is admin, he lives in ${this.address}`)
}
}
const zac = new User('zac', 28)
- super(…)是用来调用父类的constructor方法(只能在constructor里这么调用),相当于ES5中的parent.call(this).子类必须在 constructor 方法中调用 super 方法,否则新建实例时会报错。这是因为子类没有自己的 this 对象,而是继承父类的 this 对象,然后对其进行加工。如果不调用 super 方法,子类就得不到 this 对象。
- super.method(…)是用来调用父类的方法