# 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 ()
。