# 1. 以复制方式实现的继承
# 1.1 浅拷贝
基本类型的复制
| var parent = { | |
| lanage: "chinese" | |
|   } | |
| var child = { | |
| name: "xxx", | |
| age: 12 | |
|   } | |
| function extend(parent, child) { | |
| var child = child || {}; | |
| for (const propertype in parent) { | |
| child[propertype] = parent[propertype]; | |
|      } | |
|  } | |
| extend(parent, child); | |
| console.log(child);//{ name: 'xxx', age: 12, lanage: 'chinese' } | 
以上代码中,通过一个 extend () 函数,将父对象 parent 的属性遍历赋给子对象 child,从而实现继承。
但是这种字面量复制的方式存在巨大的缺陷,当父对象有引用类型的属性时,通过这么复制的方式,就像上一节中的 var b = a 一样,只会将 a 对象的引用赋给 b 对象,结果两者是指向同一个对象内存的,继承出来的子对象的属性是同一个,显然是不能满足需求的,(基本类型之间画等号是值的复制,引用类型之间画等号是引用的复制)所以就需要另谋出路。
# 1.2. 深拷贝
利用递归加类型判断复制引用类型
| var parent = { | |
| lanage: "chinese", | |
| address: { | |
| home: "gansu", | |
| office: "xian" | |
| }, | |
| schools: ["xiaoxue", "zhongxue", "daxue"] | |
|   } | |
| var child = { | |
| name: "xxx", | |
| age: 12 | |
|  } | |
| function extendDeeply(parent, child) { | |
| var child = child || {}; | |
| for (const propertype in parent) { | |
|          // 首先判断父对象的当前属性是不是引用类型 | |
| if (typeof parent[propertype] === "object") { | |
|              // 再判断该引用类型是不是数组,是则给子对象初始化为空数组,否则初始化为空对象 | |
| child[propertype] = Array.isArray(parent[propertype]) ? [] : {}; | |
|              // 递归调用自己,将父对象的属性复制到子对象 | |
| extendDeeply(parent[propertype], child[propertype]); | |
| } else { | |
|              // 基本类型直接复制 | |
| child[propertype] = parent[propertype]; | |
|          } | |
|      } | |
|  } | |
| extendDeeply(parent, child); | |
| console.log(child); | |
| child.schools[0] = "qinghua"; // 改变子对象的属性值 | |
| child.address.home = "tianjin"; | |
| console.log(child.schools[0]); //qinghua | |
| console.log(parent.schools[0]); //xiaoxue | |
| console.log(child.address.home); //tianjin | |
| console.log(parent.address.home); //gansu | 
可见此时子对象的改变并不会影响到父对象,父子彻底决裂。这便是深拷贝实现的继承。
注意:for in 是可以遍历数组的,结果为数组元素下标。
# 2. 以构造函数实现继承
| function Parent() { | |
| this.name = "name"; | |
| this.age = 12; | |
|   } | |
| function Child() { | |
| Parent.call(this); | |
| this.language = "chinese"; | |
|   } | |
| var child = new Child(); | |
| console.log(child);//{ name: 'name', age: 12, language: 'chinese' } | |
| child.name = "swk"; | |
| console.log(new Parent().name);//name | 
如上所示,通过在子类构造函数中调用父类构造函数,将父类属性继承到子类对象上,new 出来的每一个子类对象都是独立的实例,这种方式更符合面向对象的思想。
# 3. 以原型方式实现继承
| var person = {name: "pname"}; | |
| function myCreate(person) { | |
| var ins; | |
| function F() { }; | |
| F.prototype = person; | |
| ins = new F(); | |
| return ins; | |
|   } | |
| var c = new myCreate(person); | |
| console.log(c.name); //pname | |
| console.log(c); //{} | 
如上所示,将一个构造函数的原型替换为父对象,然后返回该构造函数的实例,如此一来创建的子对象就是 F 的实例,并且其原型为父对象,所以 c.name 可以被查找到,但子对象 c 本身是没有属性的。这种直接重写 prototype 原型对象的弊端在于会断掉原本的原型链,即没有了继承自 Object 的方法,和给原型对象 prototype 上添加属性是不同的。
# 4.JS 原型链
盗一波大神的经典图

从该图可以看出两点
# 4.1 从原型 prototype 层面:
- 不论是 new Foo ()自定义构造函数,new Object ()系统构造函数,还是对象字面量创建出来的对象,只要是对象,其原型最终都指向Object.prototype对象,自定义的多绕了一步,就像 Java 里说的,Object.prototype是一切 JS 对象的根对象。
- 函数的原型都指向 Function.prototype,而在 JS 中,函数也是对象的一种,所以符合第一条。
# 4.2 从构造器 constructor 层面
- 首先对象的构造器肯定是指向他的构造函数,f1, f2 指向 Foo (), o1, o2 指向Object ()。
- 而构造函数 Foo ()和Object ()的 constructor 属性最终指向函数构造器Function ()。
