再也不要被js原型搞懵逼了

508 阅读5分钟

前言

——无可否认,对于越来越卷的今天而言,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)
//如果这里采用原型链,则父类上面的引用类型数据是共享的。
function Obj1(){
    this.name = 'obj1';
    this.color = ['red','blue','white']
}
//第一个构造函数

function Obj2(){
    this.sayname = function(){
         this.name
   };
}
//第二个构造函数

Obj2.prototype.sayHi = ()=>{
  console.log('hi');
}

Obj2.prototype=new Obj1();
//一个原型对象刚好是另一个构造函数的实例,实现原型链
let obj1 = new Obj2();
let obj2 = new Obj2();   
obj.color.push('black')

//验证
console.log(obj2.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);

补充一个遇到实际小例子,注意审题哈

let Fun = function(){
    
}
let fn = new Fun() 
Object.prototype.a= ()=>{
      console.log('a')
}
Function.prototype.b = ()=>{
      console.log('b')
}

fn.a();
fn.b();

试想一下运行以上代码,会有什么输出结果呢?

TypeError: fn.b is not a function

结果就是报错,a是可以的,b就报错。

原因是什么呢,手写一下原型链就可以了

console.log(fn.__proto__ == Fun.prototype ,Fun.prototype.__proto__ == Object.prototype)
//true true
console.log( Fun.__proto__ ==Function.prototype)
//true 
console.log( Fun.prototype.__proto__ == Function.prototype)
//false

你会发现Function.prototype根本没在fn的原型链上

结语

希望小伙伴们,都找到心仪的工作,觉得不错点赞哈!