js的原型与继承


js是可以面向对象编程的,而面向对象很重要的一个特性就是继承,js的继承是通过原型链模拟的,达到了继承的效果,我们就探讨一下原型链和继承到底是什么。

什么是继承

继承是指一个对象直接使用另一个对象的属性和方法,是面向对象语言中类与类之间的一种关系。继承的类被称为子类、派生类,就是在JS中所说的实例化的对象;而被继承的类被称为父类、基类或超类,在JS中一般是构造函数。

继承的作用

通过继承,可以使得子类拥有父类的属性和方法,同时子类也可以通过加入新的属性和方法或者修改父类的属性和方法建立新的类层次。
继承体制体现了面向对象技术中的复用性、扩展性和安全性。为面向对象软件开发和模块化软件架构提供了最基本的技术基础。

JS中的类

JS本身就是一种面向对象的语言,它所涉及的元素根据其属性的不同都依附于某一个特定的类。
我们常见的类包括:对象变量(Object)、结构变量(Function)、数组变量(Array)、字符串变量(String)、数值变量(Number)、逻辑变量(Boolean)、日期变量(Data)等,而相关的类的方法,也是我们经常用到的,例如数组的push方法,字符串的split方法等。但是在实际编程中这些方法严重不足,所以也就有了prototype

原型链

在JS中,每个对象都会在其内部初始化一个属性,就是__proto__,当我们访问一个对象的属性时,如果这个对象的内部不存在这个属性,那么它就会去proto里寻找这个属性,这个proto又会有自己的proto,于是就这样一直找下去,也就是我们平时所说的原型链的概念。

__proto__是原型链中对象之间从下到上的联系的桥梁,所有的对象的proto最终都指向Object.prototype,为undefined

按照标准,proto是不对外公开的,也就是说这是个私有属性,但是现在很多浏览器引擎已经将它暴露出来称为一个公有属性,可以对外访问和设置。

但是大多是时候我们并不需要使用proto,而是使用prototype来向下继承。JS每个函数都是一个Function对象,函数对象都有一个子对象prototype对象。而JS中的类是以函数的形式来定义的。prototype表示该函数的原型,也表示一个类的成员的集合。在通过new创建一个类的实例对象时,prototype对象的成员都成为实例化对象的成员,可以被对象直接使用。

其实,prototype只是一个假象,在原型链中只起到一个辅助作用,只有在对构造函数new实例化时才会用有一定的价值,原型链的本质其实在于__proto__

下面看一个简单的例子来详细理解:

1
2
3
4
5
6
7
8
9
function A() {
this.width = 10;
this.data = [1, 2, 3];
this.key = "this is A";
}
A._objectNum = 0; //定义A的属性
A.prototype.say = function() { //给A的原型对象添加属性
console.log("hello world")
}

图解过程如下:



简单说原型就是函数的一个属性,在函数的创建过程中由JS编译器自动添加。

new运算符

在上面的例子中,A函数是一个构造函数,可以用new来实例化;

1
2
var a1 = new A;
var a2 = new A;

这是通过构造函数来创建对象的方式,那么创建对象为什么要这样创建而不是直接var a1 = {};呢?这就涉及new的具体步骤了,这里的new操作可以分成三步(以a1的创建为例):

  1. 新建一个对象并赋值给变量a1:var a1 = {};
  2. 把这个对象的proto属性指向函数A的原型对象:a1.__proto__= A.prototype;
  3. 调用函数A,同时把this指向创建的对象a1,对对象进行初始化:A.apply(a1,arguments);
    其结构图示如下:


从图中看到,无论是对象a1还是a2,都有一个属性保存了对函数A的原型对象的引用,对于这些对象来说,一些公用的方法可以在函数的原型中找到,节省了内存空间。

注意,在这里new实例化的a1a2是Object对象,而不是函数方法。

JS中的继承

js里常用的如下两种继承方式:

原型链继承

原型链继承原理

原型式继承是借助已有的对象创建新的对象,将子类的原型指向父类,就相当于加入了父类这条原型链。
例如定义一个空函数B:

1
function B() {};

这个时候产生了B的原型B.prototype;
原型本身就是一个Object对象,我们可以看看里面放着哪些数据
B.prototype实际上就是{constructor : B , __proto__: Object.prototype},因为prototype本身是一个Object对象的实例,所以其原型链指向的是Object的原型。

接下来我们让B继承A:

