原型链
- 什么是原型链:由于_proto_是任何对象都有的属性,而js里万物皆对象,所以会形成一条_proto_连起来的链条,递归访问_proto_必须最终到头,并且值是null。
- 当js引擎查找对象的属性时,先查找对象本身是否存在该属性,如果不存在,会在原型链上查找,但不会查找自身的prototype;
原型链查找机制:
- 访问对象实例属性,有则返回,没有就通过_proto_去它的原型对象查找;
- 原型对象找到即返回,找不到,继续通过原型对象的_proto_查找;
- 一层一层一直找到Object.prototype,如果找到目标属性即返回,找不到就返回undefined, 不会再往下找,因为再往下Object.prototype.proto = null;
四个概念:
- js分为函数对象和普通对象,每个对象都有_proto_属性,但是只有函数对象才有prototype属性;
- Object、Function都是js内置的函数,类似的还有我们常用到的Array、RegExp、Date、Boolean、Number、String;
- 属性_proto_是一个对象,它有两个属性,constructor和_proto_;
- 原型对象prototype有一个默认的constructor属性,用于记录实例是由哪个构造函数创建;
两个准则:
// 有以下构造函数Person,他的原型上有所属种类属性 category = 'people'
function Person(name, age) {
this.name = name;
this.age = age;
}
Person.prototype.category = 'people'
// 通过new Person()创建的person1实例
let person1 = new Person('小明', 18);
原型链我们遵循以下两个准则:
// 原型对象(Person.prototype)的constructor指向构造函数本身
Person.prototype.constructor === Person
// 实例(person1)的_proto_和原型对象指向同一个地方
person1._proto_ === Person.prototype
所有构造函数都是Function的实例,所有原型对象都是Object的实例除了Object.prototype。
原型链例子:
function F(){}
var f = new F();
// 构造器
F.prototype.constructor === F; // true
F.__proto__ === Function.prototype; // true
Function.prototype.__proto__ === Object.prototype; // true
Object.prototype.__proto__ === null; // true
// 实例
f.__proto__ === F.prototype; // true
F.prototype.__proto__ === Object.prototype; // true
Object.prototype.__proto__ === null; // true
ES5的继承和ES6的继承区别:
- ES5的继承实质上是先创建子类的实例对象,然后再将父类的方法添加到this上(Parent.call(this));
- ES6的继承有所不同,实质上是先创建父类的实例对象this,然后再用子类的构造函数修改this。因为子类没有自己的this对象,所以必须先调用父类的super()方法,否则新建实例报错
原型链继承
构造函数、原型和实例之间的关系: 每个构造函数都有一个原型对象,原型对象包含一个指向构造函数的指针,而实例都包含一个原型对象的指针;
继承的本质就是复制,即重写原型对象,代之以一个新类型实例
function SuperType() {
this.colors = ["red", "blue", "green"];
}
function subType() {}
// 创建SuperType的实例,并将该实例复制给SubType.prototype
subType.prototype = new SuperType();
let instance = new subType();
instance.colors.push('屎黄');
console.log(instance.colors); // "red", "blue", "green", "屎黄"
// 原型链存在缺点:多个实例对引用类型的操作会被篡改
let instance2 = new subType();
console.log(instance2.colors); // "red", "blue", "green", "屎黄"
借用构造函数继承
使用父类的构造函数来增强子类,等同于复制父类的实例给子类(不使用原型);
function SuperType() {
this.colors = ["red", "blue", "green"];
}
function subType() {
// 继承自SuperType
SuperType.call(this);
}
subType.prototype = new SuperType();
let instance = new subType();
instance.colors.push('屎黄');
console.log(instance.colors); // "red", "blue", "green", "屎黄"
let instance2 = new subType();
console.log(instance2.colors); // "red", "blue", "green"
核心代码是SuperType.call(this), 创建子类实例时调用SuperType构造函数,于是SubType的每个实例都会将SuperType中的属性复制一份
缺点:
- 只能继承父类的实例属性和方法,不能继续原型的属性或方法
- 无法实现复用,每个子类都有父类实例函数的副本,影响性能
组合继承
组合原型链继承和借用构造函数继承就是组合继承。用原型链实现对原型属性和方法的继承,用借用构造函数技术实现实例属性的继承
function SuperType(name) {
this.name = name;
this.colors = ["red", "blue", "green"];
}
SuperType.prototype.sayName = function() {
console.log(this.name);
}
function SubType(name, age) {
// 继承属性
// 第二次调用SuperType()
SuperType.call(this, name);
this.age = age;
}
// 继承方法
// 构建原型链
// 第一次调用SuperType()
SubType.prototype = new SuperType();
// 重写SubType.prototype的constructor属性,指向自己的构造函数SubType
SubType.prototype.constructor = SubType;
SubType.prototype.sayAge = function() {
console.log(this.age);
}
let instance1 = new SubType('张三', 18);
instance1.colors.push('屎黄');
console.log(instance1.colors); // "red", "blue", "green", "屎黄"
instance1.sayName(); // 张三
instance1.sayAge(); // 18
let instance2 = new SubType('李四', 36);
console.log(instance2.colors); // "red", "blue", "green"
instance2.sayName(); // 李四
instance2.sayAge(); // 36
缺点:
- 第一次调用SuperType():给SubType.prototype写入两个属性name,color。
- 第二次调用SuperType():给instance1写入两个属性name,color。
实例对象instance1上的两个属性就屏蔽了其原型对象SubType.prototype的两个同名属性。所以,组合模式的缺点就是在使用子类创建实例对象时,其原型中会存在两份相同的属性/方法。
原型式继承
利用一个空对象作为中介,将某个对象直接赋值给空对象构造函数的原型
function object(obj) {
function F() {};
F.prototype = obj;
return new F();
}
// object对其传入的对象执行了一次浅复制,将构造函数F的原型直接指向传入的对象。
let person = {
name: '张三',
colors: ['red', 'blue', 'green']
}
let instance1 = object(person);
instance1.name = '李四';
instance1.colors.push('屎黄');
console.log(instance1.colors); // "red", "blue", "green", "屎黄"
console.log(instance1.name);// 李四
let instance2 = object(person);
instance2.name = '王五';
console.log(instance2.colors); // 存在篡改, "red", "blue", "green", "屎黄"
console.log(instance2.name); // 王五
缺点:
- 原型链继承多个实例的引用类型属性指向相同,存在篡改的可能
- 无法传递参数
寄生式继承
在原型式继承的基础上,增强对象,返回构造函数
function object(obj) {
function F() {};
F.prototype = obj;
return new F();
}
function SuperObject(obj) {
let clone = object(obj); // 通过调用objcet()函数创建一个新对象
clone.sayHi = function() { // 以某种方式增强对象
console.log('Hi');
}
return clone; // 返回这个对象
}
let person = {
name: '张三',
colors: ['red', 'blue', 'green']
}
let instance1 = SuperObject(person);
instance1.name = '李四';
instance1.colors.push('屎黄');
console.log(instance1.colors); // "red", "blue", "green", "屎黄"
console.log(instance1.name);// 李四
instance1.sayHi();// Hi
let instance2 = SuperObject(person);
instance2.name = '王五';
console.log(instance2.colors); // 存在篡改, "red", "blue", "green", "屎黄"
console.log(instance2.name); // 王五
缺点(同原型式继承):
- 原型链继承多个实例的引用类型属性指向相同,存在篡改的可能。
- 无法传递参数
寄生组合式继承
结合借用构造函数传递参数和寄生模式实现继承
- 子类构造函数的__proto__指向父类构造器,继承父类的静态方法
- 子类构造函数的prototype的__proto__指向父类构造器的prototype,继承父类的方法。
- 子类构造器里调用父类构造器,继承父类的属性。
function inheritPrototype(subType, subperType) {
let prototype = Object.create(subperType.prototype);// 创建对象,创建父类原型的一个副本;
prototype.constructor = subType;// 增强对象,弥补因重写原型而失去默认的constructor属性
subType.prototype = prototype;// 指定对象,将新创建的对象赋值给子类原型
}
// 父类初始化实例属性和原型属性
function SuperType(name) {
this.name = name;
this.colors = ['red', 'blue', 'green'];
}
SuperType.prototype.sayName = function() {
console.log(this.name);
}
// 借用构造函数传递增强子类实例属性,支持传参和避免篡改;
function SubType(name, age) {
SuperType.call(this, name);
this.age = age;
}
// 将父类原型指向子类
inheritPrototype(SubType, SuperType);
// 新增子类原型属性
SubType.prototype.sayAge = function() {
console.log(this.age);
}
let instance1 = new SubType('李四', 36);
instance1.colors.push('屎黄');
console.log(instance1.colors); // "red", "blue", "green", "屎黄"
instance1.sayName();// 李四
instance1.sayAge();// 36
let instance2 = new SubType('王五', 18);
console.log(instance2.colors); // "red", "blue", "green"
instance2.sayName();// 王五
instance2.sayAge();// 18
混入方式继承多个对象
function SuperType(name) {
this.name = name;
this.colors = ['red', 'blue', 'green'];
}
SuperType.prototype.sayName = function() {
console.log(this.name);
}
function SubType(age) {
this.age = age;
}
SubType.prototype.sayAge = function() {
console.log(this.age);
}
function MyType(name, age) {
SuperType.call(this, name);
SubType.call(this, age);
}
// 继承一个类
MyType.prototype = Object(SuperType.prototype);
// 混合其他
Object.assign(MyType.prototype, SubType.prototype);
// 重新指定constructor
MyType.prototype.constructor = MyType;
MyType.prototype.sayHi = function() {
console.log('Hi');
}
let instance1 = new MyType('李四', 36);
instance1.colors.push('屎黄');
console.log(instance1.colors); // "red", "blue", "green", "屎黄"
instance1.sayName();// 李四
instance1.sayAge();// 36
instance1.sayHi();// Hi
let instance2 = new MyType('王五', 18);
console.log(instance2.colors); // "red", "blue", "green"
instance2.sayName();// 王五
instance2.sayAge();// 18
instance2.sayHi();// Hi
Object.assign会把SubType原型上的函数拷贝到MyType原型上,使得MyType的所有实例都可以用SubType的方法
ES6类继承extends
extends关键字主要用于类声明或者类表达式中,以创建一个类,该类是另一个类的子类。其中constructor表示构造函数,一个类中只能有一个构造函数,有多个会报出SyntaxError错误,如果没有显示指定构造函数,则会默认添加constructor方法
class SuperType {
constructor(name, age) {
this.name = name;
this.age = age;
this.colors = ['red', 'blue', 'green'];
}
// Getter
get export() {
return this.group();
}
group() {
return `${this.name}${this.age}岁,颜色是${this.colors}`
}
}
let instance = new SuperType('张三', 16);
console.log(instance.export);// 张三16岁,颜色是red,blue,green
// 继承
class SubType extends SuperType {
constructor(name, age, sex) {
super(name, age);
// 如果之类中存在构造函数,则需要在使用'this'之前首先调用super()
this.sex = sex;
}
get subExport() {
return `${this.name}${this.age}岁,性别${this.sex},颜色是${this.colors}`
}
}
let instance1 = new SubType('李四',36,'男');
console.log(instance1.subExport);// 李四36岁,性别男,颜色是red,blue,green