对于科班出身的同窗来说,绝大多数应该是从 过程化编程
起步,这种风格的代码之包含了过程(函数)调用,没有对基层进行抽象(面条式代码)。javascript
后来咱们开始接触到面向对象编程,进而又跟另一个被称为 类
的术语扯上了关系,或者能够说是面向类式编程。前端
在后来随着编程的深刻咱们开始接触到 函数式编程
,这也是一种编程的选择或者习惯。java
但咱们这次只讨论一下关于类的那些事儿。说到类,咱们便会想起三大基本特性 封装
、继承
、多态
,而咱们这次的主题即是 继承
。编程
相较于传统语言,JavaScript 中一直在模仿类行为。直到 ES6 版本出来后才出现了一些近似类的功能,如 class
、extends
、super
。可是这并不表明 JavaScript 实现了像传统语言同样的类,JavaScript 的核心机制是[[prototype]]
,而且只有对象,对象只负责定义自身的行为。像这些新定义的语法只是在原型链的基础上进行的封装(语法糖),所以搞懂[[prototype]]
才是关键。app
说了一些体外话,而后进入主题。咱们先来抛出两个小问题:函数式编程
接下来咱们一个个的讲。函数
先看一下例子:性能
function Animal() { this.categories = ['二哈', '英短', '龙猫']; } Animal.prototype.category = function() { console.log(this.categories); } var animal = function () {} animal.prototype = new Animal(); var animal1 = new animal(); animal1.category(); // ['二哈', '英短', '龙猫'];
这是最基本的类式继承,经过使用父级的构造函数调用来为 animal.prototype
进行关联。咱们先来讲一下使用构造函数调用(new)时会自动执行的一些状况:this
[[Prototype]]
关联,也就是说这个对新象会关联到animal.prototype
对象上。this
,也就是说此时的this
指的是 animal
。new
关键字调用函数后会返回这个新对象(也就是 animal{}
)。关于this
更细致的讨论能够参见js之thisprototype
因此当咱们执行 animal1.category()
操做的时候,由于 [[Get]]
操做的默认行为会检查原型链,animal1
自身没有 categories
属性因此会到自身原型链查找,因为new Animal()
操做返回的对象与Animal.prototype
自动关联而且animal.prototype
还保存着 Animal.prototype
引用,所以animal1
即可以顺利的访问到Animal
原型链及自身的属性。
咱们再来看一下例子:
function Animal() { this.categories = ['二哈', '英短', '龙猫']; } var animal = function () {} animal.prototype = new Animal(); var animal1 = new animal(); var animal2 = new animal(); console.log( animal1.categories); // ["二哈", "英短", "龙猫"] animal1.categories.push('柯基'); console.log( animal2.categories); // ["二哈", "英短", "龙猫", '柯基']
经过这个例子就能够很明显的看出使用类式继承的问题:
this
添加引用类型对象,当这个对象被更改时,全部子级都会受到牵连。针对于这些问题咱们引出了另一种继承。
function Animal(name) { this.name = name; this.features = ['装傻卖萌', '好吃懒作']; } Animal.prototype.sleep = function() { console.log(this.name + '正在睡觉'); } function Dog(name, voice) { Animal.call(this, name); this.voice = voice; } var dog1 = new Dog('二哈', '汪汪。。'); var dog2 = new Dog('柯基', '汪汪。。'); dog1.features.push('拆家小分队'); console.log(dog1.features); // ["装傻卖萌", "好吃懒作", "拆家小分队"] console.log(dog2.features); // ["装傻卖萌", "好吃懒作"] console.log(dog1.sleep()); // TypeError: sleep is not a function
前面咱们有讲到经过构造函数调用的时候发生的状况,因为未执行原型链的关联,因此当执行完构造函数调用以后自动将 this
关联到 Dog
并为其添加属性。这段代码的核心是 Animal.call(this, name)
,这里经过显示绑定将 Animal
中的属性从新添加到 Dog
对象中。
提醒:Animal.call
和Animal.apply
用法相同,都会更改当前执行上下文环境的this,这种方式称为this
显示绑定。还有一种被称为应绑定的方法:bind
,一样会更改执行上下文环境的this,但bind
会返回执行函数的一个副本。
那既然这两种都不能实现一个完整的继承过程,咱们能够结合一下这两种思想,使用构造函数将父级的公有属性与子级的公有属性进行合并,同时要将父级原型链上属性也进行合并(注意子级自已的公有属性要后执行)。注意:不要直接执行父级的构造函数调用,由于使用 call
已经执行了调用了构造函数。再使用 new
操做至关于执行了两遍重复的操做。
最先提出的这一方式的是美国的道格拉斯·克罗克福德(Douglas Crockford),世界著名的前端大师,同时也是JSON
的创立者。他提出的这个方案:
function inheritObject(proto) { function F() {}; F.prototype = proto; return new F(); }
这段代码使用了一个一次性函数,经过改写它的 .prototy
将它指向想要关联的对象,而后再使用 new
操做构造一个新对象进行关联。
function Animal(name) { this.name = name; } Animal.prototype.sleep = function() { console.log(this.name + '正在睡觉。'); } function Dog(name, voice) { Animal.call(this, name); this.voice = voice; } Dog.prototype = inheritObject(Animal.prototype); Dog.prototype.yell = function() { console.log(this.name + ': ' + this.voice); } var dog = new Dog('二哈', '汪汪。。'); dog.sleep(); // 二哈正在睡觉。 dog.yell(); // 二哈: 汪汪。。
须要注意一点:通过 inheritObject
后已经没有 Dog.prototype.constructor
属性了,由于Dog.prototype
指向的是 Animal.prototype
,因此若是还须要这个属性,须要手动修复它:
Dog.prototype.constructor = Dog
所以便出现了更加理想的继承方式:
function inheritPrototype(subClass, superClass) { var f = inheritObject(superClass.prototype); f.constructor = subClass; subClass.prototype = f; } function Animal(name) { this.name = name; } Animal.prototype.sleep = function() { console.log(this.name + '正在睡觉。'); } function Dog(name, voice) { Animal.call(this, name); this.voice = voice; } // 不考虑 construstor 指向的时候: // Dog.prototype = inheritObject(Animal.prototype); // 或者 Dog.prototype = Object.create(Animal.prototype) // 考虑 construstor 指向的时候: // 使用Object.create后手动修复construstor: Dog.prototype.constructor = Animal inheritPrototype(Dog, Animal); // 或者 Object.setPrototypeOf(Dog.prototype, Animal.prototype) Dog.prototype.yell = function() { console.log(this.name + '饿了: ' + this.voice); } var dog = new Dog('二哈', '汪汪。。'); dog.sleep(); // 二哈正在睡觉。 dog.yell(); // 二哈饿了: 汪汪。。
随着这种方式的深刻,后来ES5便出现了 Object.create
这个方法,固然这个方法内部还有不少附加功能,可是核心倒是如此。可是这样会致使 constructor
指向错误,进而咱们引出了 inheritPrototype()
方法修复其 constructor
指向的问题。一样,ES6以后出现了Object.setPrototypeOf(subProto, superProto)
,这个方法实际上跟咱们本身写的 inheritPrototype()
是相似的。
若是不考虑constructor
指向错误问题及轻微性能损失(被丢弃F对象会在适当时机被GC回收掉),使用Object.create
是彻底没问题的。此外,
Obeject.create
会建立一个拥有空原型连的对象,这个对象没有原型链,没法进行进行委托。这种特殊的空对象特别适合作为字典
结构来存储数据。所以,该对象没法使用instanceof
关键字,而且在使用for..in
遍历对象的时候,使用Object.prototype.hasOwnProperty.call
来避免类型错误。
对于多继承来说,咱们能够换个思路。咱们刚刚将到,单一方式最完善的继承是寄生组合式,其实多继承彻底能够照这个思路将多个类的公有属性经过 call
或着 apply
的从新绑定功能将属性拷贝到自身,而对于原型链上的属性,则可使用原型继承(须要将其它类的原型进行混入
)。
function SuperClass() { this.name = "SuperClass" } SuperClass.prototype.superMethod = function () { // .. } function OtherSuperClass() { this.otherName = "OtherSuperClass" } OtherSuperClass.prototype.otherSuperMethod = function () { // .. } function MyClass() { SuperClass.call(this); OtherSuperClass.call(this); } // 混入原型对象 Object.setPrototypeOf(SuperClass.prototype, OtherSuperClass.prototype) Object.setPrototypeOf(MyClass.prototype, SuperClass.prototype) MyClass.prototype.myMethod = function() { console.log('myMethod') }; var myClass = new MyClass(); console.log(myClass); // MyClass {name: "SuperClass", otherName: "OtherSuperClass"}