使用ES5实现继承的方法

使用ES5实现继承的方法

继承是面向对象的三个基本特征之一。继承机制使得子类可以继承父类的(公有)属性和(公有)方法,使程序员无需再次编写相同的代码。子类也可以定义自己的属性和方法,并且可以覆盖父类中的属性和方法,从而使其拥有与父类不同的功能。

1.原型链与原型继承

在JavaScript中,继承机制的实现与原型链紧密相关。在理解原型链的概念之前,首先要清楚构造函数、原型对象和实例之间的关系。构造函数拥有自己的原型对象(prototype),而原型对象也有一个constructor属性指向构造函数。通过new操作符调用构造函数创建实例时,实例的原型会被设置为指向构造函数的原型对象。如果我们将一个实例A的原型修改为另一个实例,那么此时A的原型也有自己的原型。如果以A的原型P1作为起点,P1拥有自己的原型P2,而P2也有自己的原型P3...以此类推,直到Object.prototype为止(任何函数的默认原型都是个 Object 的实例),这些对象构成了A的原型链。在读取A上的属性时,首先会在A中进行搜索,如果没有搜索到相关属性就会在A的原型链上自下而上地进行搜索,如果搜索到相关属性就返回结果,如果直到原型链的末端依然没有搜索到相关属性就会报错。由此可见,通过原型链可以实现继承多个引用类型的属性和方法。在理解了原型链的概念之后,我们可以通过原型链实现JS中最基础的继承方式:原型继承。

 1 function superType() {
 2     this.superProperty = "super";
 3 }
 4 
 5 superType.prototype.getSuperProperty = function () {
 6     return this.superProperty;
 7 }
 8 
 9 function subType() {
10     this.subProperty = "sub"
11 }
12 
13 subType.prototype = new superType(); // 将subType的原型对象设置为superType的实例
14 
15 subType.prototype.getSubType = function () {
16     return this.subProperty;
17 }
18 
19 let sub = new subType();
20 console.log(sub.getSuperProperty()); // super
21 console.log(sub.getSubType()); // sub
22 console.log(sub.constructor); // [Function: superType]
23 console.log(sub.toString()); // [object Object],调用原型链最末端的Object.prototype上的方法

为了确定实例与对象的关系,我们可以使用instanceof操作符或者调用原型链上的对象的isPrototypeOf方法。

1 console.log(sub instanceof subType); // true
2 console.log(sub instanceof superType); // true
3 console.log(sub instanceof Object); // true
4 
5 console.log(subType.prototype.isPrototypeOf(sub)); // true
6 console.log(superType.prototype.isPrototypeOf(sub)); // true
7 console.log(Object.prototype.isPrototypeOf(sub)); // true

我们可以在子类实例中覆盖父类的方法或者添加父类中不存在的方法,从而使得子类与父类具有不同的功能。需要注意的是,这些操作只有在实例的原型被赋值之后才能进行。

 1 subType.prototype.getSuperProperty = function () {
 2     console.log("sub");
 3 }
 4 
 5 subType.prototype.sayHello = function () {
 6     console.log("hello");
 7 }
 8 
 9 sub.getSuperProperty(); // sub,覆盖了父类的同名方法
10 sub.sayHello(); // hello,在子类上添加了新方法

原型继承的缺点在于,由于实例的原型是另一个实例,如果实例的原型的属性包含引用值,例如数组,那么一个实例对该属性的修改会在所有实例间都可见。另外,如果使用原型继承,实例化子类时无法给父类的构造函数传递参数。

2.盗用构造函数继承

盗用构造函数的思路是在子类的构造函数中调用父类的构造函数。执行子类的构造函数时,其this被绑定到了新创建的子类实例对象上,我们可以通过call或者apply方法在新创建的子类实例对象上调用父类的构造函数。

 1 function superType(message) {
 2     this.nums = [1, 2];
 3     this.message = message;
 4 }
 5 
 6 function subType() {
 7     superType.call(this,"hello");
 8 }
 9 
10 let sub1 = new subType();
11 console.log(sub1.nums); // [1, 2]
12 console.log(sub1.message); // hello,实例属性
13 sub1.nums.push(3);
14 
15 let sub2 = new subType();
16 console.log(sub2.nums); // [1, 2]

