JS实现继承
对象的原型
查看
每一个函数对象内都有一个prototype属性,其值是一个对象,这个对象就是原型对象。
获取原型对象的方式有两种:
-
通过对象的
__proto__
属性可以获取到,但这个是浏览器早期自己添加的,存在兼容性问题。 -
通过
Object.getPrototypeOf()
方法可以获取到,但其也存在老版本浏览器的兼容性问题。
通过new操作符创建的对象,会执行如下几步操作:
-
创建一个空的对象。
-
将this指向创建的空对象。
-
将构造函数的prototype属性赋值给被创建对象。
-
执行函数内的代码。
-
若返回值不是对象,则将创建的对象返回。
所以对象内存在与其对应的构造函数的prototype的相同引用,我们称之为在对象中为隐式原型对象,在函数中称之为显式原型对象。原型对象也是一个对象,如果在原型对象中找不到,会继续向原型对象的原型对象中寻找,直到找到或null为止,此链式查找也称为[隐式]原型链查找。
JS中的对象都有一个特殊的内置属性[[Prototype]]
,其指向父类的原型对象,但该属性是不可调用的,因为它仅仅是一个标准,通过此属性,可以查看对象的原型链关系。
Constructor
查看
原型对象上存在属性constructor
,其指向当前的函数对象。
1 | function Foo() { |
上面我们说可以通过原型链查找属性,那么方法也是可以的,通过在原型对象添加方法可以减少相同操作代码的编写,提高代码的复用性。
语法:Foo.prototype.变量名 = function(){}
但当我们需要添加多个方式时,一直重复上述操作有些麻烦,所有,我们可以重写原型对象来简化操作。
1 | Foo.prototype = { |
ES5实现继承
查看
前提:定义一个Person对象和Student对象。目标:实现Student对象继承Person对象。
方法一:通过原型链实现继承
通过Person构造方法创建一个person实例对象,改变Student构造函数对象的prototype指向person实例对象。
1 | function Person() { |
弊端:
-
有些属性是保存在实例对象p上的,通过直接打印实例对象s1,s2,看不到name、hobbies属性。
-
虽然我们可以通过原型链访问到属性,但原型对象对于通过同一个构造函数创建的对象是共享的,意味如果修改原型对象上的引用属性,对其他的实例对象都会受影响。
-
我们之所以要实现继承,是为了复用代码,但以上的情况,我们无法给Person传递参数,导致创建的s1,s2实例对象没有自己的属性,即无法进行定制化操作。
方法二:借用构造函数实现继承
constructor stealing:借用构造函数/经典继承/伪造对象
在Student构造函数内调用Person的构造函数,并通过call/apply改变this指向,实现继承。
1 | function Person(name, hobbies) { |
弊端:
-
直接把Person的原型对象赋值给Student的原型对象,这样会导致通过Student对原型对象的修改会直接影响到Peroson的原型对象。
方法三:组合借用继承
即将方法一和方法二结合起来。
1 | function Person(name, hobbies) { |
通过这种方法来实现继承已经问题不到了,但仍有不完美的地方,比如:
-
Person构造函数会被调用两次。
- 第一次是在赋值给Student.prototype的时候。
- 第二次是借用构造函数的时候。
-
Student实例对象上和其原型对象上会存在相同的属性,尽管值可能不同,但好在优先在对象本身上进行查找。
以上代码我们发现,实例化p仅仅是为了将其原型对象关联到Student.prototype上,那么是不是只要是个对象,然后将其隐式原型对象指向Person的显示原型对象即可?
方法四:原型式继承函数
原型式继承是由从道格拉斯·克罗克福德(Douglas Crockford,著名的前端大师,JSON的创立者)在2006年写的一篇文章说起: Prototypal Inheritance in JavaScript(在JavaScript中使用原型式继承)。
其实就是上述提出的创建一个对象,作为继承连接的原型对象桥梁。
1 | function object(obj) { |
以上代码完全不必担心兼容性的问题,但我们也可以对其进行改造,原理是一样的,不过是调用了些API,可能会存在兼容性的问题。可以通过Object.setPrototypeOf(obj, prototype)
和Object.create(proto, propertiesObject)
来实现。
Object.setPrototypeOf(obj, prototype):将obj的原型指向prototype。参数:
obj: 需要修改原型的对象。
prototype: 需要指向的原型对象。
通过该API可以将以上代码修改为:
1 | function object(obj) { |
Object.create(proto, propertiesObject):创建一个对象,并将此对象的原型指向proto,并通过propertiesObject对此对象进行属性描述符操作,然后返回此对象。参数:
proto: 需要指向的原型对象
propertiesObject(可选): 需要添加的属性描述符对象。返回值:返回一个对象,返回对象的隐式原型对象指向传入的原型对象。
例子:
1 | var obj = Object.create(person, { |
通过该API可以将以上代码修改为
1 | function object(obj) { |
综上所看,通过原型式继承函数的代码可以为(最具兼容性的方式):
1 | function Person(name, hobbies) { |
方法五:寄生式继(Parasitic)承函数
什么是寄生式继承?
-
寄生式继承式是与原型式继承紧密相连的一种思想,同样由道格拉斯·克罗克福德提出和推广。
-
寄生式继承是结合原型式继承和工厂模式的一种方式
-
即创建一个封装继承过程的函数,该函数在内部以某种方式来增强对象,最后将这个对象返回。
核心步骤:
1 | function object(obj) { |
完整实现代码:
1 | function Person(name, hobbies) { |
以上代码进一步的细分,实现一种函数实现特定功能,并在特定的函数中在原型对象上添加属性和方法。
最终方案: 寄生组合继承
结合前几种的思想,结合成最终继承方案。
1 | function Person(name, hobbies) { |
总结
通过以上多种实现继承的方案来看,在ES5时期,并没有一个统一的标准,可谓是百花齐放。
总结一下,ES5实现继承的方法有:
-
通过原型链实现继承:通过创建父类实例作为原型对象将子类与父类进行连接。
缺陷:
-
能访问到执行父类代码创建到原型对象的属性,但其在原型对象上,直接打印子类实例对象是看不到的。
-
由于实现继承的目的是为了复用父类中的属性和方法,在此种情况下,无法定制化子类实例对象的属性和方法。即多个子类对象访问的属性都是相同的,且一旦存在属性为引用对象,则修改其中一个子类对象属性,则其他子类对象属性也会被修改。
-
-
借用构造函数实现继承:通过apply/call调用父类构造函数,将父类中的属性和方法添加到子类实例对象上。
缺陷:
-
此种方法是直接将子类的原型对象指向父类的原型对象,一旦通过子类操作原型对象会直接对父类原型对象造成影响。
-
-
组合借用继承:将前两种方法结合起来使用(已基本能够作为继承的方案)。
缺陷:
-
会调用两次父类构造函数,导致实例对象本身和其原型对象上存在相同的属性。
-
且保露父类实例化在外部不太美观。
-
-
原型式继承:由道格拉斯·克洛克福德提出,将一个普通函数的原型对象指向父类原型对象,然后将此普通函数实例化返回作为子类原型对象。且可以通过新的API对其进行改造,如
Object.create(proto, propertiesObject)
和Object.setPrototypeOf(obj, prototype)
,但有一定的兼容性问题。 -
寄生式继承:由道格拉斯·克洛克福德提出,在原型式继承的基础上,将为中间原型对象添加方法的操作封装到函数中,然后返回此原型对象作为子类原型对象。
-
寄生组合继承:总结以上方法的优点,借用构造函数实现子类的定制化,通过原型式继承函数,实现子类原型对象函数的创建,通过寄生式继承函数,在函数内通过原型继承实现子类原型对象的绑定,并将原型对象的constructor指向子类。
对象方法补充
查看
-
Object.prototype.hasOwnProperty()
:判断对象是否是自身属性,而不是原型对象上的属性。 -
in/for in 操作符:判断某个属性是否在某个对象或对象的原型上。
name in obj / for (var item in obj)
-
instanceof: 用于检测构造函数的prototype是否出现在某个实例对象的原型链上。
-
isPrototypeOf: 用于检测某个对象是否出现在某个实例对象的原型链上。
1 | var obj = {} |
手动实现instanceOf
1 | function instanceof(o, c) { |
原型继承关系图

ES6实现继承
查看
ES6新增了class
关键字,用于创建类,通过类创建实例对象。
通过class创建对象的写法:
1 | class Person {} |
class与class通过extends实现继承。
1 | class Person {} |
class是个语法糖,本质上和构造函数是没有区别的,特别的是class定义的类是不能向构造函数一样调用的,内部做了调用判断。
而class的继承和ES5的寄生组合继承在核心上是一致的,只不过class加了更多的边界判断,且将子类的隐式原型指向父类,以便可以通过子类调用父类的类方法。在ES5可以通过Student.__proto__ = Person
或Object.setPrototypeOf(Student, Person)
实现。
额外知识点补充
查看
super关键字
Class中可以使用super关键字,通过super可以调用父类方法(实例方法和静态方法)和父类构造函数。
1 | class Person { |
注意:在子(派生)类的构造函数中使用this或返回默认对象之前,必须先通过super调用父类构造函数。 即如果在类中定义了constructor,必须在子类constructor中头行调用super,否则会报错。
类的混入
背景:
JS的类只支持单继承,如果在开发中我们需要在一个类中添加更多相似的功能,可以通过类的混入实现。
实现代码如下:
1 | function mixinRunner(BaseClass) { |
其实就是创建一个类继承需要扩展功能的类,如果有需求,再创建一个类继承前面创建的类,以此类推,知道所有需要扩展的功能都添加完,再交由其他类继承。
只不过上述代码对继承过程进行了封装然后将已继承的匿名类返回,一种功能封装一个函数,使代码结构更加清晰,增加了代码的可维护性,实现了代码复用,降低了耦合度,有利于模块化,便于管理和扩展等。
JS中的多态
面向对象的三大特征:封装、继承、多态。
对于JS来说,由于其的灵活性,其是否存在多态?
对于多态的定义:多态指为不同数据类型的实体提供统一的接口,或使用一个单一的符号来表示多个不同的数据类型。
从此定义来看,可以说JS中处处存在多态。
以如下代码为例:
1 | function add(arg1, arg2) { |
对于上述函数,其参数可以是任意类型,所以可以认为其存在多态。
对于单一的符号,通过var声明的变量,可以为其进行任意类型的复制,所以也可以认为其存在多态。