HTML5学习笔记(十六):原型、类和继承【JS核心知识点】

时间:2022-08-24 22:34:29

理解原型

在JavaScript中,只要声明了一个函数,就会为该函数创建一个名为prototype的属性,该属性指向当前函数的原型对象。

而函数的原型对象有一个constructor属性,该属性指向刚声明的函数。

HTML5学习笔记(十六):原型、类和继承【JS核心知识点】

需要注意的是:只有通过声明创建的函数对象才会具有原型对象和prototype属性,其它通过new关键字创建的对象都不具有prototype属性。

 1 var obj = new Object();
 2 console.log(obj.prototype); // undefined
 3 var str = new String("abc");
 4 console.log(str.prototype); // undefined
 5 var arr = new Array();
 6 console.log(arr.prototype); // undefined
 7 
 8 function Person() {}
 9 //var Person = function() {} // 也可以用这种写法,效果一致
10 console.log(Person.prototype); // Object {}
11 var p = new Person();
12 console.log(p.prototype); // undefined

实例访问原型对象

我们通过声明的函数创建得到的实例,需要获取原型对象该怎么办,非标准方式__proto__属性在Chrome、Firefox和Safari中可以获得:

1 function Person() {}
2 var p = new Person();
3 console.log(p.__proto__ === Person.prototype); // true

当然,ECMA也有提供标准的方法来获取实例的原型对象,Object.getPrototypeOf方法:

1 function Person() {}
2 var p = new Person();
3 console.log(Object.getPrototypeOf(p) === Person.prototype); // true

原型对象有什么用?

原型对象上的属性是所有实例共享的,即所有实例对象可以像调用自身的属性及方法一样调用定义在原型对象上的属性及方法。

示例如下:

 1 function Person() {
 2     Person.prototype.name = "Tom";
 3     Person.prototype.sayName = function() {
 4         console.log(this.name);
 5     };
 6 }
 7 var p1 = new Person();
 8 var p2 = new Person();
 9 p1.sayName(); // Tom
10 p2.sayName(); // Tom
11 Person.prototype.name = "Jake";
12 p1.sayName(); // Jake
13 p2.sayName(); // Jake
14 Person.prototype.sayName = function() {console.log("hello, " + this.name + "!")};
15 p1.sayName(); // hello, Jake!
16 p2.sayName(); // hello, Jake!

我们可以发现原型对象的属性改变会影响到所有的实例。

实例不能覆盖原型的属性

当实例对象尝试修改原型的属性时,实际上是将属性添加到自己身上,我们可以看下面脚本:

 1 function Person() {
 2     Person.prototype.name = "Tom";
 3 }
 4 var p = new Person();
 5 console.log(p.name); // Tom
 6 p.name = "Jake";
 7 console.log(p.name); // Jake
 8 console.log(Person.prototype.name); // Tom
 9 delete p.name;
10 console.log(p.name); // Tom

判断属性是否存在

hasOwnProperty

不考虑原型的属性,仅判断当前对象是否有指定的属性。

in

判断当前对象及原型对象上是否有指定的属性。

 1 function Person() {
 2     Person.prototype.name = "Tom";
 3 }
 4 var p = new Person();
 5 console.log(p.hasOwnProperty("name")); // false
 6 console.log("name" in p); // true
 7 p.name = "Jake";
 8 console.log(p.hasOwnProperty("name")); // true
 9 console.log("name" in p); // true
10 delete p.name;
11 console.log(p.hasOwnProperty("name")); // false
12 console.log("name" in p); // true

实例获取属性流程

console.log(p.name);这句脚本,p对象会先查找自身是否有name属性,有则返回,没有再查找p对象的原型对象是否有name属性,有则返回,还没有就查找Object原型对象身上是否有name属性,有则返回,还是没有则返回undefined。

1 function Person() {}
2 var p = new Person();
3 console.log(p.name); // undefined
4 Object.prototype.name = "Tom";
5 console.log(p.name); // Tom

在JavaScript中,我们可以通过原型来模拟类的声明,但是从上面学习的原型来看,还有有两个地方需要改进:

  1. 没有构造函数,同时也不能给构造函数传递参数。
  2. 所有属性共享,对于引用类型的属性,比如数组,是不能共享的,应该是每个实例各自持有一份独立存在于内存的属性。

解决方法并不复杂,请看实现:

 1 function Person(name, age) {
 2     this.name = name;
 3     this.age = age;
 4     this.data = ["China", "Shanghai"];
 5 
 6     Person.prototype.sayHello = function() {
 7         console.log("Hello, I am " + this.name + ", I am " + this.age + " old, I am from " + this.data[0] + " " + this.data[1] + ".");
 8     }
 9 }