1
B.prototype = new A();

这样产生的结果是:
产生一个A的实例,同时赋值给B的原型,也即B.prototype相当于对象{width :10 , data : [1,2,3] , key : "this is A" ,__proto__: A.prototype};
这样就把A的原型通过B.prototype.__proto__这个对象属性保存起来,构成了原型的链接。
但是注意,这样B产生的对象的构造函数发生了改变,因为在B中没有constructor属性,只能从原型链找到A.prototype,读出constructor:A
所以有时我们还要人为设回B本身:B.prototype.constructor = B;

原型链直接继承

1
2
3
4
5
6
7
8
9
function A() {
this.say = 'Hello';
}
A.prototype.sayHello = function() {
console.log(this.say);
}
var a = new A();
console.log(a.say); //=>Hello
a.sayHello(); //=>Hello

这样直接通过一个函数new实例化继承下来的就是直接继承了。会得到一个Object对象a,继承了构造函数A的属性和方法。

原型链多重继承

1
2
3
4
5
6
7
8
9
10
function B() {
this.say += ' World!';
};
B.prototype = new A();
B.prototype.sayHi = function() {
console.log('Hi!');
}
var b = new B();
b.sayHello(); //=>hello world!
b.sayHi(); //=>Hi!

在这里B继承里A的属性和方法,同时又声明了自己的属性和方法。然后通过new实例化,得到一个Object对象b,可以同时使用AB的属性和方法。

通过这样我们就可以使用prototype实现继承,使用父类的属性和方法,实现代码的复用,大大减少代码量。

类式继承(构造函数间的继承)

类式继承原理

类式继承是在子类型构造函数的内部调用超类型的构造函数。严格的类式继承并不是很常见,一般都是组合着用:

1
2
3
4
5
6
7
function add(a, b) {
console.log(a + b);
}
function sub(a, b) {
console.log(a - b);
}
add.call(sub, 3, 1);

这个例子中的意思就是用add来替换subadd.call(sub,3,1) == add(3,1),所以运行结果为:4;

1
2
3
4
5
6
7
8
9
10
11
12
13
function Animal() {
this.name = "Animal";
this.showName = function() {
console.log(this.name);
}
}

function Cat() {
this.name = "Cat";
}
var animal = new Animal();
var cat = new Cat();
animal.showName.call(cat); //=>Cat

call的意思是把animal的方法放到cat上执行,原来cat是没有showName()方法,现在是把animalshowName()方法放到cat上来执行,所以this.name应该是Cat;

类式直接继承

1
2
3
4
5
6
7
8
9
10
11
function Animal(name) {
this.name = name;
this.showName = function() {
console.log(this.name);
}
}
function Cat(name) {
Animal.call(this, name);
}
var cat = new Cat("Black Cat");
cat.showName(); //=>Black Cat

Animal.call(this) 的意思就是使用Animal对象代替this对象,那么Cat中不就有Animal的所有属性和方法了吗,Cat对象就能够直接调用Animal的方法以及属性了.

类式多重继承

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
function ClassA() {
this.showSub = function(a, b) {
console.log(a - b);
}
}
function ClassB() {
this.showAdd = function(a, b) {
console.log(a + b);
}
}
function ClassC() {
ClassA.call(this);
ClassB.call(this);
}
var c = new ClassC();
c.showSub(3, 1); //=>2
c.showAdd(3, 1); //=>4

很简单,使用两个 call 就实现多重继承了。

总结

JS的面向继承有很重要的作用,可以使得子类拥有父类的属性和方法,大大提高了代码的复用性、扩展性和安全性,但是JS中是用原型链模拟的继承,所以效率有些低下,代码量少的时候可以少用或不用继承。

参考链接:
JavaScript原型和继承
JavaScript继承方式详解

文章目錄
  1. 1. 什么是继承
  2. 2. 继承的作用
  3. 3. JS中的类
  4. 4. 原型链
  5. 5. new运算符
  6. 6. JS中的继承
    1. 6.1. 原型链继承
      1. 6.1.1. 原型链继承原理
      2. 6.1.2. 原型链直接继承
      3. 6.1.3. 原型链多重继承
    2. 6.2. 类式继承(构造函数间的继承)
      1. 6.2.1. 类式继承原理
      2. 6.2.2. 类式直接继承
      3. 6.2.3. 类式多重继承
  7. 7. 总结
|