JavaScript几种继承方式及优缺点整理
张渊 Lv2

ES5继承

  • 每创建一个构造函数,则该函数都会自动带有一个 prototype 属性。该属性是一个指针,指向一个对象,该对象称之为原型对象。
  • 原型对象上默认有一个属性 constructor ,该属性也是一个指针,指向其关联的构造函数。
  • 通过构造函数产生的实例对象,都拥有一个内部指向,指向了原型对象。其实例能够访问原型对象上的所有属性和方法。

构造函数、原型和实例的关系:每一个构造函数都有一个原型对象 prototype ,每一个原型对象都有一个指向构造函数的指针 constructor ,而每一个实例都包含一个指向原型对象的内部指针。

原型继承

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
function Parent() {
this.name = 'parent';
this.parentObj = {
info: 'parent中的对象'
};
this.action = function() {
return this.name;
}
}
// 往原型链添加一个方法
Parent.prototype.getName = function() {
return this.name;
}

function Child() {
this.name = 'child';
}
// 实例化Parent,链接到Child的原型链上
Child.prototype = new Parent();
const child = new Child();
console.log(child.name); // child"

// 缺点
const child1 = new Child();
child.parentObj.info = 'child中的对象';
// child 和 child1实例共享引用属性
console.log(child1.parentObj.info); // child中的对象

关键点:子类原型等于父类的实例 Child.prototype = new Person()

特点:

  1. 实例可继承的属性有:实例的构造函数的属性,父类构造函数的属性,父类原型上的属性。(新实例不会继承父类实例的属性)

注意事项:

  1. 新实例无法向父类构造函数传参
  2. 继承单一
  3. 所有新实例都会共享父类实例的属性。(原型上的属性是共享的,一个实例修改了原型属性,另一个实例的原型属性也会被修改)

构造函数继承(call或apply)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
function Parent() {
this.name = 'parent';
this.action = function () {
return this.name;
},
this.arr = [1,2];
}

Parent.prototype.getName = function () {
return this.name;
}
function Child () {
// 注意:若子类的属性名和父类的一样,则看它的顺序,谁在下面用谁的声明
this.name = 'Child';
// Parent.call(this);
Parent.apply(this);
}
const child1 = new Child();
const child2 = new Child();
child2.arr.push(3);
child.getName(); // 报错,继承不了父类原型上的东西
console.log('child1 --->', child1);
console.log('child2 --->', child2);

关键点:callapply 将父类构造函数引入子类函数(在子类函数中做了父类函数的自执行(复制))Parent.call(this, 'reng')

特点:

  1. 只继承了父类构造函数的属性,没有继承父类原型的属性
  2. 解决了原型链继承的注意事项(缺点)1,2,3
  3. 可以继承多个构造函数的属性(call可以多个)
  4. 在子实例中可以向父实例传参

注意事项:

  1. 只能继承父类构造函数的属性
  2. 无法实现构造函数的复用。(每次用每次都要重新调用)
  3. 每个新实例都有构造函数的副本,臃肿

组合继承(call + prototype)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
function Parent() {
this.name = 'parent';
this.action = function () {
return this.name;
},
this.arr = [1,2];
}
Parent.prototype.getName = function () {
return this.name;
}

function Child() {
this.name = 'child';
Parent.call(this);
}
Child.prototype = new Parent();
Child.prototype.constructor = Child;
const child1 = new Child();
console.log(child1)

关键点:结合了两种模式的优点–向父类传参(call)和复用(prototype)

特点:

  1. 可以继承父类原型上的属性,可以传参,可复用
  2. 每个新实例引入的构造函数属性是私有的

注意事项:

  1. 调用了两次父类的构造函数(耗内存)
  2. 子类的构造函数会代替原型上的那个父类构造函数(call相当于拿到了父类构造函数的副本)

原型式继承

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
function Person(name) {
this.name = name;
}
Person.prototype.job = 'IT';
Person.prototype.sayHello = function () {
console.log('Hello ' + this.name);
}
var person = new Person('zylucky');
person.sayHello(); // Hello zylucky

function objFn(o) {
o.objFnPrototype = "我是 objFnPrototype";
function F() {};
F.prototype = o;
return new F();
}

const op = new Person();
const oa = objFn(op);
console.log(oa instanceof Person); // true
console.log(oa.job); // IT
const a = objFn({
name: "name1"
});
console.log(a.name); //name1
console.log(a.objFnPrototype); //我是 objFnPrototype

关键点:用一个函数包装一个对象,然后返回这个函数的调用,这个函数就变成了可以随意增添属性的实例或对象。Object.create()就是这个原理。

特点:

  1. 类似于复制一个对象,用函数来包装

注意事项:

  1. 所有的实例都会继承原型上的属性
  2. 无法实现复用。(新实例属性都是后面添加的)

