在开始讲继承之前,我们首先来了解一下JavaScript中关于构造函数、原型和原型链的一些知识,因为JavaScript是中的继承和这几者之间息息相关。
一、构造函数、原型和原型链的三角关系
首先来看一段简单的代码:
//构造函数Foo
function Foo(name) {
this.name = name;
};
//Foo的原型对象prototype中的方法
Foo.prototype.sayHello = function() {
console.log('Hello,my name is ' + this.name + "!");
};
//使用Foo的constructor构造器实例化一个f1对象
var f1 = new Foo('Chen');
f1.sayHello();//输出:Hello,my name is Chen!
//_proto_指向原型对象prototype
console.log(f1.__proto__ === Foo.prototype); //true
复制代码
接下来通过上面代码来讲一下构造函数、原型和原型链分别是什么,有什么作用:
- 构造函数:在JavaScript中,用
new
关键字来调用定义的构造函数。默认通过constructor
构造器实例化并返回的是一个新对象,这个新对象具有构造函数定义的变量/属性和方法,包括prototype,如Foo(大写); - 原型:每个构造函数都会有一个原型对象
prototype
,该对象上定义的所有属性和方法都会被实例对象所继承,如f1
中继承的sayHello
方法,我们将在下文中继续谈到; - 原型链:每个对象都会在其内部初始化一个隐式属性——
_proto_
,当我们访问一个对象的属性 时,如果这个对象内部不存在这个属性/方法,如f1
中内部不存在sayHello
,那么他就会去_proto_
里找这个属性/方法。_proto_
是一个指向其构造函数原型对象的指针,而原型对象也有自己的原型,通过各自的_proto_
一直指向各自的原型对象,直到某个对象的原型为null
,这种一级一级的链结构称为原型链。
他们的关系可以用下面这个图简单表示:
通过对几个概念以及他们之间的关系的认识后,相信你对"JavaScript是一门原型语言"这句话也会有一定的理解。接下来我们进入下一个重点——继承。
二、什么是JavaScript继承,而又怎么样继承呢?
继承概念
相信许多人对继承都有自己的理解和定义,在这里我也提一下我对继承的认识:A对象能够访问B对象的属性,同时,A对象也能够添加自己的新属性、方法或者覆盖已存在的B对象的属性、方法,以上这种方式就叫做继承。
继承方式
1. 对象冒充
// 父类构造函数
var Parent = function(name){
this.name = name;
this.sayHello = function(){
console.log("Hello, " + this.name + "!");
}
};
// 子类构造函数
var Children = function(name){
this.method = Parent;
this.method(name); // 实现继承的关键
this.getName = function(){
console.log(this.name);
}
};
var p = new Parent("parentName");
var c = new Children("childrenName");
p.sayHello(); // 输出: Hello, parentName!
c.sayHello(); // 输出: Hello, childrenName!
c.getName(); // 输出: childrenName
复制代码
分析:
构造函数使用this
关键字给所有属性和方法赋值(即采用类声明的构造函数方式)。因为构造函数只是一个函数,所以可使Parent
成为Children
的方法,然后调用它。Children
就会收到Parent
的中定义的属性和方法。那为什么不直接执行,非要转个弯把Parent
赋值给Children
的method
属性再执行呢?这跟this
的指向有关,在函数内this
是指向window
的。当将Parent
赋值给Children
的method
时,this
就指向了Children
类的实例。
2. 使用call、applay、bind改变this方法
// 父类构造函数
var Parent = function(name){
this.name = name;
this.sayHello = function(){
console.log("Hello, " + this.name + "!");
}
};
// 子类构造函数
var Children = function(name){
Parent.call(this, name); // 实现继承的关键
this.getName = function(){
console.log(this.name);
}
};
var p = new Parent("parentName");
var c = new Children("childrenName");
p.sayHello(); // 输出: Hello, parentName!
c.sayHello(); // 输出: Hello, childrenName!
c.getName(); // 输出: childrenName
复制代码
分析:
这种方法是与对象冒充方法相似的方法,因为它也是通过call
、apply
、bind
改变了this
的指向而实现继承。这里使用call()
,其他两个类似,具体不细讲。
3. 原型链继承
// 父类构造函数
var Parent = function(){
this.name = 'parentName';
this.sayHello = function(){
console.log("Hello, " + this.name + "!");
}
};
// 子类构造函数
var Children = function(){};
Children.prototype = new Parent(); // 实现继承的关键
var p = new Parent();
var c = new Children();
p.sayHello(); // 输出: Hello,parentName!
c.sayHello(); // 输出: Hello, parentName!
复制代码
分析:
一开始我们提到,如果我们每个构造函数都有一个prototype
,而这里我们将Parent
作为Children
的原型对象,Children
通过_proto_
来找到原型Parent
,并调用sayHello
,实现了继承。注意这里实例对象时候没有传参。
4. 混合方式
// 父类构造函数
var Parent = function(name){
this.name = name;
};
Parent.prototype.sayHello = function(){
console.log("Hello, " + this.name + "!");
};
// 子类构造函数
var Children = function(name,age){
Parent.call(this, name); // 实现继承的关键
this.age = age;
};
Children.prototype = new Parent();// 实现继承的关键
Children.prototype.getAge = function(){
console.log(this.age);
};
var p = new Parent("parentName");
var c = new Children("childrenName",18);
p.sayHello(); // 输出: Hello, parentName!
c.sayHello(); // 输出: Hello, childrenName!
c.getAge(); // 输出: childrenName
复制代码
分析:
对象冒充的主要问题是必须使用构造函数方式,这不是最好的选择。不过如果使用原型链,就无法使用带参数的构造函数了。如何选择呢?答案很简单,两者都用。在JavaScript中创建类的最好方式是用构造函数定义属性,用原型定义方法。将两者混合在一起,就要实现将在原型链方式下传参给构造函数实例化对象。
5. 使用Object.create 方法
// 父类构造函数
var Parent = function(name){
this.name = name;
};
Parent.prototype.sayHello = function(){
console.log("Hello, " + this.name + "!");
};
// 子类构造函数
var Children = function(name,age){
Parent.call(this, name); // 实现继承的关键
this.age = age;
};
Children.prototype=Object.create(Parent.prototype);//实现继承的关键
Children.prototype.constructor = Children;
Children.prototype.getAge = function(){
console.log(this.age);
};
var p = new Parent("parentName");
var c = new Children("childrenName",18);
p.sayHello(); // 输出: Hello, parentName!
c.sayHello(); // 输出: Hello, childrenName!
c.getAge(); // 输出: childrenName
复制代码
分析:
Object.create
方法会使用指定的原型对象及其属性去创建一个新的对象,当执行Children.prototype = Object.create(Parent.prototype)
这个语句后,Children
的constructor
就被改变为Parent
,因此需要将Children.prototype.constructor
重新指定为Children
自身。
6. ES6中extends关键字实现继承
// 父类
class Parent {
//父类构造器
constructor(name) {
this.name = name;
}
}
// 子构造函数
class Children extends Parent {
//子类构造器
constructor(name, age) {
this.age = age; // 这里会报错
super(name);//代表父类构造器
this.age = age; // 正确
}
console.log()
}
复制代码
分析:
Class
可以通过extends
关键字实现继承,这比ES5的通过修改原型链实现继承,要清晰和方便很多。子类必须在constructor
方法中调用super
方法,否则新建实例时会报错。这是因为子类自己的this
对象,必须先通过父类的构造函数完成塑造,得到与父类同样的实例属性和方法,然后再对其进行加工,加上子类自己的实例属性和方法。如果不调用super
方法,子类就得不到this对象。
此外,在子类的构造函数中,只有调用super
之后,才可以使用this
关键字,否则会报错。这是因为子类实例的构建,基于父类实例,只有super
方法才能调用父类实例。
super
虽然代表了父类Parent
的构造函数,但是返回的是子类Children
的实例,即super
内部的this
指的是Children
,因此super()
在这里相当于Parent.prototype.constructor.call(this)
。
三、总结
结合前面的内容,可以发现JavaScript 中的面向对象部分一直是在向 Java 靠拢的。尤其增加了 class 和 extends 关键字之后,靠拢了一大步。但这些并没有改变JavaScript是基于原型这一实质在JavaScript,继承所做工作实际上是在构造原型链,所有子类的实例共享的是同一个原型。所以JavaScript中调用父类的方法实际上是在不同的对象上调用同一个方法,即“方法借用”,这种行为实际上是委托调用。