对于有其余面向对象语言开发经验的人来讲,在看到独立的构造函数和原型时,极可能会感到很是困惑。好比在Java中(没有Java经验的开发者此段可忽略,只要知道后面提出的结论就行了),有类的概念(固然ES6也引入了类,咱们这里以ES5为基础),好比如下代码所示:javascript
public class Person { private String name; private int age; private String job; public Person(String name, int age, String job) { this.name = name; this.age = age; this.job = job; } public void sayName(){ System.out.println(this.name); } }
这是很是简单的一个类,它有三个属性,一个构造函数和一个方法。若是比较JavaScript,function Person
就至关于类,可是咱们发现,Java中的类是一个总体,而JavaScript除了function Person
,还有一个Person.prototype
,被定义成了两部分。因此,JavaScript对于对象的封装性仍是不够完美,而动态原型模式正是致力于要解决这个问题,它把全部的信息都封装在了构造函数中,经过在构造函数中初始化原型,既很好地体现了封装性,又保持了组合使用构造函数和原型模式的特色,能够说一箭双雕,很是完美。下面咱们来看一个例子:java
function Person(name, age, job){ this.name = name; this.age = age; this.job = job; if(typeof this.sayName != 'function'){ Person.prototype.sayName = function(){ console.log(this.name); }; Person.prototype.sayJob = function(){ console.log(this.job); }; } } var p1 = new Person('张三', 18, 'JavaScript');//sayName不存在,添加到原型 var p2 = new Person('李四', 20, 'Java');//sayName已经存在,不会再向原型添加 p1.sayName();//张三 p2.sayName();//李四
如代码所示,第一次建立对象,执行构造函数时,判断sayName()
是否存在,若是不存在,就把它添加到原型,使用if
判断能够确保只在第一次调用构造函数时初始化原型,避免了每次调用的重复声明。函数
实际上这里不只仅可使用sayName()
作为判断条件,还可使用sayJob()
,这个条件只是为了测试原型是否已经初始化,只要是原型初始化以后应该存在的属性或方法均可用来作为判断条件。工具
以前讲过,原型也能够用对象字面量来重写,那动态原型模式可不可使用对象字面量呢?咱们来尝试一下:开发工具
function Person(name, age, job){ this.name = name; this.age = age; this.job = job; if(typeof this.sayName != 'function'){ Person.prototype = { constructor: Person, sayName: function(){ console.log(this.name); } } } } var p1 = new Person('张三', 18, 'JavaScript');//sayName不存在,添加到原型 var p2 = new Person('李四', 20, 'Java');//sayName已经存在,不会再向原型添加 //p1.sayName();//Uncaught TypeError: p1.sayName is not a function p2.sayName();//李四
发现p1.sayName()
报了不是一个函数的错误,若是把p1.sayName()
注释掉,p2.sayName()
能够正常输出李四,为何会这样呢?要想解释清楚这个问题,咱们先来看一下如下代码:测试
function Person(name, age, job){ this.name = name; this.age = age; this.job = job; } var p1 = new Person('张三', 18, 'JavaScript'); console.log(Person.prototype);//{constructor: ƒ} Person.prototype = { constructor: Person, sayName: function(){ console.log(this.name); } } var p2 = new Person('李四', 20, 'Java'); console.log(Person.prototype);//{constructor: ƒ, sayName: ƒ} // p1.sayName();//Uncaught TypeError: p1.sayName is not a function p2.sayName();//李四
也是一样的现象,p1.sayName()
不是一个函数。那么p1
、p2
的区别在哪儿呢?区别就在于经过new
关键字建立对象的前后顺序,是先于重写原型建立,仍是后于重写原型建立。this
咱们知道,经过new
关键字建立一个对象,这个对象会有一个属性__proto__
指向相应函数的原型,这里代码中的p1正是指向了这个原型,在Chrome的开发工具中能够看到,以下图所示:prototype
可是重写原型,就是建立了一个新对象,函数的指针Person.prototype
由引用旧的原型对象改成引用这个新对象,而旧的原型对象如今只被p1.__proto__
引用着,实例p1
和Person
原型之间的关系被切断了,因此调用p1.sayName()
就报了不是一个函数的错误,由于旧原型对象上没有sayName
方法。设计
再来看p2
,由于是先重写原型,因此当p2
被new
出来时,p2
的__proto__
属性指向的就是这个新原型,故而调用sayName
方法时,向上搜索原型能够找到sayName方法
,正常输出李四,下面的示意图能够直观地表示这种状况:指针
如今回过头来看一开始提出的问题,为何动态原型模式不能用对象字面量的方式重写。第一次建立实例对象时,先new
,而后执行构造函数,重写原型,那么此时实例的__proto__
指向的仍是原来的原型,不是重写后的原型。第二次建立实例,由于新原型对象已经建立好,因此实例的__proto__
指向的就是重写的这个原型。使用给原型添加属性的方式操做的一直是同一个原型,因此也就不存在前后的问题。
这就是动态原型模式,相比组合使用构造函数和原型模式而言,封装性更优秀,可是一个小缺点就是不能使用对象字面量的形式初始化原型,这是须要留意的。开发者在实际应用中可根据具体状况,灵活选择,肯定使用哪一种方式。
本文参考《JavaScript高级程序设计(第3版)》