上一篇咱们介绍了经过构造函数和原型能够实现JavaScript中的“类”,因为构造函数和函数的原型都是对象,因此JavaScript的“类”本质上也是对象。这一篇咱们将介绍JavaScript中的一个重要概念原型链,以及如何经原型链实现JavaScript中的继承。javascript
首先,咱们简单描述一下继承的概念:当一个类和另外一个类构成"is a kind of"关系时,这两个类就构成了继承关系。继承关系的双方分别是子类和基类,子类能够重用基类中的属性和方法。java
C#能够显式地定义class,也可让一个class直接继承另一个class,下面这段代码就是一个简单的继承。编程
public class Person { public string Name { get { return "keepfool"; } } public string SayHello() { return "Hello, I am " + this.Name; } } public class Employee : Person { public string Email { get; set; } }
因为Employee类是继承Person类的,因此Employee类的实例可以使用Person类的属性和方法。函数
Employee emp = new Employee(); Console.WriteLine(emp.Name); Console.WriteLine(emp.SayHello()); Console.WriteLine("emp{0}是Person类的实例", emp is Person ? "" : "不");
emp是Employee类的一个实例,同时也是Person类的实例,它能够访问定义在Person类的Name属性和SayHello()方法。post
这是C#的继承语法,JavaScript则没有提供这样的语法,如今咱们来探讨如何在JavaScript中实现继承。性能
在JavaScript中定义两个构造函数Person()和Employee(),为了方便理解和讲解,咱们能够将它们理解为Person类和Employee类。
如下内容提到的Person类、Employee类,和Person()构造函数、Employee()构造函数是一个意思。this
function Person() { this.name = 'keefool'; this.sayHello = function() { return 'Hello, I am ' + this.name; } } function Employee(email) { this.email = email; } var person = new Person(); var emp = new Employee('keepfool@xxx.com');
目前Person()和Employee()构造函数是两个彼此独立的存在,它们没有任何关系。
因此由Employee()构造函数建立的实例emp,确定是访问不到Person的name属性和sayHello()方法的。spa
使用instanceof操做符一样能够肯定emp是Employee类的实例,而不是Person类的实例。prototype
实现继承的目的是什么?固然是让子类可以使用基类的属性和方法。
在这个示例中,咱们的目的是实现Employee继承Person,而后让Employee的实例可以访问Person的name和sayHello()了。3d
JavaScript是如何实现继承的呢?
这个答案有不少种,这里我先只介绍比较常见的一种——经过原型实现继承。
当咱们定义函数时,JavaScript会自动的为函数分配一个prototype属性。
Person()也是一个函数,那么Person()函数也会有prototype属性,即Person.prototype。
function Person() { this.name = 'keefool'; this.sayHello = function() { return 'Hello, I am ' + this.name; } } // 定义了函数后,JavaScript自动地为Person()函数分配了一个prototype属性 // Person.prototype = {};
咱们能够在Person.prototype上定义一些属性和方法,这些属性和方法是能够被Person的实例使用的。
function Person() { this.name = 'keefool'; this.sayHello = function() { return 'Hello, I am ' + this.name; } } Person.prototype.height = 176; var person = new Person(); // 访问Person.prototype上定义的属性 person.height; // 输出176
同理在Employee.prototype上定义的属性和方法,也能够被Employee类的实例使用。
我们的目的是让Employee的实例可以访问name属性和sayHello()方法,若是没有Person()构造函数,我们是这么作的:
function Employee(email) { this.email = email; } Employee.prototype = { name : 'keefool', sayHello = function() { return 'Hello, I am ' + this.name; } }
既然Person()构造函数已经定义了name和sayHello(),咱们就没必要这么作了。
怎么作呢?让Employee.prototype指向一个Person类的实例。
function Person() { this.name = 'keefool'; this.sayHello = function() { return 'Hello, I am ' + this.name; } } function Employee(email) { this.email = email; } var person = new Person(); Employee.prototype = person; var emp = new Employee('keepfool@xxx.com');
如今咱们就能够访问emp.name和emp.sayHello()方法了。
在Chrome控制台,使用instanceof操做符,能够看到emp对象如今已是Person类的实例了。
若是你对这段代码仍是有所疑惑,你能够这么理解:
var person = new Person(); Employee.prototype.name = person.name; Employee.prototype.sayHello = person.sayHello;
因为person对象在后面彻底没有用到,以上这两行代码能够合并为一行。
function Person() { this.name = 'keefool'; this.sayHello = function() { return 'Hello, I am ' + this.name; } } function Employee(email) { this.email = email; } Employee.prototype = new Person(); var emp = new Employee('keepfool@xxx.com');
下面这幅图归纳了实现Employee继承Person的过程:
name和sayHello()不是Employee类的自有属性和方法,它来源于Employee.prototype。
而Employee.prototype指向一个Person的实例,这个实例是可以访问name和sayHello()的。
JavaScript的原型继承的本质:将构造函数的原型对象指向由另一个构造函数建立的实例。
这行代码Employee.prototype = new Person()
描述的就是这个意思。
如今咱们能够说Employee()构造函数继承了Person()构造函数。
用一句话归纳这个继承实现的过程:
上一篇文章有提到过,每一个对象都有constructor属性,constructor属性应该指向对象的构造函。
例如:Person实例的constructor属性是指向Person()构造函数的。
var person = new Person();
在未设置Employee.prototype时,emp对象的构造函数本来也是指向Employee()构造函数的。
当设置了Employee.prototype = new Person();
时,emp对象的构造函数却指向了Person()构造函数。
无形之中,emp.constructor被改写了。
emp对象看起来不像是Employee()构造函数建立的,而是Person()构造函数建立的。
这不是咱们指望的,咱们但愿emp对象看起来也是由Employee()构造函数建立的,即emp.constructor应该是指向Employee()构造函数的。
要解决这个问题,咱们先弄清楚对象的constructor属性是从哪儿来的,知道它是从哪儿来的就知道为何emp.constructor被改写了。
当咱们没有改写构造函数的原型对象时,constructor属性是构造函数原型对象的自有属性。
例如:Person()构造函数的原型没有改写,constructor是Person.prototype的自有属性。
当咱们改写了构造函数的原型对象后,constructor属性就不是构造函数原型对象的自有属性了。
例如:Employee()构造函数的原型被改写后,constructor就不是Person.prototype的自有属性了。
Employee.prototype的constructor属性是指向Person()构造函数的。
这说明:当对象被建立时,对象自己没有constructor属性,而是来源于建立对象的构造函数的原型对象。
即当咱们访问emp.constructor时,实际访问的是Employee.prototype.constructor,Employee.prototype.constructor实际引用的是Person()构造函数,person.constructor引用是Person()构造函数,Person()构造函数其实是Person.prototype.constructor。
这个关系有点乱,咱们能够用如下式子来表示这个关系:
emp.constructor = person.constructor = Employee.prototype.constructor = Person = Person.prototype.constructor
它们最终都指向Person.prototype.constructor!
弄清楚了对象的constructor属性的来弄去脉,上述问题就好解决了。
解决办法就是让Employee.prototype.constructor指向Employee()构造函数。
var o = {}; function Person() { this.name = 'keefool'; this.sayHello = function() { return 'Hello, I am ' + this.name; } } function Employee(email) { this.email = email; } Employee.prototype = new Person(); Employee.prototype.constructor = Employee; var emp = new Employee('keepfool@xxx.com');
若是你仍是不能理解关键的这行代码:
Employee.prototype.constructor = Employee;
你能够尝试从C#的角度去理解,在C#中Employee类的实例确定是由Employee类的构造函数建立出来的。
原型链是JavaScript中很是重要的概念,理解它有助于理解JavaScript面向对象编程的本质。
定义函数时,函数就有了prototype属性,该属性指向一个对象。
prototype属性指向的对象是共享的,这有点像C#中的静态属性。
站在C#的角度讲,由new建立的对象是不能直接访问类的静态属性的。
那么在JavaScript中,为何对象可以访问到prototype中的属性和方法的呢?
例如:当emp对象被建立时,JavaScript自动地为emp对象分配了一个__proto__属性,这个属性是指向Employee.prototype的。
在Chrome的控制台查看emp.__proto__
的内容
首先,▼Person {name: "keepfool"}
表示emp.__proto__是一个Person对象,由于Employee.prototype确实指向一个Person对象。
其次,咱们把emp.__proto__的属性分为3个部分来看。
Employee.prototype.constructor = Employee
,因此constructor是指向Employee()构造函数的。对象的__proto__属性就像一个秘密连接,它指向了建立该对象的构造函数的原型对象。
咱们注意到第3部分的内容仍然是一个__proto__属性,咱们展开它看个究竟吧。
再往下看,还有两层__proto__。
emp.__proto__.__proto__:从▶constructor:function Person()
能够看出它是Person()构造函数的原型。
Person.prototype包含两部份内容:
咱们将这一系列的__proto__
称之为原型链。
下面两幅图展现了本文示例的原型链,这两幅图表示的同一个意思。原型链的最顶端是null,由于Object.prototype是没有__proto__属性。
下表清晰地描述了每一层__proto__表示的内容:
编号 | 原型链 | 原型链指向的对象 | 描述 |
---|---|---|---|
1 | emp.__proto__ | Employee.prototype | Employee()构造函数的原型对象 |
2 | emp.__proto__.__proto__ | Person.prototype | Person()构造函数的原型对象 |
3 | emp.__proto__.__proto__.__proto__ | Object.prototype | Object()构造函数的原型对象 |
4 | emp.__proto__.__proto__.__proto__.__proto__ | null | 原型链的顶端 |
如今能够解释emp对象可以访问到name属性和sayHello()方法了。
以访问emp.sayHello()为例,咱们用几个慢镜头来阐述:
另外,在实现Employee()继承Person(),以及emp对象访问name和sayHello()时,JavaScript是帮咱们作了一些事情的,见下图:
上一篇有提到过,Person类的sayHello()方法放到它的原型对象中更合适,这样全部的Person实例共享一个sayHelo()方法副本,若是咱们把这个方法提到原型对象会发生什么?
var o = {}; function Person() { this.name = 'keefool'; } Person.prototype.sayHello = function(){ return 'Hello, I am ' + this.name; } function Employee(email) { this.email = email; } Employee.prototype = new Person(); Employee.prototype.constructor = Employee; var emp = new Employee('keepfool@xxx.com');
能够看到sayHello()方法的路径是:emp.__proto__.__proto__.sayHello()
,比直接定义在Person()构造函数中多了一层。
这样看来将方法定义在原型对象中并非绝对的好,会使得JavaScript遍历较多层数的原型链,这也会有一些性能上的损失。
为了增强对原型链的理解,咱们来作个简单的示例吧。
上图已经说明了toString()方法是属于内置的Object对象的,咱们以toString()方法来说解这个示例。
在Chrome控制台输入emp.toString(),咱们获得的结果是"[object Object]"
。
toString()方法是在emp的第3层原型链找到的,即emp.__proto__.__proto__.__proto__
,它就是Object对象。
emp.toString()输出"[object Object]"
没有什么意义,如今咱们在Person.prototype上定义一个toString()方法。
var o = {}; function Person() { this.name = 'keefool'; } Person.prototype.sayHello = function(){ return 'Hello, I am ' + this.name; } Person.prototype.toString = function() { return '[' + this.name + ']'; } function Employee(email) { this.email = email; } Employee.prototype = new Person(); Employee.prototype.constructor = Employee; var emp = new Employee('keepfool@xxx.com');
这时toString()方法是在emp对象的第2层原型链找到的,即emp.__proto__.__proto__
。emp.__proto__.__proto__
是Person()构造函数的原型对象,即Person.prototype。
这个也是一个简单的重写示例,Person.protoype重写了toString()方法,emp最终调用的是Person.prototype.toString()方法。