本文对一下几点进行探讨:
1.什么是原型
2.什么是原型链
3.继承的使用
4.圣杯模式
5.知识点补充
1 - 什么是原型?
function Test() {
this.message = '这是构造函数实例化的信息';
}
const p = new Test();
console.log(p);
- MDN的官方解释,每个实例对象都有一个私有属性,[[Prototype]],指向的是实例它的构造函数的原型对象。所以原型指的是每个实例化对象的私有属性[[Prototype]]。
- 值得注意的一点是,我们平时字面量声明的对象,和new Object()实例化出来的对象是一致的,同样拥有私有属性[[Prototype]]。详情如下:
// 方式1
var obj1 = {
message: '字面量声明方式'
}
// 方式2
var obj2 = new Object();
obj2.message = '实例化方式'
console.log('obj1--->', obj1);
console.log('obj2--->', obj2);
//浏览器控制台 对比结构一致
2 - 什么是原型链?
// 通过构造函数.prototype的方式可以访问或设置 该构造函数的原型对象
Father.prototype.age = '36';
function Father() {
this.message = '这是父级构造函数'
}
//将构造函数Father实例化的对象作为 构造函数Son的原型对象
Son.prototype = new Father();
function Son() {
this.message = '这是子级构造函数'
}
const son = new Son();
console.log('查看实例化对象son的信息-------->', son);
console.log('访问实例化对象son的age属性-------->', son.age);
var objPrototype = Object.prototype;
console.log('查看原型链的顶端Object的原型对象1', objPrototype);
console.log('查看原型链的顶端Object的原型对象2', objPrototype.__proto__);
console.log('查看是否相等----->', son.__proto__ === Son.prototype);
/**
* MDN的解释:
* 遵循ECMAScript标准,son.[[Prototype]] 符号是用于指向 Son 的原型。
* 从 ECMAScript 6 开始,[[Prototype]] 可以通过
* Object.getPrototypeOf() 和 Object.setPrototypeOf()访问器来访问。
* 这个等同于 JavaScript 的非标准但许多浏览器实现的属性 __proto__。
*
*
* 因为son.[[prototype]]会报错,所以这里我们使用 son.__proto__ 来访问 Son 的原型对象,
* */
其中一个有趣的现象,打印实例son可以发现,son的私有属性[[Prototype]]里面还存在着[[Prototype]],分析原因:
- 我们在第一点原型的分析得出,每个实例对象都拥有着一个私有属性[[Prototype]],指向实例化它的构造函数的原型对象。
- 观察代码,son.[[Prototype]] === Son.prototype,打印为true, 也就是说son的私有属性[[Prototype]]其实就是实例化son的构造函数Son的原型对象(prototype),这就可以解释为什么产生了[[Prototype]]里面保存着另一个[[Prototype]]的套娃现象。
- 继续观察代码,套娃现象并不是无止境的,顶点为构造函数Object.prototype。 另一个有趣的现象,打印son.age,值为36,分析原因:
- 正常来说,当访问对象中不存在的属性时,会得到undefined,但是现在控制台打印出了值为36。
- 我们看看MDN的解释:当试图访问一个对象的属性时,它不仅仅在该对象上搜寻,还会搜寻该对象的原型,以及该对象的原型的原型,依次层层向上搜索,直到找到一个名字匹配的属性或到达原型链的末尾。
- 也就是说,当实例son对象中不存在age属性时,系统会往son的私有属性[[Prototype]]里面寻找,当son的私有属性[[Prototype]]里面也不存在age属性时,会往son的私有属性[[Prototype]]里面的私有属性[[Prototype]]寻找,找到就会停止,找不到会一直寻找,直到套娃现象的顶端(构造函数Object.prototype)。 最后的总结:
- 在JS中,通过__proto__,依次层层向上访问,直至构造函数Object.prototype这个顶端,而形成的这么一个链条,就称为原型链。
- 当访问一个对象的属性或方法时,它不仅仅在该对象上搜寻,还会搜寻该对象的原型,以及该对象的原型的原型,依次层层向上搜索,直到找到一个名字匹配的属性或到达原型链的末尾,这称为原型链继承。
3 - 继承的使用。
一般来讲,我们使用继承的目标是,子类实例可以使用父类原型上的属性和方法,那么应该如何实现呢?下面从几个例子中看看不同继承方式的结果。
1.子类原型继承父类实例
Father.prototype.age = 20;
function Father() {
this.message2 = "这是父亲的实例",
this.hobby = {
Monday: 'swim',
Tuesday: 'run',
Wednesday: 'read'
}
}
const father = new Father();
Son.prototype = father;
function Son() {
this.message = "这是孩子的实例"
}
const son = new Son();
Son.prototype.age = 10;
console.log('更改Son.prototype后打印father实例------------->', father);
console.log('访问继承的属性------->', son.hobby);
从代码和控制台打印,可以看到,当子类继承父类的实例时,会产生一个问题:我们可以访问到father实例的属性,并且更改Son的原型时,father实例会被修改,明显就不是很合理。
2.子类原型继承父类原型
Father.prototype.name = '张三'
function Father() {
this.message2 = "这是父亲的实例";
}
Son.prototype = Father.prototype;
function Son() {
this.message = "这是孩子的实例";
}
Son.prototype.name = '李四';
const son = new Son();
const father = new Father();
console.log('访问继承的属性------->', father.name); // {name:李四}
子类原型继承父类原型,在son实例上操作时,同样会更改父类的原型对象,依旧不合理。
3.使用apply改变指向
Father.prototype.name = '张三';
function Father() {
this.message = "这是父亲的实例";
this.hobby = {
Monday: 'swim',
Tuesday: 'run',
Wednesday: 'read'
}
}
function Son() {
Father.apply(this)
this.message = '这是孩子的实例'
}
const son = new Son();
console.log('打印son实例------>', son);
console.log('访问son的name属性------>', son.name);
使用this指向,son实例拥有Father构造函数实例的属性和方法,但是son实例的私有属性指向的是Object,说明son并没有继承Father原型的属性和方法,严格说这并不是继承。
以上几种方法,都是在操作子类实例时父类原型受到了影响,那么有没有办法能够让子类继承父类的属性和方法且修改的同时,不会影响的父类原型呢?
4 - 圣杯模式。
主要思想是,构建一个缓冲构造函数(Buffer),以父类的原型当做Buffer的原型,以Buffer的实例当做子类的原型,因此,子类原型变动时,受到影响的只是Buffer的实例,而父类的原型不会受波及。
Father.prototype.name = '张三';
function Father() {
this.message = "这是父亲的实例";
}
Buffer.prototype = Father.prototype;
function Buffer() {};
function Son() {
this.message = "这是Son1的实例";
}
Son.prototype = new Buffer();
Son.prototype.name = '李四';
const son = new Son();
const father = new Father();
console.log('这是son实例--------->', son);
console.log('访问son实例属性name--------->', son.name);
console.log('这是father实例--------->', father);
console.log('访问father实例属性name--------->', father.name);
同样的效果,也能使用Object.create()来实现。
Father.prototype.name = '张三';
function Father() {
this.message = "这是父亲的实例";
}
const father = new Father();
function Son() {
this.message = "这是Son1的实例";
}
Son.prototype = Object.create(Father.prototype);
Son.prototype.name = '李四';
const son = new Son();
console.log('这是son实例--------->', son);
console.log('访问son实例属性name--------->', son.name);
console.log('这是father实例--------->', father);
console.log('访问father实例属性name--------->', father.name);
以上例子,子类既能访问父类原型上的属性和方法,又能修改自己原型上的属性且不影响所继承的父类原型,效果是不是会比较美一些?
5 - 知识点补充
1.继承后子类的constructor指向问题。
function Test() {}
console.log('查看Test的实例-------->', new Test());
function Father() {
this.message = "这是父亲的实例";
}
const father = new Father();
function Son() {
this.message = "这是Son1的实例";
}
Son.prototype = Object.create(Father.prototype);
const son = new Son();
Son.prototype.constructor = Son;
console.log('查看son实例----------->', son);
console.log('查看father实例----------->', father);
在没有继承之前,Test的原型constructor指向的是Test本身。而使用继承以后,通过之前的例子可以发现,子类原型的constructor丢失,所以我们需要手动调整。
2.继承后分别修改子类原型的引用值和原始值。
Father.prototype.name = '张三';
Father.prototype.hobby = {
workingDay: 'run',
weekend: 'watch movie'
}
function Father() {
this.message = "这是父亲的实例";
}
const father = new Father();
function Son() {
this.message = "这是Son1的实例";
}
Son.prototype = Object.create(Father.prototype);
Son.prototype.name = '李四';
Son.prototype.hobby.weekend = 'swim';
const son = new Son();
console.log('查看son实例----------->', son.name);
console.log('查看son实例----------->', son.hobby);
console.log('查看father实例----------->', father.name);
console.log('查看father实例----------->', father.hobby);
可以看到,修改原始值,不会影响父类原型,修改引用值,会影响父类原型。这个目前没有办法处理。
3.关于继承的缺点。
走到这里,大家应该都明白了,子类继承父类,可以使用父类的属性和方法,但是有没有这么一种可能,就是子类一开始只是想使用其中一两个方法,但是父类缺一股脑地把所有东西都丢给了子类,仔细想想,这样子合理吗?科学吗?那么有没有办法,可以让用户自由挑选所需要的功能函数呢?