为了能够更好的理解原型,我们首先要了解一下原型的本质:
在我看来原型的本质就是按照某种规则创建一个新的对象,这个对象拥有可以复用的属性和方法。那我们先看看都有哪些方法能够创建对象,以及他们之间的渐进式关系,这样才能够真正的理解原型存在的意义。
创建对象
不必多说,创建对象可以通过对象字面量以及直接调用Object构造函数创建实例对象。
var a = {
// 很多方法和属性的集合
};
var b = new Object();
这种方式创建对象有一个问题:使用一个接口创建单个对象,会出现很多大量的重复代码。
由于EMCA中没有类,于是就发明了一种函数,用来封装以特定接口创建对象的细节,也就是构造函数的前身。
工厂模式
工厂模式是广为人知的设计模式,通过工厂模式抽象出来创建对象的具体过程。
function factory (name,age) {
var o = new Object() ;
o.name = name;
o.age = age;
return o;
}
虽然解决了多个相似对象的问题,但是没有解决对象识别问题。随着JavaScript发展,又诞生一个新的模式
构造函数模式
不仅可以通过原生构造函数创建对象,还可以创建自定义的构造函数
function Person(name,age){
this.name = name;
this.age = age;
}
var person1 = new Person("su",23);
var person2 = new Person("su",23);
通过这种方式调用构造函数实际上会经历四个步骤
- 创建一个新对象
- 将构造函数的作用域赋给新对象(修改this,原型)
- 执行构造函数的代码
- 返回新对象
通过Person创建出来的实例对象,拥有一个constructor
属性,该属性指向Person函数,本质是标识对象类型。
但是检测对象类型还是使用instanceof
更加靠谱,是因为constructor
属性是可以更改的属性。
通过构造函数模式创建的对象,能够解决工厂模式无法解决对象识别的问题。但构造函数也不是完美的,他的主要问题是方法不能重用,导致重复内存开销。
function Test () {
this.fn = function () {
}
}
var test1 = new Test();
var test2 = new Test();
test1.fn == test2.fn // false;
很明显,我们可以将复用函数抽离出来放在全局对象上。但是如果这样操作,那全局对象就点唠了。不安全、查找性能下降、毫无封装性等等诸多问题。
合计来合集去,那我们创建一个对象叫做原型吧。把所有重复使用的方法,属性都放在这个叫做原型的对象上。由此诞生了原型模式。
原型模式
无论什么时候,只要创建了一个新函数,就会根据一组特定的规则为该函数创建一个 prototype 属性
,这个属性是一个指针,指向函数的原型对象。原型对象是包含由特定类型的所有实例
共享的属性和方法。
顺着上面我们理解的思路,可以理解为
function Test () {
}
var prototypeObj = {
fn : function () {}
}
Test.prototype = prototypeObj;
既然说到原型了,那我们就来一起回顾一下原型是什么,先来回顾一下关键的几个概念。
一图胜千言

