原型链 与 继承的理解
原型链是前端面试几乎必问的东西,原型链实现了js中的继承。在看完阮一峰老师的博客后,理解了原型链的作用。
new 的由来
关于new的由来大家可以借阅阮一峰老师的博客Javascript继承机制的设计思想
构造函数
在es5之前还没有类的时候,js都是 new 一个构造函数来生成一个构造函数的对象,这边的构造函数相当于java中的class,在es6之后,为了方便js也诞生了 class 关键字。
构造函数和普通的function最直观的区别是 构造函数的函数名是大写的,它张这个样子
function Dog(name){
this.type = 'dog';
this.name = name;
}
这边是定义了一个 Dog 的构造函数,那么我们可以使用 new 来创建一只狗
let dog = new Dog('哈士奇');
console.log(dog.name); // 哈士奇
至于构造函数中的this指向问题,这边就不说了,在我的上一篇博客详细的介绍了js中的this指向问题。
下面我们了解一下 new 一个对象的中间发生了什么
new 的过程
节选自js高级程序设计(第三版),创建一个实例,一共分为四个步骤
(1)创建一个新对象;
(2)将构造函数的作用域赋给新对象(因此this就指向了这个新对象);
(3)执行构造函数中的代码(为这个新对象添加属性);
(4)返回新对象。
new 的缺点
前面提到了 new 运算符可以实例化一个对象,但是 new 也有一个缺点,就是new出来的两个对象之间没有任何的联系,做不到数据共享,这个缺点有违 new 创建的初衷。下面看一段示例
function Dog(name){
this.name = name;
this.type = 'dog';
}
let dogA = new Dog('哈士奇');
let dogB = new Dog('大金毛');
dogA.type = '大型犬';
console.log(dogB.type); // 'dog'
通过 Dog 构造函数实例化出了两个对象,修改其中一个对象的属性,并不会改变另一个对象的属性,这并不是 new 设计的初衷, dogA 和 dogB 两个对象之间无法做到数据共享,就好像没有任何关系一样。
prototype 的引入
为了解决上面所述的问题,new 的创始人引入了 prototype 属性,
这个属性包含一个对象(以下简称"prototype对象"),所有实例对象需要共享的属性和方法,都放在这个对象里面;那些不需要共享的属性和方法,就放在构造函数里面。
实例对象一旦创建,将自动引用prototype对象的属性和方法。也就是说,实例对象的属性和方法,分成两种,一种是本地的,另一种是引用的。
function Dog(name){
this.name = name;
}
Dog.prototype.type = 'dog';
let dogA = new Dog('哈士奇');
let dogB = new Dog('大金毛');
console.log(dogA.type); // dog
Dog.prototype.type = '大型犬';
console.log(dogA.type, dogB.type); //大型犬 大型犬
type属性是 Dog实例化出的对象所共享的属性,只要修改了 prototype中的type属性,每一个对象的 type 值都会被改变。
构造函数实现继承
构造函数实现继承有很多中实现方式,首先看第一种
call apply 绑定
这种是直接将父构造函数绑定在子构造函数中
function Animal(){
this.type = 'dog';
}
function Dog(name){
Animal.call(this);
this.name = name;
}
let dogA = new Dog('哈士奇');
console.log(dogA.type); // dog
使用prototype进行继承
由于 prototype 属性可以存在对象共享的属性,那么我们可以按照封装的思想,将公共的部分抽离出来,变成一个公共的构造函数,也就是这些对象的父类也是基类,我觉得叫基类更加的合适,因为它存储着每一个对象的公共部分。实现方式只需要将子构造函数的 prototype 属性赋值为父构造函数
function Animal(){
this.type = 'dog';
}
function Dog(name){
this.name = name;
}
Dog.prototype = new Animal();
Dog.prototype.constructor = Dog;
let dogA = new Dog('哈士奇');
console.log(dogA.type); // dog
- 第 9 行:将Dog 的原型属性赋值为Animal的实例
- 第 10 行:修改 prototype 属性的costructor对象为Dog,这是因为每一个 prototype 对象都包含了它的构造函数对象,在上一行将 Dog 的原型直接赋值为 Animal的构造函数,所以此时Dog的原型对象中的 constructor其实是指向Animal的构造函数的,所以我们这边要修改回来,不然引起原型链的絮乱。
直接继承prototype
第三种方法是对第二种方法的改进。由于Animal对象中,不变的属性都可以直接写入Animal.prototype。所以,我们也可以让Cat()跳过 Animal(),直接继承Animal.prototype。
function Animal(){ };
Animal.prototype.type = 'dog';
function Dog(name){
this.name = name;
}
Dog.prototype = Animal.prototype;
Dog.prototype.constructor = Dog;
let dogA = new Dog('哈士奇');
console.log(dogA.type); // dog
这种方法相比于第二种方法,优点是效率高,因为他跳过了执行创建一个 Animal 实例,省内存。但是也有缺点,缺点是直接将 Animal 的prototype 属性赋值给了 Dog 的 prototype 这是一个浅拷贝,因为prototype 是一个 object,所以对于任何Dog 的 prototype的修改都会响应在 Animal.prototype;
console.log(Animal.prototype.constructor === Dog); // true
我们在第 9 行修改了Dog.prototype中的 constructor属性,这边打印发现 Animal 的 prototype中的 constructor 也指向了 Dog
利用空对象作为中介
为了解决上面一种方式引起的问题,可以采用空对象作为媒介。
function Animal(){ };
Animal.prototype.type = 'dog';
function Dog(name){
this.name = name;
}
var Temp = function(){ };
Temp.prototype = Animal.prototype;
Dog.prototype = new Temp();
Dog.prototype.constructor = Dog;
let dogA = new Dog('哈士奇');
console.log(dogA.type); // dog
console.log(Animal.prototype.constructor === Dog); // false
利用空对象作为媒介,空对象几乎不占内存,也不会影响 Animal 的 prototype
拷贝继承
顾名思义,就是将父对象的所有属性和方法,拷贝进子对象。
function Animal(){ };
Animal.prototype.type = 'dog';
function Dog(name){
this.name = name;
}
function extendOfCopy(Child, Parent){
let c = Child.prototype;
let p = Parent.prototype;
for(var i in p){
c[i] = p[i];
}
}
extendOfCopy(Dog, Animal);
let dogA = new Dog('哈士奇');
console.log(dogA.type); // dog