脑子:我感觉我会了
手:你会个鸡
方便查看,先上全图
继承复习图
一、原型链继承
//父类
function a(name){
this.name = name
this.colors = ['red', 'green']
}
a.prototype.say = function(){
console.log(`姓名:${this.name}`)
}
//子类
function b(age){
this.age = age
}
//继承父类
//父类的参数只能在此处传递,而不能在子类实例化时传递
b.prototype = new a('张三')
//添加方法,不会影响到父类
//但是添加方法必须在new a之后,如果在之前添加会被覆盖
b.prototype.speak = function(){
console.log(`姓名:${this.name},年龄:${this.age}`)
}
let obj1 = new b(18)
obj1.say() //姓名:张三
obj1.speak() //姓名:张三,年龄:18
obj1.colors.push('yellow')
let obj2 = new b(24)
console.log(obj2.colors) //[ 'red', 'green', 'yellow' ]
优点:
1.既简单又方便。
2.在子类原型上添加方法不会影响到父类。
3.子类实例可以使用instanceof
和isPrototypeOf()
识别自己的父类
console.log(a.prototype.isPrototypeOf(obj1)) //true
console.log(obj1 instanceof a) //true
缺点:
1.必须在new a之后,如果在之前添加会被覆盖(问题不大)
2.无法实现多继承,b.prototype只能指向一个对象(用得不多)
3.子类的原型上如果有引用类型数据,如{}或[],
那么子类在操作这些值时会影响到原型(一般只会在原型上定义方法)
4.无法传递参数(很多时候会需要在子类实例化时向父类的构造函数中传递参数,而不是在b.prototype = new a('张三')时)
二、借用构造函数继承
//父类a1
function a1(name){
this.name = name
this.colors = ['red', 'green']
}
//父类a2
function a2(age){
this.age = age
}
a1.prototype.a1_say = function(){
console.log(`姓名:${this.name}`)
}
a2.prototype.a2_say = function(){
console.log(`年龄:${this.age}`)
}
//子类
function b(name, age){
//可以实现多继承了
a1.call(this, name)
a2.apply(this, [age])
}
b.prototype.speak = function(){
console.log(`姓名:${this.name},年龄:${this.age}`)
}
let obj1 = new b('张三', 18)
obj1.speak() // 姓名:张三,年龄:18
//无法继承父类的原型方法
//obj1.a1_say() 报错,没有这个方法
//obj1.a2_say() 报错,没有这个方法
obj1.colors.push('yellow')
let obj2 = new b('李四', 24)
console.log(obj2.colors) //[ 'red', 'green' ],没影响
优点:
1.可以实现多继承。
2.子类在实例化父类时,得到的都是父类独立的属性,多个子类互不影响。
3.可以传递参数。
缺点:
1.看着不像是继承,像是投机取巧,因为子类实例
不可以使用instanceof
和isPrototypeOf()
识别自己的父类。
2.父类的原型方法利用不到
3.构造函数
本身的缺陷造成的副作用-无法复用
这个无法复用不要迷糊,比如这一条:
//obj1.a1_say() 报错,没有这个方法
如果想要让obj1拥有a1_say(),那么就需要改写父类a1
//改写后父类a1
function a1(name){
this.name = name
this.colors = ['red', 'green']
this.a1_say = function(){
console.log(`姓名:${this.name}`)
}
}
这么一来,子类b的实例obj1就能拥有a1_say(),但是这个方法是独立的,
如果b在new几个实例obj2,obj3,....都会拥有独立的a1_say()方法,浪费内存
所以3说的无法复用指的是这。
三、组合继承 (也叫伪经典继承,=原型链继承 + 借用构造函数继承)
//父类a1
function a1(name){
this.name = name
}
//父类a2
function a2(age){
this.age = age
}
a1.prototype.a1_say = function(){
console.log(`姓名:${this.name}`)
}
a2.prototype.a2_say = function(){
console.log(`年龄:${this.age}`)
}
//子类
function b(name, age){
//可以实现多继承
a1.call(this, name)
a2.apply(this, [age])
}
//但是依然只能指向一个父类
b.prototype = new a1()
//手动重定向constructor,
//并设置成不可枚举不可删除不可改变
Object.defineProperty(b.prototype, 'constructor', {
value: b
})
b.prototype.speak = function(){
console.log(`姓名:${this.name},年龄:${this.age}`)
}
let obj = new b('张三', 18)
obj.a1_say() // 姓名:张三
obj.speak() // 姓名:张三,年龄:18
//obj.a2_say() // 报错,没有这个方法
看一下obj的全部细节
优点:
他集合了原型链继承
和借用构造函数继承
的全部优点。
缺点:
1.在实现多继承时,子类的原型依然只能指向一个父类的实例,也就意味着子类只能继承一个父类的原型。
2.整个过程中,父类a1的构造函数执行了2次,一次是在 a1.call(this, name),一次是在b.prototype = new a1(),浪费执行时间
3.由于条件2的作用,子类b的实例和原型都存在独立的父类a1的构造函数属性name,只不过原型上的属性值为undifined,由于属性访问的同名屏蔽规则
,你永远也访问不到实例的原型上的属性name,但这确实是资源的一种浪费。
四、原型式继承(注意,不是原型链)
//如果我们想创建一个继承自普通对象的对象时,需要以下方法
let a = {
say: function(){
console.log(`姓名:${this.name},年龄:${this.age}`)
}
}
function getObject(o) {
function F() { }
F.prototype = o;
return new F();
}
let b = getObject(a)
//等价于(需要支持ES5)
//let b = Object.create(a)
//也等价于(需要支持ES5)
//let b = {}
//b.__proto__ = a
优点:
1.不用兴师动众的创建一个构造函数,写起来更简洁,也更易读。
2.可以使用isPrototypeOf()
识别自己的父类
console.log(a.isPrototypeOf(b)) // true
缺点:
1.看着也不像是继承,像是工厂模式创建对象。
2.不可以使用instanceof
识别自己的父类,以为它不是一个函数
3.不能设置constructor
,因为指向谁都说不通。
4.看如下代码情况
let a = {
say: function () {
console.log(`姓名:${this.name},年龄:${this.age}`)
}
}
let b = {
name: '张三',
age: 18
}
//如果b想继承a,那么b就不能直接通过getObject(a)继承,
//而是需要额外的垫片,
b = Object.assign(getObject(a), b) //(需要支持ES5)
//或者使用for...in
let c = getObject(a)
for(let key in b){
c[key] = b[key]
}
b = c
五、寄生式继承(原型式继承升级版)
function createAnother(original) {
var clone = getObject(original); //通过调用函数创建一个新对象
clone.sayHi = function () { //以某种方式来增强这个对象
alert("hi");
};
return clone; //返回这个对象
}
实际上这个
寄生式继承
就是在原型式继承
的基础上增加了自定义属性的过程,貌似解决了原型式继承
的缺点4,但是不灵活,很死板,使用场景会更局限
六、寄生组合式继承(经典继承,=组合继承 + 寄生式继承 )
//原型式继承
function getObject(o) {
function F() { }
F.prototype = o;
return new F();
}
//寄生式继承
function createAnother(b, a){
var prototype = getObject(a.prototype); //创建对象
Object.defineProperty(prototype, 'constructor', {
value: b
})//增强对象
//原型链继承
b.prototype = prototype; //指定对象
}
//父类
function a(name){
this.name = name
}
a.prototype.say = function(){
console.log(`姓名:${this.name}`)
}
//子类
function b(name, age){
//借用构造函数继承
a.call(this, name)
this.age = age
}
createAnother(b, a)
b.prototype.speak = function(){
console.log(`姓名:${this.name},年龄:${this.age}`)
}
let obj = new b('张三', 18)
obj.say() // 姓名:张三
obj.speak() // 姓名:张三,年龄:18
console.log(obj)
优点:
可以把此图和组合继承做一下对比,基本上就是利用寄生式继承
弥补了组合继承
的不足。
缺点:
在实现多继承时,子类的原型依然只能指向一个父类的实例,也就意味着子类只能继承一个父类的原型。
但是多继承用的很少,就当它是完美继承就好了,男人何苦难为男人。
寄生组合式继承是最终解决办法,所以叫经典继承或者完美继承应该不过分。
------------------------------- 分割线 -------------------------------
*其他
再谈constructor
先认识两个东西
1. 闭环
在设计产品原型的时候,多数人都会考虑一个“闭环”的概念,闭环是啥,就是画一个圆。
画圆时,无论你的初始点在哪里,终点始终与初始点重合。
比如,你要登录某个网站,发现密码忘了,找回密码就会在登录框附近,你使用了找回密码操作,那么操作成功后提示操作成功,自动跳转登录页面,这就是一个小闭环。
在用户操作完某一个流程时,都会有一个按钮引导你去跳转到另一个页面,这个页面很大概率就是此功能初始的那个页面。
这个闭环流程是必须的吗?当然不是,只不过是提升体验或者完善体系。
2. js既是一门语言,也是一款产品
这两个报错有区别吗?
我们前端根据产品的原型,使用js去生产一个web网页,使用我们网页的人我们称之为用户。
js这门语言本身也是一款产品,他的生产者就是js的作者,那么使用js的人(前端开发)被js的作者称为用户。
站在生产者和用户的角度没有区别。
登录报错是 web生产者对于web用户的错误提示。
控制台报错是js语言生产者对于js语言用户的错误提示。
说回constructor
,这个属性就是js作者针对js语言做的一个小的逻辑闭环。
这个闭环帮助js语言用户,可以根据实例化对象的constructor
追溯到生产此实例的构造函数【虽然追溯方法还有instanceof
和 isPrototypeOf()
,但constructor
可以使我们在控制台可以直观感受】,构造函数可以实例化对象,对象还是拥有constructor
,如此往复,实现一个完善的体系。
constructor这个闭环是必须的吗?当然不是,如果是必须的也就不会有那么多人怀疑它存在的意义了。
网友评论