原型和继承
- 原型和原型链分别是什么?描述 JavaScript 内置的原型链。
原型
红宝书p224
- 每个函数都会创建一个 prototype 属性,这个属性是一个对象,包含应该由特定引用类型的实例共享的属性和方法。实际上,这个对象就是通过调用构造函数创建的对象的原型。使用原型对象的好处是,在它上面定义的属性和方法可以被对象实例共享。
- 原型对象上有一个constructor属性指向对应的构造函数。
- 注意:函数也是一个对象实例,也有一个__proto__属性指向函数的原型对象(指向内置匿名函数 anonymous)。
原型链:
红宝书p238
- 如果原型是另一个类型的实例呢?那就意味着这个原型本身有一个内部指针指向另一个原型,相应地另一个原型也有一个指针指向另一个构造函数。这样就在实例和原型之间构造了一条原型链。
- 原型对象也可能拥有原型,并从中继承方法和属性,一层一层、以此类推。这种关系常被称为原型链
描述 JavaScript 内置的原型链
- 描述实例,构造函数,原型的联系。
- 构造函数、原型和实例的关系:每个构造函数都有一个原型对象,原型有一个属性指回构造函数,而实例有一个内部指针指向原型。
- 默认原型 所有对象的都继承Object.prototype。
- 原型与继承关系 用于检测实例对象的原型链上是否出现过该构造函数
- 关于方法 子有用子,无则用父。
1. 罗列各种实现继承的方式,并用代码展示。
原型链继承
function SuperType() { this.superName = 'parent' this.colors = ["red", "blue", "green"]; } function SubType(name) { this.subName = name } SubType.prototype = new SuperType() console.log(new SubType().superName) // parent console.log(new SubType('child').subName) // child let instance1 = new SubType(); instance1.colors.push("black"); console.log(instance1.colors); // "red,blue,green,black" let instance2 = new SubType(); console.log(instance2.colors); // "red,blue,green,black" 复制代码
原型链继承问题
- 属性值是引用值时,不同的实例会共享该属性。
- 子类型在实例化时不能给父类型的构造函数传参。
盗用构造函数
function SuperType(name) { this.superName = name } function SubType(name) { // SuperType.call(this,name) SuperType.apply(this,[name]) } let sub = new SubType(); console.log(new SubType('child').superName) // child 复制代码
目的:为了解决原型包含引用值导致的继承问题 实现手段:使用apply()和 call()方法以新创建的对象为上下文执行构造函数 特点:相比第一种原型链继承方式,父类的引用属性不会被共享,优化了第一种继承方式的弊端,但是只能继承父类的实例属性和方法,不能继承原型属性或者方法
组合继承
function SuperType(name) { this.superName = name } SuperType.prototype.getName = function() { return this.superName } function SubType(name) { // 第二次调用父构造函数 // SuperType.call(this,name) SuperType.apply(this,[name]) } // 第一次调用父构造函数 SubType.prototype = new SuperType() // 添加constructor属性 SubType.prototype.constructor = SubType let sub = new SubType('child') console.log(sub.superName) // child console.log(sub.getName());// child 复制代码
- 是原型链继承和盗用构造函数继承的组合,但是调用了2次父构造函数
原型式继承
本质就是浅拷贝升级,克隆的新对象可以添加一下新的属性
规范化:使用Object.create()
function object(o) { function F() {} F.prototype = o return new F() } let parent = { name:'parent', color:['red','green'], } let child = object(parent) let child1 = Object.create(parent,{ name:{ enumerable:'true', value:'child1' } }); child.color.push('yellow') child1.color.push('black') console.log(child.name); // parent console.log(child1.name); // child1 console.log(child.color); // [ 'red', 'green', 'yellow', 'black' ] console.log(child1.color); // [ 'red', 'green', 'yellow', 'black' ] console.log(child1,child1.__proto__);//{ name: 'child1' } { name: 'parent', color: [ 'red', 'green', 'yellow', 'black' ] } for(let i in child1) { console.log(i); // name,color } 复制代码
寄生式继承
let parent = { name:'parent', color:['red','green'], getName: function() { return this.name; } } function clone(object) { let child = Object(parent) child.getName = function() { return this.color } return child } let child = clone(parent) console.log(child.getName()); // [ 'red', 'green' ] 复制代码
特点和原型式继承一样,浅拷贝。可以添加为克隆对象方法
寄生式组合继承
核心:就是为了解决调用父构造函数2次的效率问题,还有就是避免子类原型上有不必要的属性。
function Parent(name,color) { this.name = name this.color = color } Parent.prototype.getName = function() { return this.name; } function Child(name,color) { this.name = name; this.color = color; } function inheritPrototype(child,parent) { // 创建副本做原型 child.prototype = Object.create(parent.prototype) // 修改constructor指向 child.prototype.constructor = child } inheritPrototype(Child,Parent) Child.prototype.getColor = function() { return this.color } let sub = new Child('child','red') console.log(sub); // Child { name: 'child', color: 'red' } console.log(sub.getColor()); // red console.log(sub.getName()); // child 复制代码
书上是这样的
先原型链继承,但是直接改写子类构造函数的原型,用父类实例赋值。 因为存在共享引用值,构造实例时不能传参,
然后就诞生了盗用父类构造函数。
使用apply,call改变作用域对象。实现了,创建子类实例时可以传参的目的,也不会共享属性是引用值的情况,但是没有办法继承父类原型对象的属性方法。这样方法都得写到子类构造函数中,没有办法复用。
然后又抛出来了一个原型式继承。核心就是浅拷贝,将拷贝的对象进行加工,添加属性,方法。实现手段是Object.create。创建的副本对象的__proto__属性值就是原本的对象。
然后寄生式继承和原型式继承类型都是写了一个构造函数创建副本对象,在副本对象上添加属性,方法。
然而寄生式组合继承就是为了优化组合继承而诞生的。
与组合继承比较核心改变的地方就是子类构造函数的prototype属性的值不是通过父类构造函数new出来的,而是直接把父类构造函数的原型对象创建一个副本直接赋值。并且这个也不用在去修改constructor指向。封装的函数就已经改写的constructor属性值了。也避免了子类的原型因为new带有一些用不上的属性,且这些属性值的undefined。