在开始讲继承之前,我们首先来了解一下JavaScript中关于构造函数、原型和原型链的一些知识,因为JavaScript是中的继承和这几者之间息息相关。
一、构造函数、原型和原型链的三角关系
首先来看一段简单的代码:
//构造函数Foofunction 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_指向原型对象prototypeconsole.log(f1.__proto__ === Foo.prototype); //true复制代码
接下来通过上面代码来讲一下构造函数、原型和原型链分别是什么,有什么作用:
- 构造函数:在JavaScript中,用
new
关键字来调用定义的构造函数。默认通过constructor
构造器实例化并返回的是一个新对象,这个新对象具有构造函数定义的变量/属性和方法,包括prototype,如Foo(大写); - 原型:每个构造函数都会有一个原型对象
prototype
,该对象上定义的所有属性和方法都会被实例对象所继承,如f1
中继承的sayHello
方法,我们将在下文中继续谈到; - 原型链:每个对象都会在其内部初始化一个隐式属性——
_proto_
,当我们访问一个对象的属性 时,如果这个对象内部不存在这个属性/方法,如f1
中内部不存在sayHello
,那么他就会去_proto_
里找这个属性/方法。_proto_
是一个指向其构造函数原型对象的指针,而原型对象也有自己的原型,通过各自的_proto_
一直指向各自的原型对象,直到某个对象的原型为null
,这种一级一级的链结构称为原型链。
他们的关系可以用下面这个图简单表示:
二、什么是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中调用父类的方法实际上是在不同的对象上调用同一个方法,即“方法借用”,这种行为实际上是委托调用。