@zhouweicsu
2017-04-11T10:00:45.000000Z
字数 4876
阅读 879
JavaScript
继承
基础
要了解 JavaScript 继承,需要先弄明白相关的4 个概念,3 个关系,3 个属性以及 1 个操作符。
构造函数
:任何函数,只要能通过 new
操作符来调用,那它就可以作为构造函数;构造函数本质就是一个函数;
原型对象
:通过调用构造函数而创建的那个对象实例的原型对象;该对象包含由特定类型的所有实例共享的属性和方法;
实例
:通过 new
操作符调用构造函数得到的对象;
原型链
:每个构造函数都有一个原型对象,我们如果将这个原型对象改为另一个对象的实例,就会形成原型链。
P.S. 原型链后面会详细讲解,所以没有理解没关系。
代码示例 1:
function Point(x, y) { //构造函数
this.x = x || 10;
this.y = x || 10;
this.show = function() {
console.log('x: ' + this.x + ', y:' + this.y);
}
}
function ColorPoint(color) {
this.color = color || 'red'
}
// ColorPoint 继承了 Point
// ColorPoint.prototype 原型对象
// new Point() 实例
ColorPoint.prototype = new Point();
3 个关系是构造函数,原型对象与实例之间的关系:
根据代码示例 1 中 Point 类的定义,我们可以得到如下关系图:
图 1:Point 类构造函数,原型对象与实例之间的关系图
原型链
:构造函数 Point 和 ColorPoint,我们知道这两个构造函数都会有默认的原型对象。上面我们说到如果我们将 ColorPoint 的原型对象改为 Point 的实例,我们就会得到原型链。那原型链是如何形成的呢?就是通过原型对象与实例之间的这个关系形成的。具体形成过程我们需要再了解一下关系中涉及到的 3 个属性。
3 个属性与 3 个关系相关联:prototype
,constructor
,__proto__
:
根据代码示例 1 中 Point 与 ColorPoint 的定义,我们可以得到如下关系图:
图 2:ColorPoint、Point、Object 之间的关系图
原型链
:本来构造函数 Point 和 ColorPoint 是两个独立的函数,之间没有关系。如果我们将 ColorPoint 的原型对象重写,即 ColorPoint.prototype = new Point()。那 prototype 属性就指向了 Point 的实例,而从上面我们已经知道了每个实例都有指向原型对象的内部指针 __proto__,到这里,我们可以得到一个原型链:
1. ColorPoint 的实例中 __proto__ 指向 ColorPoint 的原型对象,即 Point 的实例;
2. 而 Point 的实例也有一个 __proto__ 指向 Point 的原型对象;
3. 而 JavaScript 中任何对象都是继承自 Object,所以 Point 的原型对象也有一个 __proto__ 指针指向 Object 的原型对象;
4. 直到 Object 的原型对象的 __proto__ 被指向 null;
5. 根据定义,null没有原型,并且作为这个原型链 prototype chain 中的最终链接。
通过 __proto__ 指针所形成这个实例与原型的链条就是原型链,即图 2 中蓝色链条,基于原型链的属性和方法查找就是按照这个蓝色链条层层往上的。
前面说过,new
操作符是区别构造函数与普通函数的关键,我们看一下new
操作符执行的步骤:
继承的本质就是通过重写原型对象实现原型链,使得子类型可以通过原型链找到父类型中的属性和方法。有了上面的基础,我们看一下 JavaScript 中如何实现继承,讨论每种方法的优缺点。
代码实例 1 中通过重写 ColorPoint 的原型对象,形成一条原型链实现了继承。原来存在于 Point 的实例中的属性 x
、y
和方法 show
现在也存在于 ColorPoint.prototype 中。但这种实现存在两个问题:
代码示例 2:
function Point(x, y) { //构造函数
this.x = x || 10;
this.y = x || 10;
this.shapes = ['square','rectangle'];
this.show = function() {
console.log('x: ' + this.x + ', y:' + this.y);
}
}
function ColorPoint(color) {
this.color = color || 'red'
}
// ColorPoint 继承了 Point
ColorPoint.prototype = new Point();
var cp1 = new ColorPoint();
cp1.shapes.push('circle');
console.log(cp1.shapes); // ["square", "rectangle", "circle"]
var cp2 = new ColorPoint();
// cp2 被影响了
console.log(cp2.shapes); // ["square", "rectangle", "circle"]
所以原型链这种方式只是实现了继承而已,并不能满足实际工作的需求。
为了解决上面说的两个问题,我们可以通过 call() 和 apply() 在子类的构造函数中将 this
和参数
传递给父类构造函数。
代码示例 3:
function Point(x, y) { //构造函数
this.x = x || 10;
this.y = x || 10;
this.shapes = ['square','rectangle'];
this.show = function() {
console.log('x: ' + this.x + ', y:' + this.y);
}
}
function ColorPoint(x, y, color) {
Point.call(this, x, y);
this.color = color || 'red';
}
var cp1 = new ColorPoint();
cp1.shapes.push('circle');
console.log(cp1.shapes); // ["square", "rectangle", "circle"]
cp1.show() //x: 10, y: 10
var cp2 = new ColorPoint(100, 100);
console.log(cp2.shapes); // ["square", "rectangle"]
cp1.show() //x: 100, y: 100
虽然解决了原型链中的两个问题,但这种借用构造函数的方法还是存在其他问题,就是函数复用的问题。父类 Point 中的 show 方法无法复用,子类 ColorPoint 的所有实例都会重新创建 show 方法。所以这种继承方式在实际工作中也是很少用的。
为了解决上一小节中函数复用的问题,我们可以借用原型对象中的属性是共享这一特性。思路是使用原型链实现对原型属性和方法的继承,而通过借用构造函数来实现对实例属性的继承。
代码示例 4:
function Point(x, y) { //构造函数
this.x = x || 10;
this.y = x || 10;
this.shapes = ['square','rectangle'];
}
Point.prototype.show = function() {
console.log('x: ' + this.x + ', y:' + this.y);
}
function ColorPoint(x, y, color) {
Point.call(this, x, y); // 继承属性
this.color = color || 'red';
}
ColorPoint.prototype = new Point(); // 继承方法
ColorPoint.prototype.showColor = function() {
console.log('color: ' + this.color);
}
var cp1 = new ColorPoint();
cp1.shapes.push('circle');
console.log(cp1.shapes); // ["square", "rectangle", "circle"]
cp1.show(); //x: 10, y:10
cp1.showColor(); //color: red
var cp2 = new ColorPoint(100, 100, 'yellow');
console.log(cp2.shapes); // ["square", "rectangle"]
cp2.show(); //x: 100, y:100
cp2.showColor(); //color: yellow
根据代码示例 4 ,我们可以得到如下关系图:
图 3:ColorPoint、Point、Object 之间新的关系图
这种继承的实现方式,解决了前两个小节中出现的问题,但也有一些小瑕疵,就是父类的构造函数会被调用两次:一次是创建子类型的原型对象, 还有一次是子类型的构造函数中的 call。虽然存在这个问题,但影响不是特别大,所以这种方式还是 JavaScript 最常用的继承模式。
ES6 提供了更接近传统面向对象语言的继承写法,引入了 Class 的概念, Class 之间可以通过 extends 继承。
代码示例 5:
class Point{
constructor(x,y) {
this.x = x;
this.y = y;
}
}
class ColorPoint extends Point {
constructor(x,y,color) {
super(x, y);
this.color = color;
}
}
var p1 = new Point(2,3);
var p2 = new ColorPoint(3,4, 'green');
这段代码中各个类之间的原型链图可以参考另一篇博客《图解 JavaScript 中的 __proto__ 与 prototype》。
ES6 虽然有 Class 的概念,但是其本质还是基于原型链的,我们可以把 Class 看作一个语法糖。ES6 中关于 Class 的关键字除了 class 和 extends,还新增了 constructor, static 和 super。关于 Class 的详细知识可以参考阮一峰老师的《ES6 标准入门》中的 Class 章节。
除了上面提到的继承方法,大神 Douglas Crockford 还推荐了两种实现继承的方法: 原型式继承 和 寄生式继承。还有 YUI 的 YAHOO.lang.extend() 采用的基于寄生式继承和组合继承的 寄生组合式继承,该继承方法可以解决组合继承中父类构造函数被调用两次的问题。这三种继承方法在红宝书《JavaScript 高级程序设计》第 6 章中都有详细介绍。
书该读还是得读,常读常新,需要了解的基础都在里面。