前言
原型对象和原型链是Javascript中重要的知识点之一,也是面试官经常提及的问题,特别是刚接触Js的“新生代农民工”,一提到原型链,你的脑海里就出现下面这张图:
(现在看不懂不要紧,看完本篇文章后,闭眼你也能画出来,何况理解)
原型链说容易吧,还真不是那么好理解,说难不太难,把上面的图看懂就行,那接下来我们就一起去认识认识它!
在介绍原型和原型链之前,我们先聊一下构造函数、实例对象以及原型对象的关系;
对象
在Javascript中,一切皆对象,所有的对象本质上都是通过new 函数创建的,而所有的函数本质上都是通过new Function创建的。但是对象也是又区别的,可以划分为普通对象和函数对象;(可参考下图)
其实就是通过 new Function() 创建的对象都是函数对象,上图中的Function 、Object 、Foo也都是通过 New Function()创建的,他们属于函数对象;
其他通过new 函数创建的实例对象属于普通对象,其实原型对象也是函数的一个实例,所以也属于普通对象,但是只有一个是例外,Function.prototype这个原型对象是函数对象;
构造函数
其实刚开始我们最常见的创建对象的方式是利用Object或者对象字面量,但是这些方式有明显的缺点,使用同一个接口创建很多对象,会产生大量的重复代码,为了解决这个问题就就开始使用工厂模式创建对象,批量生产对象,这样就方便很多;
function createPerson (name,age,job){
var o = new Object()
o.name = name
o.age = age
o.job = job
o.sayName = function() {
alert(this.name)
}
return o
}
var person1 = createPerson ("张三",29,"前端")
var person2 = createPerson ("李四",30,"测试")
函数createPerson()能够根据接收的参数去构建一个包含所有必要信息的person对象。可以无数次的调用这个函数,而每次都会返回一个包含三个属性和一个方法的对象。工厂函数虽然解决了创建多个相似对象的问题,但却没有解决对象识别的问题(即怎么知道一个对象的类型)所以才出现了构造函数模式;
构造函数也是用来创建特定类型的对象;像上图右侧的Object和Array这样的原生构造函数,在运行时会自动出现在执行环境中,此外我们还可以自定义一些构造函数,从而自定义对象的属性和方法,例如上图左侧的Foo和Person;
function Person (name,age,job){
this.name = name
this.age = age
this.job = job
this.sayName = function() {
alert(this.name)
}
}
var person1 = new Person ("张三",29,"前端")
var person2 = new Person ("李四",30,"测试")
上面的例子中,Person()函数取代了createPerson()函数,构造函数的代码和工厂函数的代码有很大的相似,但是也存在以下不同之处:
- 没有显示的创建对象
- 直接将属性和方法赋给了this对象
- 没有return语句
除了上面的不同外,也要切记以下几点构造函数的特点:
- 构造函数始终都是以一个大写字母开头;
- 创建类的新实例对象,必须使用 new 操作符;
- 创建的不同的实例对象(person1和person2)都有一个constructor属性,改属性指向创建它们的构造函数(Person);即:person1.constructor == Person
- 对象的constructor属性最初是用来标识对象类型的,后来用instanceof操作符检测更靠谱;
创建自定义的构造函数意味着将来可以将它的实例标识为一种特定的类型,而这正是构造函数模式胜过工厂模式的地方;
构造函数模式虽然好用,但是也并非没有缺点,使用构造函数的主要问题就是每个方法都要再每个实例上重新的创建一遍。上面的例子中,person1和person2都有一个名为sayName()的方法,但是两个方法不是同一个Function的实例,不要忘了,js中的函数是对象,因此每定义一个函数,也就是实例化了一个对象,
实例化少量的对象还可以,但是如果要创建上百上万个对象呢,那么sayName()方法也会被重复创建上百上万次,既然上百上万个实例都需要同一个方法,那么能不能实现共享这个方法,只被创建一次,所有实例都能使用,当然有,这个问题就可以通过使用原型模式去解决!
原型对象
在 JavaScript 中,每当定义一个对象时候,对象中都会包含一些预定义的属性。其中每个函数对象都有一个prototype 属性,这个属性指向函数的原型对象。
重点:每个对象都有 _ proto _ 属性,但只有函数对象才有 prototype 属性
那什么是原型对象呢?
请看下面优化过的代码:
function Person (name,age,job){
this.name = name
this.age = age
this.job = job
}
Person.prototype.sayName = function() {
alert(this.name)
}
var person1 = new Person ("张三",29,"前端")
var person2 = new Person ("李四",30,"测试")
原型对象的用途是包含可以由特定类型的所有实例共享的属性和方法。上面代码中,person1和person2 两个不同的实例都能访问到sayName方法;
可以结合下图理解: 由上图也可知,只要创建一个函数,就会根据一组特定的规则为该函数创建一个prototype属性,这个属性指向函数的原型对象;
并且其原型对象都会自动获得一个constructor属性,这个属性是一个指向prototype属性所在函数的指针;
Person.prototype.constructor == Person
其实原型对象也属于实例对象,为什么呢?
person1 为什么有 constructor 属性?那是因为 person1 是 Person 的实例。 那 Person.prototype 为什么有 constructor 属性??同理, Person.prototype 也是Person 的实例。
结论:原型对象(Person.prototype)也是 构造函数(Person)的一个实例;
proto
当调用构造函数创建一个新实例后,高实例对象内部将包含一个指针(内部属性_proto_),指向构造函数的原型对象;
person1._proto_ == Person.prototype
要明确最重要的一点就是:这个连接存在于实例与构造函数的原型对象之间,而不存在于实例与构造函数之间;
我们可以调用person1.sayName()或者person2.sayName(),虽然这两个实例都不包含这个方法,但是可以通过查找对象属性的过程来实现;其实这就是最简单的一条原型链;
原型链
原型链是就是实例对象在查找属性时,如果查找不到,就会沿着__proto__去与对象关联的原型上查找,有则返回,如果找不到,就去找原型的原型,直至查到最顶层Object函数的原型,其原型对象的_proto__已经没有可以指向的上层原型,因此其值为null,返回undefind;
原型链是实现继承的主要方法,其基本思想就是利用原型让一个引用类型继承另一个引用类型的属性和方法; 前面我们已经熟悉了构造函数、原型以及实例的关系,上图所示估计大家都理解了;
可能有的小伙伴说Foo函数与其实例对象的关系我看懂啦,但是它跟Object和Function的关系还是不太清楚,其实我们可以举一反三,他们的关系也离不开上面我们总结的知识点,接下来我们就 一 一 解释 :
为了让你们懂,我还特意在自己画了张图:
首先,函数对象内部有个prototype属性指向其原型对象;原型对象都有一个 constructor属性指向 其构造函数;实例对象内部属性_proto_,指向构造函数的原型对象;也就是下图所示:
然后,每个对象都有 proto 属性;因为函数都是通过new Function()创建的,也相当于算Function函数的实例吧,所以所有函数的原型都是Function函数的原型对象Function.prototype,故Foo()和Objuect()的_proto_属性指向Function函数的原型对象;而Function函数是通过new自己产生的,所以Function函数的_proto_属性指向自身的原型对象;也就是下图所示:
最后,我们都知道,万物皆对象,所有的对象都是Oblect()的实例,而Foo函数的原型和Function函数的原型都属于对象,归根结底都是通过new Object函数产生的,所以它俩的_proto_属性指向Object函数的原型;而Object.prototype也是一个对象,自身也有一个_proto_属性,只不过__proto__已经没有可以指向的上层原型,因此其值为null;也就是下图所示:
到最后成型的图其实也就是一开始大家看到的原型链的图,其实很简单,大家没事也可以自己动手画画,多记,多理解!
以上都是个人自己的理解,如有错误,请留言指出,大家一起学习,一起进步!
|