JavaScript进阶(一)原型链

本文最后更新于: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(); //Liu

函数Person的原型对象通过prototype属性访问。而new出来的Person实例并没有这个属性,但是可以通过__proto__属性访问到

image-20220513104304562

image-20230201170220966

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)
}
}

image-20230201170818725

可以看到person1.constructorperson1.__proto__.constructor指向相同,前者可以视为后者的简写。但是对于Person原型,却不是如此。

image-20230201171135230

从上图可以得出结论:new + Person,实际上是new + Person.prototype.constructor()

二、原型链继承方式

1、原型链继承

  • 通过修改子类构造函数的prototype原型指向,接到父类的一个实例上,来实现原型链的扩展。从而子类可以访问父类的所有属性
  • 缺点是所有子类实例,共享了同一个父类原型实例,无法做到实例私有
1
2
3
function SuperType();
function SubType();
SubType.prototype = new SuperType();
Screenshot_20221021_170359_com.myscript.nebo.huawei_edit_202074986942068
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

Sub.prototype = new Super() //原型链继承

Sub.prototype.constructor.name // Super, 实际上是Sub.prototype.prototype.constructor.name

const sub1 = new Sub()
const sub2 = new Sub()
console.log('sub1 colors:',sub1.colors,'sub2 colors:',sub2.colors)
//sub1 colors: (3) ['red', 'blue', 'green'] sub2 colors: (3) ['red', 'blue', 'green']

sub1.colors.push('black')
console.log('sub1 colors:',sub1.colors,'sub2 colors:',sub2.colors)
//sub1 colors: (4) ['red', 'blue', 'green', 'black'] sub2 colors: (4) ['red', 'blue', 'green', 'black']
//继承的属性会在实例之间共享,无法做到实例私有

console.log(sub1.id) //1
  • 子类实例化时,不能给父类传参,如下
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) //undefined

2、盗用构造函数继承

为了解决子类实例化时不能给父类传参的缺陷,以及解决无法做到实例私有的问题,于是出现了下面的方法。

  • 做法:在子类构造函数中调用父类构造函数
  • 优点:实现了实例私有化
  • 缺点:子类不能访问父类原型上的方法,即原型链没有接上
1
2
3
4
5
6
7
8
9
10
11
function SuperType(name){
this.name=name
console.log('调用Super',this) //调用Super SubType {name: 'Liu'}
}
function SubType(){
SuperType.call(this,'Liu'); //SubType调用了SuperType,所以根据“函数的this为调用函数的对象”,这里的this为SubType
this.age = 21;
}
var instance = new SubType();
instance.__proto__ //constructor: ƒ SubType()
instance.__proto__.__proto__ //constructor: ƒ Object()
Screenshot_20221021_174519

如图,该方法并没有扩展原型链。实际上,是将父类构造函数里面涉及到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() //Hi from q 说明继承到了父类原型上的属性
p1.log() //q24red,green,blue
const p2 = new Sub('L',22)
p1.colors.push('white')
p2.log() //L22red,green,blue p1修改colors,并没有影响到p2的colors,说明colors是子类实例的属性
Screenshot_20221021_180449

4、原型式继承

使用场景:不需要单独创建构造函数,但需要在实例对象间共享信息的场景。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
function object(o){		//object函数所做的工作,在ES5之后,被Object.create()方法规范化了
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) //{name: 'qq', friends: Array(4)}
console.log(anotherPerson) //{name: 'Q'}
Screenshot_20221021_201752

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__ //{name: 'Liu', friends: Array(2)}
Screenshot_20221021_203119

这个例子基于person对象返回了一个新对象。新返回的anotherPerson对象具有person的所有属性和方法,还有一个新方法叫sayHi()

往原型链上添加函数,只能用函数表达式的方式

6、寄生式组合继承

改良的组合继承改良的是扩展原型链这一步操作,盗用父类构造函数这一步不变

盗用构造函数继承属性,但是是不通过调用父类构造函数给子类原型赋值,而是取得父类原型的一个副本,

让子类的prototype = 父类的prototype,而在组合继承中,是通过让子类的prototype = 父类实例。这就是区别

1
2
3
4
5
function inheritPrototype(subType,superType){
let prototype = Object.create(superType.prototype); //创建父类原型prototype的副本,不是浅复制
prototype.constuctor = subType;//新建原型对象的constructor指回子类构造函数,解决重写原型导致constructor丢失的问题
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;
}
Screenshot_20221021_204352
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() //Hi from q
p1.sayAge() //24
p1.friends.push('c')
console.log(p1) //Sub {name: 'q', friends: Array(3), age: 24}

const p2 = new Sub('L',22)
console.log(p2) //Sub {name: 'L', friends: Array(2), age: 22}
Screenshot_20221021_210727

ES6 类 的extends的原理就是寄生组合继承。它改善了组合继承中,父类构造函数会被调用两次都效率问题(具体分析,见组合继承一节中的代码)

优点:

  • 原型链保持不变
  • 父类原型上的方法共享
  • 可以传参到父类构造函数
  • 父类构造函数里的实例属性相互独立,即引用值修改不会产生副作用
  • 效率提升,相比组合继承,少调用了一次父类构造函数

JavaScript进阶(一)原型链
http://timegogo.top/2023/01/31/JavaScript/JavaScript进阶(一)原型链/
作者
丘智聪
发布于
2023年1月31日
更新于
2023年7月16日
许可协议