彻底搞懂原型、原型链、原型对象和继承
一、为什么有了原型?
🏷️从构造函数模式到原型模式
1、📝构造函数模式
构造函数可用来创建特定类型的对象,可以创建自定义的构造函数来定义自定义对象类型的属性和方法
如下代码:
function Person(name, age) {
this.name = name;
this.age = age;
this.sayName = function () {
console.log(this.name);
}
}
const person1 = new Person('LiLi', 25);
const person2 = new Person('Bob', 26);
通过构造函数创建了自定义对象person1 person2,分别有自己的属性和方法,但是这种创建对象的方式有一个问题,就是在每个Person实例中都要重新创建sayName方法,如下,输出是false可以证明
console.log(person1.sayName === person2.sayName)
这是因为函数也是对象,每定义一个函数也就是实例化一个对象,即下面两段代码是等价的。所以不同的实例对象具有了不同的作用域链。当然,也可以把sayName函数的定义定义到构造函数外面,可以解决上面的问题,但是这种方式不具有封装性,不利于代码的维护。由此引出了原型模式。
this.sayName = function () {
console.log(this.name);
}
}
this.sayName = new Function('console.log(this.name)')
2、📝原型模式
首先,每个函数都有一个prototype(原型)属性,当然构造函数中也有原型属性,这个属性是一个指针,指向一个对象,而这个对象中包含了特定类型的所有实例共享的属性和方法。也就是prototype就是调用构造函数而创建的实例对象的原型对象。使用原型对象的好处是可以让所有的实例对象共享它所包含的属性和方法。换句话说,不必在构造函数中定义实例对象的信息,而是可以将这些信息直接添加到原型对象中。
function Person() {}
Person.prototype.name = 'LiLi';
Person.prototype.age = 25;
Person.prototype.sayName = function () {
console.log(this.name);
};
const person1 = new Person();
const person2 = new Person();
console.log(person1.sayName === person2.sayName)
结果为true,说明所有实例访问的都是同一组属性和同一个sayName()函数
📌理解原型对象
1、无论什么时候,只要创建了一个新函数,就会根据一组特定的规则为该函数创建一个prototype属性,这个属性指向的就是原型对象。所以构造函数中天然的带有一个指针prototype,指向一个对象,我们就把这个对象叫做原型对象
以上面的为例来看一下Person.prototype具体是什么?
console.log(Person.prototype)
console.log(Person.prototype.constructor)
2、构造函数一旦创建,它的原型对象会自动获取一个constructor(构造函数)属性,这个属性是一个指向prototype属性所在函数的指针。比如上面的例子:Person.prototype.constructor=Person
🚩注意:构造函数刚创建的时候,原型对象中只有constructor属性,里面其它的方法都是从Object继承而来的
3、当调用构造函数创建一个新实例后,该实例的内部将包含一个指针__proto__,指向构造函数的原型对象。
用图来直观的感受下构造函数、原型对象和实例三者之间的关系:
到此,原型链的概念也就引出来了,任何对象内部都有一个指针__proto__,指向构造函数的原型对象,通过这个__proto__属性连起来的原型对象就叫原型链,原型链的尽头是构造函数Object原型对象的__proto__,为null。
这也是查找对象中属性和方法的查找机制,搜索首先从对象实例开始,如果没有找到,则继续搜索__proto__指针指向的原型对象,依次在原型链上查找,直到找到为止,或者查找到null为止。
因为这个查找机制,对象实例是不能改变原型对象中的值,因为搜索的时候就直接查找到实例中的属性,相当于屏蔽了原型对象中保存的同名属性。
二、继承
1、📝原型链实现继承
📌思想:
利用实例和原型对象之间的关系(如果不清楚继续返回去看上一节),让一个引用类型继承另一个引用类型的属性和方法,即把子类的 prototype(原型对象)直接设置为父类的实例。
function Parent() {
this.name = "parent";
this.arr = [1, 2, 3];
}
Parent.prototype.getName = function () {
return this.name;
}
function Son() {
this.type = "child";
}
Son.prototype = new Parent();
Son.prototype.getType = function () {
return this.type;
}
const s1 = new Son();
const s2 = new Son();
📌本质:
重写子类的原型对象,替换成父类的实例。也就是原来存在于父类Parent实例中的属性和方法,现在也存在于子类的原型对象Son.prototype中了。
以下代码的运行结果印证了这一点:
console.log(s1.__proto__)
console.log(s1.__proto__.__proto__)
console.log(s1.__proto__.constructor)//因为Son.prototype = new Parent();改变了s1.__proto__,现在s1.__proto__
//中没有constructor指针,所以往原型链上查找到Parent
📌存在的问题:
1、当父类的构造函数中定义的实例属性会作为子类原型中的属性,所以子类所有的实例对象都会共享这一个属性,当子类实例对象上进行值修改时,如果是修改的原始类型的值,那么会在实例上新建这样一个值;但如果是引用类型的话,它就会去修改子类上唯一一个父类实例里面的这个引用类型,这会影响所有子类实例。
2、在创建子类型的实例时,不能向父类型的构造函数中传递参数。
2、📝借用构造函数法实现继承
📌思想:
在子类构造函数的内部调用父类的构造函数。
function Parent(_name) {
this.name = _name;
this.arr = [1, 2, 3];
}
Parent.prototype.getName = function () {
return this.name;
}
function Son(_name) {
//继承了Parent 同时还传递了参数
Parent.call(this, _name)
}
Son.prototype.getType = function () {
return this.type;
}
const s1 = new Son();
s1.arr.push(4)
console.log(s1.arr)
const s2 = new Son('Bob');
console.log(s2.arr)
console.log(s2.name)
📌运行结果:
从运行结果清楚的看到,构造函数法完美解决了原型链继承中存在的两个问题
📌本质:
函数不过是在特定环境中执行代码的对象,因此通过apply()和call()方法可以在将来心创建的对象上执行构造函数。
📌存在的问题:
父类原型链上的属性和方法并不会被子类继承
console.log(s2.__proto__)
console.log(s2.__proto__.__proto__)
console.log(s2.getName)
3、📝组合继承
📌思想:
将原型链和借用构造函数的技术组合到一起
function Parent(_name) {
this.name = _name;
this.arr = [1, 2, 3];
}
Parent.prototype.getName = function () {
return this.name;
}
function Son(_name) {
Parent.call(this, _name)
}
Son.prototype = new Parent();
//Son.prototype = new Parent();导致Son.prototype.constructor指向改变 所以要改回来
Son.prototype.constructor = Son;
Son.prototype.getType = function () {
return this.type;
}
const s1 = new Son();
s1.arr.push(4)
const s2 = new Son('Bob');
console.log(s2.name)
console.log(s2.arr)
console.log(s2.__proto__)
console.log(s2.__proto__.__proto__)
console.log(s2.getName())
📌运行结果:
📌本质:
使用原型链实现对父类原型属性和方法继承,通过构造函数来实现对实例属性的继承
📌存在的问题:
无论在什么情况下,都会调用两次父类构造函数:一次是在创建子类型原型的时候,另一次是在子类型构造函数内部
4、📝寄生组合式继承
📌思想:
不需要为了子类的原型而调用父类的构造函数,只需要父类的__proto__提供查找组成原型链即可
function Parent(_name) {
this.name = _name;
this.arr = [1, 2, 3];
}
Parent.prototype.getName = function () {
return this.name;
}
function Son(_name) {
//继承了Parent 同时还传递了参数
Parent.call(this, _name)
}
//提供__proto__就可以了
// 不用这种形式Son.prototype = Parent.prototype; 是因为子类
// 不可直接在 prototype 上添加属性和方法,因为会影响父类的原型
const pro = Object.create(Parent.prototype) // pro.__proto__即Parent.prototype
pro.constructor = Son
Son.prototype = pro
Son.prototype.getType = function () {
return this.type;
}
const s1 = new Son();
s1.arr.push(4)
const s2 = new Son('Bob');
console.log(s2.name)
console.log(s2.arr)
console.log(s2.__proto__)
console.log(s2.__proto__.__proto__)
📌运行结果:
![]
📌本质:
使用原型链的混成模式实现对父类原型属性和方法继承,通过构造函数来实现对实例属性的继承
📌存在的问题:
是最理想的继承方式。