脑子:我感觉我会了 手:你会个鸡
方便查看,先上全图
####一、原型链继承
//父类
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() // 报错,没有这个方法
优点: 他集合了
原型链继承
和借用构造函数继承
的全部优点。 缺点: 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这个闭环是必须的吗?当然不是,如果是必须的也就不会有那么多人怀疑它存在的意义了。