JavaScript 作为一门面对对象语言,但是却不支持接口继承,只支持实现继承。JavaScript 中实现继承就是通过原型链来实现。
简介
首先了解构造函数、原型对象和实例之间的关系。
- 通过 new 操作符加构造函数的方式可以创建实例。
- 构造函数具有相应的原型对象。
- 在原型对象中定义的原型方法在所有实例中都能够访问到。
原理
接下来了解它们之间的关系是如何构建的。
构造函数
在 JavaScript 中声明构造函数的时候,都会为这个函数创建一个 prototype 属性。
//声明构造函数
function Person(name) {
this.name = name;
}
在浏览器下看 Person 构造函数函数就是以上这样,prototype 属性指向了这个构造函数的原型对象。
(实际上声明任何函数都会有这个属性,只不过在构造函数中这个属性才能够发挥作用)
原型对象
声明构造函数之后会为这个构造函数创建一个原型对象,为这个原型对象写入属性就可以在实例中访问到。
//声明构造函数
function Person(name) {
this.name = name;
}
//为原型对象写入属性和方法
Person.prototype.age = 18;
Person.prototype.sayName = function() {
console.log(this.name);
};
在浏览器中展示如下
在浏览器中可以清楚地看到它的内容,其中 constructor 属性指回了构造函数本身。constructor 属性和 prototype 属性构建起了构造函数和原型对象之间的相互关系。
实例
用 new 操作符可以进行实例化,创建这个构造函数的一个实例。
//声明构造函数
function Person(name) {
this.name = name;
}
//为原型对象写入属性和方法
Person.prototype.age = 18;
Person.prototype.sayName = function() {
console.log(this.name);
};
//实例化
var person1 = new Person("demonly");
new 操作符的作用就是首先新建一个对象,然后在这个对象中执行构造函数中的代码,最终返回这个对象。
依然还是在浏览器中看这个对象。
其中包含有构造函数写入的 name 属性和一个 _proto_ 属性。 _proto_ 属性指向这个实例的构造函数的原型对象,这是由浏览器生成的一个内部属性,无法被访问也无法修改。
由实例访问原型对象中的属性和方法就是通过 _proto_ 属性。当访问一个属性或者方法时,首先会寻找实例中是否存在这个属性,如果没有的话就会到 _proto_ 指向的原型对象中寻找。因此只需要在原型对象中添加属性和方法就让这个构造函数创建的所有实例都能访问到,包括在添加属性之前实例化的实例。
为了更加直观地表现它们的关系画了这样一张图。
原型链
原型链的实现就是重写构造函数的 prototype 属性的指向,从而使得浏览器在实例中访问属性的时候能够在自定义的对象中找到。
我们现在再加入一个构造函数。
//声明构造函数
function Person(name) {
this.name = name;
}
//新建构造函数
function Animal() {};
//为新类型的原型对象写入方法
Animal.prototype.breath = function() {
console.log("i can breath");
}
//继承
Person.prototype = new Animal();
//为原型对象写入属性和方法
Person.prototype.sayName = function() {
console.log(this.name);
};
//实例化
var person1 = new Person("demonly");
继承的实现部分我们改写了 Person 构造函数的 prototype 的指向,使它指向了 Animal 类型的一个实例。在浏览器中我们也可以看到 _proto_ 属性的指向变成了 Animal 的实例。
我们在 Person 类型的实例中也可以访问 Animal 类型的方法。
当我们调用 breath 方法的时候,浏览器会首先在这个实例本身中寻找 breath 方法,没有找到就转为到 _proto_ 所指向的 Person 的原型对象(即 Animal 类型的实例)中寻找,依然没有找到就转为到 _proto_ 所指向的 Animal 的原型对象中寻找。
原型链就是这样通过 _proto_ 属性的逐级连接实现的。如果再改写 Animal 的原型对象,那么就可以再增加原型链的长度。
这里需要注意的是,重写原型对象仅仅改变了 Person 的prototype 属性的指向,而在这之前写入的原型方法都还会留在原来的原型对象上,在这之前实例化的实例的 _proto_ 属性也依然会指向原来的原型对象。