假设有两个类(Son 类和 Father 类),假设让 Son 类的原型对象引用 Father 类的实例对象,则 Son 类的原型对象指向将包含一个指向 Father 类的原型对象的内部指针【prototype】,从而使 Son 类的原型对象可以共享 Father 类原型对象和实例对象的方法和属性,从而又使 Son 类的实例对象可以共享 Father 类的原型对象的方法和属性,这就是 原型链。
function Father(){
this.names = ['one','two','three'];
}
Father.prototype.getName = function(){
return Father.name;
}
function Son(){}
//Son类的原型对象包含了Father类原型对象的方法和属性
// 同时也包含了Father类实例对象的实例方法和实例属性
Son.prototype = new Father();
// 重写了Son类的原型对象
// Son.prototype.constructor !== Son;
// true
console.log(Son.prototype.constructor === Father);
let son = new Son();
//继承Father类实例对象的引用类型属性
//['one','two','three']
console.log(son.names);
// 继承Father类原型对象的方法
console.log(son.getName());
读取对象的属性和方法使,会执行搜索,从实例本身开始进行查找,实例本身没有,则顺者原型链继续向上查找,搜索不到,到原型链的末端停止。
实现原型链,本质上是扩展了原型搜索机制
缺陷 😕:
在原型中使用引用类型的属性,在所有的实例对象中的该属性都引用了同一个物理空间,一旦空间的值发生了改变,那么所有实例对象的该属性值就发生了变化。
// son实例对象修改Father类实例对象(Son类的原型对象)的引用类型属性
son.names.push('four');
let son1 = new Son();
// son1中仍然引用的是Father类实例对象(Son类的原型对象)的引用类型属性
// ["ziyi", "ziyi1", "ziyi2", "ziyi3"]
console.log(son1.names);
为了避开原型中有引用类型数据的问题,做到子类继承父类的实例方法和实例属性,在子类的构造函数中调用父类的构造函数,从而使子类的 this 对象在父类构造函数中执行,并最终返回的是子类的 this 对象。(子类的 this 对象在执行过程中,都是开辟新的对象空间,因此引用类型的实例属性都是不同的指针地址)
function Father(){
this.names = ['one','two','three'];
}
function Son(){
//使用new关键字执行的构造函数this指向实例对象
// 如果不用 new 关键字 执行this指向全局对象 window
// 这里的Father类当做一个普通的执行函数
// 此时只是让Son类的实例对象创建和Father类实例对象一样的实例属性和方法
Father.call(this)
}
let son = new Son();
son.names.push('four');
//
console.log(son.names);
let son1 = new Son();
console.log(son1.names);
为了防止原型对象引用类型在实例对象中是同一个指针的问题,在原型链的实现中,可以混合 借用构造函数 实现组合继承。
function Father(name,age){
this.name = name;
this.age = age;
//引用类型的实例属性
this.names = [];
}
Father.prototype.getNames = function(){
return this.names;
}
function Son(name, age, job){
//使用new关键字执行的构造函数this指向实例对象
// 如果不用 new 关键字 执行this指向全局对象 window
// 这里的Father类当做一个普通的执行函数
// 此时只是让Son类的实例对象创建和Father类实例对象一样的实例属性和方法
Father.call(this,name,age);
this.job = job;
}
// 创建原型链
// 需要注意此时Son类的原型对象中还有有Father类的实例对象的属性和方法
Son.prototype = new Father()
// 调整Son原型对象的constructor指向
Son.prototype.constructor = Son
let son = new Son('one', 21, 'web');
son.names.push('four')
//Son {name: "one", age: 21, names: Array(1), job: "web"}
console.log(son);
let son1 = new Son('one', 21, 'web')
//Son.names和Son1.names是不同的指针,指向不同的物理空间
console.log(son1.names);// []
需要注意的是 Son 类的实例对象中有 name、age 和 names 属性,Son 类的原型对象中也有这些属性,只是 根据原型链的搜索机制,在使用实例对象访问这些属性时,首先搜索了实例对象中的该同名属性,因此原型对象中的该属性被屏蔽。
// undefined
console.log(Son.prototype.age)
// undefined
console.log(Son.prototype.name)
// []
console.log(Son.prototype.names)
缺陷 😶:子类的原型对象中还存在父类实例对象的实例属性
可以解决在继承的过程中子类的原型对象中还存在着父类实例对象的实例属性的问题。
function Parent(value){
this.val = value;
}
Parent.prototype.getName = function(){
console.log(this.val);
}
function Child(value){
Parent.call(this,value);
}
Child.prototype = Object.create(Parent.prototype,{
constructer:{
value: Child,
enumerable: false, //true:当且仅当在枚举相应对象上的属性时该属性显现
writable: true, // true:当且仅当与该属性相关联的值可以用assignment operator(赋值运算符)改变时
configurable: true // true : 当且仅当该属性描述符的类型可以被改变并且该属性可以从对应对象中删除。
}
})
const child = new Child(1);
child.getName();
console.log( child instanceof Parent)
以上继承实现的核心就是将父类的原型赋值给了子类,并且将构造函数设置为子类,这样既解决了无用的父类属性问题,还能正确的找到子类的构造函数。
ES6 中的类只是 ES5 封装后的语法糖而已。
ES5 的继承使用借助构造函数实现,实质上是先创建子类的实例对象 this,然后再将父类的方法添加到 this 上面。ES6 的继承机制完全不同,实质是先创造父类的实例对象 this(所以必须先调用 super 方法),然后再用子类的构造函数修改 this。
子类必须在 constructor 方法中调用 super 方法,否则新创建实例会报错。这是因为子类自己的 this 必须在父类的构造函数中塑造,得到与父类相同的实例属性和方法,然后再对其进行加工,加上子类自己的实例属性和方法。如果不调用 super,子类就得不到 this 对象。
class ES6Person{
constructor(name){
console.log(new.target.name); // ES6WebDeveloper
console.log(this); // ES6WebDeveloper {}
this.name = name
}
}
class ES6WebDeveloper extends ES6Person{
constructor(name,age){
super(name)
this.age = age
}
}
let developer = new ES6WebDeveloper('WEB')
此时类的继承的语法上显然也考虑的更加周到,子类的原型对象中没有父类的实例对象和属性
console.log(ES6WebDeveloper.prototype)
ES6 在继承的语法上显然考虑的更加周到,不仅继承了类的原型对象,还继承了类的静态属性和静态方法
class ES6Person{
static getClassName(){
return ES6Person.name
}
constructor(name){
this.name = name
}
}
class ES6WebDeveloper extends ES6Person{
constructor(name,age){
super(name)
this.age = age
}
}
let developer = new ES6WebDeveloper('WEB')
console.log(ES6WebDeveloper.getClassName()) // ES6Person
_proto_存在于实例与构造函数的原型对象之间
class Es6Person {
constructor(name) {
this.name = name
}
}
let es6Person = new Es6Person('ziyi2')
// true
console.log(es6Person.__proto__ === Es6Person.prototype)
ES6 的继承原理
class Es6Person{}
class Es6WebDeveloper extends Es6Person{}
//Es6WebDeveloper可看成是一个实例对象
// Es6Person看成是一个原型对象
// 因此Es6WebDeveloper继承了Es6Person所有属性和方法
// 实现了类的静态属性和方法的继承
// 子类的原型是父类
console.log(Es6WebDeveloper.__proto__ === Es6Person); // true
// 这里类似于 Es6WebDeveloper.prototype = new Es6Person()
// 和ES5的原型链一样
// 子类的原型对象是父类的原型对象的实例
// 子类的实例继承父类的实例
console.log(Es6WebDeveloper.prototype.__proto__ === Es6Person.prototype);// true
参考自:js 类和继承