前端知识

原型链1:原型与原型链

2019-04-26  本文已影响37人  如梦初醒Tel

Why???

在上面写了一篇博文介绍了普通类型和对象的区别。下面就深究一下原理。

公用属性(原型)

什么是公共属性?
__proto__:object //__proto__这个属性不用写就有,每一个对象的 __proto__存储「公用属性组成的对象」的地址。

所有对象都有 toString 和 valueOf 属性,那么我们是否有必要给每个对象一个 toString 和 valueOf 呢?

看图理解JS标准库里几个构造函数之间的关系

image.png

每个对象都有toString()valueOf()函数,如果每个对象里都存这样的相同的函数,那会浪费内存.
问题解决方法:原型链

toString()valueOf()等函数单独存放在一个对象里,原来对象里的toString()valueOf()只存地址,指向这个地址

image.png image.png

JS 的做法是把 toString 和 valueOf 放在一个对象里(暂且叫做公用属性组成的对象)
然后让每一个对象的 __proto__存储这个公用属性组成的对象的地址。

image.png image.png

通过四个全局对象来了解原型和原型链
这四个全局对象为:Number()、String()、Boolean()、Object()。
上述四个对象都可以通过下边两种方法实现:

var n1=1
var n2=new Number(1)
var s1='sss'
var s2=new String('sss')
var b1=true
var b2=new Boolean(true)
var o1={}
var o2=new Object()

他们的区别在于内存:

1.toString()会报错,但是这里我们通过n1.toString()返回的就是字符串'1'n1.toString()方法不会报错,但是n1只是通过赋值的方法的来的,并没有.toString()方法。这个时候就是JS通过创建临时对象tempvar temp=new Number(1)在这个临时对象中对n1的值进行.toString()方法,再将得到的返回给n1.toString(),临时对象用完就自动抹杀,被垃圾回收。
在上面的博文中,曾写过:我们对n1进行n1.xxx=2这会不会报错,结果是不会报错。原因就是用临时方法在heap内存上创建的,但是再取n1.xxx时为undefined。就验证了用完就抹杀,即不存在的原理。
为什么new Number().toString()方法呢?我们打印出n2看看他有没有这个属性。

image.png

可以看到除了打印出n2的值还有一个__proto__:Number,点开这个属性可以看到很多属性,其中就包括.toString(),所以n2可以调用.toString()方法。
__proto__:Number所表示的就是number共有的属性。

从上面的图中发现,在这个__proto__:Number中还有一个__protp__属性。

image.png image.png

此时在图中我们看到n2.__proto__===Number.prototype的结果为true。就是指n2__proto__指向了Number.prototype,即Number的共有属性中去,可以从第一张图中可以看到n2的__proto__中还有一个__proto__:Object的隐藏属性。点开就可以看到关于Object的共有属性。

根据同样的方法,依次获取String,Boolean,Object,发现:

image.png image.png

综上总结:
以上四个全局函数构造的对象都有一些公有的属性,比如:toString()、valueOf()。不可能每声明一个对象都在对象里加上这些公有属性,这样内存太浪费,Js的做法是把共用属性放在一起,然后每个对象都有一个键名为proto的键,它的值就是这些共用属性所在的内存地址。
但是Number、String和Boolean这三个全局函数都有一些自己的属性,比如Number函数有toFixed()、toExponential()、toString(16)等等这些只有Number函数才有的属性,并不属于Object共有的属性。所以Js的方法是在Number对象的proto中放Number共有属性的地址,在Number共有属性对象的proto中放对象共有属性的地址,对象Object的proto中放的是Null,如下图所示:

image.png

我们可以总结出来,当我们new出来一个对象的时候,再去使用一个方法时,会首先从它自身的构造函数(或者系统自带函数)的原型对象(函数.prototype)中去找,如果没有改属性是就会通过__proto__属性去下一个Object共有属性中去寻找,直到最后为null

image.png
var n = new Number();
n.__proto__===Number.prototype//true
n.__proto__.__proto__===Object.prototype//true
n.__proto__.__proto__.__proto__===null//true

流程是这样的

  1. 浏览器先看n是不是对象,不是做临时转换。
  2. 是的话就先去看看n对应的函数公共类型里找有没有toString()这个操作符
  3. 如果没有,就进入prote对应的公共属性里找有没有toString()操作符
  4. 有的话,就调用这个toString()

这样的,形成的穿过多个节点的流程,就叫原型链
原型===共有属性

image.png

当解析一句代码时,如果这个不是对象,就包装成一个对象,自动生成__proto__

所有对象的公有属性(也叫原型): Object.prototype;

所有number的公有属性(也叫原型): Number.prototype

所有string的公有属性(也叫原型): String.prototype

所有boolean的公有属性(也叫原型): Boolean.prototype

image.png

prototype__proto__的区别:前者的函数的属性,后者是对象的属性。
__proto__prototype的关系
[__proto__指向prototype]

内存图理解

