javascript的继承主要是依托其
原型与原型链
的概念来实现的。
嗯…先来补习一下有关原型与原型链吧。
一、名词解释
function Cat(){};
const cat = new Cat();//new 操作都做了什么?
- 构造函数:用来初始化新对象的函数叫做构造函数。如示例中的
Cat
函数就是构造函数 - 实例对象:通过
new
关键字创建出来的对象。如示例中的cat
就是实例对象 - 原型对象及
prototype
:构造函数有一个prototype
属性,它指向实例对象的原型。常使用原型对象来实现继承 - constructor:原型对象具有
constructor
属性,指向原型对象的构造函数 - proto:
__proto__
属性类似是指针,指向构造函数的原型对象。 prototype
和__proto__
的区别,简单理解为:构造函数.prototype === 实例对象.__proto__
,他们都指向同一个原型对象。
原型链:JavaScript
对象(除了 null
)在创建的时候就会关联一个对象,这个对象就是原型,每一个对象都会从原型上继承属性,原型也是对象,所以原型也有原型对象,层层往上,直到 Object.prototype
,这就是原型链。
new 操作都做了什么?
- 在内存中创建一个新的对象
{}
- 将新对象内部的
__proto__
赋值为构造函数的prototype
- 将构造函数内部的
this
指向新对象 - 执行构造函数内部的代码,给新对象添加属性
- 如果构造函数返回非空对象,则返回该对象,否则返回
this
模拟实现new操作:
function fakeNew() {
// 创建新对象
var obj = Object.create(null);
var Constructor = [].shift.call(arguments);
// 将对象的 __proto__ 赋值为构造函数的 prototype 属性
obj.__proto__ = Constructor.prototype;
// 将构造函数内部的 this 赋值为新对象
var ret = Constructor.apply(obj, arguments);
// 返回新对象
return typeof ret === "object" && ret !== null ? ret : obj;
}
function Group(name, member) {
this.name = name;
this.member = member;
}
var group = fakeNew(Group, "hzfe", 17);
二、ES6中实现继承
ES6提供了Class
关键字来实现类的定义,Class
可以通过extends
关键字实现继承,让子类继承父类的属性和方法;还可以通过 static
关键字定义类的静态方法,这比 ES5 的通过修改原型链实现继承,要清晰和方便很多。 需要注意⚠️的是:class
关键字只是原型的语法糖, JavaScript
继承仍然是基于原型实现的。
例子:
class Pet{
constructor (name, age) {
this.name = name;
this.age = age;
}
showName () {
console.log('调用父类的方法');
console.log(this.name, this.age);
}
}
class Dog extends Pet {
constructor(name,age,color) {
// 通过 super 调用父类的构造方法
super(name, age);
this.color = color;
}
showName() {
console.log('调用子类的方法');
console.log(this.name, this.age, this.color);
}
}
缺点: 不是所有的浏览器都支持 class,存在兼容问题
接下来重点看一下ES5的四种常用的实现方式。
三、ES5中实现继承
1.原型链继承
原型链继承的原理很简单,直接让子类的原型对象指向父类实例,当子类实例找不到对应的属性和方法时,就会往它的原型对象,也就是父类实例上找,从而实现对父类的属性和方法的继承。
举个🌰:
function Person () {
this.name='lili';
}
Person.prototype.getName = function () {
return this.name;
}
function Student () {};
Student.prototype = new Person();
// 根据原型链的规则,顺便绑定一下constructor, 这一步不影响继承, 只是在用到constructor时会需要
// 原型的实例等于自身
Student.prototype.constructor = Student;
const student = new Student();
console.log(student.name); // lili
console.log(student.getName()); // lili
缺陷:
- 由于所有
Student
实例原型都指向同一个Person
实例, 因此对某个Student
实例的来自父类的引用类型变量修改会影响所有的Student
实例。 - 在创建子类实例时无法向父类构造传参, 即没有实现
super()
的功能
2.构造函数继承
构造函数继承,即在子类的构造函数中执行父类的构造函数,并为其绑定子类的this,让父类的构造函数把成员属性和方法都挂到子类的this上去,这样既能避免实例之间共享一个原型实例,又能向父类构造方法传参。
举个🌰:
function Person (name) {
this.name = name;
}
Person.prototype.getName = function () {
return this.name;
}
function Student () {
Person.apply(this, arguments)
}
const student = new Student('lili');
console.log(student.name); // lili
缺陷:
- 实例并不是父类的实例,只是子类的实例,
- 只能继承父类的实例属性和方法,不能继承原型属性和方法
Students
类实际上是调用Person
类来生成的实例 - 无法实现函数复用,每个子类都有父类实例函数的副本,影响性能
3. 组合继承
组合是继承结合了原型继承和构造函数继承的特点:使用原型链实现对原型属性和方法的继承,借用构造函数实现对实例属性的继承。
function Person (name) {
this.name= name;
}
Person.prototype.getName = frunction () {
return this.name
}
function Student () {
// 构造函数继承
Person.apply(this, arguments);
}
// 原型继承
Student.prototype = new Person();
// 原型的实例等于自身
Student.prototype.constructor = Student;
const strudent= new student('lili');
console.log(student.name); // lili
console.log(student.getName()); // lili
缺陷:
- 每次创建子类实例都执行了两次构造函数(
Person.apply和new Person()
),虽然这并不影响对父类的继承,但子类创建实例时,原型中会存在两份相同的属性和方法,这并不优雅。
4.寄生式组合继承(这是目前ES5中比较成熟的继承方式了)
解决构造函数被执行两次的问题, 我们将指向父类实例改为指向父类原型, 减去一次构造函数的执行。
function Person(name) {
this.name = name;
}
Person.prototype.getName = function() {
return this.name;
}
function Student() {
// 构造函数继承
Person.apply(this, arguments)
}
// 原型式继承
// Student.prototype = new Person();
Student.prototype = Object.create(Person.prototype);
// 原型的实例等于自身
Student.prototype.constructor = Student;
const student = new Student('lili');
console.log(student.name); // lili
console.log(student.getName()); // lili
5. 原型式继承
原型式继承,主要借助Object.create()
方法实现对普通对象的继承。
let parent4 = {
name: "parent4",
friends: ["p1", "p2", "p3"],
getName: function() {
return this.name;
}
};
let person4 = Object.create(parent4);
person4.name = "tom";
person4.friends.push("jerry");
let person5 = Object.create(parent4);
person5.friends.push("lucy");
console.log(person4.name); // tom
console.log(person4.name === person4.getName()); // true
console.log(person5.name); // parent4
console.log(person4.friends); // ["p1", "p2", "p3","jerry","lucy"]
console.log(person5.friends); // ["p1", "p2", "p3","jerry","lucy"]
这种继承方式的缺点也很明显,因为Object.create
方法实现的是浅拷贝,多个实例的引用类型属性指向相同的内存,存在篡改的可能。
参考地址: https://juejin.cn/post/7069403417906528264?utm_source=gold_browser_extension