从上面的代码可以看出,每个子类实例都拥有自己的nums属性,每个实例对这个属性的修改不会影响其他属性。此外,我们也可以在创建子类实例时给父类的构造函数传递参数。盗用构造函数继承的缺点在于子类实例无法访问父类原型上定义的属性和方法。另外,由于盗用构造函数继承使用构造函数定义类型,必须在构造函数内定义方法,可能导致函数不能重用的问题。

3.组合继承

组合继承的思想在于结合了原型继承和盗用构造函数继承,既使用原型链继承原型上的属性和方法,又通过盗用构造函数创建实例自身的属性。

 1 function superType(message) {
 2     this.nums = [1, 2];
 3     this.message = message;
 4 }
 5 
 6 superType.prototype.say = function () {
 7     console.log(this.message);
 8 }
 9 
10 function subType(message, time) {
11     superType.call(this, message); // 继承原型的属性
12     this.time = time; // 实例自身的属性
13 }
14 
15 subType.prototype = new superType(); // 继承原型的方法
16 
17 subType.prototype.clock = function () {
18     console.log(this.time);
19 }
20 
21 let sub1 = new subType("hello", 1997);
22 sub1.nums.push(3);
23 console.log(sub1.nums); // [1, 2, 3]
24 sub1.say(); // hello
25 sub1.clock(); // 1997
26 
27 let sub2 = new subType("hi", 2021);
28 console.log(sub2.nums); // [1, 2]
29 sub2.say(); // hi
30 sub2.clock(); // 2021

4.原型式继承

原型式继承的思想是在不创建构造函数的情况下使得多个实例继承同一个对象,从而在多个实例间共享信息。实现原型式继承最常用的方法是使用Object.create方法。

 1 let superType = {
 2     nums : [1, 2],
 3     message : "hello"
 4 };
 5 
 6 let sub1 = Object.create(superType), sub2 = Object.create(superType);
 7 sub1.nums.push(3);
 8 console.log(sub2.nums); // [1, 2, 3],信息共享
 9 sub1.message = "hi";
10 console.log(sub1.message); // hi,遮蔽同名对象
11 console.log(sub2.message); // hello
12 
13 let sub3 = Object.create(superType, {
14     message : {
15         value : "hello world",
16         writable : true,
17         enumerable : true,
18         configurable : true
19     }
20 });
21 
22 console.log(sub3.message); // hello world
23 console.log(Object.keys(sub3)); // [ 'message' ]

5.寄生式继承

寄生式继承结合了原型式继承和工厂模式,先创建继承原型的实例,然后增强这个实例。

 1 let superObj = {
 2     nums : [1, 2],
 3     message: "hello"
 4 }
 5 
 6 function createObj(obj) {
 7     let newObj = Object.create(obj);
 8     newObj.say = function () {
 9         console.log(this.message);
10     }
11 
12     return newObj;
13 }
14 
15 let sub = createObj(superObj);
16 sub.nums.push(3);
17 console.log(sub.nums); // [1, 2, 3]
18 sub.say(); // hello

6.寄生式组合继承

寄生式组合继承的目的是解决组合继承的效率问题。在组合继承中,父类的构造函数始终会被调用两次,子类实例的原型是父类的实例,子类实例和子类实例的原型中均包含父类的属性和方法。寄生式组合继承通过盗用构造函数继承父类的属性和方法,但不再使用父类的实例为子类的原型赋值,而是设置一个对象作为连接子类实例与父类原型的桥梁。

 1 function superType(message) {
 2     this.nums = [1, 2];
 3     this.message = message;
 4 }
 5 
 6 superType.prototype.say = function () {
 7     console.log(this.message);
 8 }
 9 
10 function subType(message, time) {
11     superType.call(this, message); // 继承原型的属性
12     this.time = time; // 实例自身的属性
13 }
14 
15 function connect(subType, superType) {
16     let proto = Object.create(superType.prototype);
17     proto.constructor = subType;
18     subType.prototype = proto;
19 }
20 
21 connect(subType, superType);
22 
23 subType.prototype.clock = function () {
24     console.log(this.time);
25 }
26 
27 let sub1 = new subType("hello", 1997);
28 sub1.nums.push(3);
29 console.log(sub1.nums); // [1, 2, 3]
30 sub1.say(); // hello
31 sub1.clock(); // 1997
32 
33 let sub2 = new subType("hi", 2021);
34 console.log(sub2.nums); // [1, 2]
35 sub2.say(); // hi
36 sub2.clock(); // 2021
posted @ 2021-05-11 11:08  曹冲字仓舒  阅读(771)  评论(0)    收藏  举报