前端基础6/5-原型链

211 阅读10分钟

原型链

原型链:原型链通俗易懂的理解就是可以把它想象成一个链条,互相连接构成一整串链子!

而原型链中就是实例对象和原型对象之间的链接。 每个函数都有一个prototype属性,这个prototype属性就是我们的原型对象,我们拿这个函数通过new构造函数创建出来的实例对象,这个实例对象自己会有一个指针_proto_指向他的构造函数的原型对象!这样构造函数和实例对象之间就通过 _proto_ 连接在一起形成了一条链子

原型链继承:原型链继承原理:让子类的原型对象指向父项的实例,当子类找不到爹时候,它会沿着原型链找爷爷。

image.png

为什么要使用原型链呢?

1.为了实现继承,简化代码,实现代码重用!

2.只要是这个链条上的内容,都可以被访问和使用到!

使用原型链有什么作用?

1:继承

2:prototype用来实现基于原型的继承与属性的共享

3:避免了代码冗余,公用的属性和方法,可以放到原型对象中,这样,通过该构造函数实例化的所有对象都可以使用该对象的构造函数中的属性和方法!

4:减少了内存占用

原型链的特点

1:就近原则,当我们要使用一个值时,程序会优先查找离自己最近的,也就是本身有没有,如果自己没有,他就会沿着原型链向上查找,如果还没有找到,它还会沿着原型链继续向上查找,找到到达Object

2:引用类型,当我们使用或者修改原型链上的值时,其实使用的是同一个值!

3:JS中每个函数都存在原型对象属性prototype。并且所有函数的默认原型都是Object的实例。

4:每个继承父函数的实例对象都包含一个内部属性_proto_。该属性包含一个指针,指向父函数的prototype。若父函数的原型对象的_proto_属性为再上一层函数。在此过程中就形成了原型链。

//构造函数
function fn(){
	num = 20
}
//为构造函数添加原型对象
fn.prototype.num = 30;
//实例化构造函数
var obj = new fn();
//此时打印出来的obj本身有一个属性num=20,在它的__proto__中有num=30
console.log(obj);
console.log(obj.__proto__.num == fn.prototype.num) 
//上面两个num的比较结果是true,这个就说明了实例对象的__proto__是有关联的,并且obj的__proto__shi 指向构造函数的原型对象的,所以两个值是相等的!

原型链的源头是object。所以,万物皆可对象由此而来

庖丁解牛式分析面试

1:创建对象的几种方法

1-1:字面量创建对象

使用对象字面量可以在创建对象时,直接指定对象中的属性,语法格式:{属性名:属性值,属性名:属性值}

 var obj = { 
 name: "齐天大圣",
 sex: "男",
 like: "打妖怪" } 
 console.log(obj);

1-2:用构造函数创建对象

fucntion Star(name,age){
  this.name=name;
  this,age=age;
  this.sing=function(){
    console.log("我会唱歌")
  }
}
var ldh = new Star("刘德华",20);
ldh.sing();

1-3:new Object


var obj2 = new Object();

2:原型,构造函数,实例,原型链

2-1:constructor 和 prototype 以及实例之间啥关系?

