咱们知道C++/Java/C#等面向对象语言,都原生地支持类的继承。继承的核心做用大抵是建立一个派生类,并使其复用基本类(即父类)的字段和/或方法。而且派生类能够重写基本类的方法。这样基本类和派生类相同签名的方法在被调用时,就会有不一样的行为表现,即为多态的实质。换句话说,多态是透过继承和重写实现的。javascript
举例:实现不一样的动物叫声不一样。
过程式编程(Java代码):java
void animalSpeak(String animal) { if(animal == "Dog") { System.out.println("汪汪"); } else if(animal == "Cat") { System.out.println("喵喵"); } else { System.out.println("动物发声"); } } //调用代码 animalSpeak("Dog"); //汪汪 animalSpeak("Cat"); //喵喵 animalSpeak(""); //动物发声
这里一个问题是,若是增长一种动物,就要在speak方法增长if分支,此方法逐渐变得臃肿难维护。偶尔因增长一种动物,会偶尔误伤其余动物的逻辑,也何尝可知。面向对象式的动态应运而生。程序员
面向对象实现(java代码)编程
class Animal { void speak() { System.out.println("动物发声:"); } } class Dog extends Animal { void speak() { super.speak(); System.out.println("汪汪"); } } class Cat extends Animal { void speak() { super.speak(); System.out.println("喵喵"); } } void animalSpeak(Animal animal) { animal.speak(); } //调用代码 animalSpeak(new Cat()); //动物发声: //喵喵 animalSpeak(new Dog()); //动物发声: //汪汪
当要增长一种动物时,只需增长一个class继承 Animal,不会影响其余已有的动物speak逻辑。可看出,面向对象多态编程的一个核心思想是便于扩展和维护函数
结语:面向对象编程以继承产生派生类和重写派生类的方法,实现多态编程。核心思想是便于扩展和维护代码,也避免if-elsethis
Java继承是class的继承,而JavaScript的继承通常是经过原型(prototype)实现。prototype的本质是一个Object实例,它是在同一类型的多个实例之间共享的,它里面包含的是须要共享的方法(也能够有字段)。
JavaScript版原型继承的实现:prototype
function Animal() { } Animal.prototype.speak = function () { console.log('动物发声:'); } function Dog(name) { this.name = name; } Dog.prototype = Object.create(Animal.prototype); Dog.prototype.constructor = Dog; Dog.prototype.speak = function () { //经过原型链找‘基本类’原型里的同名方法 this.__proto__.__proto__.speak.call(this); console.log('汪汪, 我是', this.name); } function Cat(name) { this.name = name; } Cat.prototype = Object.create(Animal.prototype); Cat.prototype.constructor = Cat; Cat.prototype.speak = function () { //经过原型链找‘基本类’原型里的同名方法 this.__proto__.__proto__.speak.call(this); console.log('喵喵, 我是', this.name); } //调用代码 function animalSpeak(animal) { animal.speak(); } animalSpeak(new Dog('大黄')) console.log() animalSpeak(new Cat('小喵')) //动物发声: //汪汪, 我是 大黄 //动物发声: //喵喵, 我是 小喵
传统面向对象语言的class继承是为代码(方法和字段)复用,而JavaScript的prototype是在同类型实例之间共享的对象,它包含共享的方法(也可有字段)。因此java的class继承和javascript的原型继承,可谓异曲同工。为了方便理解js原型,提出两个概念:原型的design-time和run-time设计
可理解为咱们(程序员)如何设计js类型的自上而下的继承关系。以上例看出,design-time是经过prototype赋值实现。code
// Dog类型继承自Animal Dog.prototype = Object.create(Animal.prototype); Dog.prototype.constructor = Dog;
Run-time可理解为,原型设计好以后,要建立实例了, 而且还能向上查找其继承自哪一个原型对象
// 建立Dog类型的一个实例 var dog = new Dog('大黄'); // ***打起精神*** 这里到了关键的地方:如何查找dog建立自哪一个类型: // 显而易见, dog是由Dog创造出来的 dog.__proto__ // Dog { constructor: [Function: Dog], speak: [Function] } // 再往上查找原型链: // 看出来是继承了Animal的原型 dog.__proto__.__proto__ //Animal { speak: [Function] } // 再往上查找原型链: // 看出来是继承了Object的原型 dog.__proto__.__proto__.__proto__ // {} // 再往上查找原型链: // 到了原型链的顶端。 dog.__proto__.__proto__.__proto__.__proto__ // null
因此在调用实例的方法,它会在原型链上自下而上,直到找到该方法。若是到了原型链顶端尚未找到,就抛错了。
结语: design-time原型是经过prototype赋值,设计好自上而下的继承关系; run-time时经过实例的__proto__,自下而上在原型链中查找须要的方法。
如前文述prototype可当作design-time的概念,以此prototype创造出一个实例。
var dog = new Dog('大黄'); //实质是: //new 是便利构造方法 var dog = Object.create(Dog.prototype); dog.name = '大黄'
__proto__是属于实例的,可反查实例出自哪一个prototype。 因此dog.__proto__显然等于Dog.prototype.
dog.__proto__ == Dog.prototype //true
而Dog.prototype创自于 Animal.prototype:
Dog.prototype = Object.create(Animal.prototype);
因此Dog.prototype的__proto__即为Animal.prototype
Dog.prototype.__proto__ == Animal.prototype //true dog.__proto__.__proto__ == Animal.prototype //true
这样就实现了run-time原型链自下而上的查找
原型继承是JS老生常谈的话题,也是很重要但不易深刻理解的技术点。本文里提出了design-time原型设计和run-time原型链查找,但愿有助于此技术点的理解。
上文是对自定义函数类型实例的原型分析,漏掉了对JS内置类型的原型分析。你们知道JS内置类型是有:
undefined null bool number string object Function Date Error symbol (ES6)
基本类型有undefined, null, bool, number, string和symbol. 基本类型是以字面量赋值或函数式赋值的,而非经过new或Object.create(...)出来。
基本类型是没有design-time的prototype。但undefined/null以外的基本练习,仍是有run-time的__proto__
// 经常使用基本类型的字面量赋值: var b = true; var n = 1; var s = 'str'; // 经常使用基本类型的run-time __proto__ // 须要说明的是,基本类型自己是没有任何方法和字段的。 // 例如undefined/null, 不能调用其任何方法和字段。 // 这里调用b.__proto__时,会临时生成一个基本类型包装类的实例, // 即生成var tmp = new Boolean(b)。这是个object实例,返回__proto__ b.__proto__ // [Boolean: false] n.__proto__ // [Number: 0] s.__proto__ // [String: ''] // 以上 *.__proto__ 打印出的是,字面量值出自哪一个函数, 因此亦能够函数方式赋值, 跟字面量彻底等价。 // 基本类型的函数式赋值: // *注意*的是,这里仅是调用函数,没有new。若用了new, 就构造出一个对象实例,而再非基本类型了 b = Boolean(true) n = Number(1) s = String('') sym = Symbol('IBM') // ES6 新增的symbol没有字面量赋值方式 // 特殊的case是undefined和null, 只有字面量赋值(没函数方式赋值) // null 和 undefined是没有__proto__的。 // 也能够理解为,null处于任何原型链的最顶端,这是由于null是object类型(typeof null == 'object')。undefined不是object类型。 var nn = null; var un = undefined;
若是经过new 构造基本类型的对象实例,那么就是对象而非原生态基本类型了。
var b = new Boolean(true) var n = new Number(1) var s = new String('')
它们就遵循自定义函数类型对象的原型法则了。以上述Number n为例:
// Number的 design-time的prototype: Number.prototype //[Number: 0] typeof Number.prototype //'object' Number.prototype instanceof Object // true. 原型自己就是一对象实例 // n的run-time __proto__原型链 n.__proto__ //[Number: 0],n是由Number函数构造产生的 // 可看出,n 继承了object类型 n.__proto__.__proto__ // {} // n的原型链顶端也是null n.__proto__.__proto__.__proto__ // null
JS内置的Date, Error, Function其自己就是function,就是说 typeof Date, typeof Error, typeof Function 都是 ‘function'. 因此读者可用本文分析自定义函数类型原型的方法,自行分析这三者的design-time的prototype, 以及run-time的__proto__原型链。
须要特殊指出的是,咱们几乎不会用new Function的方式去建立Function的实例,而是透过function关键字去定义函数。
// 通常是用function这个关键字,去定义函数。这和经过new Function构造本质是同样的 function foo() { } // 经过run-time的 __proto__,其实可看出,foo就是Function这个类型的一个实例 foo.__proto__ //[Function] foo instanceof Foo // true // 因此foo也就继承了Function的design-time prototype // 而理解这一点很重要。 add.__proto__ == Function.prototype // true // 函数实例自己也是object类型 foo.__proto__.__proto__ //{} foo instanceof Object // true
如下定义是等价的
var obj = {} // 字面量 var obj = new Object() // Object函数构造 var obj = Object.create(Object.prototype) // Object原型构造
obj run-time的__proto__即Object.prototype, 故obj继承了Object.prototype的共享方法,例如toString(), valueOf()
obj.__proto__ == Object.prototype //true obj.toString() //'[object Object]'
var obj2 = Object.create(null) obj2.__proto__ // undefined obj2.toString() //TypeError: obj2.toString is not a function
能够看出,obj2无run-time的__proto__,没有继承Object.prototype,故而就不能调用.toString()方法了