继承一直是面向对象语言中的一个最为人津津乐道的概念,在JavaScript中,继承也是难点之一,下面我尽量用通俗的语言来介绍一下实现继承的几种方法。
原型链
ECMAScript 中描述了原型链的概念,并将原型链作为实现继承的主要方法。其基本思想是利用原型让一个引用类型继承另一个引用类型的属性和方法。这个基本思想说的一点也不基本,那么先说一个在之前博文中提到的概念,原型与实例的关系。我们知道:每一个实例里包含了原型对象中的方法和属性。这是因为任何一个对象都有一个内部属性[[prototype]](相当于ES6中的__proto__
属性),我们通常称之为原型,该原型指向的对象叫做原型对象,也就是说实例里的原型指向了该原型对象,因此获得了该原型对象的属性和方法。
下面通过例子来说明:
1
2
3
4
5
6
7
|
<code class = "language-function Student(grade){ hljs javascript" > this .grade = grade; } Student.prototype.showGrade = function(){ alert( "grade : " + this .grade); }; var student1 = new Student( 2 , "sean" , 22 ); student1.show();</code> |
我们创建一个Student对象,并创建一个Student的实例student1,这样实例student1中的原型指向Student.prototype,如下图:
这里Student的原型对象没有画出来,其实是指向了Object.prototype,所有的引用类型都继承了Object,不用显示的指明。
由上面的分析可知,实例都包含一个指向原型对象的内部指针,那么如果我们让A原型对象等于另一个类型的实例,那么另一个类型实例中的所有属性和方法也将存在这个A原型对象中。还是以例子来说明:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
|
<code class = "hljs javascript" >function Person(name, age){ this .name = name; this .age = age; } Person.prototype.showInfo = function(){ alert( "name:" + this .name+ "; age" + this .age); }; function Student(grade){ this .grade = grade; } //继承了Person Student.prototype = new Person( "sean" , 22 ); Student.prototype.showGrade = function(){ alert( "grade : " + this .grade); }; var student1 = new Student( 2 ); student1.showGrade(); student1.showInfo();</code> |
实现继承的本质是重写原型对象,代之以一个新类型的实例。让Student的原型对象由默认的Object.prototype改成Person的一个实例。换句话说,原来存在于 Person 的实例中的所有属性和方法,现在也存在于 SubType.prototype 中了。最终原型链的指向如下:
在上面的代码中,我们没有使用 Student默认提供的原型(默认原型对象为Object.prototype),而是给它换了一个新原型;这个新原型就是Person的实例。于是,新原型不仅具有作为一个Person的实例所拥有的全部属性和方法,而且其内部还有一个指针,指向了Person的原型。name和age都在Student.prototype中,这是因为 Student.prototype 现在是Person的实例,而name和age都是Person实例的属性,那么当然也就位于 Student.prototype 中。
这时我们再把构造函数(constructor)加进来:每个构造函数都有一个原型对象,原型对象都包含一个指向构造函数的指针。
需要注意的是:Student.constructor 现在指向的是 Person,这是因为原来 Student.prototype 中的 constructor 被重写了的缘故。准确地说:不是 Student 的原型的 constructor 属性被重写了,而是 Student 的原型指向了另一个对象,而这个原型对象的 constructor 属性指向的是 Person。
下面是上述例子完整的原型链(画得挺好的,它自己长歪了O(∩_∩)O~)
如何确定原型和实例的关系
我们可以通过两种方式来确定原型和实例的关系
第一种是使用 instanceof 操作符,只要用这个操作符来测试实例与原型链中出现过的构造函数,结果就会返回 true。
1
2
3
|
<code class = "hljs javascript" >alert(student1 instanceof Object); //true alert(student1 instanceof Person); //true alert(student1 instanceof Student); //true</code> |
我们可以说 instance 是 Object、 SuperType 或 SubType 中任何一个类型
的实例。
第二种是使用 isPrototypeOf()方法。同样,只要是原型链中出现过的原型,都可以说是该原型链所派生的实例的原型。
1
2
3
|
<code class = "hljs scss" >alert(Object.prototype.isPrototypeOf(student1)); //true alert(Person.prototype.isPrototypeOf(student1)); //true alert(Student.prototype.isPrototypeOf(student1)); //true</code> |
不能使用对象字面量创建原型方法
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
|
<code class = "hljs javascript" > function Person(name, age){ this .name = name; this .age = age; } Person.prototype.showInfo = function(){ alert( "name:" + this .name+ "; age" + this .age); }; var person1 = new Person( "sean" , 22 ); function Student(grade){ this .grade = grade; } //继承了Person Student.prototype = new Person( "sean" , 22 ); //使用字面量添加新方法,会导致上一行代码无效 Student.prototype = { showGrade : function (){ alert( "grade : " + this .grade); } }; var student1 = new Student( 2 , "sean" , 22 ); student1.showGrade(); //grade : 2 student1.showInfo(); // student1.showInfo is not a function</code> |
这是因为刚刚把 Person 的实例赋值给Student的原型,紧接着又将原型替换成一个对象字面量而导致的问题。由于现在的原型包含的是一个 Object 的实例,而非 Person 的实例,因此我们设想中的原型链已经被切断——Student 和 Person 之间已经没有关系了。
原型链存在的问题
通过上面的例子,我们可以发现Person实例中的name和age属性在Student.prototype中,而被所有的Student实例共享。基于上面情况,我们不能在不影响其他实例对象的情况下向Person中传递参数。
还是通过例子来说明;
1
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
|
<code class = "hljs javascript" > function Person(name, age){ this .name = name; this .age = age; this .friends = [ "lily" , "Tom" ]; } Person.prototype.showFriends = function(){ alert( this .friends); }; function Student(grade){ this .grade = grade; } Student.prototype = new Person( "sean" , 22 ); Student.prototype.showGrade = function (){ alert( "grade : " + this .grade); }; var student1 = new Student( 2 ); student1.showGrade(); //grade : 2 student1.showFriends(); // lily, Tom var student2 = new Student( 1 ); student2.friends.push( "Lucy" ); student1.showGrade(); //grade : 1 student1.showFriends(); // lily, Tom, Lucy student1.showFriends(); // lily, Tom, Lucy</code> |
上述代码中,friends属性是Person的实例属性,同时也是Student的原型属性,所以我们改变student2中的friend属性,student1中的属性也会跟着改变。
借用构造函数
借用构造函数(constructor stealing)的技术有时候也叫做伪造对象或经典继承。这种技术的基本思想相当简单,即在子类型构造函数的内部调用超类型构造函数。我们可以通过使用 apply()和 call()方法来改变函数的运行环境。
下面是通过apply()方法实现
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
|
<code class = "hljs javascript" >function Person(name, age){ this .name = name; this .age = age; this .showInfo = function(){ alert( "name:" + this .name+ "; age" + this .age); }; } function Student(grade){ //继承了Person Person.apply( this , [ "sean" , 22 ]); this .grade = grade; this .showGrade = function (){ alert( "grade : " + this .grade); }; } var student1 = new Student( 2 , "sean" , 22 ); student1.showGrade(); //grade : 2 student1.showInfo(); // name: sean; age:22</code> |
下面是使用call()方法
1
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
|
<code class = "hljs javascript" >function Person(name, age){ this .name = name; this .age = age; this .friends = [ "lily" , "Tom" ]; this .showInfo = function(){ alert( "name:" + this .name+ "; age:" + this .age + "; friends:" + this .friends); }; } function Student(grade,name, age){ //继承了Person Person.call( this , name, age); this .grade = grade; this .showGrade = function (){ alert( "grade : " + this .grade); }; } var student1 = new Student( 2 , "sean" , 22 ); student1.showGrade(); //grade : 2 student1.showInfo(); // name: sean; age:22; friends: lily, Tom var student2 = new Student( 1 , "jack" , 11 ); student2.friends.push( "Lucy" ); student2.showGrade(); //grade : 1 student2.showInfo(); // name: jack; age:11; friends: lily, Tom, Lucy student1.showInfo(); // name: jack; age:11; friends: lily, Tom</code> |
通过使用 call()方法(或 apply()方法),我们在 Student 的环境下调用了 Person 构造函数。这样一来,就会在新 Student 对象上执行 Person()函数中定义的所有对象初始化代码。结果,Student 的每个实例就都会具有自己的friends属性的副本了,因此,即使我们改变了student2的friends属性,也不会影响到student1中的friends属性。而且通过 call()方法(或 apply()方法),我们可以向Person 构造函数中传递属性值而不影响其他Student实例中的属性值。
借用构造函数实现的继承,通过上面两种方式来测试原型和实例的关系如下:
1
2
3
4
5
6
7
|
<code class = "hljs scss" > alert(student1 instanceof Object); //true alert(student1 instanceof Person); //false alert(student1 instanceof Student); //true alert(Object.prototype.isPrototypeOf(student1)); //true alert(Person.prototype.isPrototypeOf(student1)); //false alert(Student.prototype.isPrototypeOf(student1)); //true</code> |
说明借用构造函数实现的继承并没有改变子类的原型对象。
构造函数模式存在的问题同样也存在借用构造函数中。因为,属性和方法都在构造函数中定义,没创建一个子类,就会获得一个所有属性和方法的副本,这样很浪费资源,复用性较低。
组合继承
上面介绍的原型链和借用构造函数各有优缺点,那么如果把两者组合一下,取各自的优缺点,于是得到了组合继承(combination inheritance)。其思路是使用原型链实现对原型属性和方法的继承,而通过借用构造函数来实现对实例属性的继承。这样,既通过在原型上定义方法实现了函数复用,又能够保证每个实例都有它自己的属性。
1
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
|
<code class = "hljs javascript" > function Person(name, age){ this .name = name; this .age = age; this .friends = [ "lily" , "Tom" ]; } Person.prototype.showInfo = function(){ alert( "name:" + this .name+ "; age:" + this .age + "; friends:" + this .friends); }; function Student(grade,name, age){ Person.call( this , name, age); this .grade = grade; } Student.prototype = new Person(); Student.prototype.constructor = Student; Student.prototype.showGrade = function (){ alert( "grade : " + this .grade); }; var student1 = new Student( 2 , "sean" , 22 ); student1.showGrade(); //grade : 2 student1.showInfo(); // name: sean; age:22; friends: lily, Tom var student2 = new Student( 1 , "jack" , 11 ); student2.friends.push( "Lucy" ); student2.showGrade(); //grade : 1 student2.showInfo(); // name: jack; age:11; friends: lily, Tom, Lucy student1.showInfo(); // name: jack; age:11; friends: lily, Tom</code> |
在这个例子中,Person 构造函数定义了两个属性: name 、age 和 friends。 Person 的原型定义了一个方法 showInfo()。 Student 构造函数在调用 Person 构造函数时传入了 name 参数,紧接着又定义了它自己的属性 grade。然后,将 Person 的实例赋值给 Student 的原型,然后又在该新原型上定义了方法 showGrade。这样一来,就可以让两个不同的 Student 实例既分别拥有自己属性——包括 friends 属性,又可以使用相同的方法了。
组合继承避免了原型链和借用构造函数的缺陷,融合了它们的优点,成为 JavaScript 中最常用的继承模式。而且, instanceof 和 isPrototypeOf()也能够用于识别基于组合继承创建的对象。
1
2
3
4
5
6
7
|
<code class = "hljs scss" > alert(student1 instanceof Object); //true alert(student1 instanceof Person); //true alert(student1 instanceof Student); //true alert(Object.prototype.isPrototypeOf(student1)); //true alert(Person.prototype.isPrototypeOf(student1)); //true alert(Student.prototype.isPrototypeOf(student1)); //true</code> |
原型式继承
ECMAScript 5 通过新增 Object.create()方法规范化了原型式继承。Object.create(prototype, descriptors) :创建一个具有指定原型且可选择性地包含指定属性的对象。这个方法接收两个参数:一个用作新对象原型的对象和(可选的)一个为新对象定义额外属性的对象。
先看一个简单的例子
1
2
3
4
5
6
7
8
9
10
|
<code class = "hljs javascript" > var person = { say : function(){ alert( "person saying..." ); } }; // 指定新对象原型的对象 var o = Object.create(person); o.say(); // person saying... alert( 'say' in person); //true alert(o.hasOwnProperty( 'say' )); //false</code> |
上面代码中,指定了新对象o的原型的对象为person,因此虽然say不是对象o的方法。而是person中的方法,但是在对象o中也能访问到。
当然,我们也可以为新对象定义额外属性
1
2
3
4
5
6
7
8
9
10
11
12
13
14
|
<code class = "hljs javascript" > var person = { say : function(){ alert( "person saying..." ); } }; // 指定新对象原型的对象,并为其添加属性对象 var o = Object.create(person,{ name :{ value : "sean" } }); o.say(); // person saying... alert(o.name); //sean alert( 'name' in o); //true alert(o.hasOwnProperty( 'name' )); //true</code> |
上面代码在指定了新对象o的原型的对象时,还为新对象增加了name属性对象、这里需要指出的是:为新属性增加的属性都是属性对象。
支持 Object.create()方法的浏览器有 IE9+、 Firefox 4+、 Safari 5+、 Opera 12+和 Chrome。
在没有必要兴师动众地创建构造函数,而只想让一个对象与另一个对象保持类似的情况下,原型式继承是完全可以胜任的。不过需要注意的是,包含引用类型值的属性始终都会共享相应的值,就像使用原型模式一样。如下:
1
2
3
4
5
6
7
8
9
10
11
|
<code class = "hljs avrasm" >var person = { friends : [ "lily" , "Tom" ] }; var person1 = Object.create(person); person1.friends.push( "Lucy" ); alert(person1.friends); //lily, Tom, Lucy var person2 = Object.create(person); person2.friends.push( "jack" ); alert(person2.friends); //lily, Tom, Lucy, jack</code> |
另外,还有两种继承方式:
寄生式继承,与原型式继承非常相似,也是基于某个对象或某些信息创建一个对象,然后增强对象,最后返回对象。为了解决组合继承模式由于多次调用超类型构造函数而导致的低效率问题,可以将这个模式与组合继承一起使用。 寄生组合式继承,集寄生式继承和组合继承的优点与一身,是实现基于类型继承的最有效方式。感兴趣的读者可以去了解一下,这里我就不详细介绍了。