本文最后更新于:8 个月前
什么是对象原型、原型链
原型链继承方式包括:原型链继承+盗用构造函数继承=组合继承,原型式继承、寄生式继承(扩展的原型式继承)、寄生组合继承(改良的组合继承)6种
JavaScript进阶(一)原型链
一、对象原型
1、什么是原型
每个对象拥有一个原型对象,对象以其原型为模板、从原型继承方法和属性。
对象实例的原型通过__proto__
获取,函数(不包括函数new出来的实例)的原型通过prototype
属性获取。
对象原型中包含了特定引用类型的所有实例共享的属性和方法(如构造函数constructor
、变量、自定义函数)
2、什么是原型链
原型对象也可能拥有原型,并从中继承方法和属性,一层一层、以此类推。这种关系常被称为原型链。正是这种机制,使得一个对象可以拥有定义在其它对象中的属性和方法。
3、prototype属性
在 JavaScript 中,函数可以有属性。每个函数都会创建一个prototype属性,这个属性是一个对象,包含应该由特定引用类型的实例共享的属性和方法。
1 2 3 4
| function Person(name){this.name=name;} Person.prototype.sayName = function(){console.log(this.name);} let person1 = new Person('Liu',21); person1.sayName();
|
函数Person的原型对象通过prototype
属性访问。而new出来的Person实例并没有这个属性,但是可以通过__proto__
属性访问到
4、constructor属性
在上图中可以看到一个原型对象中有一个constructor
属性。
每个实例对象都从原型中继承了一个 constructor 属性,该属性指向了用于构造此实例对象的构造函数。即是说constructor
属性是每个实例对象独有的,可以联系到Class类中,类实例对象的私有成员都需定义在constructor()
构造函数中。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| function Person(name){ this.name = name } Person.prototype.sayHi = function(){ console.log('Hi,',this.name) }
class Person{ constructor(name){ this.name = name } sayHi(){ console.log('Hi,',this.name) } }
|
可以看到person1.constructor
与person1.__proto__.constructor
指向相同,前者可以视为后者的简写。但是对于Person原型,却不是如此。
从上图可以得出结论:new + Person
,实际上是new + Person.prototype.constructor()
二、原型链继承方式
1、原型链继承
- 通过修改子类构造函数的
prototype
原型指向,接到父类的一个实例上,来实现原型链的扩展。从而子类可以访问父类的所有属性
- 缺点是所有子类实例,共享了同一个父类原型实例,无法做到实例私有
1 2 3
| function SuperType(); function SubType(); SubType.prototype = new SuperType();
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24
| function Super(){ this.colors = ['red', 'blue', 'green'] } Super.prototype.id = 1 function Sub(){}
Sub.prototype.constructor.name
Sub.prototype = new Super()
Sub.prototype.constructor.name
const sub1 = new Sub() const sub2 = new Sub() console.log('sub1 colors:',sub1.colors,'sub2 colors:',sub2.colors)
sub1.colors.push('black') console.log('sub1 colors:',sub1.colors,'sub2 colors:',sub2.colors)
console.log(sub1.id)
|
1 2 3 4 5 6 7 8 9 10
| function Super (name) { this.name = name }
function Sub () {}
Sub.prototype = new Super()
const p = new Sub('wzq') console.log(p.name)
|
2、盗用构造函数继承
为了解决子类实例化时不能给父类传参的缺陷,以及解决无法做到实例私有的问题,于是出现了下面的方法。
- 做法:在子类构造函数中调用父类构造函数
- 优点:实现了实例私有化
- 缺点:子类不能访问父类原型上的方法,即原型链没有接上
1 2 3 4 5 6 7 8 9 10 11
| function SuperType(name){ this.name=name console.log('调用Super',this) } function SubType(){ SuperType.call(this,'Liu'); this.age = 21; } var instance = new SubType(); instance.__proto__ instance.__proto__.__proto__
|
如图,该方法并没有扩展原型链。实际上,是将父类构造函数里面涉及到this.
的代码放到了子类的域里面来执行,相当于给 子类添加了属性
3、组合继承
原型链继承,子类实例化时,无法给父类传参,无法实现实例私有
盗用构造函数继承,无法继承父类原型上的属性
组合继承,是将前两种方式结合起来,用原型链继承的方式继承父类原型上的属性,用盗用构造函数继承的方式继承实例属性。
公用的方法写在父类原型上,独立的属性写在父类函数里
- 优点:既可以把方法定义在父类原型上实现共享,又可以让每个实例拥有私有属性
- 缺点:可能会继承一些不需要的属性
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
| function Super(name){ this.name=name this.colors=['red','green','blue'] } Super.prototype.sayHi = function(){ console.log('Hi from '+this.name) } function Sub(name,age){ Super.call(this,name) this.age=age } Sub.prototype = new Super() Sub.prototype.log = function(){ console.log(this.name + this.age + this.colors) }
const p1 = new Sub('q',24) p1.sayHi() p1.log() const p2 = new Sub('L',22) p1.colors.push('white') p2.log()
|
4、原型式继承
使用场景:不需要单独创建构造函数,但需要在实例对象间共享信息的场景。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| function object(o){ function F(){} F.prototype = o return new F() } let person = { name: 'q', friends: ['a','b','c'] } let anotherPerson = object(person) anotherPerson.name = 'Q' anotherPerson.friends.push('d') anotherPerson.__proto__.name = 'qq' console.log(person) console.log(anotherPerson)
|
ES5
里通过增加Object.create()
方法将原型式继承的概念规范化了。这个方法接收两个参数:作为新对象原型的对象,以及给新对象定义的额外属性的对象(可选),只有一个参数时,Object.create()
与这里的object()
效果相同。
5、寄生式继承
原型式继承的扩展
适用场景:适合主要关注对象,而不关注类型和构造函数的场景
在原型式继承的基础上,添加新的实例属性。即anotherPerson
既有person
的属性,又有它自己的属性,所以叫寄生式继承
1 2 3 4 5 6 7 8
| let person ={ name:'Liu', friends:['Van','Mike'] }
let anotherPerson = Object.create(person) anotherPerson.sayHi = function(){console.log('Hi');} anotherPerson.__proto__
|
这个例子基于person对象返回了一个新对象。新返回的anotherPerson对象具有person的所有属性和方法,还有一个新方法叫sayHi()
往原型链上添加函数,只能用函数表达式的方式
6、寄生式组合继承
改良的组合继承,改良的是扩展原型链这一步操作,盗用父类构造函数这一步不变
盗用构造函数继承属性,但是是不通过调用父类构造函数给子类原型赋值,而是取得父类原型的一个副本,
即让子类的prototype = 父类的prototype,而在组合继承中,是通过让子类的prototype = 父类实例。这就是区别
1 2 3 4 5
| function inheritPrototype(subType,superType){ let prototype = Object.create(superType.prototype); prototype.constuctor = subType; subType.prototype = prototype; }
|
1 2 3 4 5 6 7
| function inherits(Child,Parent){ var F = function(){}; F.prototype = Parent.prototype; Child.prototype = new F(); Child.prototype.constructor = Child; }
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30
| function inheritPrototype(subType,superType){ const prototype = Object.create(superType.prototype) prototype.constructor = subType subType.prototype = prototype }
function Super(name){ this.name = name this.friends = ['a','b'] } Super.prototype.sayHi = function(){ console.log('Hi from '+this.name) } function Sub(name,age){ Super.call(this,name) this.age=age } inheritPrototype(Sub,Super) Sub.prototype.sayAge = function(){ console.log(this.age) }
const p1 = new Sub('q',24) p1.sayHi() p1.sayAge() p1.friends.push('c') console.log(p1)
const p2 = new Sub('L',22) console.log(p2)
|
ES6 类 的extends
的原理就是寄生组合继承。它改善了组合继承中,父类构造函数会被调用两次都效率问题(具体分析,见组合继承一节中的代码)
优点:
- 原型链保持不变
- 父类原型上的方法共享
- 可以传参到父类构造函数
- 父类构造函数里的实例属性相互独立,即引用值修改不会产生副作用
- 效率提升,相比组合继承,少调用了一次父类构造函数