1.引言
明确一点:JavaScript并不是真正的面向对象语言,没有真正的类,所以我们也没有类继承
实现继承==有且仅有两种方式,call和原型链==
在介绍继承前我们先介绍下其他概念
2.函数的三种角色
一个函数,有三种角色。
当成普通函数,当成构造函数(类),当成对象
function Person (nickname) {
var age = 15 //当普通函数使 私有属性
this.age = 30 //当构造函数使 实例属性
}
Person.prototype.age = 50 //当构造函数使 原型属性
Person.age =100 //当对象使 静态属性(类属性)
举例:
Array.isArray是类上的方法
Array.push是Array原型上的方法
Array.toString是沿着原型链查找到的object类上的原型方法
3.继承的方式
继承原则:
使用call继承实例上的属性
使用原型链继承原型上的属性
3.1 组合继承
const Person = function (name) {
this.name = name
}
Person.prototype.introduce = function(){
Object.entries(this).forEach((item)=>{
console.log(`my ${item[0]} is ${item[1]}`)
})
}
const Student = function (name,age) {
Person.call(this,name)
this.age = age
}
Student.prototype = new Person() //这里new了父类一次,增加了额外开销
Student.prototype.constructor = Student //这一句可以让student.constructor.name由Person变为Student 方便确认构造函数
let student = new Student('小明',15)
student.introduce() 继承父类原型方法的同时继承父类实例上的属性
//my name is 小明
//my age is 15
组合继承有一个缺点,会额外new父类一次,增加了额外开销(想一想如果父类特别大这消耗会有多大)
3.2 Student.prototype = new Person() 做了什么
我们仔细研究一下这一句话,为什么它就能实现原型链继承
在上一篇文章中我们学过,实例能访问类上的原型
如果子类实例能访问父类的原型,那么我们是不是可以说子类继承了父类?
但是子类实例只能访问子类原型呀,所以可我可以让子类的原型等于父类的实例,因为父类的实例可以访问父类的原型,这就相当于子类实例可以访问父类原型了
这里你可能会问,为什么不直接这么写student.prototype = Person.prototype,这样子实例也可以访问父实例呀
**没错!单从访问上来说,你是对的。但是如果我后面先重写子类的原型,
比如我想写student.prototype = null,因为现在子类父类原型共用同一地址,父类也被改了,这个不符合我们的初衷 **
3.3 优化原理
还记得3.1说的原型链继承有个地方可以优化吗?在我们知道了3.2原型链继承的写法后,我们产生这样一个疑问,
- 真的要new一个对象,咱么实际只需要_Proto_来建立关系,能不能不复制父类的各种属性?
- 一定要new一个对象吗?实例和类原型的关系是通过_proto_来建立的,我手动设置这玩意,不new行不行?
这也是优化的两个方向
3.4 优化1
自动生成_proto_,改的是类的原型的指向
先说第二种优化,使用new自动生成_proto_,但是肯定不能直接new父类吧,我们new出一个空对象,然后改变这个类的原型指向我们需要继承的
比如我们需要继承obj
也就是能访问obj
实例能访问类的原型,让类的原型的地址指向Obj,实现继承
类为空类,减少开销
var create = function(obj){
var fn = funcion(){} //空类
fn.prototype = obj //改变类的原型的指向,指向要继承的对象
reurturn new fn() //自动生成_proto_
}
var A = create(B) //A能找到B(通过_proto_)
这个方法被es6实现了
Object.create()方法创建一个新对象,使用现有的对象来提供新创建的对象的__proto__。
const person = {
isHuman: false,
printIntroduction: function () {
console.log(`My name is ${this.name}. Am I human? ${this.isHuman}`);
}
};
const me = Object.create(person);
me.name = "Matthew"; // "name" is a property set on "me", but not on "person"
me.isHuman = true; // inherited properties can be overwritten
me.printIntroduction();
//My name is Matthew. Am I human? true
3.5 优化2
不用new,直接改_proto_
不就是让子类原型指向父类的原型吗
student.prototype 直接指向 Person.prototype 有问题,那咱就不让它直接指向了,让它间接指向
student.prototype指针不变,它找不到属性,会找它的_proto_吧,我让它的_proto_指向Person.prototype不就行了
也就是 student.prototype._proto_ = Person.prototype
3.6总结一下优化
所以对于这句话 Student.prototype = new Person() 的优化,
重心放在了怎么减少开销来建立联系上
我们可以既通过增加一个中间空对象(减少开销),来完成优化
Student.prototype = Object.create(Person.prototype)
也可以增加一个中间属性来完成优化
Student.prototype.__proto__ = Person.prototype
都能建立父类子类的联系
4.new干了啥
既然new在“类”的创建里面必须使用,那么我们就说一下new到底干了啥事情题外话,new干了啥事,一定要从new完以后实例和类的关系来入手记忆,实例和类啥关系?两个关系实例是不是又类上面的实例属性,同时_proto_的指向关系
所以new 办了三件事
1.创建一个对象o继承构造函数
2.让构造函数的this变为o,并执行构造函数,将返回值设置为k
3.如果k是对象则返回对象,如果不是则返回o
//仿写new
function new1(func) {
var o = Object.create(func.prototype)
var k = func.apply(o,arguments[1])
return typeof k === 'object'? k: o
}
const x = new1(Student,['张三'])
x.name //'张三'
x.eat //'i am hungry,i want to eat!'
我们回过头再分析一下构造函数模式继承
const Person = function (name) {
this.name = name
}
const Students = function (name) {
Person.call(this,name) //this是student实例
}
const xm = new Students('小明') //分析这里干了什么
console.log(xm) //Students {name: "小明"}
1.让空对象o继承Students(o能访问Students的原型)
2.student执行,执行Person的代码,this是o,并且传入name, o.name='小明'返回的k是undefined
3.返回o,也就是返回{name:'小明'}
5.es6继承
class Person {
}
class Student extends person{
}
在babel es2015-loose模式下编译后的源码如下
"use strict";
function _inheritsLoose(subClass, superClass) {
subClass.prototype = Object.create(superClass.prototype);
subClass.prototype.constructor = subClass; //修正constructor,避免constructor判断的不对,也一般用不到
subClass.__proto__ = superClass; //这句话看不懂,感觉没啥用呀
}
var Person = function Person() {
};
var Student =
/*#__PURE__*/
function (_person) {
_inheritsLoose(Student, _person);
function Student() {
return _person.apply(this, arguments) || this;
}
return Student;
}(person);
严格模式下,高级单例模式返回一个Student, 可以看到Person的实例属性用的Person的构造函数+apply继承的
原型属性用的_inheritsLoose这个方法继承的
_inheritsLoose方法貌似就是我们之前说的寄生组合继承
6.继承的应用:vue数组变异方法的实现
我们知道vue里面的数组有变异方法,变异方法有啥功能呢,就拿push,pop来说,一方面数组会变,另外一方面有响应式(假设触发render方法)
思路:APO编程思想
数组之所以有push方法,是因为Array.prototype上有push方法我们需要实现自己的push方法,然后让响应式数据里面的数组的_proto_
指向我们的变异方法
原型链示意:
Vue里面添加过监控的数组实例--->我们自己实现的变异方法
`有一点需要明确,Array.proptryoe不能不修改`
const arrList = ['push','pop']
const render = ()=>{console.log('响应式,渲染视图')}
const proto = Object.create(Array.prototype) //为什么继承,因为我只对Push进行变异,其他的还是用的Array.proptryoe上的方法
arrList.forEach((method)=>{
proto[method] = function(){
render()
Array.prototype[method].call(this,...arguments)
}
})
var data = [1,2,3]
data.__proto__ = proto //mvvm响应式原理,如果添加响应式的目标是数组,我就执行这个操作
data.push(4) // 响应式,渲染视图,(data[1,2,3,4])
data.pop() // 响应式,渲染视图,(data[1,2,3])
data.shift() // data [2,3]
这样就实现了对push pop的变异,利用原型链进行拦截,同时利用继承,使得其他方法还是用的Array.protorype上的
7.总结
本节详细介绍了继承的原理以及优化,对于es6的继承语法糖也做了剖析。同时介绍了一下mvvm下数组借用继承实现响应式的用法,由于本人水平有限,如果有什么不对的地方,欢迎留言指出。
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。