注:今天去面试,面试官问了很多实现优缺点,所以从新整理的文章。现在更通俗易懂了。
前置知识--实例、构造函数、原型链关系
// 构造函数:ConstructorFunc
function ConstructorFunc() {
}
// 实例:instance
let instance = new ConstructFunc()
__proto__隐式原型prototype显示原型
实例与构造函数
instance.__proto__ === ConstructFunc.prototype // true
构造函数与Object对象关系
ConstructFunc.prototype.__proto__ === Object.prototype // true
// 特别说明
Object.prototype.__proto__ === null // true
构造函数与Function对象关系
ConstructFunc.__proto__ === Function.prototype // true
// 特别说明
Function.prototype.__proto__ === Object.prototype // true
你要懂new操作符做了什么
new 关键字会进行如下的操作:
- 创建一个空的简单JavaScript对象(即{});
- 链接该对象(设置该对象的constructor)到另一个对象 ;
- 将步骤1新创建的对象作为this的上下文 ;
- 如果该函数没有返回对象,则返回this。 可能第 2、4 步大家看的不是很明白,这里重新总结一下这 4 个步骤:
function User(name) {
this.name = name;
}
let u = new User('vien');
- 创建一个空对象
u = {} - 绑定原型,
u.__proto__=User.prototype - 调用 User() 函数,并把空对象 u 当做 this 传入,即
User.call(u) - 如果 User() 函数执行完自己 return 一个 object 类型,那么返回此变量,否则返回 this,注意:如果构造函数返回基本类型值,则不影响,还是返回 this
继承
注意:不要修改显示原型上的属性或方法
实例沿着隐式原型查找属性或者方法
instance.__proto__ === child.prototype
child.prototype.__proto__ === parent.prototype
ECMAScript 只支持实现继承,而且其实现继承主要是依靠原型链来实现的。
原型链继承
原理:原型链继承法是运用 Javascript 的原型来实现,在 Javascript 中任意函数都拥有prototype和__proto__这两个属性,而每个对象都拥有一个__proto__属性,对象里__proto__属性的值是来自于构造这个对象的函数的prototype属性,通过prototype 和__proto__,我们构造出原型链,然后利用原型链来实现继承。
// 原型链继承
// 父类
function Parent() {
this.name = 'vien';
this.hobby = ["basketball", "football"];
}
// 子类
function Child() {}
// 继承
Child.prototype = new Parent();
问题:
- 如果父类属性有基本数据类型,实例无法修改这个属性的值;实例会给实例新增一个同名的属性,我们只能对新增的属性进行操作
- 如果父类属性是引用类型,修改这个属性的属性值会影响所有的实例(注意不是对属性直接赋值,如果直接赋值了就像基本数据类型一样,在实例本身上新建一个同名属性)
- 在创建子类型的实例时,不能向父类传递参数。
- 由于原型对象被替换,原本原型对象的 constructor 属性丢失。
注:
child.constructor属性直接沿着原型链找到new Parent()的__proto__的constructor属性
构造函数继承
原理:利用 call或者apply执行父类构造函数
// 构造函数继承
// 父类
function Parent(name) {
this.name = name;
}
// 子类
function Child() {
Parent.apply(this, arguments);
}
let child = new Child("Vien");
console.log(child); // { name: 'Vien' }
优点
- 可以向父类传参
缺点
- 父类原型对象无法继承;即父类的原型对象对子类而言也是不可见的
- 由于原型对象被替换,原本原型对象的 constructor 属性丢失。
组合继承
原理:原型链继承+构造函数继承
// 构造函数原型链组合继承
// 父类
function Parent(name) {
this.name = name;
}
Parent.prototype.sayName = function() {
console.log(this.name);
};
// 子类
function Child() {
// 第一次调用Parent
Parent.apply(this, arguments); // apply() 方法调用一个具有给定this值的函数,以及以一个数组(或类数组对象)的形式提供的参数。
}
// 原型链继承
Child.prototype = new Parent(); // 第二次调用Parent
let child = new Child("Vien");
console.log(child); // { name: 'Vien' }
c.sayName(); // Panda
优点
- 组合继承避免了原型链和借用构造函数的缺陷,融合了它们的优点,成为 JavaScript 中最常用的继承模式。
缺点
- 父类被执行了两次,在使用 call 或 apply 继承属性时执行一次,在创建实例替换子类原型时又被执行了一次。
- 由于原型对象被替换,原本原型对象的 constructor 属性丢失。
原型式继承
// 原型式继承
// 父类
function Parent(name) {
this.name = name;
}
// 子类
function Child() {
Parent.apply(this, arguments);
}
// 继承函数
function create(obj) {
function F() {}
F.prototype = obj;
return new F();
}
// 继承
Child.prototype = create(Parent.prototype); // 避免实例化Parent
let child = new Child("Vien");
console.log(child); // { name: 'Vien' }
说明:
- 原型式继承其实是借助了一个中间的构造函数,将中间构造函数 F 的 prototype 替换成了父类的原型,并创建了一个 F 的实例返回,这个实例是不具备任何属性的(干净的),用这个实例替换子类的原型,因为这个实例的原型指向 F 的原型,F 的原型同时又是父类的原型对象,所以子类实例继承了父类原型的方法,父类只在创建子类实例的时候执行了一次,省去了创建父类实例的过程。
- 原型式继承在 ES5 标准中被封装成了一个专门的方法
Object.create,该方法的第一个参数与上面 create 函数的参数相同,即要作为原型的对象,第二个参数则可以传递一个对象,会把对象上的属性添加到这个原型上,一般第二个参数用来弥补 constructor 的丢失问题,这个方法不兼容 IE 低版本浏览器。
缺点
- 由于原型对象被替换,原本原型对象的 constructor 属性丢失。
寄生式继承
问题:组合继承最大的 问题就是无论什么情况下,都会调用两次超类型构造函数:一次是在创建子类型原型的时候,另一次是 在子类型构造函数内部。
// 寄生式继承
// 父类
function Parent(name) {
this.name = name;
}
// 子类
function Child() {
Parent.apply(this, arguments);
}
// 继承函数
function create(obj) {
function F() {}
F.prototype = obj;
return new F();
}
// 将子类方法私有化函数
function creatFunction(obj) {
// 调用继承函数
let clone = create(obj); //
// 子类原型方法(多个)
clone.sayName = function() {};
clone.sayHello = function() {};
return clone;
}
// 继承
Child.prototype = creatFunction(Parent.prototype);
缺点
- 缺点:因为寄生式继承最后返回的是一个对象,如果用一个变量直接来接收它,那相当于添加的所有方法都变成这个对象自身的了,如果创建了多个这样的对象,无法实现相同方法的复用。
寄生组合式继承
// 寄生组合式继承
// 父类
function Parent(name, age) {
this.name = name;
this.age = age;
}
Parent.prototype.headCount = 1;
Parent.prototype.eat = function() {
console.log("eating...");
};
// 子类
function Child(name, age) {
Parent.apply(this, arguments);
}
// 寄生组合式继承方法(子类原型对象继承父类原型对象)
function myCreate(Child, Parent) {
function F() {}
F.prototype = Parent.prototype;
Child.prototype = new F();
// constructor属性从新指向子类构造函数
Child.prototype.constructor = Child;
// 让 Child 子类的静态属性 super 和 base 指向父类的原型
Child.super = Child.base = Parent.prototype;
}
// 调用方法实现继承
myCreate(Child, Parent);
// 向子类原型对象添加属性方法,因为子类构造函数的原型被替换,所以属性方法仍然在替换之后
Child.prototype.language = "javascript";
Child.prototype.work = function() {
console.log("writing code use " + this.language);
};
Child.work = function() {
this.super.eat();
};
寄生组合式继承基本规避了其他继承的大部分缺点,应该比较强大了,也是平时使用最多的一种继承,其中 Child.super 方法的作用是为了在调用子类静态属性的时候可以调用父类的原型方法。
缺点
- 子类没有继承父类的静态方法。
class继承
原理:寄生组合式继承
// class...extends... 继承
// 父类
class Parent {
constructor(name, age) {
this.name = name;
this.age = age;
}
sayName() {
console.log(this.name);
}
static sayHi() {
console.log("Hello");
}
}
// 子类继承父类
class Child extends Parent {
constructor(name, age) {
super(name, age); // 继承父类的属性
}
sayHello() {
Parent.sayHi();
}
static sayHello() {
super.sayHi();
}
}
let child = new Child("Vien", 18);
child.sayName(); // Vien
child.sayHello(); // Hello
Child.sayHi(); // Hello
Child.sayHello(); // Hello
var __extends = (this && this.__extends) || (function () {
var extendStatics = function (d, b) {
extendStatics = Object.setPrototypeOf ||
({ __proto__: [] } instanceof Array && function (d, b) { d.__proto__ = b; }) ||
function (d, b) { for (var p in b) if (b.hasOwnProperty(p)) d[p] = b[p]; };
return extendStatics(d, b);
}
return function (d, b) {
extendStatics(d, b);
function __() { this.constructor = d; }
d.prototype = b === null ? Object.create(b) : (__.prototype = b.prototype, new __());
};
})();
var Parent = /** @class */ (function () {
function Parent(name, age) {
this.name = name;
this.age = age;
}
Parent.prototype.sayName = function () {
console.log(this.name);
};
Parent.sayHi = function () {
console.log("Hello");
};
return Parent;
}());
var Child = /** @class */ (function (_super) {
__extends(Child, _super);
function Child(name, age) {
return _super.call(this, name, age) || this;
}
Child.prototype.sayHello = function () {
Parent.sayHi();
};
Child.sayHello = function () {
_super.sayHi.call(this);
};
return Child;
}(Parent));
在子类的 constructor 中调用 supper 可以实现对父类属性的继承,父类的原型方法和静态方法直接会被子类继承,在子类的原型方法中使用父类的原型方法只需使用 this 或 super 调用即可,此时 this 指向子类的实例,如果在子类的静态方法中使用 this 或 super 调用父类的静态方法,此时 this 指向子类本身。