10 
11 var p1 = new Person("Li Lei", 28);
12 p1.data[1] = "Guangzhou";
13 p1.sayHello(); // Hello, I am Li Lei, I am 28 old, I am from China Guangzhou.
14 var p2 = new Person("Han Meimei", 27);
15 p2.data[1] = "Beijing";
16 p2.sayHello(); // Hello, I am Han Meimei, I am 27 old, I am from China Beijing.
  1. 我们定义的函数就可以作为构造函数,同时可以支持传递参数。
  2. 类的属性直接添加到当前对象上,不放在共享的原型对象中,只有方法才放到原型对象中。

稳妥对象(Durable Objects)

在某些情况下,我们不希望我们的数据在外部被改变,就可以用上稳妥对象:

 1 function Person(name) {
 2     var o = new Object();
 3 
 4     o.sayName = function() {
 5         console.log(name);
 6     }
 7 
 8     return o;
 9 }
10 
11 var p = new Person("Li Lei");
12 p.sayName(); // Li Lei
13 p.name = "Tom";
14 p.sayName(); // Li Lei
15 p.__proto__.name = "Tom";
16 p.sayName(); // Li Lei

该方法特别适合在某些需要提供更安全的环境下使用。

继承

在JavaScript中,可以通过原型链来实现类似面向对象语言的继承。

原型链

ECMA中提供了原型链,用来作为实现继承的基础,那么什么是原型链呢?之前我们说过,当我们获取一个对象的属性时,会判断该对象是否存在属性,如果不存在则查找该对象的原型对象是否存在属性。那么,当对象的原型指向另一个对象时,就会出现一种类似于链表一样的数据结构,成为原型链。

看例子:

 1 function SuperType() {
 2     this.property = true;
 3 }
 4 SuperType.prototype.getSuperValue = function() {
 5     return this.property;
 6 }
 7 
 8 function SubType() {
 9     this.subproperty = false;
10 }
11 //将子类的原型设定为父类的实例, 在此之前不要给子类的原型添加任何的属性或方法, 否则会被覆盖
12 SubType.prototype = new SuperType();
13 //为新的原型对象添加方法
14 SubType.prototype.getSubValue = function() {
15     return this.subproperty;
16 }
17 
18 var instance = new SubType();
19 console.log(instance.getSuperValue()); // true

我们看下图示:

HTML5学习笔记(十六):原型、类和继承【JS核心知识点】

这里就可以看做一个存在3个元素的原型链,因为所有的函数原型对象默认都指向Object对象。

我们下面以调用toString方法为例来看看一个方法在原型链上是如何查找的:首先查找instance对象是否有toString属性,没有查找原型对象SuperType的实例,发现也没有时,继续查找SuperType实例的原型对象,也没有发现,在向上查找就到了SuperType的原型Object类型对象了,在这里找到了toString方法,即可调用到该方法。如果还没有找到则返回undefined。

判断是否是指定对象的实例

使用instanceof关键字即可:

1 console.log(instance instanceof SubType); // true
2 console.log(instance instanceof SuperType); // true
3 console.log(instance instanceof Object); // true

原型链实现继承的问题

使用原型链实现继承时会遇到2个问题,我们下面来看看:

引用类型共享

由于实例变成了原型对象,所以实例对象中的所有引用对象都会被共享,如下:

 1 function SuperType() {
 2     this.property = ["apple", "Banana"];
 3 }
 4 
 5 function SubType() {
 6 }
 7 SubType.prototype = new SuperType();
 8 
 9 var instance1 = new SubType();
10 var instance2 = new SubType();
11 instance1.property.push("orange");
12 console.log(instance1.property); // ["apple", "Banana", "orange"]
13 console.log(instance2.property); // ["apple", "Banana", "orange"]

这显然不是我们想要的效果。

如何向父类构造函数传递参数

如果父类是可以传递参数的,那么这种写法无法从子类向父类传递参数。

为了解决上面的问题,在实际开发中出现了多种继承方式,我们下面一个一个来看。

组合继承

属性使用组合调用的方式(属性不能共享直接添加到实例上),方法使用外部定义到原型的方式(方法需要共享)。

 1 function SuperType(name) {
 2     this.name = name;
 3     this.colors = ["red", "green"];
 4 }
 5 
 6 SuperType.prototype.sayName = function() {
 7     console.log(this.name);
 8 }
 9 
10 function SubType(name, age) {
11     //调用父类构造函数方法, 可以将实例上的属性直接设置到新对象上
12     SuperType.call(this, name);
13     //这里可以添加自身拥有的属性
14     this.age = age;
15 }
16 
17 //子类原型指向父类实例
18 SubType.prototype = new SuperType();
19 //默认的原型对象呗覆盖, 需要加上constructor属性
20 SubType.prototype.constructor = SubType;
21 //子类的方法
22 SubType.prototype.sayAge = function() {
23     console.log(this.age);
24 }
25 
26 var obj = new SubType("Li Lei", 28);
27 obj.colors.push("blue");
28 obj.sayName(); // Li Lei
29 obj.sayAge(); // 28
30 console.log(obj.colors); // ["red", "green", "blue"]
31 var obj2 = new SubType("Han Meimei", 27);
32 obj2.colors.unshift("blue");
33 obj2.sayName(); // Han Meimei
34 obj2.sayAge(); // 27
35 console.log(obj2.colors); // ["blue", "red", "green"]