prototype
、constructor
、[[prototype]]
、以及__proto__
分别指什么?
-
prototype
我们创建的每个函数都有一个 prototype(原型)属性,这个属性是一个指针,指向一个对象(原型对象) 如果按照字面意思来理解,那么 prototype 就是通过调用构造函数而创建的那个对象实例的原型对象。 -
constructor
在默认情况下,所有原型对象都会自动获得一个constructor
(构造函数)属性,这个属性包含一个指向prototype 属性
所在函数的指针 -
[[prototype]]
ECMA-262 第 5 版中管这个指针叫[[Prototype]]
-
_proto_ 在脚本中 没有标准的方式访问
[[Prototype]]
, 但 Firefox、Safari 和 Chrome 在每个对象上都支持一个属性__proto__
注意:这个连接存在于实例与构造函数的原型对象之间,而不是存在于实例与构造函数之间
细节
- 搜索 目标: 目标是具有给定名字的属性 过程: 原型链 本质: 这正是多个对象实例共享原型所保存的属性和方法的基本原理
- 原型最初只包含
constructor
属性,而该属性也是共享的,因此可以通过对象实例访问 - 虽然可以通过对象实例访问保存在原型中的值,但却不能通过对象实例重写原型中的值(遮蔽效果)
- 用一个包含所有属性和方法的对象字面量来重写整个原型对象
本质上完全重写了默认的 prototype 对象,因此
constructor
属性也就变成了新对象的constructor
属性(指向 Object 构造函数),不再指向原函数。 注意: 以这种方式(constructor : xxx,) 重设 constructor 属性会导致它[[Enumerable]]特性被设置为 true(原生的 constructor 属性是不可枚举的)可以通过属性描述符来重设 constructor属性
原型模式的问题
它省略了构造函数传递参数的过程,但是所有实例默认情况下都取得相同的属性和方法。一旦他的属性中包含引用值,那么问题是很严重的。
function Person() {}
Person.prototype = {
arr : [1,2,3];
}
var person1 = new Person();
var person2 = new Person();
person1.arr.push(1);
person2.arr; // [1,2,3,1];
因此我们通常使用配合构造函数一起使用,构造函数负责定义实例属性,原型模式用于定义方法和共享的属性。
继承
实现继承主要依靠的是原型链。
原型链继承
其基本思想是利用原型让一个引用类型继承另一个引用类型的属性和方法
简单回顾一下构造函数、原型和实例的关系:每个构造函数都有一个原型对象,原型对象都包含一个指向构造函数的指针,而实例都包含一个指向原型对象的内部指针。那么,假如我们让原型对象等于另一个类型的实例,结果会怎么样呢?显然,此时的原型对象将包含一个指向另一个原型的指针,相应地,另一个原型中也包含着一个指向另一个构造函数的指针。假如另一个原型又是另一个类型的实例,那么上述关系依然成立,如此层层递进,就构成了实例与原型的链条。
function Super(){
this.property = true;
}
Super.prototype.getSuperValue = function () {
return this.property;
}
function Sub () {
this.subproperty = false;
}
Sub.prototype = new Super();
Sub.prototype.getSubValue = function () {
return this.subproperty;
}
原型链继承模式存在的问题:
- 原型中引用类型值将会被所有实例共享
function Super(){
this.property = true;
this.firends = ["Jack","Mark"]
}
function Sub () {
this.subproperty = false;
}
//Sub继承自Super.
Sub.prototype = new Super();
var instance1 = new Sub();
instance1.firends.push("Lee"); //["Jack", "Mark", "Lee"]
var instance2 = new Sub();
instance2.firends.push("Mery"); //["Jack", "Mark", "Lee", "Mery"]
- 在创建子类型的实例时,不能向超类型的构造函数中传递参数(在不影响所有实例的情况下)
借用构造函数
也称之为经典继承 基本思想是在子类构造函数内部调用超类型构造函数
function Super(){
this.property = true;
this.firends = ["Jack","Mark"];
this.name = name;
}
Super.prototype.getSuperValue = function () {
return this.property;
}
function Sub (name) {
Super.call(this,name)
}
var instance1 = new Sub("su");
instance1.firends.push("Lee"); //["Jack", "Mark", "Lee"]
var instance2 = new Sub("su1");
instance2.firends.push("Mery"); //["Jack", "Mark", , "Mery"]
console.log(instance1.getSuperValue);//undefined;
instance1.name // "su"
isntance2.name // "su1"
- 优点以及缺点
- 引用类型实例不会被共享
- 优点是能够向超类型构造函数传递参数,在不影响所有实例的情况下。
- 缺点是和构造函数模式一样,无法通过原型对象进行函数复用。
组合继承
有时候也叫做伪经典继承,指的是将原型链和借用构造函数的技术组合到一块,从而发挥二者之长的一种继承模式。 基本思想是使用原型链实现对原型属性和方法的继承,通过借用构造函数方式实现对实例属性的继承。
var count = 0;
function Super(){
this.property = true;
this.firends = ["Jack","Mark"];
count ++;
}
Super.prototype.getSuperValue = function () {
return this.property;
}
function Sub () {
// 继承构造函数中的属性
Super.call(this)
}
// 继承原型中的函数
Sub.prototype = new Super();
count; // 1
Sub.prototype.getSubValue = function () {
return this.subproperty;
}
var instance1 = new Sub();
instance1.firends.push("Lee"); //["Jack", "Mark", "Lee"]
var instance2 = new Sub();
instance2.firends.push("Mery"); //["Jack", "Mark", "Lee", "Mery"]
count; // 3
存在的问题: 无论什么情况下,都会调用至少两次超类型构造函数:一次是在创建子类型原型的时候,另一次是在子类型构造函数内部(本例中是实例化对象instance1和实例化对象instance2时共3次)。
原型式继承
道格拉斯·克罗克福德在 2006年写了一篇文章,他介绍了一种实现继承的方法,这种方法并没有使用严格意义上的构造函数。 基本思想借助原型可以基于已有的对象创建新对象,同时还不必因此创建自定义类型。
function createObj (origin) {
function F () {};
F.prototype = origin;
return new F();
}
var Father = {
name : "Father",
favorite : ["smoking","fishing"],
}
var Son = createObj(Father);
Son.favorite.push("FE");
Son.favorite; //["smoking", "fishing", "FE"]
Son.name; // Father;
Son.name = "Son";
Father.favorite; //["smoking", "fishing", "FE"]
Father.name; // "Father";
存在的问题:
由于createObj()
对传入的对象执行了一次浅复制,导致包含引用类型值的属性始终都会共享相应的值,就像使用原型模式一样。
ECMAScript5通过新增Object.create()
方法规范化了原型式继承。
这个方法接收两个参数:
- 一个用作新对象原型的对象(可选的)
- 一个为新对象定义额外属性的对象。
在传入一个参数的情况下,Object.create()
与 object()
方法的行为相同。
Object.create()
并没有改良共享指针的问题。
应用场景:
只想让一个对象与另一个对象保持原型继承关系的情况下。
寄生式继承
寄生式继承是与原型式继承紧密相关的一种思路,并且同样也是由克罗克福德推而广之的。
基本思想:创建一个仅用于封装继承过程的函数,该函数在内部以某种方式来增强对象,最后再像真地是它做了所有工作一样返回对象。
function createAnother(original) {
var clone = object(original);
clone.sayHi = function () {
alert ("对象增强")
}
}
存在的问题: 使用寄生式继承来为对象添加函数,会由于不能做到函数复用而降低效率;这一点与构造函数模式类似。
不明白为什么要创造寄生式继承,难道说是为了更便于阅读?
寄生组合式继承
YUI3的圣杯继承
本质:使用寄生式继承来继承超类型的原型,然后再将结果指定给子类型的原型。
function createObj(origin){
function F () {};
F.prototype = origin.prototype;
return new F();
}
function inherit(sub,super){
var prototype = createObj(super.prototype); // Step 1;
prototype.constructor = sub; // Step 2;
sub.prototype = prototype;
}
步骤:
- 创建超类型原型的一个副本
- 为创建的副本添加
constructor属性
,弥补重写原型而丢失默认的constructor属性
。 - 新创建的对象复制给子类型的原型。
优点: 体现在它只调用了一次 SuperType
构造函数,并且因此避免了在 SubType. prototype
上面创建不必要的、多余的属性。 提高效率。
一道练习题来进行验收吧~
function Parent() {
this.a = 1;
this.b = [1, 2, this.a];
this.c = { demo: 5 };
this.show = function () {
console.log(this.a , this.b , this.c.demo );
}
}
function Child() {
this.a = 2;
this.change = function () {
this.b.push(this.a);
this.a = this.b.length;
this.c.demo = ++this.a;
}
}
Child.prototype = new Parent();
var parent = new Parent();
var child1 = new Child();
var child2 = new Child();
child1.a = 11;
child2.a = 12;
parent.show();
child1.show();
child2.show();
child1.change();
child2.change();
parent.show();
child1.show();
child2.show();
到时候整理典型题型时候会详细解说的!尽情期待!
欠下的文章梳理:
- 精进EMCA-手写new
- 十面埋伏的原型题
参考书籍
《红宝书》 - Zakas