JavaScript继承
前置知识
1.原型
原型是比较重要的概念,也是了解继承的基础。
2.构造函数创建对象(new操作符调用构造函数做了什么)
function Person(name, age) {
this.name = name;
this.age = age;
this.sayName = function() {
console.log(this.name);
}
}
let person1 = new Person("Lili", 27);
let person2 = new Person("Leo", 28);
person1.sayName(); //Lili
person2.sayName(); //Leo
使用new操作符调用构造函数Person创建实例时,new操作符会进行如下操作:
1.创建一个新对象
2.将新对象的 __ proto __ 指向构造函数的prototype对象
3.this指向新对象
4.执行构造函数中的代码(为这个新对象添加属性)
5.若构造函数返回非空对象则返回该对象,否则返回刚刚新创建的对象
首先要明确,继承是指子类继承父类的属性和方法,子类可以选择继承父类构造函数中的实例属性和方法、父类原型上的属性和方法。
方式一、原型链继承
关键
通过修改子类的原型为父类的实例来实现继承
//父类型
function Person(name, age) {
this.name = name,
this.age = age,
this.play = [1, 2, 3]
this.setName = function () { }
}
Person.prototype.setAge = function () { }
//子类型
function Student(price) {
this.price = price
this.setScore = function () { }
}
Student.prototype = new Person() // 子类型的原型为父类型的一个实例对象
var s1 = new Student(15000)
var s2 = new Student(14000)
console.log(s1,s2)
在 Student.prototype = new Person() // 子类型的原型为父类型的一个实例对象之前
在 Student.prototype = new Person() // 子类型的原型为父类型的一个实例对象之后
加上所有属性和方法:
从上图可以看到,S1作为Student构造函数new出来的一个实例,S1拥有price属性和setScore方法;
而执行代码Student.prototype = new Person()后,Student.prototype 作为Person构造函数new出来的一个实例,Student.prototype拥有name、age、play属性和setName方法。根据原型链S1会拥有它的构造函数原型对象(Student.prototype)上的方法和属性,所以S1除了price属性和setScore方法,还拥有name、age、play属性和setName方法,再往上一层查找,也就是Student.prototype的原型对象Person.prototype,则S1实例也拥有setAge方法。S2等实例也是如此。
这种方式实现的本质是通过将子类的原型指向了父类的实例,所以**子类的实例就可以通过__proto__访问到 Student.prototype 也就是Person的实例,这样就可以访问到父类的私有方法,然后再通过__proto__指向父类的prototype就可以获得到父类原型上的方法**。于是做到了将父类的私有、公有方法和属性都当做子类的公有属性。原型链继承,子类可以继承父类的实例属性与方法、原型上的属性与方法。
问题:
1.子类实例共享父类引用类型的属性,会造成“一改全改”。
function Person(name, age) {
this.name = name,
this.age = age,
this.play = [1, 2, 3]
this.setName = function () { }
}
function Student(price) {
this.price = price;
this.setAge = function() { }
}
Student.prototype = new Person();
let s1 = new Student(500);
let s2 = new Student(800);
s1.play.push(4);
console.log(s1);
console.log(s2);
我们都知道在操作基本数据类型的时候操作的是值,在操作引用数据类型的时候操作的是地址,如果说父类的私有属性中有引用类型的属性,那它被子类继承的时候会作为公有属性,这样子类1操作这个属性的时候,就会影响到子类2。
2.我们需要在子类中添加新的方法或者是重写父类的方法时候,切记一定要放到替换原型的语句之后
优点:
- 简单,易于实现
- 父类新增原型方法/原型属性,子类都能访问到
缺点:
- 无法实现多继承
- 多个子类的实例将共享同一个父类的属性,引用类型的属性会互相影响造成一改全改
- 创建子类实例时,无法向父类构造函数传参
- 要想为子类新增属性和方法,必须要在
Student.prototype = new Person()之后执行,不能放到构造器中
方式二、借用构造函数继承
关键
在子类构造函数中通过call调用父类构造函数
function Person(name, age) {
this.name = name,
this.age = age,
this.play = [1, 2, 3]
this.setName = function () { }
}
function Student(price, name, age) {
Person.call(this, name, age)
this.price = price;
this.setAge = function() { }
}
let s1 = new Student(100, "lili", 27);
let s2 = new Student(200, "dodo", 28);
console.log(s1);
console.log(s2);
这相当于新的Student对象上运行了Person()函数中所有的初始化代码,所以每个实例都会有自己的name、age、setName()属性,实例间的属性互不干扰。
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.Person(name, age)
/*this.name = name
this.age = age*/
this.price = price
}
var s1 = new Student('Tom', 20, 15000)
借用构造函数继承子类只能继承父类实例的属性和方法,不能继承父类原型上的属性和方法。
优点:
- 创建子类实例时,可以在子类构造函数中向父类构造函数传递参数
- 父类的引用属性不会被子类实例共享
缺点:
- 子类实例对象只能继承父类的实例属性和方法,不能继承父类的原型属性和方法
- 必须在构造函数中定义方法,无法实现函数复用,每个子类都有父类实例函数的副本,影响性能
方式三、组合式继承(原型链+借用构造函数)
这种继承方式将上面两种继承方式都用上,思路是通过原型链继承来继承父类原型上的属性和方法(在原型上的方法可以实现复用)、通过借用构造函数来继承父类实例上的属性并且可以给父类的构造函数传参。
function Person(name, age) {
this.name = name,
this.age = age,
this.setAge = function () { }
}
Person.prototype.setAge = 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
console.log(p1.constructor) //Person
这种方式融合原型链继承和构造函数的优点,是 JavaScript 中最常用的继承模式。不过也存在缺点就是无论在什么情况下,都会调用两次构造函数:一次是在创建子类型原型的时候,另一次是在子类型构造函数的内部,子类型最终会包含父类型对象的全部实例属性,但我们不得不在调用子类构造函数时重写这些属性。
优点:
- 可以继承实例属性/方法,也可以继承原型属性/方法
- 不存在引用属性共享问题
- 可传参
- 函数可复用
缺点:
- 调用了两次父类构造函数
方式四、原型式继承
原型式继承实际上是对原型链继承的一种封装,只不过其独特之处在于,定义了一个干净的中间类:
例1
function object(o) {
function F() {}
F.prototype = o;
return new F();
}
let person = {
name: "Nino",
friends: ["Van", "Court"]
};
let anotherPerson = object(person);
anotherPerson.name = "Greg";
anotherPerson.friends.push("Array");
let yetAnotherPerson = object(person);
yetAnotherPerson.name = "Kiko";
yetAnotherPerson.friends.push("Bob");
console.log(person.friends); // ["Van", "Court", "Array", "Bob"]
这其实就是 Object.create()。
在已有对象person的基础上创建对象anotherPerson和yetAnotherPerson,自定义object函数的作用就是把传入的对象(在上例中是person)变为构造函数F()的原型对象,这样通过new F()创建出来的实例对象的__ prop __属性就会指向它构造函数的原型——person;也就实现了后面创建的对象继承了原来已有对象上的属性和方法。
let person = {
name: "Nino",
friends: ["Van", "Court"]
};
let anotherPerson = Object.create(person);
anotherPerson.name = "Greg";
anotherPerson.friends.push("Array");
let yetAnotherPerson = Object.create(person);
yetAnotherPerson.name = "Kiko";
yetAnotherPerson.friends.push("Bob");
console.log(person.friends); // ["Van", "Court", "Array", "Bob"]
Object.create()函数解释如下:
语法: Object.create(proto, [propertiesObject]) //方法创建一个新对象,使新创建的对象的__ proto __ 指向 现有的构造函数的原型对象 (第一个参数)。
参数:
- proto : 必须。表示新建对象的原型对象,即该参数会被赋值到目标对象(即新对象,或说是最后返回的对象)的原型上。该参数可以是
null,对象, 函数的prototype属性(创建空的对象时需传null , 否则会抛出TypeError异常)。- propertiesObject : 可选。 添加到新创建对象的可枚举属性(即其自身的属性,而不是原型链上的枚举属性)对象的属性描述符以及相应的属性名称。这些属性对应
Object.defineProperties()的第二个参数。 3 返回值: 在指定原型对象上添加新属性后的对象。
当Object.create()函数只有一个参数时,与例1效果是一样的。
优点:
- 父类方法可复用
缺点:
- 父类的引用会被所有子类所共享
- 子类实例不能向父类传参
方法五、寄生式继承
核心:在原型式继承的基础上,增强对象,返回构造函数
创建一个仅用于封装继承过程的函数,该函数在内部以某种形式来做增强对象,最后返回对象。
function createObj (o) {
var clone = Object.create(o);
clone.sayName = function () {
console.log('hi');
}
return clone;
}
缺点:跟借用构造函数模式一样,每次创建对象都会创建一遍方法。
方法六、寄生组合式继承
结合借用构造函数传递参数和寄生模式实现继承
function inheritPrototype(subType, superType){
var prototype = Object.create(superType.prototype); // 创建对象,创建父类原型的一个副本
prototype.constructor = subType; // 增强对象,弥补因重写原型而失去的默认的constructor 属性
subType.prototype = prototype; // 指定对象,将新创建的对象赋值给子类的原型
}
// 父类初始化实例属性和原型属性
function SuperType(name){
this.name = name;
this.colors = ["red", "blue", "green"];
}
SuperType.prototype.sayName = function(){
alert(this.name);
};
// 借用构造函数传递增强子类实例属性(支持传参和避免篡改)
function SubType(name, age){
SuperType.call(this, name);
this.age = age;
}
// 将父类原型指向子类
inheritPrototype(SubType, SuperType);
// 新增子类原型属性
SubType.prototype.sayAge = function(){
alert(this.age);
}
var instance1 = new SubType("xyc", 23);
var instance2 = new SubType("lxy", 23);
instance1.colors.push("2"); // ["red", "blue", "green", "2"]
instance1.colors.push("3"); // ["red", "blue", "green", "3"]
复制代码
这个例子的高效率体现在它只调用了一次SuperType 构造函数,并且因此避免了在SubType.prototype 上创建不必要的、多余的属性。于此同时,原型链还能保持不变;因此,还能够正常使用instanceof 和isPrototypeOf()
这是最成熟的方法,也是现在库实现的方法
组合式继承最大的缺点是会调用两次父构造函数。
一次是设置子类型实例的原型的时候:
Child.prototype = new Parent();
一次在创建子类型实例的时候:
var child1 = new Child('kevin', '18');
回想下 new 的模拟实现,其实在这句中,我们会执行:
Parent.call(this, name);
在这里,我们又会调用了一次 Parent 构造函数。
所以,在这个例子中,如果我们打印 child1 对象,我们会发现 Child.prototype 和 child1 都有一个属性为colors,属性值为['red', 'blue', 'green']。
那么我们该如何精益求精,避免这一次重复调用呢?
如果我们不使用 Child.prototype = new Parent() ,而是间接的让 Child.prototype 访问到 Parent.prototype 呢?
看看如何实现:
function Parent (name) {
this.name = name;
this.colors = ['red', 'blue', 'green'];
}
Parent.prototype.getName = function () {
console.log(this.name)
}
function Child (name, age) {
Parent.call(this, name);
this.age = age;
}
// 关键的三步
var F = function () {};
F.prototype = Parent.prototype;
Child.prototype = new F();
var child1 = new Child('kevin', '18');
console.log(child1);
最后我们封装一下这个继承方法:
function object(o) {
function F() {}
F.prototype = o;
return new F();
}
function prototype(child, parent) {
var prototype = object(parent.prototype);
prototype.constructor = child;
child.prototype = prototype;
}
// 当我们使用的时候:
prototype(Child, Parent);