方式一:原型链的继承
// 父类
function Person() {}
// 子类
function Student(){}
// 继承
Student.prototype = new Person()
我们只要把子类的prototype设置为父类的实例,就完成了继承,怎么样? 是不是超级简单? 有没有比Java,C#的清晰?
事实上,以上就是js里面的原型链继承
当然,通过以上代码,我们的Student只是继承了一个空壳的Person,这样视乎是毫无意义的,我们使用继承的目的,
就是要通过继承获取父类的内容,那我们先给父类加上一点点简单的内容(新增的地方标记 '// 新增的代码'):
// 父类
function Person(name,age) {
this.name = name || 'unknow' // 新增的代码
this.age = age || 0 // 新增的代码
}
// 子类
function Student(name){
this.name = name // 新增的代码
this.score = 80 // 新增的代码
}
// 继承
Student.prototype = new Person()
使用
var stu = new Student('lucy')
console.log(stu.name) // lucy --子类覆盖父类的属性
console.log(stu.age) // 0 --父类的属性
console.log(stu.score) // 80 --子类自己的属性
这里为了降低复杂度,我们只演示了普通属性的继承,没有演示方法的继承,事实上,方法的继承也很简单,
我们再来稍微修改一下代码,基于上面的代码,给父类和子类分别加一个方法
// 父类
function Person(name, age) {
this.name = name || "unknow";
this.age = age || 0;
}
// 为父类新曾一个方法
Person.prototype.say = function () {
console.log("I am a person");
};
// 子类
function Student(name) {
this.name = name;
this.score = 80;
}
// 使用父类的实例对象来重写子类的原型对象
// 继承 注意,继承必须要写在子类方法定义的前面
Student.prototype = new Person("zgc", 18);
// 修复子类原型对象上constructor 的指向
Student.prototype.constructor = Student;
// 为子类新增一个方法(在继承之后,否则会被重写的原型覆盖)
Student.prototype.study = function () {
console.log("I am studing");
};
使用
var stu = new Student("lucy");
console.log(stu.name); // lucy --子类覆盖父类的属性
console.log(stu.age); // 18 --父类的属性
console.log(stu.score); // 80 --子类自己的属性
stu.say(); // I am a person --继承自父类的方法
stu.study(); // I am studing --子类自己的方法
这样,看起来我们好像已经完成了一个完整的继承了,这个就是原型链继承,怎么样,是不是很好理解?
但是,原型链继承有一个缺点,来自原型对象引用属性会被所有实例共享,请看以下代码
// 父类
function Person(name, age) {
this.name = name || 'unknow'
this.age = age || 0
this.hobbies = ['music', 'reading']
}
// 子类
function Student(name) {
this.name = name
this.score = 80
}
// 继承
Student.prototype = new Person()
var stu1 = new Student('zgc')
//修改stu1的name属性,stu2的name不会改变
var stu2 = new Student('wf')
stu1.hobbies.push('basketball')
console.log(stu1.hobbies) // music,reading,basketball
console.log(stu2.hobbies) // music,reading,basketball
console.log(stu1.name === stu2.name); //true or false 不传参相等,传参根据参数判断
我们可以看到,当我们改变stu1的引用类型的属性时,stu2对应的属性,也会跟着更改,这就是原型链继承缺点 --来自原型链引用属性会被所有实例共享
特点:
- 共享了父类构造函数的方法和属性
- 简单,易于实现
缺点:
- 继承的属性会被多个实例共享, 如果某个属性是引用类型, 那么这个属性在多个实例中互相影响
- 创建子类实例时,无法向父类构造函数传参
- 要想为子类新增属性和方法,必须要在
Student.prototype = new Person()之后执行,不能放到构造器中 - 不可以实现多继承
例题:
function Parent(name) {
this.name = name || '⽗亲'; // 实例基本属性 (该属性,强调私有,不共享)
this.arr = [1]; // (该属性,强调私有)
}
Parent.prototype.say = function () { // -- 将需要复⽤、共享的⽅法定义在⽗类原型上
console.log('hello')
}
function Child(like) {
this.like = like;
}
Child.prototype = new Parent() // 核⼼,但此时Child.prototype.constructor==Parent
Child.prototype.constructor = Child // 修正constructor指向
let boy1 = new Child()
let boy2 = new Child()
// 优点:共享了⽗类构造函数的say⽅法
console.log(boy1.say(), boy2.say(), boy1.say === boy2.say); // hello , hello , true
// 缺点1:不能向⽗类构造函数传参
console.log(boy1.name, boy2.name, boy1.name === boy2.name); // ⽗亲,⽗亲,true
// 缺点2: ⼦类实例共享了⽗类构造函数的引⽤属性,⽐如arr属性
boy1.arr.push(2);
// 修改了boy1的arr属性,boy2的arr属性,也会变化,因为两个实例的原型上(Child.prototype)有了⽗类构造函数的实例属性arr;
所以只要修改了boy1.arr,boy2.arr的属性也会变化。
console.log(boy2.arr); // [1,2]
// 注意1:修改boy1的name属性,是不会影响到boy2.name。因为设置boy1.name相当于在⼦类实例新增了name属性。
// 注意2:
console.log(boy1.constructor); // Parent 你会发现实例的构造函数居然是Parent。
// ⽽实际上,我们希望⼦类实例的构造函数是Child, 所以要记得修复构造函数指向。
// 修复方法如下:Child.prototype.constructor = Child;
方式二: 借用构造函数继承
这种方式关键在于:在子类型构造函数中通用call()调用父类型构造函数
function Person(name, age) {
this.name = name;
this.age = age;
this.setName = function () {};
}
Person.prototype.setAge = function () {};
function Student(name, age, price) {
Person.call(this, name, age);
/* 等于下面两行代码
this.name = name
this.age = age
*/
this.price = price;
}
var s1 = new Student("Tom", 20, 15000);
这种方式只是实现部分的继承,如果父类的原型还有方法和属性,子类是拿不到这些方法和属性的。
console.log(s1.setAge())//Uncaught TypeError: s1.setAge is not a function
特点:
- 解决了原型链继承中子类实例共享父类引用属性的问题
- 创建子类实例时,可以向父类传递参数
- 可以实现多继承(call多个父类对象)
缺点:
- 实例并不是父类的实例,只是子类的实例
- 只能继承父类的实例属性和方法,不能继承父类原型属性和方法
- 无法实现函数复用,每个子类都有父类实例函数的副本,影响性能
例题:
function Parent(name) {
this.name = name; // 实例基本属性 (该属性,强调私有,不共享)
this.arr = [1]; // (该属性,强调私有)
this.say = function () { // 实例引⽤属性 (该属性,强调复⽤,需要共享)
console.log('hello')
}
}
function Child(name, like) {
Parent.call(this, name); // 核⼼ 拷⻉了⽗类的实例属性和⽅法
this.like = like;
}
let boy1 = new Child('⼩红', 'apple');
let boy2 = new Child('⼩明', 'orange ');
// 优点1:可向⽗类构造函数传参
console.log(boy1.name, boy2.name); // ⼩红, ⼩明
// 优点2:不共享⽗类构造函数的引⽤属性
boy1.arr.push(2);
console.log(boy1.arr, boy2.arr);// [1,2] [1]
// 缺点1:⽅法不能复⽤
console.log(boy1.say === boy2.say) // false (说明,boy1和boy2的say⽅法是独⽴,不是共享的)
// 缺点2:不能继承⽗类原型上的⽅法
Parent.prototype.walk = function () { // 在⽗类的原型对象上定义⼀个walk⽅法。
console.log('我会⾛路')
}
boy1.walk; // undefined (说明实例,不能获得⽗类原型上的⽅法)
方式三: 原型链+借用构造函数的组合继承
这种方式关键在于:
- 普通属性 使用 借用构造函数继承(继承的属性和方法都在实例自身),
- 方法函数 使用 原型链继承(继承的属性和方法都在实例的原型里面),
- 通过调用父类构造,继承父类的属性并保留传参的优点,然后通过将父类实例作为子类原型,实现函数复用。
function Person(name, age) {
this.name = name;
this.age = age;
this.setAge = function () {};
}
Person.prototype.sayBy = function () {
console.log("111");
};
function Student(name, age, price) {
Person.call(this, name, age); //核心代码
this.price = price;
this.setScore = function () {};
}
Student.prototype = new Person(); // 核心代码
Student.prototype.constructor = Student; //组合继承也是需要修复构造函数指向的
Student.prototype.sayHello = function () {};
var s1 = new Student("Tom", 20, 15000);
var s2 = new Student("Jack", 22, 14000);
console.log(s1);
console.log(s1.constructor); //Student
这种方式融合原型链继承和构造函数的优点,是 JavaScript 中最常用的继承模式。不过也存在缺点
- 就是无论在什么情况下,都会调用两次父类构造函数:
- 一次是在创建子类型原型的时候调用,
- 一次是在子类型构造函数的内部调用,
优点:
- 可以继承实例属性/方法,也可以继承原型属性/方法
- 不存在引用属性共享问题
- 可传参
- 函数可复用
缺点:
- 调用了两次父类构造函数,子类型实例上会有两份父类型的属性和方法
- 一份在子类实例自身, 一份在子类实例的原型对象中
例题:
function Parent(name) {
this.name = name; // 实例基本属性 (该属性,强调私有,不共享)
this.arr = [1]; // (该属性,强调私有)
}
Parent.prototype.say = function () { // --- 将需要复⽤、共享的⽅法定义在⽗类原型上
console.log('hello')
}
function Child(name, like) {
Parent.call(this, name) // 核⼼ 第⼆次
this.like = like;
}
Child.prototype = new Parent() // 核⼼ 第⼀次
Child.prototype.constructor = Child // 修正constructor指向
let boy1 = new Child('⼩红', 'apple')
let boy2 = new Child('⼩明', 'orange')
// 优点1:可以向⽗类构造函数传参数
console.log(boy1.name, boy1.like); // ⼩红,apple
// 优点2:可复⽤⽗类原型上的⽅法
console.log(boy1.say === boy2.say) // true
// 优点3:不共享⽗类的引⽤属性,如arr属性
boy1.arr.push(2)
console.log(boy1.arr, boy2.arr); // [1,2] [1] 可以看出没有共享arr属性。
// 缺点1:由于调⽤了2次⽗类的构造⽅法,会存在⼀份多余的⽗类实例属性
方式四: 原型式继承
核心:原型式继承的Object.create方法本质上是对参数对象的一个浅复制。
var person = {
name: "Nicholas",
age: 18,
friends: ["Shelby", "Court", "Van"],
};
person.__proto__.sayHello = function () {
console.log("Hello");
};
var anotherPerson = Object.create(person);
anotherPerson.name = "Greg";
anotherPerson.age = 22;
anotherPerson.friends.push("Rob");
var yetAnotherPerson = Object.create(person);
yetAnotherPerson.name = "Linda";
yetAnotherPerson.friends.push("Barbie");
console.log(person.friends); // ['Shelby', 'Court', 'Van', 'Rob', 'Barbie']
console.log(person); // {name: 'Nicholas', age: 18, friends: Array(5)}
anotherPerson.sayHello(); // Hello
console.log(yetAnotherPerson);
优点:
- 子类可以继承父类和父类原型上的属性和方法
缺点:
- 父类的引用属性会被所有子类实例共享
- 子类构建实例时不能向父类传递参数
拓展: 创建一个新对象,使用现有的对象来提供新创建的对象的__proto__
var obj = {
name: "zgc",
age: 18,
};
// 1.
var info1 = {};
info1.__proto__ = obj;
// 2.
function createObject(o) {
const F = function () {};
F.prototype = o;
return new F();
}
var info2 = createObject(obj);
// 3.
Object.create(obj);
方式五: 寄生式继承
核心:使用原型式继承获得一个目标对象的浅复制,然后增强这个浅复制的能力。
function createAnother(original, name) {
var clone = Object.create(original); //通过调用函数创建一个新对象
clone.name = name
clone.sayHi = function () {
//以某种方式来增强这个对象
alert("hi");
};
return clone; //返回这个对象
}
var person = {
name: "Nicholas",
friends: ["Shelby", "Court", "Van"],
};
var anotherPerson = createAnother(person, 'zgc);
anotherPerson.sayHi(); //"hi"
函数的主要作用是为构造函数新增属性和方法,以增强函数
缺点(同原型式继承):
- 原型链继承多个实例的引用类型属性指向相同,存在篡改的可能。
- 无法传递参数
方式六: 寄生组合式继承
结合借用构造函数传递参数和寄生模式实现继承
function Person(name, age) {
this.name = name;
this.age = age;
this.say = function () {};
}
Person.prototype.setAge = function () {
console.log("111");
};
// Person.prototype.msg = "Hello";
function Student(name, age, price) {
Person.call(this, name, age);
// 子类继承父类自身的属性和方法
this.price = price;
this.setScore = function () {};
}
function inheritPrototype(subType, superType) {
subType.prototype = Object.create(superType.prototype); //核心代码
subType.prototype.constructor = subType; //核心代码
}
// Object.create(obj); 创建一个新对象,使用现有的对象来提供新创建的对象的__proto__
// subType.prototype.constructor = subType; 将子类原型对象的构造器指向自身构造函数
inheritPrototype(Student, Person);
var s1 = new Student("Tom", 20, 15000);
console.log(s1 instanceof Student, s1 instanceof Person); // true true
console.log(s1.constructor); //Student
console.log(s1);
// 完美写法: inheritPrototype函数封装
// function createObject(o) {
// const F = function () {};
// F.prototype = o;
// return new F();
// }
// function inheritPrototype(subType, superType) {
// subType.prototype = createObject(superType.prototype); //核心代码
// Object.defineProperty(subType.prototype, "constructor", {
// value: subType,
// enumerable: false,
// writable: true,
// configurable: true,
// });
// }
同样的,Student继承了所有的Person原型对象的属性和方法。目前来说,最完美的继承方法!
方式七:ES6中class 的继承
ES6中引入了class关键字,class可以通过extends关键字实现继承,还可以通过static关键字定义类的静态方法,这比 ES5 的通过修改原型链实现继承,要清晰和方便很多。
ES5 的继承,实质是先创造子类的实例对象this,然后再将父类的方法添加到this上面(Parent.apply(this))。ES6 的继承机制完全不同,实质是先将父类实例对象的属性和方法,加到this上面(所以必须先调用super方法),然后再用子类的构造函数修改this。
需要注意的是,class关键字只是原型的语法糖,JavaScript继承仍然是基于原型实现的。
class Person {
//调用类的构造方法
constructor(name, age) {
this.name = name
this.age = age
}
//定义一般的方法
showName() {
console.log("调用父类的方法")
console.log(this.name, this.age);
}
}
let p1 = new Person('kobe', 39)
console.log(p1)
//定义一个子类
class Student extends Person {
constructor(name, age, salary) {
super(name, age)//通过super调用父类的构造方法
this.salary = salary
}
showName() {//在子类自身定义方法
console.log("调用子类的方法")
console.log(this.name, this.age, this.salary);
}
}
let s1 = new Student('wade', 38, 1000000000)
console.log(s1)
s1.showName()
优点:
- 语法简单易懂,操作更方便
缺点:
- 并不是所有的浏览器都支持class关键字