几乎全部语言都有面向对象的概念,JavaScript 的面向对象实质是基于原型的对象系统。说到面向对象,不得不提的就是继承。数组
一个没有其余语言经验的人要更容易理解 JavaScript 的继承。
不一样于其余语言的类的继承,JavaScript 中使用的是原型继承,不过从表面上看更像是基于类的继承,缘由多是由于 new 关键字的使用。new 关键字是用来调用构造函数的,一个函数之因此称为构造函数,并非由于函数自己有什么特性,而是由于 new 。也就是说只有经过 new 调用的函数才可能成为构造函数。
new 既然这么神奇,有必要探究下内部到底实现了什么。app
下面是简易的实现代码:函数
function myNew(constructor, param) { // constructor 就是构造函数,param 模拟构造函数的参数,这里只用一个参数举例 // 建立一个空对象,且这个空对象继承构造函数的 prototype 属性 const obj = Object.create(constructor.prototype) // 将构造函数的 this 指向 obj,执行构造函数获得返回结果 const result = constructor.apply(obj, param) // 若是造函数执行后,返回结果是对象类型,就直接返回,不然返回 obj 对象 return (typeof result === 'object' && result != null) ? result : obj } function Animal(species) { this.species = species } const cat = myNew(Animal, '猫科动物') console.log(cat) // {species: "猫科动物"}
结合上面 new 实现的三步,再看代码就比较容易理解了。
须要注意的就是,若是构造函数有显式返回值,而且返回值类型为对象。那么构造函数返回的结果再也不是目标实例,而是这个显式的返回值。this
JavaScript 实现继承的方式有多种,各有优缺点,下面就将常见的几种方式一一列出。prototype
1、构造函数的继承方式code
function Animal(species) { this.species = species || "动物" } function Cat(name, color, species) { Animal.call(this, species) this.name = name this.color = color } Animal.prototype.age = 10 var cat1 = new Cat("毛毛", "黑色", "猫科动物") console.log(cat1.species) // 猫科动物 console.log(cat1.age) // undefined
这种继承的实现方式是在子类构造函数中执行父类构造函数,并将父类构造函数的this指向子类的实例。优势简单易懂,可是缺点也很明显。
==缺点==:没法继承父类原型上的属性和方法。orm
2、原型链继承模式对象
function Animal() { this.species = "动物" this.list = [1, 2, 3] } function Cat(name, color) { this.name = name this.color = color } // 将Cat的prototype对象指向一个Animal的实例 // 它至关于彻底覆盖了 prototype 对象原先的值。 Cat.prototype = new Animal() // 本行下面会有详细解释 Cat.prototype.constructor = Cat var cat1 = new Cat("大黄", "黄色") var cat2 = new Cat("小黄", "黑色") //这种方式实现的继承,不一样实例的原型对象都指向同一个Animal的实例,访问属性的时候,若是实例内没有该属性,就会向上找到Cat.prototype(Animal的一个实例)中。 //可是这里调用cat1.species去赋值,不会向上寻找,而只是在cat1实例中添加一个species属性,并不会影响cat1原型对象(Animal实例)中的属性,所以cat2.species的值没有变化,这种方式并不能说明问题,看下面的代码 cat1.species = "猫科动物" console.log(cat1.species) // 猫科动物 console.log(cat2.species) // 动物 // 调用数组的push方法,就会顺着原型链搜索,找到原型对象中的list并修改值,上面说了不一样实例的原型对象都指向同一个Animal的实例,因此 cat2.list 读取到的值也是改变后的。 cat1.list.push(4) console.log(cat1.list) // [1,2,3,4] console.log(cat2.list) // [1,2,3,4]
关于Cat.prototype.constructor = Cat
,是给Cat构造函数的原型对象上的constructor属性从新赋值。
由于任何一个prototype对象都有一个constructor属性,指向它的构造函数,若是没有Cat.prototype = new Animal()
时,Cat.prototype.constructor
是指向Cat
的,可是执行了这句代码后Cat.prototype.constructor
指向Animal
。
至关于继承
Cat.prototype.constructor === Animal //true
而且,构造函数创造出的每个实例也有一个constructor
属性,读取的是构造函数的prototype
对象的constructor
属性。
至关于ip
cat1.constructor === Cat.prototype.constructor // true
所以,cat1.constructor也指向Animal
cat1.constructor === Animal // true
这样致使的结果是继承关系混乱
手动修改了constructor,虽然解决了这个关系混乱的问题,可是代码中也能够看到这种实现方式也是有缺点的。
==优势==:
实例是子类实例,同时也是父类的实例;
实例能够访问到父类新增的原型属性和原型方法;
子类原型共享父类原型,父类原型不共享子类原型;
==缺点==:
继承的实例属性,全部子类共享同一个父类实例的实例属性;
没法向父类构造函数传参;
3、原型链继承改版,直接继承prototype
基于第二种原型链方式的改进,想要解决以前方式的缺点。
function Animal() { this.age = 10 } function Cat() {} Animal.prototype.species = "动物" Cat.prototype = Animal.prototype Cat.prototype.constructor = Cat var cat1 = new Cat() console.log(cat1.species) // 动物 console.log(cat1.age) // undefined Cat.prototype.gender = "formall" var a = new Animal() console.log(a.gender) // formall
这种方式跳过new Animal()
直接继承Animal.prototype
。想象的是不共享同一个父类实例属性,可是又致使一个问题,Cat.prototype
和Animal.prototype
如今指向了同一个对象,那么任何对Cat.prototype
的修改,都会体现到Animal.prototype
上。同时子类实例没法访问父类实例属性。
==缺点==:
子类父类共享原型对象;
没法继承父类实例属性;
4、原型链继承改版,利用空对象
先来解决子类父类共享原型对象的问题,实用的办法是建立一个中间对象。
function Animal() { this.age = 10 } function Cat() { } var F = function () { } F.prototype = Animal.prototype Cat.prototype = new F() Cat.prototype.constructor = Cat Animal.prototype.species = "动物" Cat.prototype.gender = "formall" var cat1 = new Cat() var a = new Animal() console.log(cat1.species) // 动物 console.log(cat1.age) // undefined console.log(a.gender) // undefined
显然这种方式解决了子父类共享原型对象的问题,可是没法继承父类实例属性的问题还在。依然不能访问父类的实例属性。
==优势==:
子类添加原型属性,父类不会更新;
==缺点==:
没法继承父类实例属性;
5、组合继承(构造函数+原型链)
实现了这么多种继承,可是每种都有缺点不足。能不能去其糟粕取其精华呢?实现一个较优的继承方式。
function Animal(species, age) { this.species = species || "动物" this.age = age || 10 this.list = [1,2,3] } function Cat() { Animal.call(this) } Cat.prototype = Object.create(Animal.prototype) Cat.prototype.constructor = Cat var c1 = new Cat('cat1') var c2 = new Cat('cat2') var a1 = new Animal('ani',10) // 验证子父类原型对象共享问题 Animal.prototype.area = "Asia" Cat.prototype.gender = "formall" console.log(c1.area) // Asia console.log(a1.gender) // undeifined // 验证没法访问父类实例属性问题 console.log(c1.species) // 动物 // 验证不一样实例共享父类实例属性问题 c1.list.push(4) console.log(c1.list) // [1,2,3,4] console.log(c2.list) // [1,2,3]
其实继承还有不少种实现方式,就不一一举例了。并无最好的方式,不一样的实现有各自的优缺点,找到最适合的就是最好的。