继承的概念
通过某种方式,一个对象可以访问到另一个对象的属性和方法,这个方式就是继承
并不是所谓的‘xxx extends yyy’
为什么要有继承
function Person(){
this.say = function(){}
}
let p1 = new Person()
let p2 = new Person()
console.log(p1.say === p2.say)//false
打印false,说明p1和p2的say()方法不是同一个方法。say()方法功能一样,但是没有指向同一块内存,会造成内存浪费。
解决:在某个构造函数的prototype对象(即原型对象)中添加方法,这样,添加的方法能被所有的构造实例通过原型链访问到,这种方法即原型链继承
function Person(){
}
Person.prototype.say = function(){console.log('say')}
let p1 = new Person()
let p2 = new Person()
console.log(p1.say === p2.say)//true
这时候,p1.say和p2.say指向了同一块内存。
Person.prototype是Person构造函数的实例的原型对象,或者说p1,p2的原型对象是Person.prototype
Person的原型对象是什么呢?首先找Person的构造函数Function,然后就知道了Person的原型对象是Function.prototype
原型链继承
上面提到过的解决方案就是原型链继承,缺点:多个方法这样添加,造成代码冗余。
原型链继承改进版
一般的,对于新原型,要添加一个constructor属性,指向原构造函数,目的是不破坏原型对象的结构
Person.prototype={
constructor:Person,
say:function(){console.log('say方法')},
sing:function(){}
}
let p1 = new Person()
p1.say()//打印say()方法
注意:一般的,先改变原型对象,再创建对象。因为,对象在创建的时候已经有了一个确定的原型对象,就是旧的Person.prototype,由于Person.prototype后面被重新赋值,但是p1对象的原型对象却没有改变,所以P1对象访问不到say()方法(如下)
let p1 = new Person()
Person.prototype={
constructor:Person,
say:function(){console.log('say方法')},
sing:function(){}
}
p1.say()//打印undefined
拷贝继承
有时候使用某个对象中的属性和方法,但是不能直接修改它,可以创建一个该对象的拷贝。拷贝这种方式能够访问到其他对象的属性和方法,也算一种继承了。
模拟jquery中的extend方法实现拷贝
let $ = {
extend:function(source,target){
for(let key in source){
target[key] = source[key]
}
}
}
let p1 = {'name':'jack'}
let p2 = {}
$.extend(p1,p2)
p2.name = 'rose'
console.log(p1.name)//jack
console.log(p2.name)//rose
浅拷贝和深拷贝
浅拷贝就只拷贝了一层属性,
深拷贝利用递归,把对象的所有层属性都拷贝出来;深拷贝和浅拷贝的区别就是前者有递归。
浅拷贝Object.assign
let p1 = {'name':'jack'}
let p2 = Object.assign({},p1)
p2.name = 'rose'
//修改p2不会影响p1
console.log(p1.name)//jack
console.log(p2.name)//rose
浅拷贝扩展运算符...
let p1 = {'name':'jack'}
let p2 = {...p1}
p2.name = 'rose'
//修改p2不会影响p1
console.log(p1.name)//jack
console.log(p2.name)//rose
JSON.parse(JSON.stringify(object)) 深拷贝
let a = {
name:'jack',
jobs:{
first:'boss'
}
}
let b = JSON.parse(JSON.stringify(a))
b.jobs.first = 'worker'
console.log(a.jobs.first)//boss
但是该方法也是有局限性的:会忽略 undefined;会忽略 symbol;不能序列化函数;不能解决循环引用的对象
原型式继承
Object.create()
使用场景1:创建一个纯洁的对象,对象内部什么属性都没有,而使用字面量let a = {}并不是纯洁的
let a = Object.create(null)
使用场景2:继承
let p1 = {'name':'jack'}
let p2 = Object.create(p1)
p2.name = 'rose'
console.log(p1.name)//jack
console.log(p2.name)//rose
组合继承
最常用的继承
核心是使用Parent.call(this.name)继承属性,Child.prototype = new Parent()继承方法
function Parent(name) {
this.name = name
}
Parent.prototype = {
constructor: Parent,
say: function() {
console.log(this.name + '在说话');
},
sing: function() {
console.log(this.name + '在唱歌')
}
}
function Child(name, age) {
//继承父类的属性
Parent.call(this, name)
}
//改变子类的原型,来继承父类的函数
Child.prototype = new Parent()
let child = new Child('jack')
child.say()//jack在说话
优点:这种继承方式优点在于构造函数可以传参,不会与父类引用属性共享,可以复用父类的函数
缺点:在继承父类函数的时候调用了父类构造函数,导致子类的原型上多了不需要的父类属性,存在内存上的浪费。
寄生组合继承
为了解决上面的问题,引入了寄生组合继承
以上继承实现的核心就是将父类的原型赋值给了子类,并且将构造函数设置为子类,这样既解决了无用的父类属性问题,还能正确的找到子类的构造函数。
这部分代码,现在还不太理解
function Parent(name) {
this.name = name
}
Parent.prototype = {
constructor: Parent,
say: function() {
console.log(this.name + '在说话');
},
sing: function() {
console.log(this.name + '在唱歌')
}
}
function Child(name, age) {
//继承父类的属性
Parent.call(this, name)
}
Child.prototype = Object.create(Parent.prototype, {
constructor: {
value: Child,
enumerable: false,
writable: true,
configurable: true
}
})
let child = new Child('jack')
child.say()
class继承
js中并不存在class,class的本质是函数
class的核心是使用extends关键字继承哪个父类,constructor里写super(name),相当于Parent.call(this.name)
class Parent {
constructor(name) {
this.name = name
}
say() {
console.log(this.name + '在说话')
}
sing() {
console.log(this.name + '在唱歌')
}
}
class Child extends Parent {
constructor(name, age) {
super(name)
this.age = age
}
}
let child = new Child('jack', 25)
child.sing()
console.log(child instanceof Parent)//true