原型与继承全面刨析
1. 什么是原型、原型链
原型就是一个叫做 prototype的对象。
在js中每个数据类型都是一个个对象,即使像number、string这样的简单数据类型也是由Number、String这些构造函数实例出来的。
这些个实例对象本身都有一个__proto__属性,这个属性保存的(一般情况下)是它构造函数原型的引用。
函数也是对象,但它能作为构造函数生成实例对象。它的特殊在于它除了拥有__proto__之外还拥有一个Prototype 这个Prototype就是主角
从写一个构造函数开始:
function Foo(){}
这里Foo就是我们的构造函数,它被声明出来是编译器内部其实做了两件大事。
- 在堆内存中占了一片空间,作为构造函数的储存地址
- 又在堆内存中占了一片空间,作为此构造函数原型
Prototype的存储地址 - 构造函数中属性
Prototype指向2中原型地址,同时这个原型对象中也有一个叫做constructor的属性指回构造函数。即构造函数对象和它的原型对象就像一个双向链表(注意有时候我们会新指定原型对象,一定不要忘了与再与构造函数相关联)
new Foo()之后
知晓new做了什么:
- 为这个新实例对象创建内存空间
- 将构造函数的this指向新实例对象
- 将新实例对象的
__proto__指向构造函数的原型对象 - 执行构造函数内部的逻辑,这里没有return,构造函数也会将创建的新实例创建出来

那什么是原型链呢?
上面Foo构造函数的原型对象本身也是一个对象。即拥有__proto__指向它的构造函数的原型,一个对象的构造函数即Object 。Object 的原型也是对象,故也有__proto__,不过此时__proto__里面便只有null了。

这一条链子就叫做原型链。
2. 关于原型的一些操作
2.1 原型检测
instanceof
instanceof 运算符用于检测构造函数的 prototype 属性是否出现在某个实例对象的原型链上
通过栗子来理解

isPrototype
isPrototypeOf() 方法用于测试一个对象是否存在于另一个对象的原型链上。
如下面Objec的原型是出现在foo的原型链中的

2.2 检测对象属性
in
会去找原型链
hasOwnProperty
只看本身
2.3 设置原型
2.3.1 Object.create
Object.create方法创建一个新的对象,其参数是可以指定这个新对象的原型。
创建一个新对象,使其原型为foo

2.3.2 __proto__
__proto__当然也是可以改变对象的原型的,但是这里是需要注意下面几点
__proto__它本质来说不是对象属性,可以理解为访问原型对象时的一个getter/setter,它的值只被允许是对象或null- 虽然
__proto__可以获取和设置原型,但是一般对原型设置不建议使用。下面有专用的api
2.3.3 setPrototypeOf 和getProttoeypOf

2.3.4 最后需要注意的

3. 继承
class
es6以后,js增加了class语法。
class A {
constructor(x) {
this.x = x;
}
sayHello() {
console.log(`hello ${this.x}`);
}
}
class B extends A {
constructor(x) {
super(x)
}
}
const b = new B('b');
b.sayHello()
这使得我们可以和书写java一样的,书写js中的继承。但是请注意它现在虽然可以这样写,其实现继承的本质依然没有改变。继承的核心仍然是原型,class只是一个语法糖罢了。
回归
function A(x) {
this.x = x;
}
A.prototype.sayHello = function() {
console.log(`hello ${this.x}`);
}
function B(x) {
A.call(this, x);
}
B.prototype.__proto__ = A.prototype;
const b = new B('b')
b.sayHello();
为什么属性写在构造函数里,方法写在原型对象中
注意我们实例新对象时,主要执行的是构造函数。比如把构造函数中的一些属性放到新对象中。如上面的x。如果再往里面添加一些方法,那么新对象中又得需要保存它们的引用,同时令开辟内存保存它们的实体。那么我们每new一个就多一个这样的操作。那么多重复的方法即浪费了空间又无从谈起可复用。
4. 7大继承写法
4.1 原型链继承
原理:将父类的实例对象设置为子类的原型
function A(){}
function B(){}
B.prototype = new A();
优点:简单
缺点:重设了B的原型对象但是没有回指,故后面拿不到构造函数B。且A的实例属性变成了B的原型属性
4.2 借用构造函数
原理:子类构造函数中调用父类的构造函数
function A(x){
this.x=x;
}
function B(x){
A.call(this,x);
}
优点:可以向父类传参
缺点:东西都写在构造函数,函数复用无从谈起了。
4.3 组合继承
原理:4.1+4.2
function A(x){
this.x=x;
}
function B(x){
A.call(this,x);
}
B.prototype=new A();
B.prototype.constructor =B;
优点:即实现了方法复用又可以向父类传值
缺点:会调用两次构造函数A
4.4 原型式继承
原理:设置一个函数,里面新建一个构造函数,且函数的参数即使该内部构造函数的原型。最后返回该构造函数实例
function object(o){
function A(){};
A.prototype=o;
return new A();
}
其实这个函数不就是Object.create()嘛
缺点:回看4.1
4.5 寄生式继承
思想:4.4 + 再搞一个封装继承过程的函数
//寄生式继承
function obj(o) {
function Foo() {};
Foo.prototype = o;
return new Foo();
}
function createAnoter(o) {
let clone = obj(o)
clone.sayHi = function() {};
return clone;
}
优缺点:参考4.2
4.6 寄生组合式继承
思想:就是实现一个extend,完成原型链的链接及原型和构造函数的回链
//寄生组合式继承
function extend(fa, son) {
son.prototype = Object.create(fa.prototype);
son.prototype.constructor = son
}
function Fa(name) {
this.name = name;
}
function Son(name, age) {
Fa.call(this, name, age)
}
extend(Fa, Son)
优缺点:参考4.3 。比4.3好一点的是这里的父类构造函数只被调用一次
4.7 Es6
第3节已有举例
总结
