Why???
在上面写了一篇博文介绍了普通类型和对象的区别。下面就深究一下原理。
公用属性(原型)
什么是公共属性?
__proto__
:object //__proto__
这个属性不用写就有,每一个对象的 __proto__
存储「公用属性组成的对象」的地址。
所有对象都有 toString 和 valueOf 属性,那么我们是否有必要给每个对象一个 toString 和 valueOf 呢?
看图理解JS标准库里几个构造函数之间的关系
每个对象都有toString()
和valueOf()
函数,如果每个对象里都存这样的相同的函数,那会浪费内存.
问题解决方法:原型链
把toString()
和valueOf()
等函数单独存放在一个对象里,原来对象里的toString()
和valueOf()
只存地址,指向这个地址
JS 的做法是把 toString 和 valueOf 放在一个对象里(暂且叫做公用属性组成的对象)
然后让每一个对象的 __proto__
存储这个公用属性组成的对象的地址。
通过四个全局对象来了解原型和原型链
这四个全局对象为: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看看他有没有这个属性。
可以看到除了打印出n2的值还有一个__proto__:Number
,点开这个属性可以看到很多属性,其中就包括.toString()
,所以n2可以调用.toString()
方法。
__proto__:Number
所表示的就是number共有的属性。
从上面的图中发现,在这个__proto__:Number
中还有一个__protp__
属性。
此时在图中我们看到n2.__proto__===Number.prototype
的结果为true
。就是指n2
的__proto__
指向了Number.prototype
,即Number的共有属性中
去,可以从第一张图中可以看到n2的__proto__
中还有一个__proto__:Object
的隐藏属性。点开就可以看到关于Object的共有属性。
根据同样的方法,依次获取String,Boolean,Object,发现:
综上总结:
以上四个全局函数构造的对象都有一些公有的属性,比如:toString()、valueOf()。不可能每声明一个对象都在对象里加上这些公有属性,这样内存太浪费,Js的做法是把共用属性放在一起,然后每个对象都有一个键名为proto
的键,它的值就是这些共用属性所在的内存地址。
但是Number、String和Boolean这三个全局函数都有一些自己的属性,比如Number函数有toFixed()、toExponential()、toString(16)等等这些只有Number函数才有的属性,并不属于Object共有的属性。所以Js的方法是在Number对象的proto中放Number共有属性的地址,在Number共有属性对象的proto中放对象共有属性的地址,对象Object的proto中放的是Null,如下图所示:
我们可以总结出来,当我们new
出来一个对象的时候,再去使用一个方法时,会首先从它自身的构造函数(或者系统自带函数)的原型对象(函数.prototype
)中去找,如果没有改属性是就会通过__proto__
属性去下一个Object共有属性
中去寻找,直到最后为null
。
var n = new Number();
n.__proto__===Number.prototype//true
n.__proto__.__proto__===Object.prototype//true
n.__proto__.__proto__.__proto__===null//true
流程是这样的
- 浏览器先看n是不是对象,不是做临时转换。
- 是的话就先去看看n对应的函数公共类型里找有没有toString()这个操作符
- 如果没有,就进入prote对应的公共属性里找有没有toString()操作符
- 有的话,就调用这个toString()
这样的,形成的穿过多个节点的流程,就叫原型链
原型===共有属性
当解析一句代码时,如果这个不是对象,就包装成一个对象,自动生成__proto__
。
所有对象的公有属性(也叫原型): Object.prototype;
所有number的公有属性(也叫原型): Number.prototype
所有string的公有属性(也叫原型): String.prototype
所有boolean的公有属性(也叫原型): Boolean.prototype
prototype
和__proto__
的区别:前者的函数的属性,后者是对象的属性。
__proto__
与prototype
的关系
[__proto__
指向prototype
]
内存图理解
var o1 = new Number(8)
var o2 = 8
o1 === o2 // false
o1.toString === o2.toString // true
因为他们调用的方法是一样的
o1 是一个对象,在内存中的地址是125
o2 不是对象,是Number类型的
所以在第三行语句中 o1 ===o2 答案是
false
但是在第四行的时候,o1 使用了
o1.toString()
的时候,他会自动生成一个temp的对象,并且去调用。所以它会在内存中生成一个地址为50的对象,但是因为125地址和50地址都是共有属性中的toString() ,所以最后他们的结果都是true
重要公式-原型与原型链
无代码的时候,即为下面这样,浏览器已经将其初始化好了.
可以看到prototype
是用来指向这些共有属性的,不然这些共有属性就被垃圾回收了,所以要用一个线来牵引着.
写代码之后
在看这张图前,我们先考虑一下浏览器的垃圾回收机制,垃圾回收会回收没有被引用的对象。
这些全局对象如果不被引用的话,就会被浏览器回收掉,如何避免这些全局属性被回收掉呢?
浏览器在打开的时候,就会创建一个名为window
的全局属性,这个全局属性包含了所有全局函数
的地址值。
这些地址又引用了该函数特有的公共属性
(对象),每个函数特有的公共属性(对象)又用_prote_
来储存Object对象的地址
所以:
-
prototype
是浏览器开始就准备好了的,用来防止共有属性被垃圾回收的, -
__proto__
是在开始写代码的时候用来引用共有函数的. -
String.prototype
是String的公用属性的引用,是JS在初始化的时候就已经存在的,用他是因为如果不用他,那么公用属性就跑了,被垃圾回收了
*'s'.__proto__
是String
的公用属性的引用,是在声明新对象的时候存在的,有他是因为我要用他,用公用属性
共同点就是都是公共属性的引用.
var o1 = {};
o1.__proto__ === Object.prototype//true
Number.prototype.__proto__===Object.prototype//true
var s1 = new String('s1');
s1.__proto__ === String.prototype//true
String.prototype.__proto__ ===Object.prototype//true
function DOG(name){
this.name = name;
}
DOG.prototype.__proto__ === Object.prototype//true
对象与构造函数
var 对象 = new 函数()
对象.__proto__ === 对象的构造函数.prototype
形式
总结的式子
由此,我们可以得出一个公式,结合原型解释图理解
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 的实例!
只有函数才能有prototype
两个属性对比:
共同点:存的地址相同,都指向同一个对象
不同点:一个是对象的属性,一个是函数的属性
面试题
-
'1'.__proto__
答:'1'会创建一个临时String对象,然后指向String.prototype
-
函数.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对象举例,说给面试官听:
比如说
- 我们新创建一个构造函数
function Person() {}
- 然后根据构造函数构造一个新对象
var person1 = new Person();
- 每个函数都有一个 prototype 属性,这个构造函数的 prototype 属性指向了一个对象,这个对象正是调用该构造函数而创建的实例的原型。
- 当我们给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
每一个新的实例对象对象都会从原型"继承"属性,实例对象拥有该原型的所有属性。
- 说白了,原型就是 构造函数 用来 构造 新实例 的 模板对象。
- 这就是原型。
开始解释原型链
那么我们该怎么表示实例与实例原型,也就是 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