随着JS深入学习,JS继承这个重点,难点肯定是绕不开的,总是被js的继承、原型、原型链等机制混淆,因为语言的特性,实际上没有“类”和”实例”的概念,故而引入了原型链的概念,靠一种原型链的一级一级的指向来实现继承。那么当时的创造JavaScript这种的语言的人为什么要这样实现js独有的继承,大家可以阅读阮一峰老师的Javascript继承机制的设计思想,就像讲故事一样,从古代至现代说明了js继承这种设计模式的缘由。
了解了js继承的设计思想后,我们需要学习原型链上的第一个属性prototype,这个属性是一个指针,指向的是原型对象的内存堆。从阮一峰老师的文章中,我们可以知道prototype是为了解决构造函数的属性和方法不能共享的问题而提出的,下面我们先实现一个简单的继承:
function constructorFn (state, data) {
this.data = data;
this.state = state;
this.isPlay = function () {
return this.state + ' is ' + this.data;
}
}
var instance1 = new constructorFn ('1', 'doing');
var instance2 = new constructorFn ('0', 'done');
console.log(instance1.isPlay()); // 1 is doing
console.log(instance2.isPlay()); // 0 is done
此时,实例1 和实例2 都有自己的data属性、state属性、isPlay方法,造成了资源的浪费,既然两个实例都需要调用isPlay方法,便可以将isPlay方法挂载到构造函数的prototype对象上,实例便有了本地属性方法和引用属性方法,如下:
function constructorFn (state, data) {
this.data = data;
this.state = state;
}
constructorFn.prototype.isPlay = function () {
return this.state + ' is ' + this.data;
}
constructorFn.prototype.isDoing = 'nonono!';
var instance1 = new constructorFn ('1', 'doing');
var instance2 = new constructorFn ('0', 'done');
console.log(instance1.isPlay()); // 1 is doing
console.log(instance2.isPlay()); // 0 is done
console.log(instance1.isDoing); // nonono!
console.log(instance2.isDoing); // nonono!
我们将isPlay方法挂载到prototype对象上,同时增加isDoing属性,既然是共享的属性和方法,那么修改prototype对象的属性和方法,实例的值都会被修改,如下:
constructorFn.prototype.isDoing = 'yesyesyes!';
console.log(instance1.isDoing); // yesyesyes!
console.log(instance2.isDoing); // yesyesyes!
问题又来了,可以看到修改实例1的isDoing属性,实例2的实例并未受到影响。这是为什么呢?
那如果修改实例1的isDoing属性的原型属性,实例2的isDoing会不会受到影响?如下:
instance1.__proto__.isDoing = 'yesyesyes!';
console.log(instance1.isDoing); // yesyesyes!
console.log(instance2.isDoing); // yesyesyes!
问题又又来了,为什么修改实例1的__proto__属性上的isDoing的值就会影响到构造函数的原型对象的属性值?
我们先整理一下,未解决的三个问题:
这时候不得不背后真正的操作者搬出来了,就是new操作符,同样是面试最火爆的问题之一,new操作符干了什么?相信有人也是跟我一样,已经背的滚瓜烂熟了,以 Var instance1 = new constructorFn();为例,就是下面三行代码:
var obj = {};
obj.__proto__ = constructorFn.prototype;
constructorFn.call(obj);
第一行声明一个空对象,因为实例本身就是一个对象。
第二行将实例本身的__proto__属性指向构造函数的原型,obj新增了构造函数prototype对象上挂载的属性和方法。
第三行将构造函数的this指向替换成obj,再执行构造函数,obj新增了构造函数本地的属性和方法。
理解了上面三行代码的含义,那么三个问题也就迎刃而解了。
问题1:实例在新建的时候,本身的__ptoto__指向了构造函数的原型。
问题2:实例1和实例2 在新建后,有了各自的this,修改实例1的isDoing属性,只是修改了当前对象的isDoing的属性值,并没有影响到构造函数。
问题3:修改实例1的__proto__,即修改了构造函数的原型对象的共享属性
到此处,涉及到的内容大家可以再回头捋一遍,理解了就会觉得醍醐灌顶。
同时,你可能又会问,__proto__是什么? 简单来说,__proto__是对象的一个隐性属性,同时也是一个指针,可以设置实例的原型。 实例的__proto__指向构造函数的原型对象。
需要注意的是,
每个对象都有内置的__proto__属性,函数对象才会有prototype属性。
用chrome和FF都可以访问到对象的__proto__属性,IE不可以。
我们继续用上面的例子来说明:
function constructorFn (state, data) {
this.data = data;
this.state = state;
}
constructorFn.prototype.isPlay = function () {
return this.state + ' is ' + this.data;
}
constructorFn.prototype.isDoing = 'nonono!';
var instance1 = new constructorFn ('1', 'doing');
console.log(instance1.__proto__ === constructorFn.prototype); // true
构造函数的原型对象也是对象,那么constructor.prototype.__proto__指向谁呢?
定义中说对象的__proto__指向的是构造函数的原型对象,下面我们验证一下constructor.prototype.__proto__的指向:
console.log(instance1.__proto__ === constructorFn.prototype); // true
console.log(constructorFn.prototype.__proto__ === Object.prototype) // true
可以看出,constructor.prototype.__proto__的指向是Object的原型对象。
那么,Object.prototype.__proto__的指向呢?
console.log(instance1.__proto__ === constructorFn.prototype); // true
console.log(constructorFn.prototype.__proto__ === Object.prototype) // true
console.log(Object.prototype.__proto__); // null
用图形表示的话,如下:
可以发现,Object.prototype.__proto__ === null;
这样也就形成了原型链。通过将实例的原型指向构造函数的原型对象的方式,连通了实例-构造函数-构造函数的原型,原型链的特点就是逐层查找,从实例开始查找一层一层,找到就返回,没有就继续往上找,直到所有对象的原型Object.prototype。
继承机制实例 UML(统一建模语言),UML 的主要用途之一是,可视化地表示像继承这样的复杂对象关系。下面的图示是解释 Shape 和它的子类之间关系的 UML 图示:
在 UML 中,每个方框表示一个类,由类名说明。三角形 、矩形和五边形顶部的线段汇集在一起,指向形状,说明这些类都由形状继承而来。同样,从正方形指向矩形的箭头说明了它们之间的继承关系。
最后,对于具体的不同继承方法,会在下一篇详解......