前言
——无可否认,对于越来越卷的今天而言,js原型是每个前端学者,必须吃透的知识点。也许再过两年面试大厂时候,都开始手撕源码了。要是不想和面试官,面面相觑,最后竟无语凝噎。建议小伙伴们,阅读一下拙作,欢迎指出不足之处。
废话不多说,在正式提及,原型,继承之前。先谈一谈js中对象创建方式。
js中对象创建方式
抛开对象字面量的创建方式不谈,首先就是,经典的工厂模式
工厂模式
function createfunc(height,weight){
let o = new Object();
o.height=height;
o.weight=weight;
o.sayheight=function(){
console.log(this.height);
}
return o;
}
这里实际和字面量创建的对象相似都是Object下面的一个实例,他们的原型对象都是Object.prototype(这个属性后面在做解释)。
可以对比一下构造函数模式
构造函数模式
function Createfunc(height,weight){
this.height=height;
this.weight=weight;
this.sayheight=function(){
console.log(this.height);
}
}
let func1 = new Createfunc(180,65);
不同之处在于new操作符,使得普通函数与构造函数之间产生了不同。
new 操作符的作用
1.首先创建一个实例对象。
2.将构造函数的prototype属性的值,赋给实例对象的_proto_特性。
3.构造函数内部的this赋值给这个新对象。
4.执行构造函数里面的代码。
5.然后返回该对象。
同样的例子如果不用构造函数就是如下这种情况;
let obj = new Object();
createfunc.call(obj,180,65);
由于不同的实例之间是互不相干的,所以为了节约内存,为性能考虑。有更实用的原型模式
提出概念prototype.只要创建一个函数,就会有该属性,是指向原型对象。实际上,该原型对象就是通过构造函数创建的对象的原型。上面定义的属性,方法。可以被实例对象所共享。
如下可以通过修改prototype指向的对象,实现共享方法,属性。
Createfunc.prototype.name = 'heihei'
原型的问题,在引用类型之中,由于共享没有自己的属性。
原型链
来说一些枯燥无味的概念。 这些概念一定要去理解,就算是面试的时候,面试官在意的是你是否真正理解了,而不是你是否背会了。
明确构造函数,原型,实例之间的关系。
构造函数中的prototype属性指向原型对象,实例的__proto__属性指向原型对象。原型对象的constructor指向构造函数。
那么如果一个实例对象A是另一个对象B的原型对象。那么B可以继承A的方法,属性,同时也可以继承A的原型对象的方法和属性,以此类推就构成了原型链。所有引用类型都继承自Object的原型对象,换句话说,Object的原型对象就是原型链顶层。Object.prototype.__proto__ == null;
口说无凭,来看一些实际的例子方便记忆。
例子
代码
function Obj1(){
this.name = 'obj1';
}
//第一个构造函数
function Obj2(){
this.sayname = function(){
this.name
};
}
//第二个构造函数
Obj2.prototype.sayHi = ()=>{
console.log('hi');
}
Obj1.prototype=new Obj2();
//一个原型对象刚好是另一个构造函数的实例,实现原型链
let obj1 = new Obj1();
let obj2 = new Obj2();
//验证
console.log(obj1.sayHi)
图像
可以关注一下黄色的线条,就是该例子的原型链。
实例obj1是没有sayHi方法的,于是编译器顺着原型链找到obj1.__proto__(Obj1的原型对象)发现该对象上也没有sayHi方法,继续向上找obj1.__proto__.__proto__(Obj2的原型对象)发现该对象上有sayHi方法然后执行。
当然,如果还是没找到会一直往上走,直到顶部Object.prototype。
除了用原型链继承之外,还有好几种继承方式,接下来一一介绍。
原型链存在缺点,就是父构造函数上面的引用属性,无法共享
继承
盗用构造函数继承
看如下的代码,首先是盗用构造函数继承,在子类里面,改变this指向调用父类
function Obj1(){ //父类
this.name = 'obj1';
this.color = ['red','blue','white']
}
//第一个构造函数
function Obj2(){//子类
Obj1.call(this)
}
//第二个构造函数
Obj1.prototype.sayHi = ()=>{
console.log('hi');
}
let obj2 = new Obj2()
obj2.color.push('black')
let obj3 = new Obj2()
obj3.color.push('orange')
//验证,每个实例都有自己的引用属性
console.log(obj2.color,obj3.color)
结果:
但是,这样我们也发现,父类原型上的sayHi没有被访问到,这就是盗用构造函数的缺点。
所以下面的组合继承,结合原型链和盗用构造函数的优点去实现继承
组合继承
function Obj1(){ //父类
this.name = 'obj1';
this.color = ['red','blue','white']
}
//第一个构造函数
function Obj2(){//子类
Obj1.call(this)
}
//第二个构造函数
Obj1.prototype.sayHi = ()=>{
console.log('hi');
}
//组合继承特点,结合原型链
Obj2.prototype = new Obj1();
let obj2 = new Obj2()
obj2.color.push('black')
//验证sayHi方法
console.log(obj2.sayHi);
结果
当然,他的缺点也是显而易见的,父类构造函数被调用了两次,父类构造函数本身的属性即存在于子类构造函数本身(call调用),又存在于子类的原型链上。显得有些多余。
原型式继承
主要利用 object.create().
function Obj1(){ //父类
this.name = 'obj1';
this.color = ['red','blue','white']
}
//第一个构造函数
Obj1.prototype.sayHi = ()=>{
console.log('hi');
}
let obj1 = new Obj1();
let obj2 = Object.create(obj1);
//验证
console.log(obj2.color,obj2.name,obj2.sayHi)
结果
寄生式继承
在原型式继承上面,增强对象。
function Obj1(){ //父类
this.name = 'obj1';
this.color = ['red','blue','white']
}
//第一个构造函数
Obj1.prototype.sayHi = ()=>{
console.log('hi');
}
//封装一下Object.create();
function object(obj){
let newobj = Object.create(obj);
newobj.age = 20;
newobj.sayAge = function(){
console.log(this.age)
}
return newobj
}
let obj1 = new Obj1();
let obj2 = object(obj1);
//验证
console.log(obj2.color,obj2.name,obj2.sayAge(),obj2.age)
结果
最后为了解决组合继承的问题,结合寄生式继承,出现了寄生式组合继承
寄生式组合继承
function Obj1(){ //父类
this.name = 'obj1';
this.color = ['red','blue','white']
}
//第一个构造函数
function Obj2(){//子类
Obj1.call(this)
}
//第二个构造函数
Obj1.prototype.sayHi = ()=>{
console.log('hi');
}
//封装一下Object.create();
function object(obj1,obj2){
let newobj = Object.create(obj1.prototype);
newobj.constructor = obj2;
obj2.prototype = newobj;
}
object(Obj1,Obj2);
let obj2 =new Obj2()
//验证sayHi方法
console.log(obj2.sayHi,obj2.name);
结语
希望小伙伴们,都找到心仪的工作,觉得不错点赞哈!