浅谈我对原型链的理解。。。
说到原型链,首先我们要想到三个东西构造函数-实例对象-原型对象。
我们先说说构造函数,之前我们通常创建对象就是var一个对象出来,例如:
var person = {
name: 'zhangsan',
age: 18,
sayName: function () {
console.log(this.name)
}
}
这样创建对象是最简单粗暴的,但是,如果我想创建多个对象呢?有的人会说我多写几个对象不就行了,可是同样的代码重复多次,是我们所有程序员的忌讳,这时候我们可以采用函数的办法来创建我们需要的对象:
//例如我们可以创建一个函数,它是用来创建一个对象的
function createPerson (name, age) {
return {
name: name,
age: age,
sayName: function () {
console.log(this.name)
}
}
}
这个时候你想创建多个对象,你就可以直接使用函数来执行:
var person_1 = creatPerson('zhangsan', 18);
var person_2 = creatPerson('lisi', 18);
var person_3 ......
这样是不是就一目了然了,避免代码冗余的同时,也达到了我们想要的效果。
但是,我们还可以使用一种更为简单粗暴的方法:
function Person (name, age) {
this.name = name
this.age = age
this.sayName = function () {
console.log(this.name)
}
}
var p1 = new Person('zhangsan', 18)
p1.sayName() // => zhangsan
var p2 = new Person('lisi', 18)
p2.sayName() // => lisi
细心的人可能会发现Person()中的代码与creatPerson有以下几点不同之处:
-
没有显示的创建对象
-
直接将属性和方法赋给了
this
对象 -
没有return语句
-
函数名使用的是大写的Person
而我们在使用Person函数时,需要new一个新的实例化对象出来,在new一个实例化对象的时候,执行了创建一个新对象 -- > 将构造函数的作用域赋给新的对象 --> 执行构造函数中的代码 --> 返回新的对象。简单的用代码实现上述过程,如下:
function Person (name, age) {
// 当使用 new 操作符调用 Person() 的时候,实际上这里会先创建一个对象
// var instance = {}
// 然后让内部的 this 指向 instance 对象
// this = instance
// 接下来所有针对 this 的操作实际上操作的就是 instance
this.name = name
this.age = age
this.sayName = function () {
console.log(this.name)
}
// 在函数的结尾处会将 this 返回,也就是 instance
// return this
}
通过上述简单的描述,我们可以得出构造函数和实例对象的关系:在每一个实例对象中同时有一个constructor
属性,该属性指向创建该实例的构造函数,我们可以用代码检测:
//判断P1的constructor属性指向
console.log(p1.constructor === Person) // => true
//判断P2的constructor属性指向
console.log(p2.constructor === Person) // => true
console.log(p1.constructor === p2.constructor) // => true
综上所述,构造函数具有一下特点:
-
构造函数是根据具体的事物抽象出来的抽象模板
-
实例对象是根据抽象的构造函数模板得到的具体实例对象
-
每一个实例对象都具有一个
constructor
属性,指向创建该实例的构造函数
当然了,上述所写的构造函数还有些缺陷,比如造成内存浪费等,在这里我就不过多赘述了,下面我们来说说原型。
在JS中,每一个构造函数都有一个prototype属性,指向另外一个对象。这也就说明这个对象所有的属性和方法都会被构造函数所拥有。
我们可以再写一段代码,还是创建一个人的函数,这次用prototype来做:
function Person (name, age) {
this.name = name
this.age = age
}
console.log(Person.prototype)
Person.prototype.type = 'human'
Person.prototype.sayName = function () {
console.log(this.name)
}
var p1 = new Person('zhansan', 18);
var p2 = new Person('lisi', 18);
console.log(p1.sayName === p2.sayName) // => true
这时候,我们就可以列出构造函数/实例和原型之间的关系:
图示中我们可以理解到,任何一个构造函数都有一个prototype属性,该属性是一个object对象。
构造函数的prototype对象都有一个默认的constructor属性,指向prototype对象所在函数:
function F = (){};
console.log(F.prototype.constructor === F); //true
通过构造函数得到的实例对象内部会包含一个指向构造函数的prototype对象的指针 _proto_ 。
var instance = new F()
console.log(instance.__proto__ === F.prototype) // => true
综上所述,我们对原型可以做出如下总结:
-
任何函数都具有一个
prototype
属性,该属性是一个对象 -
构造函数的
prototype
对象默认都有一个constructor
属性,指向prototype
对象所在函数 -
通过构造函数得到的实例对象内部会包含一个指向构造函数的
prototype
对象的指针__proto__
-
所有实例都直接或间接继承了原型对象的成员
好了,说了这么多,我们来说一下重点,也就是原型链,结合前面对构造函数-实例-原型对象介绍,我们来说一下实例对象是如何访问原型对象中的成员:
每当代码读取某个对象的某个属性时,都会执行一次搜索,目标是具有给定名字的属性
-
搜索首先从对象实例本身开始
-
如果在实例中找到了具有给定名字的属性,则返回该属性的值
-
如果没有找到,则继续搜索指针指向的原型对象,在原型对象中查找具有给定名字的属性
-
如果在原型对象中找到了这个属性,则返回该属性的值
也就是说,在我们调用 person1.sayName()
的时候,会先后执行两次搜索:
-
首先,解析器会问:“实例 person1 有 sayName 属性吗?”答:“没有。
-
”然后,它继续搜索,再问:“ person1 的原型有 sayName 属性吗?”答:“有。
-
”于是,它就读取那个保存在原型对象中的函数。
-
当我们调用 person2.sayName() 时,将会重现相同的搜索过程,得到相同的结果。
而这正是多个对象实例共享原型所保存的属性和方法的基本原理,这也就是所谓的原型链。
下面附上几张图示,方便大家理解:
(以上纯属个人意见,不足之处,还希望大家提出意见和建议)