一.什么是原型与原型链
根据MDN官方解释: JavaScript 常被描述为一种基于原型的语言
——每个对象拥有一个原型对象[[Prototype]]
,对象以其原型为模板、从原型继承方法和属性。原型对象也可能拥有原型,并从中继承方法和属性,一层一层、以此类推。这种关系常被称为原型链
(prototype chain),它解释了为何一个对象会拥有定义在其他对象中的属性和方法。
以前可以通过__proto__
访问原型,但现在它已经从相关web标准中被移除,后续可通过Object.getPrototypeOf(obj)
来获取原型。在 javascript 中,函数可以有属性。每个函数都有一个特殊的属性
叫作原型(prototype)
,而它主要是用作继承
。
二.原型的作用
之所以存在原型,是因为JS语言需要实现面向对象,而原型是面向对象的实现手段之一。一个能支持面向对象的语言必须做到一点:能判断一个实例的类型. 在JS中,通过原型就可以知晓某个对象从属于哪个类型,换句话说,原型的存在避免了类型的丢失。在使用方面,有了原型就可以抽离实例的公共方法到原型上,这样就避免了把方法放到实例中,从而减少了内存的消耗。
三.隐式原型和显示原型的区别
在过去我们常用__proto__
表示隐式原型(它其实就是[[Prototype]]
)。用prototype
代表显示原型。
每个对象(数组,对象,函数)都有自己的__proto__
,通过__proto__可以添加数据到原型,也可以通过它一层一层查找数据。构造函数默认会有prototype属性,prototype属性是用来继承的,实例化之后__proto会指向构造函数的prototype。
四.JS的继承
父类
function Person(name) {
this.name = name
this.say = function () {
console.log('1 + 1 = 2')
}
this.colors = ["red", "blue", "green"];
}
Person.prototype.listen = function () {
console.log('エウテルペ')
}
Person.prototype.sayName = function () { // 父类原型方法
return this.name;
};
4.1 原型继承
将父类的实例作为子类的原型
缺点:
- 无法实现多继承,即一个子类不能同时继承多个父类
- 所有实例共享同一个父类的原型,修改子类的原型会影响父类的原型
- 创建子类实例时,无法向父类构造函数传参
// TODO:思路:将父类的实例作为子类的原型
function Student() {
this.type = "学生"
this.name = "张三"
}
Student.prototype = new Person('人') //TODO:关键点 通过原型继承父类 =>无法实现多继承,即一个子类不能同时继承多个父类
Student.prototype.xx = 123
Student.prototype.fn = ()=>{}
const zhangsan = new Student()
console.log(zhangsan);
Person.prototype.eat = function () {
console.log('吃东西')
}
console.log(zhangsan);
// TODO:缺点
// 1 无法实现多继承,即一个子类不能同时继承多个父类
// 2 所有实例共享同一个父类的原型
function Student2() {
this.type = "学生"
this.name = "张三"
}
Student2.prototype = new Person('人2')
// 3 创建子类实例时,无法向父类构造函数传参
const lisi = new Student2()
4.2 构造继承
在子类的构造函数内部调用父类构造函数并改变构造函数的this指向。
缺点:
- 父类原型上的属性没有继承过来
- 因为原型没有继承过来所以无法实现函数复用,每个子类都有父类构造函数的副本,影响性能
- 实例并不是父类的实例,只是子类的实例
// TODO:思路:在子类型的构造函数内部调用父类构造函数并改变构造函数的this指向。
function Student(name) {
// this.person = new Person(name)
Person.call(this,name) // TODO:关键点
this.age = 16
}
Student.prototype.run = '跑路'
const lisi = new Student('李四') // 能实现多继承 可以向父类传递参数
// 优点
// 解决了所有实例共享同一个父类的原型,实现多继承,创建子类实例时,可以向父类传递参数
// TODO:缺点
// 1 父类原型上的属性没有继承过来
function Student2(name) {
Person.call(this,name)
this.age = 18
}
// 2 因为原型没有继承过来所以无法实现函数复用,每个子类都有父类构造函数的副本,影响性能
// 3 实例并不是父类的实例,只是子类的实例(只是实例化了Student,根本就没有实例化Person)
const zhangsan = new Student('张大帅')
4.3 组合继承
顾名思义,组合继承就是将原型链继承与构造函数继承组合在一起,从而发挥两者之长的一种继承模式。
缺点:
- 所有实例共享同一个父类的原型,修改子类的原型会影响父类的原型
- 调用了两次父类的构造函数
// TODO:思路:顾名思义,组合继承就是将原型链继承与构造函数继承组合在一起,从而发挥两者之长的一种继承模式。
function Student(name, subName) {
Person.call(this, name) // 构造继承 第一次调用构造函数
this.subName = subName;
}
Student.prototype = new Person() // TODO:关键点 原型继承 第二次调用构造函数
// Student.prototype = Person.prototype
Student.prototype.constructor = Student // 组合继承需要修复构造函数指向?
Student.prototype.saySubName = function () { // 子类原型方法
return this.subName;
}
// 子类实例
let instance = new Student('王五', 'sisterAn')
instance.colors.push('black')
console.log(instance.colors) // ["red", "blue", "green", "black"]
console.log(instance.sayName()) // 王五
console.log(instance.saySubName()) // sisterAn
console.log(instance);
let instance1 = new Student('赵六', 'sisterAn1')
console.log(instance1.colors) // ["red", "blue", "green"]
console.log(instance1.sayName()) // 赵六
console.log(instance1.saySubName()) // sisterAn1
console.log(instance1);
// 优点:解决了构造继承的缺陷,可以继承原型属性/方法,实现复用
// TODO:缺点:调用了两次父类的构造函数
4.4 寄生组合继承
通过原型链的混成形式来继承方法,在组合继承的基础上.利用 Object.create()
将Person.prototype通过原型的方式继承给一个对象,这样的访问顺序就还是保持子类优先,同时后续添加原型也不会影响父类的原型
优点:
- 解决了组合继承的缺陷,只调用一次父类的构造函数
- 修改子类的原型,父类不变
补充
- Student.prototype.isPrototypeOf(instance)
- 用于检测构造函数Student.prototype是否存在于另一个对象instance的原型链上
- instance instanceof Student
- 用于检测实例对象instance的原型链上是否存在构造函数Student的 prototype。
// TODO:思路:通过原型链的混成形式来继承方法
function Student(name, subName) {
Person.call(this, name) // 构造继承
this.subName = subName;
}
// 用 Object.create() 将参数1通过原型的方式继承给参数2,这样的访问顺序就还是保持子类优先,同时后续添加原型也不会影响父类的原型
console.log(Object.create(Person.prototype));
Student.prototype = Object.create(Person.prototype, {
varB : {
value: null,
enumerable: true,
configurable: true,
writable: true
},
doSomething : {
value: function(){ // override
A.prototype.doSomething.apply(this, arguments);
// call super
// ...
},
enumerable: true,
configurable: true,
writable: true
}
}); // TODO:关键点
Student.prototype.constructor = Student // 组合继承需要修复构造函数指向
// 子类实例
let instance = new Student('王五', 'sisterAn')
instance.colors.push('black')
console.log(instance.colors) // ["red", "blue", "green", "black"]
console.log(instance.sayName()) // 王五
console.log(instance.doSomething())
console.log(instance);
let instance1 = new Student('赵六', 'sisterAn1')
console.log(instance1.colors) // ["red", "blue", "green"]
console.log(instance1.sayName()) // 赵六
console.log(instance1.doSomething())
console.log(instance1);
// 优点:解决了组合继承的缺陷,只调用一次父类的构造函数,原型链保持不变
// Student.prototype.isPrototypeOf(instance) true // 用于检测构造函数Student.prototype是否存在于另一个对象instance的原型链上
// instance instanceof Student // 用于检测实例对象instance的原型链上是否存在构造函数Student的 prototype。
4.5 ES6继承
可以理解ES6继承就是组合寄生继承的一个语法糖.
使用
class People {
constructor(name) {
this.name = name
}
run() { }
}
// extends 相当于方法的继承
// 替换了上面的3行代码
class Man extends People {
constructor(name) {
// super 相当于属性的继承
// 替换了 People.call(this, name)
super(name)
this.gender = '男'
}
fight() { }
}
extends 继承的核心代码如下,其实和寄生组合式继承方式一样
function _inherits(subType, superType) {
// 创建对象,Object.create 创建父类原型的一个副本
// 增强对象,弥补因重写原型而失去的默认的 constructor 属性
// 指定对象,将新创建的对象赋值给子类的原型 subType.prototype
subType.prototype = Object.create(superType && superType.prototype, {
constructor: { // 重写 constructor
value: subType,
enumerable: false,
writable: true,
configurable: true
}
});
if (superType) {
Object.setPrototypeOf
? Object.setPrototypeOf(subType, superType)
: subType.__proto__ = superType;
}
}
五.关于Object.prototype.__proto__已经弃用
后续可通过Object.getPrototypeOf(obj)
来获取原型!
示例
let Circle = function () {};
let shape = {};
let circle = new Circle();
// 设置该对象的原型链引用
// 过时且不推荐使用的。这里只是举个例子,尽量不要在生产环境中这样做。
shape.__proto__ = circle;
// 判断该对象的原型链引用是否属于 circle
console.log(shape.__proto__ === circle); // true