该方法简单易懂,可以说是JavaScript中用得最多的继承实现方法。

寄生组合继承

该模式是JavaScript开发中目前为止认为最理想的继承方式。

我们下面慢慢来分析该模式。

JavaScript领域大师道格拉斯·克罗克福德提出了一种继承方法,并给出了一个方法:

1 function object(o) {
2     function F() {}
3     F.prototype = o;
4     return new F();
5 }

实际上该方法就是创建了一个原型为参数的新空对象出来。

而在ECMAScript5里,已经对该方法进行了封装,Object.create方法即为该方法的官方提供版本,create方法额外提供了第二个参数,用来覆盖原型对象上的同名属性。

原型式继承

即使用Object.create方法来实现的继承,该方法实现的继承存在引用对象共享的问题,如下:

 1 var person = {
 2     name: "Le Lei",
 3     colors: ["red", "green"]
 4 };
 5 
 6 var p1 = Object.create(person);
 7 p1.name = "Tom";
 8 p1.colors.push("blue");
 9 
10 var p2 = Object.create(person);
11 p2.name = "Han Meimei";
12 p2.colors.unshift("black");
13 
14 console.log(person.colors); // ["black", "red", "green", "blue"]
15 console.log(p1.colors); // ["black", "red", "green", "blue"]
16 console.log(p2.colors); // ["black", "red", "green", "blue"]

目前看来这不是一种靠谱的继承方式,我们接着看。

寄生继承

该方式基于上面的实现提供了一个方法用来加强子类,如下:

 1 var person = {
 2     name: "Le Lei",
 3     colors: ["red", "green"]
 4 };
 5 
 6 function createSubPerson(o) {
 7     var clone = Object.create(o);
 8     clone.sayName = function() {
 9         console.log(this.name);
10     };
11     return clone;
12 }
13 
14 var p = createSubPerson(person);
15 p.sayName(); // Le Lei

好吧,这种方式不但没有解决属性共享的问题,每次创建一个子类都需要重新创建对应的方法,这个也不是好方法,我们接着往下看。

组合继承的小问题

组合继承已经解决了继承的主要问题,但是还是存在两个小问题,而接下来我们将使用寄生+组合的方式来解决这两个小问题。

  1. 失去了默认创建的原型的constructor属性,需要手动加上;
  2. 原型使用父类的实例,导致原型中包含并不需要的属性及值(原型中只需要共享方法,但是比如name等属性也被包含到原型对象了);

实现寄生组合继承

使用该模式可以解决组合继承的问题,同时可以得到组合继承的所有优点。

 1 /**
 2  * 将指定函数设定为另一个函数的原型对象, 可以实现继承功能
 3  * @param subType 用做子类的函数
 4  * @param superType 用做父类的函数
 5  */
 6 function inheritPrototype(subType, superType) {
 7     var prototype = Object.create(superType.prototype);
 8     prototype.constructor = subType;
 9     subType.prototype = prototype;
10 }

以上的方法定义了用来实现继承的函数,第一行代码创建了superType原型对象的副本,第二行代码设定constructor指向当前函数对象,第三行代码将创建的对象赋予subType的原型作为subType的原型对象。

 1 /**
 2  * 将指定函数设定为另一个函数的原型对象, 可以实现继承功能
 3  * @param subType 用做子类的函数
 4  * @param superType 用做父类的函数
 5  */
 6 function inheritPrototype(subType, superType) {
 7     var prototype = Object.create(superType.prototype);
 8     prototype.constructor = subType;
 9     subType.prototype = prototype;
10 }
11 
12 function SuperType(name) {
13     this.name = name;
14     this.colors = ["red", "green"];
15 }
16 SuperType.prototype.sayName = function() {
17     console.log(this.name);
18 }
19 
20 function SubType(name, age) {
21     // 调用父类构造函数添加不共享的属性
22     SuperType.call(this, name);
23     this.age = age;
24 }
25 
26 // 设定 SubType 继承自 SuperType
27 inheritPrototype(SubType, SuperType);
28 
29 // 添加SubType的方法, 一定要放在设置继承关系之后
30 SubType.prototype.sayAge = function() {
31     console.log(this.age);
32 }
33 
34 var instance = new SubType("Nicholas", 29);
35 instance.sayName(); // Nicholas

我们来看一下图示:

HTML5学习笔记(十六):原型、类和继承【JS核心知识点】