Object.create()方法规范了原型式继承。这个方法接收两个参数,一个用作新对象原型的对象和(可选的)一个为新对象定义额外属性的对象。

1
2
3
4
5
6
7
8
9
10
11
// 传一个参数的时候
var anotherPerson = Object.create(new Person());
console.log(anotherPerson.job); // IT
console.log(anotherPerson instanceof Person); // true
// 传两个参数的时候
var anotherPerson = Object.create(new Person(), {
name: {
value: 'zylucky'
}
});
anotherPerson.sayHello(); // Hello zylucky

寄生式继承

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
function Person(name) {
this.name = name;
}
...

function object(obj) {
function F(){}
F.prototype = obj;
return new F();
}
var sup = new Person();
// 以上是原型式继承,给原型式继承再套个壳子传递参数
function subobject(obj) {
var sub = object(obj);
sub.name = 'zylucky';
return sub;
}
var sup2 = subobject(sup);
// 这个函数经过声明后就成了可增添属性的对象
console.log(sup2.name); // 'zylucky'
console.log(sup2 instanceof Person); // true

关键点:就是给原型式继承外面套个壳子。

特点:

  1. 没有创建自定义类型,因为只是套了个壳子,返回对象,这个函数顺理成章就成了创建的新对象。

注意事项:

  1. 没用到原型,无法复用

寄生组合式继承

它跟组合继承一样,都比较常用。

寄生:在函数内返回对象然后调用

组合:

  1. 函数的原型等于另一个实例
  2. 在函数中用apply或call引入另一个构造函数,可传参
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 Person(name) {
this.name = name;
}
...
// 寄生
function object(obj) {
function F(){}
F.prototype = obj;
return new F();
}
// object是F实例的另一种表示方法
var obj = object(Person.prototype);
// obj实例(F实例)的原型继承了父类函数的原型
// 上述更像是原型链继承,只不过只继承了原型属性

// 组合
function Sub() {
this.age = 100;
Person.call(this); // 这个继承了父类构造函数的属性
} // 解决了组合式两次调用构造函数属性的特点

// 重点
Sub.prototype = obj;
console.log(Sub.prototype.constructor); // Person
obj.constructor = Sub; // 一定要修复实例
console.log(Sub.prototype.constructor); // Sub
var sub1 = new Sub();
// Sub实例就继承了构造函数属性,父类实例,object的函数属性
console.log(sub1.job); // frontend
console.log(sub1 instanceof Person); // true

重点:修复了组合继承的问题

在上面的问题中,你可能发现了这么一个注释obj.constructor = Sub; // 一定要修复实例。为什么要修正子类的构造函数的指向呢?

因为在不修正这个指向的时候,在获取构造函数返回的时候,在调用同名属性或方法取值上可能造成混乱。比如下面:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
function Car() { }
Car.prototype.orderOneLikeThis = function() { // Clone producing function
return new this.constructor();
}
Car.prototype.advertise = function () {
console.log("I am a generic car.");
}

function BMW() { }
BMW.prototype = Object.create(Car.prototype);
BMW.prototype.constructor = BMW; // Resetting the constructor property
BMW.prototype.advertise = function () {
console.log("I am BMW with lots of uber features.");
}

var x5 = new BMW();

var myNewToy = x5.orderOneLikeThis();

myNewToy.advertise(); // => "I am BMW ..." if `BMW.prototype.constructor = BMW;` is not
// commented; "I am a generic car." otherwise.

ES6继承

在 ES6 中,直接使用 extends 关键字来实现 JavaScript 继承,Class之间通过使用extends关键字,这比通过修改原型链实现继承,要方便清晰很多。并且 babel 编辑之后,它采用的也是 寄生组合继承的方式,这种方式是较优的解决继承的方式。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
class Parent2 {
constructor(name) {
this.name = name;
}
getName = function (){
return this.name;
}
}
class Child2 extends Parent2 {
constructor(name, age) {
super(name);
this.age = age;
}
}
const child3 = new Child2('parent', 22);
const child4 = new Child2('parent1', 25);
console.log(child3.getName());
console.log(child3);
console.log(child4);

子类必须在constructor方法中调用super方法,否则新建实例时会报错。这是因为子类没有自己的this对象,而是继承父类的this对象,然后对其进行加工,如果不调用super方法,子类就得不到this对象。因此,只有调用super之后,才可以使用this关键字。

prototype __proto__

一个继承语句同时存在两条继承链:一条实现属性继承,一条实现方法的继承。

1
2
3
class A extends B{}
A.__proto__ === B; //继承属性
A.prototype.__proto__ == B.prototype;//继承方法

总结

总结ES5

ES5的继承可以用下图概括:

image

总结ES6

ES6的继承可以用下图概括:

image

 评论
评论插件加载失败
正在加载评论插件
由 Hexo 驱动 & 主题 Keep
本站由 提供部署服务
总字数 12.9k 访客数 访问量