本文由笔者师妹LazyCurry创做,收录于笔者技术文章专栏下面试
在JS中,咱们常常会遇到原型。字面上的意思会让咱们认为,是某个对象的原型,可用来继承。可是其实这样的理解是片面的,下面经过本文来了解原型与原型链的细节,再顺便谈谈继承的几种方式。数组
在讲到原型以前,咱们先来回顾一下JS中的对象。在JS中,万物皆对象,就像字符串、数值、布尔、数组
等。ECMA-262把对象定义为:无序属性的集合,其属性可包含基本值、对象或函数。对象是拥有属性和方法的数据,为了描述这些事物,便有了原型的概念。函数
不管什么时候,只要建立了一个新函数,就会根据一组特定的规则为该函数建立一个prototype属性,这个属性指向该函数的原型对象。全部原型对象都会得到一个constructor属性,这个属性包含一个指向prototype属性所在函数的指针。学习
这段话摘自《JS高级程序设计》
,很好理解,以建立实例的代码为例。this
function Person(name, age) { this.name = name; this.age = age; this.sayName = function() { alert(this.name); }; } const person1 = new Person("gali", 18); const person2 = new Person("pig", 20);
上面例子中的person1跟person2都是构造函数Person()
的实例,Person.prototype指向了Person函数的原型对象,而Person.prototype.constructor又指向Person。Person的每个实例,都含有一个内部属性__proto__
,指向Person.prototype,就像上图所示,所以就有下面的关系。spa
console.log(Person.prototype.constructor === Person); // true console.log(person1.__proto__ === Person.prototype); // true console.log(person2.__proto__ === Person.prototype); // true
JS是基于原型的语言,跟基于类的面向对象语言有所不一样,JS中并无类这个概念,有的是原型对象这个概念,原型对象做为一个模板,新对象可从原型对象中得到属性。那么JS具体是怎样继承的呢?prototype
在讲到继承这个话题以前,咱们先来理解原型链这个概念。设计
原型链
构造函数,原型和实例的关系已经很清楚了。每一个构造函数都有一个原型对象,原型对象都包含一个指向构造函数的指针,而实例对象都包含一个指向与原型对象的指针。这样的关系很是好理解,可是若是咱们想让原型对象等于另外一个类型的实例对象呢?那么就会衍生出相同的关系,此时的原型对象就会含有一个指向另外一个原型对象的指针,而另外一个原型对象会含有一个指向另外一个构造函数的指针。若是另外一个原型对象又是另外一个类型的实例对象呢?这样就构成了原型链。文字可能有点难理解,下面用代码举例。3d
function SuperType() { this.name = "张三"; } SuperType.prototype.getSuperName = function() { return this.name; }; function SubType() { this.subname = "李四"; } SubType.prototype = new SuperType(); SubType.prototype.getSubName = function() { return this.subname; }; const instance = new SubType(); console.log(instance.getSuperName()); // 张三
上述例子中,SubType的原型对象做为SuperType构造函数
的实例对象,此时,SubType的原型对象
就会有一个__proto__
属性指向SuperType的原型对象
,instance做为SubType的实例对象,必然能共享SubType的原型对象的属性,又由于SubType的原型对象
又指向SuperType原型对象
的属性,所以可得,instance继承了SuperType原型的全部属性。指针
咱们都知道,全部函数的默认原型都是Object的实例,因此也能得出,SuperType的默认原型必然有一个__proto__
指向Object.prototype。
图中由__proto__
属性组成的链子,就是原型链,原型链的终点就是null。
上图可很清晰的看出原型链的结构,这不由让我想到JS的一个运算符instanceof,instanceof可用来判断一个实例对象是否属于一个构造函数。
A instanceof B; // true
实现原理其实就是在A的原型链上寻找是否有原型等于B.prototype,若是一直找到A原型链的顶端null,仍然找不到原型等于B.prototype,那么就可返回false。下面手写一个instanceof
,这个也是不少大厂经常使用的手写面试题。
function Instance(left, right) { left = left.__proto__; right = right.prototype; while (true) { if (left === null) return false; if (left === right) return true; // 继续在left的原型链向上找 left = left.__propo__; } }
原型链继承
上面例子中,instance继承了SuperType原型的属性,其继承的原理其实就是经过原型链实现的。原型链很强大,可用来实现继承。但是单纯的原型链继承也是有问题存在的。
function SuperType() { this.colorArr = ["red", "blue", "green"]; } function SubType() {} SubType.prototype = new SuperType(); const instance1 = new SubType(); instance1.colorArr.push("black"); console.log(instance1.colorArr); // ["red", "blue", "green", "black"] const instance2 = new SubType(); console.log(instance2.colorArr); // ["red", "blue", "green", "black"]
当SubType的原型做为SuperType的实例时,此时SubType的实例对象经过原型链继承到colorArr属性,当修改了其中一个实例对象从原型链中继承到的原型属性时,便会影响到其余实例。对instance1.colorArr的修改,在instance2.colorArr便能体现出来。
组合继承
组合继承指的是组合原型链和构造函数的技术
,经过原型链实现对原型属性和方法的继承,而经过借用构造函数实现对实例属性的继承。
function SuperType(name) { this.name = name; this.colors = ["red", "blue", "green"]; } SuperType.prototype.sayName = function() { console.log(this.name); }; function SubType(name, age) { // 继承属性,借用构造函数实现对实例属性的继承 SuperType.call(this, name); this.age = age; } // 继承原型属性及方法 SubType.prototype = new SuperType(); SubType.prototype.constructor = SubType; SubType.prototype.sayAge = function() { console.log(this.age); }; const instance1 = new SubType("gali", 18); instance1.colors.push("black"); console.log(instance1.colors); // ["red", "blue", "green", "black"] instance1.sayName(); // gali instance1.sayAge(); // 18 const instance2 = new SubType("pig", 20); console.log(instance2.colors); // ["red", "blue", "green"] instance2.sayName(); // pig instance2.sayAge(); // 20
上述例子中,借用构造函数继承实例属性,经过原型继承原型属性与方法。这样就可以让不一样的实例分别拥有本身的属性,又可共享相同的方法。而不会像原型继承那样,对实例属性的修改影响到了其余实例。组合继承是JS最经常使用的继承方式。
寄生组合式继承
虽说组合继承是最经常使用的继承方式,可是有没有发现,就上面的例子中,组合继承中调用了2次SuperType函数。回忆一下,在第一次调用SubType时。
SubType.prototype = new SuperType();
这里调用完以后,SubType.prototype会从SuperType继承到2个属性:name和colors。这2个属性存在SubType的原型中。而在第二次调用时,就是在创造实例对象时,调用了SubType构造函数,也就会再调用一次SuperType构造函数。
SuperType.call(this, name);
第二次调用以后,便会在新的实例对象上建立了实例属性:name和colors。也就是说,这个时候,实例对象跟原型对象拥有2个同名属性。这样实在是浪费,效率又低。
为了解决这个问题,引入了寄生组合继承方式。重点就在于,不须要为了定义SubType的原型而去调用SuperType构造函数,此时只须要SuperType原型的一个副本,并将其赋值给SubType的原型便可。
function InheritPrototype(subType, superType) { // 建立超类型原型的一个副本 const prototype = Object(superType.prototype); // 添加constructor属性,由于重写原型会失去constructor属性 prototype.constructor = subType; subType.prototype = prototype; }
将组合继承中的:
SubType.prototype = new SuperType(); SubType.prototype.constructor = SubType;
替换成:
InheritPrototype(SubType, SuperType);
寄生组合继承的优势在于,只须要调用一次SuperType构造函数。避免了在SubType的原型上建立多余的没必要要的属性。
温故而知新,再次看回《JS高级程序设计》
这本书的原型与原型链部分,发现不少之前忽略掉的知识点。而此次回看这个知识点,并输出了一篇文章,对我来讲受益不浅。写文章每每不是为了写出怎样的文章,其实中间学习的过程才是最享受的。