var o1 = new Number(8)
var o2 = 8
o1 === o2 // false
o1.toString === o2.toString  //  true
因为他们调用的方法是一样的
image.png
o1 是一个对象,在内存中的地址是125
o2 不是对象,是Number类型的
所以在第三行语句中 o1 ===o2 答案是false
但是在第四行的时候,o1 使用了o1.toString()的时候,他会自动生成一个temp的对象,并且去调用。所以它会在内存中生成一个地址为50的对象,但是因为125地址和50地址都是共有属性中的toString() ,所以最后他们的结果都是true

重要公式-原型与原型链

无代码的时候,即为下面这样,浏览器已经将其初始化好了.

原型图

可以看到prototype用来指向这些共有属性的,不然这些共有属性就被垃圾回收了,所以要用一个线来牵引着.

写代码之后

image.png

在看这张图前,我们先考虑一下浏览器的垃圾回收机制,垃圾回收会回收没有被引用的对象。
这些全局对象如果不被引用的话,就会被浏览器回收掉,如何避免这些全局属性被回收掉呢?
浏览器在打开的时候,就会创建一个名为window的全局属性,这个全局属性包含了所有全局函数的地址值。
这些地址又引用了该函数特有的公共属性(对象),每个函数特有的公共属性(对象)又用_prote_来储存Object对象的地址

所以:

共同点就是都是公共属性的引用.

var o1 = {};

o1.__proto__ ===  Object.prototype//true
Number.prototype.__proto__===Object.prototype//true
var s1 = new String('s1');

s1.__proto__ === String.prototype//true
image.png
String.prototype.__proto__ ===Object.prototype//true

function DOG(name){
    this.name = name;
}
DOG.prototype.__proto__ === Object.prototype//true

对象与构造函数

image.png
var 对象 = new 函数()
对象.__proto__ === 对象的构造函数.prototype

形式

总结的式子

由此,我们可以得出一个公式,结合原型解释图理解

image.png
var number = new Number()
number.__proto__ = Number.prototype
Number.__proto__ = Function.prototype // 因为 Number 是 Function 的实例

var object = new Object()
object.__proto__ = Object.prototype
Object.__proto__ = Function.prototype // 因为 Object 是 Function 的实例

var function = new Function()
function.__proto__ = Function.prototype
Function.__proto__ == Function.prototye // 因为 Function 是 Function 的实例!
image.png 推论图

只有函数才能有prototype
两个属性对比:
共同点:存的地址相同,都指向同一个对象
不同点:一个是对象的属性,一个是函数的属性

面试题

  1. '1'.__proto__
    答:'1'会创建一个临时String对象,然后指向String.prototype
  2. 函数.prototype 是一个对象,那么
var obj = 函数.prototype;
obj.__proto__ === Object.prototype;//true
函数.prototype.__proto__ === Object.prototype;//true

成立(可以看上图无代码的时候)

Number.prototype.__proto__ === Object.prototype
//true
String.prototype.__proto__ === Object.prototype
//true
Boolean.prototype.__proto__ === Object.prototype
//true

JS 原型是什么?

答:举例
var a = [1,2,3]
只有0、1、2、length 4 个key
为什么可以 a.push(4) ,push 是哪来的?
a.__proto__ === Array.prototype(a是实例数组对象,Array是构造函数)
push函数 就是沿着 a.__proto__找到的,也就是 Array.prototype.push
Array.prototype 还有很多方法,如 join、pop、slice、splice、concat
Array.prototype 就是 a 的原型(proto)

举完例子后用new对象举例,说给面试官听:
比如说

  1. 我们新创建一个构造函数
function Person() {}
  1. 然后根据构造函数构造一个新对象
var person1 = new Person();
  1. 每个函数都有一个 prototype 属性,这个构造函数的 prototype 属性指向了一个对象,这个对象正是调用该构造函数而创建的实例的原型。
  2. 当我们给Person的prototype的name属性赋值为'Kevin'
Person.prototype.name = 'Kevin';
var person1 = new Person();
var person2 = new Person();
console.log(person1.name) // Kevin
console.log(person2.name) // Kevin

每一个新的实例对象对象都会从原型"继承"属性,实例对象拥有该原型的所有属性。

  1. 说白了,原型就是 构造函数 用来 构造 新实例 的 模板对象。
  2. 这就是原型。

开始解释原型链
那么我们该怎么表示实例与实例原型,也就是 person1 和 Person.prototype 之间的关系呢,这时候我们就要讲到第二个属性__proto__

什么是原型链?

参考这篇博文什么是 JS 原型链?
先回答什么是原型。在上面,然后继续从proto开始往下说。
说:
JavaScript对象除了 null 都具有的一个属性,叫__proto__,这个属性会指向该对象的原型对象。
当读取实例的属性时,如果找不到,就会通过__proto__查找原型中的属性,如果还查不到,就去找原型的原型。
例如Person.prototype这个原型的原型就是Object这个构造函数的prototype,既Object.prototype这个原型对象。然后,Person.prototype.__proto__就指向Object.prototype这个原型。然后Object.prototype原型是null
这些原型对象通过__proto__像链子一样连起来,就叫做原型链。
然后给面试官画:

链子上都画上__proto__
person1----->Person.prototype----->Object.prototype----->null

Array.prototype----->Object.prototype----->null

上一篇 下一篇

猜你喜欢

热点阅读