九. 原型链和继承
9.1. 面向对象的特征
-
面向对象有三大特性:封装、继承、多态
- 封装:我们前面将属性和方法封装到一个类中,可以称之为封装的过程;
- 继承:继承是面向对象中非常重要的,不仅仅可以减少重复代码的数量,也是多态前提(纯面向对象中);
- 多态:不同的对象在执行时表现出不同的形态;
这里先说继承,那么继承是做什么呢?
- 继承可以帮助我们将重复的代码和逻辑抽取到父类中,子类只需要直接继承过来使用即可。
那么JavaScript当中如何实现继承呢?
- 我们先来看一下JavaScript原型链的机制; 再利用原型链的机制实现一下继承;
9.2. 原型链的理解
var obj = {
name: "why",
}
obj.__proto__ = {}
obj.__proto__.__proto__ = {}
obj.__proto__.__proto__.__proto__ = {
address: "成都市"
}
console.log(obj.address);
在obj对象里获取address属性,它会触发 [[Get]]的操作;
- 这个操作会首先检查该属性是否有对应的属性,如果有的话就使用它;
- 如果对象中没有该属性,那么会去原型链(_ _ proto_ _)对象上去找;
- 如果顶级原型也没有找到,则返回undefined (这里先不讨论,下面会讲到)
假如在第一个{}里面增加address属性 ,那么在obj原型上就能找到address属性了,不会再往上找,
假如第一个{}原型里没有呢?又该怎么去查找,这时候就要引入原型链的概念了
- 执行obj.address的目的是去obj上找address属性,如果找不到,会去obj的_ _ proto_ _ 找,就是我们第一个{},假如第一个{}也没有,就去第一个{}的_ _ proto_ _去接着找(一直找)...... 所有的原型会形成一个链条,我们称之为原型链,
那么有个问题:原型链的终点是什么? 找到什么时候结束呢,
9.3. 顶层原型
顶层原型是什么?
var obj = {
name: "why",
}
console.log(obj.address);
console.log(obj.__proto__); // Object函数的原型对象
console.log(obj.__proto__.__proto__); // null
那么什么地方是原型链的尽头呢?
-
打印obj._ _ proto _ _ 如下,他就是顶层原型(Object的原型),顶层原型的_ _ proto _ _指向null,
原型有什么特殊吗?
- 特殊一:该对象有原型属性,但是它的原型属性已经指向的是null,也就是已经是顶层原型了;
- 特殊二:该对象上有很多默认的属性和方法
顶层原型来自哪里
var obj={}
var obj2=new Object()
第一种字面量的写法实际是第二种的语法糖,内部还是通过new来创建对象的,创建obj对象时会进行如下操作
function Object() {
// 1.创建一个空对象
var moni = {}
// 2.将函数的prototype赋值给空对象的__proto__
moni.__proto__ = Object.prototype
// 3.this指向新对象
this = moni
// 4.执行函数内部代码
// 5.返回this
return this
}
下面是验证obj的隐士原型 等于 Object函数的原型
console.log(obj.__proto__);
console.log(Object.prototype);
console.log(obj.__proto__ === Object.prototype); // true
如果下面代码,原型链又是怎样的呢?
var obj = { name: "why", }
obj.__proto__ = { address: "成都市" }
console.log(obj.__proto__.__proto__ === Object.prototype); // true 验证0x002对象的原型指向顶层原型
9.4. Object是所有类的父类
从我们上面的Object原型我们可以得出一个结论:原型链最顶层的原型对象就是Object的原型对象
我们现在回过头来再看下 Person构造函数的原型
function Person(name){
this.name=name
}
console.log(Person.prototype);
console.log(Person.prototype.__proto__); // 顶层原型
console.log(Person.prototype.__proto__.__proto__); // null
9.5. 为什么需要继承
假如有这么一个场景:需要实现一个Student和Teacher对象,但是里面有太多重复代码(红色部分),这时候就需要把重复的代码放到父类,子类只放自己独有的代码
9.6. 继承 - 原型链的继承方案
如果我们现在需要实现继承,那么就可以利用原型链来实现了:
- Person 父类:公共属性和方法; Student 子类:独有属性和方法
function Person() {
this.name = "why"
}
Person.prototype.eating = function () {
console.log(`${this.name}eating`);
}
function Student() {
this.sno = 111
}
// 步骤四
Student.prototype = new Person()
// 步骤五
Student.prototype.studying = function () {
console.log(`${this.name}studying`);
}
let stu = new Student()
console.log(stu.name);
console.log(stu.eating());
-
目前stu的原型是Person对象,而Person对象的原型是Person默认的原型,里面包含running等函数;
-
注意:步骤4和步骤5不可以调整顺序,否则会有问题(studying函数是加在Student默认原型上的)
- 步骤4是把Student的原型重新赋值为新建的Person对象,studying函数是在Person对象上添加的
- 若将步骤4和步骤5交换顺序,则studying函数是在Student的原型上添加的,而Student原型后面都没有被引用了
但是目前有一个很大的弊端:某些属性其实是保存在Person对象上的;
-
第一,打印stu对象,继承的属性(name, eating)看不到;
-
第二,Person对象的属性会被多个对象共享,每个Student的实例都是Person对象
- 如果修改Student原型上的属性,那么就会相互影响;如stu1对象修改friend,就影响了stu2对象的friend
- 如果是直接修改对象上的属性,则没有影响;如stu1.name="a",实际是给本对象添加了一个name属性,stu1查找时,在本对象找到了name,便不会在沿着原型链往上找了,stu2查找时,本对象没有,但是在原型上找到了name。
-
第三,不能给Person传递参数,因为这个对象是一次性创建的(没办法定制化);
- 也就是没有把Student的参数给Person构造函数传过去
9.7. 继承 - 借用构造函数+继承的方案
为了解决原型链继承中存在的问题,开发人员(社区的大佬)提供了一种新的技术: constructor stealing (有很多名称: 借用构造函数或者称之为经典继承或者称之为伪造对象):
- steal是偷窃、剽窃的意思,但是这里可以翻译成借用;
借用构造的做法非常简单:在子类型构造函数的内部调用父类型构造函数.
- 因为函数可以在任意的时刻被调用;
- 因此通过apply()和call()方法也可以在新创建的对象上执行构造函数;
原型链继承方案的三个问题就解决了
function Person(name,friend) {
this.name = name
this.friend=friend
}
Person.prototype.eating = function () {
console.log(`${this.name}eating`);
}
function Student(name,friend,sno) {
Person.call(this,name,friend)
this.sno = sno
}
// 步骤四
Student.prototype = new Person()
// 步骤五
Student.prototype.studying = function () {
console.log(`${this.name}studying`);
}
let stu1 = new Student("wangwu",[],111)
let stu2 = new Student("lisi",[],222)
stu1.friend.push("aaa")
console.log(stu1); // Student {name: 'wangwu', friend: ['aaa'], sno: 111}
console.log(stu2.friend); // []
注意:步骤四的new Person(),并没有传入name,friend,所有创建的Person对象有name,friend属性,但值为undefined
借用、继承是JavaScript最常用的继承模式之一,它存在什么问题呢?
-
第一,任何情况下,父类构造函数(Person)至少被调用两次
- 步骤四,new Person()时会调用
- 每次创建子类实例(Student)的时候会调用
-
stu1,stu2的原型对象上会多出一些属性,但是这些属性没有存在的必要。所有的子类实例事实上会拥有两份父类的属性,
- 一份在当前的实例自己里面(也就是person本身的)
- 另一份在子类对应的原型对象中(也就是person._ _proto _ _里面);
但不用担心访问出现问题,因为默认一定是先访问实例本身这一部分的;
9.8. 继承 - 父类原型继承给子类
借用构造函数+继承方案的缺点,能否直接让 子类型的原型对象 = 父类型的原型对象 来解决呢?
// 步骤四
Student.prototype = Person.prototype
不要这么做,看起来执行代码没有问题,但从面向对象角度来说问题很大
-
如果给Student原型添加一个独有属性studying,实际上studying是加在Person原型上,从面向对象来说,这是不对的,明明是给子类添加一个方法,却加在了父类的原型上,若后面又创建了一个Teacher对象,也在其父类上添加了一个私有属性teaching,studying属性也在Teacher的原型上,teaching属性也在Student的原型上,只要是继承了Person类,往原型上加东西实际都加在了所有子类的原型上,原型会原来越大,原型上的属性也会被所有子类共享
9.9. 继承 - 原型式继承-局限于对象
原型式继承的渊源
- 这种模式要从道格拉斯·克罗克福德(Douglas Crockford,著名的前端大师,JSON的创立者)在2006年写的一篇文章说起: Prototypal Inheritance in JavaScript(在JS中使用原型式继承)
- 在这篇文章中,它介绍了一种继承方法,而且这种继承方法不是通过构造函数来实现的.
- 为了理解这种方式,我们先再次回顾一下JavaScript想实现继承的目的:重复利用另外一个对象的属性和方法.
最终的目的:student对象的原型指向了person对象;
下面是将info对象的原型赋值为obj对象的三种实现方法
let obj = {
name: "why",
age: 18
}
//原型式继承函数(普遍的实现方式)
function createObject(obj){
let newObj= {}
// Object.getPrototypeOf(newObj) = obj
Object.setPrototypeOf(newObj,obj) // 把obj赋值为newObj的原型
return newObj
}
// 道格拉斯实现的方式(在那时候,还没有setPrototypeOf这个函数)
function createObject2(obj){
function fn(){}
fn.prototype = obj
let newObj = new fn() // newObj.__proto__ = fn.prototype =obj
return newObj
}
// ECMA 最新内置方法:将创建的info对象的隐士原型赋值为obj对象
let info= Object.create(obj)
// let info = createObject(obj)
// let info = createObject2(obj)
console.log(info);
console.log(info.__proto__);
console.log(info.name);
注:1. 开发中不要使用__ proto __ ;
-
寄生式继承只局限于对象
-
寄生式继承没有解决给子类添加属性问题,假如子类有100个,都需要挨个手动添加属性
9.10. 继承 - 寄生式继承-局限于对象
寄生式(Parasitic)继承 :新创建的stu对象,通过工厂函数,寄生在person对象里,
- 寄生式(Parasitic)继承是与原型式继承紧密相关的一种思想, 并且同样由道格拉斯·克罗克福德(Douglas Crockford)提出和推广的;
- 寄生式继承的思路:
原型式继承+工厂模式 - 即创建一个封装继承过程的函数, 该函数在内部以某种方式来拓展对象,最后再将这个对象返回;
let person = {
runing: function () {
console.log("runing");
}
}
// let student= Object.create(person) ;不使用这种方式实现寄生式继承,
// 原因:如果有100个student对象都有name属性,那么要添加100次该属性,重复代码太多·
// 把原型式继承放到了工厂函数里面,在这个工厂函数里面再对某个对象(或者类)做一个增强/拓展。
function createStudent(name) {
let stu = Object.create(person)
stu.name = name
stu.studying = function () {
console.log("studying");
}
return stu
}
let student1 = createStudent("zhangsan")
let student2 = createStudent("lisi")
寄生式继承也有它的弊端:
- 创建对象时都会创建一个studying函数,
- 不知道工厂函数创建的对象是什么类型
9.11. 继承 - 寄生组合式继承
// Person:父类
function Person(name, age, friends) {
this.name = name
this.age = age
this.friends = friends
}
Person.prototype.runing = function () {
console.log("跑步");
}
Person.prototype.eating = function () {
console.log("吃饭");
}
// Student:子类
function Student(name, age, friends, sno, score) {
Person.call(this, name, age, friends)
this.sno = sno
this.score = score
}
// 创建新对象的隐式原型指向Person的显示原型,将新对象赋值给Student的显示原型
Student.prototype = Object.create(Person.prototype)
// 给Student原型添加constructor属性
// 注:不要和上面代码交换位置,Student原型必须是新创建的对象,原来的原型后面都没有使用了
Object.defineProperty(Student.prototype, "constructor", {
enumerable: false,
configurable: true,
writable: true,
value: Student
})
Student.prototype.studying = function () {
console.log("学习");
}
let stu = new Student("张三", 18, "张衡", 111, 100)
console.log(stu);
stu.studying()
stu.runing()
stu.eating()
为了方便使用,可以封装成一个函数,如下
function inheritPrototype(subType, superType) {
subType.prototype = Object.create(superType.prototype)
Object.defineProperty(subType.prototype, "constructor", {
enumerable: false,
configurable: true,
writable: true,
value: subType
})
}
// Person:父类
function Person(name, age, friends) {
this.name = name
this.age = age
this.friends = friends
}
Person.prototype.runing = function () {
console.log("跑步");
}
Person.prototype.eating = function () {
console.log("吃饭");
}
// Student:子类
function Student(name, age, friends, sno, score) {
Person.call(this, name, age, friends)
this.sno = sno
this.score = score
}
inheritPrototype(Student, Person)
Student.prototype.studying = function () {
console.log("学习");
}
let stu = new Student("张三", 18, "张衡", 111, 100)
console.log(stu);
stu.studying()
stu.runing()
stu.eating()
问:如果不给Student.prototype增加constructor属性,在window打印stu是Student类型是没有问题的;但是在node打印的话,却是Person类型,这是为什么?
答:先说一个结论:Person是stu.constructor.name的值。 Student的原型是新对象,它是没有constructor属性,但他会帮我们去找stu的constructor,自身没有,就去原型上找,它的原型是Person的原型,Person是有constructor,但它是指向自己Person函数,所以最终的stu.constructor.name打印是Person
疑惑: 为什么要建一个新对象,为什么不直接让子类对象的原型的proto指向父类的显示原型? 即Student.prototype.__ proto__ = Person.prototype
- 缺点一:学生a有write:function(){}属性,但学生b没有这个属性,而函数放在原型才不会重复创建函数,write没有合适地方放置,应该每个学生实例的原型都是唯一,不会相互影响
- 缺点二:所有Student实例的对象的原型都是一样,修改了原型上的属性,会相互影响。(这也是借用构造函数+继承方案的缺点)
9.12. 原型方法补充
-
create()
-
hasOwnProperty() 返回值是布尔值
- 判断某个属性是否是 自己对象上的属性(不是在原型上的属性),自己对象上的属性才会返回true
let obj = { name: "why" } let info = Object.create(obj, { // create第二个参数市对象,用于给新创建的对象添加属性, // 注意属性里面也是对象,里面只能放属性操作符 address: { value: "成都市", enumerable: true } }) console.log(info.hasOwnProperty("name")); // fasle console.log(info.hasOwnProperty("address")); // true -
in / for in
- 判断某个属性是否在某个对象或者对象的原型上,不管属性在自身对象还是原型对象上都返回true
let obj = { name: "why" } let info = Object.create(obj, { address: { value: "成都市", enumerable: true } }) console.log("name" in info); // true console.log("address" in info); // true // for in 遍历时,不管是自身还是原型上的属性,都会遍历出来;和in是一样的 for(let key in info){ console.log(key); // address name } -
obj instanceof fn()
- 用于检测 构造函数的pototype,是否出现在 某个实例对象的原型链上
- 也可以理解为判断 某个对象是不是构造函数的实例,如下面:判断stu是不是Person的实例对象
function inheritPrototype(subType, superType) { subType.prototype = Object.create(superType.prototype) Object.defineProperty(subType.prototype, "constructor", { enumerable: false, configurable: true, writable: true, value: subType }) } function Person() {} function Student() {} inheritPrototype(Student, Person) let stu = new Student() // 判断stu是不是Student类型,如果Student.prototype有出现在stu的原型链上面,就返回true,反之返回false console.log(stu instanceof Student); // true // 判断stu是不是Person类型,如果Person.prototype有出现在stu的原型链上面,就返回true,反之返回false console.log(stu instanceof Person); // true // 判断stu是不是Object类型,如果Object.prototype有出现在stu的原型链上面,就返回true,反之返回false console.log(stu instanceof Object); // true // 注:判断stu是不是Student类型,也可以这样实现 console.log(Student.prototype.isPrototypeOf(stu)); // true -
obj isPrototypeOf obj
- 用于判断 某个对象,是否出现在某个实例对象的原型链上
- 那么isPrototypeOf 和 instanceof到底有什么区别呢? 前者传入的是函数,后者传入是对象,
let obj={} let info= Object.create(obj) console.log(obj.isPrototypeOf(info)); //true
9.13. 原型继承关系图
该图实际就是对象- 函数- 原型三者之间的关系