# 1、原型链继承
构造函数、原型和实例之间的关系:每个构造函数都有一个原型对象,原型对象都包含一个指向构造函数的指针,而实例都包含一个原型对象的指针。
若通过new Parent()
创建了child
,则 child.__proto__ = Parent.prototype
,而原型链则是顺着__proto__
依次向上查找。因此,可以通过修改子类的原型为父类的实例来实现继承。
//父级构造函数
function Parent() {
this.name = "myName";
}
//在父级原型上添加方法
Parent.prototype.getParentValue = function () {
return this.name;
};
//子级构造函数
function Child() {
this.age = 12;
}
// 这里是关键,创建父级的实例,并将该实例赋值给子级的原型对象
Child.prototype = new Parent();
Child.prototype.getChildValue = function () {
return this.age;
};
var child = new Child();
console.log(child.name); // myName
console.log(child.getParentValue()); // myName
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
实例child
上查找属性时查找顺序依次是 child实例 -> parent实例-> Parent().prototype
function Parent() {
this.colors = ["red", "blue", "green"];
}
function Child() {}
Child.prototype = new Parent();
var child1 = new Child();
child1.colors.push("black");
alert(child1.colors); // "red,blue,green,black"
var child2 = new Child();
alert(child2.colors); // "red,blue,green,black"
2
3
4
5
6
7
8
9
10
11
12
13
总结缺点:
- 原型上的变量被所有实例共享,当一个实例改变引用类型里面的值时会影响其他实例
- 不能向父类构造函数传参
# 2、借用构造函数继承
使用父类的构造函数来增强子类实例,等同于复制父类的实例给子类(不使用原型)
function Parent() {
this.name = "one";
}
Parent.prototype.sex="女"
function Child() {
//继承自Parent,this指向新创建的对象
Parent.call(this);
}
var child1 = new Child();
child1.name = "two";
console.log(child1.name); //two
var child2 = new Child();
console.log(child2.name); //one 不会被影响
console.log(child2.sex); //undefined 不能继承父类原型的属性 / 方法
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
原理:通过call
实现的继承本质是改变了this
指向,让父类里面的this
指到子类的上下文,这样在父类里面通过this
设置的属性或者方法会被写到子类上面。
缺点:
- 只能继承父类实例的属性和方法,不能继承原型的属性 / 方法
- 无法实现复用,每个子类都有父类实例函数的副本,影响性能
# 3、组合继承
组合上述两种方法就是组合继承。
- 使用原型链实现对原型属性和方法的继承
- 通过借用构造函数来实现对实例属性的继承
function Parent(name) {
this.name = name;
this.sex = "女";
}
Parent.prototype.sayName = function () {
console.log(this.name);
};
function Child(name, age) {
// 继承属性
// 第二次调用Parent() 构造函数继承
Parent.call(this, name);
this.age = age;
}
// 第一次调用Parent() 原型链继承
Child.prototype = new Parent();
// 重写Child.prototype原型对象的constructor属性,指向自己的构造函数Child
// Child.prototype.constructor = Child;
Child.prototype.sayAge = function () {
console.log(this.age);
};
var child1 = new Child("Nicholas", 29);
child1.sex = "男";
console.log(child1.sex);//男
child1.sayName(); //"Nicholas";
child1.sayAge(); //29
var child2 = new Child("Greg", 27);
console.log(child2.sex);//女
child2.sayName(); //"Greg";
child2.sayAge(); //27
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
- 第一次调用
Parent()
:给Child.prototype
写入两个属性name,sex。 - 第二次调用
Parent()
:给child
写入两个属性name,sex。
缺点:
实例对象
child
上的两个属性就屏蔽了其原型对象Child.prototype
的两个同名属性。所以,组合模式的缺点就是在使用子类创建实例对象时,其原型中会存在两份相同的属性/方法。
简化代码:
function Parent(name) {
this.name = name;
this.sex = "女";
}
function Child(name, age) {
Parent.call(this, name);
this.age = age;
}
Child.prototype = new Parent();
var child = new Child("yy", 20);
console.log(child);
2
3
4
5
6
7
8
9
10
11
12
13
14
# 4、原型式继承
function object(obj){
//创建临时构造函数
function F(){}
//将传入的对象作为该构造函数的原型
F.prototype = obj;
//返回了这个临时类型的新实例
return new F();
}
2
3
4
5
6
7
8
上面代码从本质上讲,object()
对传入其中的对象执行了一次浅复制,将构造函数F
的原型直接指向传入的对象。
var person = {
name: 'Alvin',
friends: ['Yannis','Ylu']
}
var p1 = object(person);
//p1.__proto__= person
p1.name = 'Bob';
p1.friends.push('Lucy');
var p2 = object(person);
p2.name = 'Lilei';
p2.friends.push('Hanmeimei');
console.log(p1);
console.log(p2);
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
这种原型式继承要求必须有一个对象可以作为另一个对象的基础。如果有这么一个对象的话, 可以把它传递给object
函数,然后再根据具体需求对得到的对象加以修改。
在这个例子中可以作为另一个对象基础的是person
对象,于是我们把它传入到object
函数中,然后该函数就会返回一个新对象,这个新对象将person
作为原型,所以它的原型中就包含了一个基本类型属性name
和一个引用类型属性friends
。这就意味着person.friends
不仅属于person
所有,而且也会被p1
和p2
共享。实际上就相当于创建了person
对象的两个副本。
缺点:
- 原型链继承多个实例的引用类型属性指向相同,存在篡改的可能。
- 无法传递参数
在ES5中新增了Object.create()
方法,该方法规范了原型式继承,能够代替上面的object
方法。
Object.create()
方法用于创建一个新对象,使用现有的对象来作为新创建对象的原型(prototype)
。
这个方法接收两个参数:
- 一个用作新对象原型的对象,
- 另一个是可选的,用于新对象定义额外的属性的对象。
在只传入一个参数的情况下,Object.create()
与上面的object()
方法行为相同。看下面示例:
var person = {
name: 'Alvin',
friends: ['Yannis','Ylu']
}
var p1 = Object.create(person);
p1.name = 'Bob';
p1.friends.push('Lucy');
var p2 = Object.create(person);
p2.name = 'Lilei';
p2.friends.push('Hanmeimei');
console.log(person.friends);//Yannis, Ylu, Lucy, Hanmeimei
2
3
4
5
6
7
8
9
10
11
12
13
14
Object.create()
方法的第二个参数与Object.defineProperties()
方法的第二个参数格式相同:每个属性都是通过自己的描述符定义。以 这种方式指定任何属性都会覆盖原型对象上的同名属性。如:
var person = {
name: 'Alvin',
friends: ['Yannis','Ylu']
}
var p1 = Object.create(person,{
name:{
value:'Lucy'
}
})
console.log(p1.name);//Lucy
2
3
4
5
6
7
8
9
10
11
12
使用场景:
在没有必要兴师动众的创建构造函数,而只想让一个对象与另一个对象保持类似的情况下,原型式继承是完全可以考虑的。当然同样的问题就是:包含的所有引用类型的属性始终都会共享相应的值。
# 5、寄生式继承
核心:在原型式继承的基础上,增强对象,返回构造函数
function createAnother(original){
var clone = object(original); // 通过调用 object() 函数创建一个新对象
clone.sayHi = function(){ // 以某种方式来增强对象
alert("hi");
};
return clone; // 返回这个对象
}
2
3
4
5
6
7
函数的主要作用是为构造函数新增属性和方法,以增强函数
var person = {
name: "Nicholas",
friends: ["Shelby", "Court", "Van"]
};
var anotherPerson = createAnother(person);
anotherPerson.sayHi(); //"hi"
2
3
4
5
6
缺点(同原型式继承):
- 原型链继承多个实例的引用类型属性指向相同,存在篡改的可能。
- 无法传递参数
# 6、寄生组合式继承
结合借用构造函数传递参数和寄生模式实现继承
function inheritPrototype(subType, superType){
var prototype = Object.create(superType.prototype); // 创建对象,创建父类原型的一个副本
prototype.constructor = subType; // 增强对象,弥补因重写原型而失去的默认的constructor 属性
subType.prototype = prototype; // 指定对象,将新创建的对象赋值给子类的原型
}
// 父类初始化实例属性和原型属性
function Parent(name){
this.name = name;
this.colors = ["red", "blue", "green"];
}
Parent.prototype.sayName = function(){
alert(this.name);
};
// 借用构造函数传递增强子类实例属性(支持传参和避免篡改)
function Child(name, age){
Parent.call(this, name);
this.age = age;
}
// 将父类原型指向子类
inheritPrototype(Child, Parent);
// 新增子类原型属性
Child.prototype.sayAge = function(){
alert(this.age);
}
var instance1 = new Child("xyc", 23);
var instance2 = new Child("lxy", 23);
instance1.colors.push("2"); // ["red", "blue", "green", "2"]
instance1.colors.push("3"); // ["red", "blue", "green", "3"]
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
这个例子的高效率体现在它只调用了一次Parent
构造函数,并且因此避免了在Child.prototype
上创建不必要的、多余的属性。于此同时,原型链还能保持不变;因此,还能够正常使用instanceof
和isPrototypeOf()
这是最成熟的方法,也是现在库实现的方法
# 7、混入方式继承多个对象
function MyClass() {
SuperClass.call(this);
OtherSuperClass.call(this);
}
// 继承一个类
MyClass.prototype = Object.create(SuperClass.prototype);
// 混合其它
Object.assign(MyClass.prototype, OtherSuperClass.prototype);
// 重新指定constructor
MyClass.prototype.constructor = MyClass;
MyClass.prototype.myMethod = function() {
// do something
};
2
3
4
5
6
7
8
9
10
11
12
13
14
15
Object.assign
会把 OtherSuperClass
原型上的函数拷贝到 MyClass
原型上,使 MyClass 的所有实例都可用 OtherSuperClass 的方法。
# 8、ES6类继承extends
extends
关键字主要用于类声明或者类表达式中,以创建一个类,该类是另一个类的子类。其中constructor
表示构造函数,一个类中只能有一个构造函数,有多个会报出SyntaxError
错误,如果没有显式指定构造方法,则会添加默认的 constructor
方法,使用例子如下。
class Rectangle {
// constructor
constructor(height, width) {
this.height = height;
this.width = width;
}
// Getter
get area() {
return this.calcArea()
}
// Method
calcArea() {
return this.height * this.width;
}
}
const rectangle = new Rectangle(10, 20);
console.log(rectangle.area);
// 输出 200
-----------------------------------------------------------------
// 继承
class Square extends Rectangle {
constructor(length) {
super(length, length);
// 如果子类中存在构造函数,则需要在使用“this”之前首先调用 super()。
this.name = 'Square';
}
get area() {
return this.height * this.width;
}
}
const square = new Square(10);
console.log(square.area);
// 输出 100
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
extends
继承的核心代码如下,其实现和上述的寄生组合式继承方式一样
function _inherits(subType, superType) {
// 创建对象,创建父类原型的一个副本
// 增强对象,弥补因重写原型而失去的默认的constructor 属性
// 指定对象,将新创建的对象赋值给子类的原型
subType.prototype = Object.create(superType && superType.prototype, {
constructor: {
value: subType,
enumerable: false,
writable: true,
configurable: true
}
});
if (superType) {
Object.setPrototypeOf
? Object.setPrototypeOf(subType, superType)
: subType.__proto__ = superType;
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
# 总结
1、函数声明和类声明的区别
函数声明会提升,类声明不会。首先需要声明你的类,然后访问它,否则像下面的代码会抛出一个ReferenceError。
let p = new Rectangle();
// ReferenceError
class Rectangle {}
2
3
4
2、ES5继承和ES6继承的区别
- ES5的继承实质上是先创建子类的实例对象,然后再将父类的方法添加到this上(Parent.call(this)).
- ES6的继承有所不同,实质上是先创建父类的实例对象this,然后再用子类的构造函数修改this。因为子类没有自己的this对象,所以必须先调用父类的super()方法,否则新建实例报错。