当我们创建一个函数,系统就会为这个函数自动分配一个prototype指针,指向它的原型对象。并且可以发现,这个原型对象包含两个部分(constructor_proto_如图,所以下划线有特殊含义,打不出来。请自行脑补proto左右都有下划线。其中constructor指向函数自身。(这里形成了一个小闭环)

当我们将该函数作为模版创建实例(new方法)的时候,我们发现创建出的实例是一个与构造函数同名的object,这个object是独立的,他只包含了一个 _proto_ 指针(实例没有prototype,强行访问则会输出undefined),这个指针指向上面提到的构造函数的prototype原型对象。

这时候我们发现三者形成了一个大"闭环"。之所以加上引号,因为构造函数和实例之间无法直接访问,需要通过 _proto_ 指针间接读取。

image.png

2-2:prototype是啥,_proto_又是啥,他们之间啥关系?

在JavaScript中大部分类型的值都拥有_proto_属性

image.png

image.png

image.png

object和function对象也有:

不过也有不存在__proto___属性的类型,比如:

然而。只有function对象才有prototype属性,其他任何类型的值都没有。即使是使用new方法从function构造出的实例对象也没有prototype属性。

image.png (object类型的值的prototype输出undefined) image.png 改变了test的prototype的值,将其链接到一个函数名为test的函数,接着,函数类型的值的prototype输出了一个原型对象

2-3:如果改变一个 constructor 的 prototype,他的实例会发生什么改变?

image.png

改变了prototype之后创建的实例指向了新的prototype对象,而之前的依然指向老的prototype对象。

原型链:原型链通俗易懂的理解就是可以把它想象成一个链条,互相连接构成一整串链子! 而原型链中就是实例对象和原型对象之间的链接。每个函数都有一个prototype属性,这个prototype属性就是我们的原型对象,我们拿这个函数通过new构造函数创建出来的实例对象,这个实例对象自己会有一个指针(proto)指向他的构造函数的原型对象!这样构造函数和实例对象之间就通过( proto )连接在一起形成了一条链子

原型链继承:原型链继承原理:让子类的原型对象指向父项的实例,当子类找不到爹时候,它会沿着原型链找爷爷。

image.png

总结_proto_和prototype的区别

_proto_ :是实例对象指向原型对象的指针,隐式原型,是每个对象都会有的一个属性。

prototype:是构造函数的原型对象,显式原型,只有函数才会有。

3:instanceof的原理

原理:判断实例对象的_proto_属性和构造函数的prototype属性是不是引用同一地址 但用constructor比instanceof更严谨。 image.png

4:new运算符

以下内容引自饥人谷-方应杭

大部分讲 new 的文章会从面向对象的思路讲起,但是我始终认为,在解释一个事物的时候,不应该引入另一个更复杂的事物。

今天我从「省代码」的角度来讲 new。


想象我们在制作一个策略类战争游戏,玩家可以操作一堆士兵攻击敌方。

我们着重来研究一下这个游戏里面的「制造士兵」环节。

一个士兵的在计算机里就是一堆属性,如下图:

我们只需要这样就可以制造一个士兵:

var 士兵 = {
  ID: 1, // 用于区分每个士兵
  兵种:"美国大兵",
  攻击力:5,
  生命值:42, 
  行走:function(){ /*走俩步的代码*/},
  奔跑:function(){ /*狂奔的代码*/  },
  死亡:function(){ /*Go die*/    },
  攻击:function(){ /*糊他熊脸*/   },
  防御:function(){ /*护脸*/       }
}

兵营.制造(士兵)

制造一百个士兵

如果需要制造 100 个士兵怎么办呢?

循环 100 次吧:

var 士兵们 = []
var 士兵
for(var i=0; i<100; i++){
  士兵 = {
    ID: i, // ID 不能重复
    兵种:"美国大兵",
    攻击力:5,
    生命值:42, 
    行走:function(){ /*走俩步的代码*/},
    奔跑:function(){ /*狂奔的代码*/  },
    死亡:function(){ /*Go die*/    },
    攻击:function(){ /*糊他熊脸*/   },
    防御:function(){ /*护脸*/       }
  }
  士兵们.push(士兵)
}

兵营.批量制造(士兵们)

哎呀好简单。

质疑

上面的代码存在一个问题:浪费了很多内存。

  1. 行走、奔跑、死亡、攻击、防御这五个动作对于每个士兵其实是一样的,只需要各自引用同一个函数就可以了,没必要重复创建 100 个行走、100个奔跑……

  2. 这些士兵的兵种和攻击力都是一样的,没必要创建 100 次。

  3. 只有 ID 和生命值需要创建 100 次,因为每个士兵有自己的 ID 和生命值。

改进

用原型链可以解决重复创建的问题:我们先创建一个「士兵原型」,然后让「士兵」的 __proto__ 指向「士兵原型」

var 士兵原型 = {
  兵种:"美国大兵",
  攻击力:5,
  行走:function(){ /*走俩步的代码*/},
  奔跑:function(){ /*狂奔的代码*/  },
  死亡:function(){ /*Go die*/    },
  攻击:function(){ /*糊他熊脸*/   },
  防御:function(){ /*护脸*/       }
}
var 士兵们 = []
var 士兵
for(var i=0; i<100; i++){
  士兵 = {
    ID: i, // ID 不能重复
    生命值:42
  }

  /*实际工作中不要这样写,因为 __proto__ 不是标准属性*/
  士兵.__proto__ = 士兵原型 

  士兵们.push(士兵)
}

兵营.批量制造(士兵们)

优雅?

有人指出创建一个士兵的代码分散在两个地方很不优雅,于是我们用一个函数把这两部分联系起来:

function 士兵(ID){
  var 临时对象 = {}

  临时对象.__proto__ = 士兵.原型

  临时对象.ID = ID
  临时对象.生命值 = 42
  
  return 临时对象
}

士兵.原型 = {
  兵种:"美国大兵",
  攻击力:5,
  行走:function(){ /*走俩步的代码*/},
  奔跑:function(){ /*狂奔的代码*/  },
  死亡:function(){ /*Go die*/    },
  攻击:function(){ /*糊他熊脸*/   },
  防御:function(){ /*护脸*/       }
}

// 保存为文件:士兵.js

然后就可以愉快地引用「士兵」来创建士兵了:

var 士兵们 = []
for(var i=0; i<100; i++){
  士兵们.push(士兵(i))
}

兵营.批量制造(士兵们)

JS 之父的关怀

JS 之父创建了 new 关键字,可以让我们少写几行代码:

只要你在士兵前面使用 new 关键字,那么可以少做四件事情:

  1. 不用创建临时对象,因为 new 会帮你做(你使用「this」就可以访问到临时对象);

  2. 不用绑定原型,因为 new 会帮你做(new 为了知道原型在哪,所以指定原型的名字为 prototype);

  3. 不用 return 临时对象,因为 new 会帮你做;

  4. 不要给原型想名字了,因为 new 指定名字为 prototype。

这一次我们用 new 来写

function 士兵(ID){
  this.ID = ID
  this.生命值 = 42
}

士兵.prototype = {
  兵种:"美国大兵",
  攻击力:5,
  行走:function(){ /*走俩步的代码*/},
  奔跑:function(){ /*狂奔的代码*/  },
  死亡:function(){ /*Go die*/    },
  攻击:function(){ /*糊他熊脸*/   },
  防御:function(){ /*护脸*/       }
}

// 保存为文件:士兵.js

然后是创建士兵(加了一个 new 关键字):

var 士兵们 = []
for(var i=0; i<100; i++){
  士兵们.push(new 士兵(i))
}

兵营.批量制造(士兵们)

new 的作用,就是省那么几行代码。(也就是所谓的语法糖)

注意 constructor 属性

new 操作为了记录「临时对象是由哪个函数创建的」,所以预先给「士兵.prototype」加了一个 constructor 属性:

士兵.prototype = {
  constructor: 士兵
}

如果你重新对「士兵.prototype」赋值,那么这个 constructor 属性就没了,所以你应该这么写:

士兵.prototype.兵种 = "美国大兵"
士兵.prototype.攻击力 = 5
士兵.prototype.行走 = function(){ /*走俩步的代码*/}
士兵.prototype.奔跑 = function(){ /*狂奔的代码*/  }
士兵.prototype.死亡 = function(){ /*Go die*/    }
士兵.prototype.攻击 = function(){ /*糊他熊脸*/   }
士兵.prototype.防御 = function(){ /*护脸*/       }

或者你也可以自己给 constructor 重新赋值:

士兵.prototype = {
  constructor: 士兵,
  兵种:"美国大兵",
  攻击力:5,
  行走:function(){ /*走俩步的代码*/},
  奔跑:function(){ /*狂奔的代码*/  },
  死亡:function(){ /*Go die*/    },
  攻击:function(){ /*糊他熊脸*/   },
  防御:function(){ /*护脸*/       }
}

完。