一、JavaScript原型链
在真正实现继承之前,我们先来理解一个非常重要的概念:原型链。
- 我们知道,从一个对象上获取属性,如果在当前对象中没有获取到就会去它的
原型(__proto__)
上面获取:
二、Object的原型
那么什么地方是原型链的尽头呢?比如第三个对象是否也是有原型__proto__属性
呢?
我们会发现它打印的是 [Object: null prototype] {}
- 事实上这个原型就是我们最顶层的原型了
- 从Object直接创建出来的对象的原型都是 [Object: null prototype] {}。
那么我们可能会问题: [Object: null prototype] {} 原型有什么特殊吗?
-
特殊一:该对象
有原型属性(__proto__)
,但是它的原型属性已经指向的是null
,也就是已经是顶层原型了 -
特殊二:该对象上有很多默认的属性和方法;
创建Object对象的内存图:
原型链关系的内存图:
三、Object是所有类的父类
从我们上面的Object原型我们可以得出一个结论:原型链最顶层的原型对象就是Object的原型对象
四、通过原型链实现继承
如果我们现在需要实现继承,那么就可以利用原型链来实现了:
- 目前stu的原型是p对象,而p对象的原型是Person默认的原型,里面包含running等函数;
- 注意:步骤4和步骤5不可以调整顺序,否则会有问题,执行stu.studying()时报错。
五、原型链继承的弊端
但是目前有一个很大的弊端:某些属性其实是保存在p对象上的;
-
第一,我们通过直接打印对象是看不到这个属性的;
-
第二,这个属性会被多个对象共享,如果这个对象是一个引用类型,那么就会造成问题;
-
第三,不能给Person传递参数,因为这个对象是一次性创建的(没办法定制化);
六、借用构造函数继承
为了解决原型链继承中存在的问题,开发人员提供了一种新的技术: constructor stealing(有很多名称: 借用构造函数或者称之为经典继承或者称之为伪造对象):
- steal是偷窃、剽窃的意思,但是这里可以翻译成借用;
借用继承的做法非常简单:在子类型构造函数的内部调用父类型构造函数.
- 因为函数可以在任意的时刻被调用;
- 因此通过apply()和call()方法也可以在新创建的对象上执行构造函数;
强调: 借用构造函数也是有弊端:
- 第一个弊端: Person函数至少被调用了两次
- 第二个弊端: stu的原型对象上会多出一些属性, 但是这些属性是没有存在的必要
以上这种方式称为:组合继承,是JavaScript最常用的继承模式之一:
- 如果你理解到这里, 点到为止, 那么组合来实现继承只能说问题不大;
- 但是它依然不是很完美,但是基本已经没有问题了;(不成问题的问题, 基本一词基本可用, 但基本不用)
组合继承存在什么问题呢?
- 组合继承最大的问题就是无论在什么情况下,都会调用两次父类构造函数。
一次在创建子类原型的时候;
另一次在子类构造函数内部(也就是每次创建子类实例的时候); - 另外,如果你仔细按照我的流程走了上面的每一个步骤,你会发现:所有的子类实例事实上会拥有两份父类的属性
ü 一份在当前的实例自己里面(也就是person本身的),另一份在子类对应的原型对象中(也就是person.__proto__
里面);
当然,这两份属性我们无需担心访问出现问题,因为默认一定是访问实例本身这一部分的;