原型模式(Prototype Pattern),利用原型对象来添加公有属性和方法。

使用原型模式的好处是可以让所有实例共享原型对象所包含的属性和方法。

  1. function Person() {
  2. }
  3. Person.prototype.name = 'Nicholas';
  4. Person.prototype.age = 29;
  5. Person.prototype.job = 'Software Engineer';
  6. Person.prototye.sayName = function () {
  7. console.log(this.name);
  8. }
  9. var person1 = new Person();
  10. person1.sayName(); //=> 'Nicholas';
  11. var person2 = new Person();
  12. person2.sayName(); //=>'Nicholas'

JS 原型模式 - 图1

1. 理解原型对象

(1)所有的函数都天生自带一个 prototype 属性,这个属性是一个对象(也就是原型对象),默认给它开辟一个堆内存

(2)在给 prototype 属性开辟的堆内存中,也就是原型对象中,天生自带一个 constructor 属性,存储的是当前构造函数的指针

(3)每一个对象都有一个内部属性 [[Prototype]] 指向当前实例所属类的原型对象(如果不能确定它是谁的实例,都是 Object 的实例),浏览器通过 __proto__ 属性来实现

1.1 __proto__

当调用构造函数创建一个实例后,该实例内部将包含一个指针(内部属性 [[Prototype]]),指向构造函数的原型对象,这个连接存在于实例与构造函数的原型对象之间,而不是存在于实例与构造函数之间。

构造函数本身也是一个对象,也有一个 [[Prototype]] 属性,都指向函数的原型对象

  1. Array.__proto__ == Function.prtototype; //=> true
  2. Object.__proto__ == Function.prtototype; //=> true
  3. Function.__proto__ == Function.prototype; //=> true

JS 原型模式 - 图2

另外一个需要注意的是,Function.prototype 是一个函数,anonymous(匿名) 函数,其没有 prototype 属性,可以当做对象来看

而原型对象也是一个对象,也会有一个 [[Prototype]] 属性,其指向创建其的构造函数的原型对象,以此类推,最终会指向基类 Object 的原型对象。

而 Object 的原型对象也是一个对象,也会有一个 [[Prototype]] 属性,它本身就是 Object 的一个实例,所以要指向 Object 的原型对象,也就是它自己本身,没有意义,所以 JS 强制让它的值为 null

注意前后都是两个 _

1.2 isPrototypeOf

通过 isPrototypeOf() 方法可以确定对象之间是否存在这种关系。只要 [[Prototype]] 指向调用 isPrototype 方法的对象,那么这个方法就会返回 true

  1. Person.prototype.isPrototypeOf(person1); //=> true
  2. Person.prototype.isPrototypeOf(person2); //=> true

1.3 Object.getPrototypeOf

Object.getPrototypeOf() 返回某个对象的 [[Prototype]] 的值

  1. Object.getPrototypeOf(person1) == Person.prototype; //true
  2. Object.getPrototypeOf(person2).name; //'Nicholas'

2. 原型链

原型链与作用域一样,它是基于 __proto__ 属性向上查找的机制。

__proto__ 属性连接起来的各个原型对象:
实例 -> 原型对象 -> 原型对象所属类的原型对象-> … -> Object 的原型对象。

所有原型链的终点都是 Object 的原型对象

原型链中的查找机制

每当代码需要读取某个对象的某个属性时,
首先在对象实例本身开始查找。如果在实例中找到具有给定名字的属性,则返回该属性的值
如果没有找到,则继续搜索 __proto__ 指向的原型对象,在原型对象中查找,找到则使用这个公用的
如果没有找到,则继续搜索这个原型对象的 __proto__ 指向的原型对象
以此类推,直到找到 Object 的原型对象,没有找到,则不存在该属性

在实例上的属性,称为私有属性
在原型链上的属性,称为公有属性

虽然可以通过对象实例访问保存在原型中的值,但却不能通过对象实例重写原型中的值。

如果在实例中添加一个与实例原型同名的属性,那么就会在实例中创建该属性,并且屏蔽原型链中的属性。

  1. function Person() {
  2. }
  3. Person.prototype.name = 'Nicholas';
  4. Person.prototype.age = 29;
  5. Person.prototype.job = 'SoftWare Engineer';
  6. Person.prototype.sayName = function() {
  7. alert(this.name);
  8. };
  9. var person1 = new Person();
  10. var person2 = new Person();
  11. person1.name = 'Greg';
  12. alert(person1.name); //'Greg' 来自实例
  13. alert(person2.name);//'Nicholas' 来自原型

即使将这个属性设置为 null,也只会在实例中设置这个属性,而不会恢复其指向原型的连接。

使用 delete 操作符可以完全删除实例属性,从而让我们能够重新访问原型中的属性。

  1. delete person1.name;
  2. alert(person1.name); //'Nicholas' 来自原型

JS 原型模式 - 图3

3. 检测对象属性

3.1 in 操作符

在单独使用时,in 操作符会在通过对象能够访问给定属性时返回 true,无论属性存在于实例中还是原型中。

  1. function Person() {
  2. }
  3. Person.prototype.name = 'Nicholas';
  4. Person.prototype.age = 29;
  5. Person.prototype.job = 'SoftWare Engineer';
  6. Person.prototype.sayName = function() {
  7. alert(this.name);
  8. };
  9. var person1 = new Person();
  10. var person2 = new Person();
  11. alert(person1.hasOwnProerty('name')); //false
  12. alert('name' in person1); //true
  13. person1.name = 'Greg';
  14. alert(person1.name); //'Greg' 来自实例
  15. alert(person1.hasOwnProperty('name')); //true
  16. alert('name' in person1); //true
  17. alert(person2.name);//'Nicholas' 来自原型
  18. alert(person2.hasOwnProerty('name')); //false
  19. alert('name' in person2); //true
  20. delete person1.name;
  21. alert(person1.name); //'Nicholas' 来自原型
  22. alert(person1.hasOwnProerty('name')); //false
  23. alert('name' in person1); //true

3.2 hasOwnProperty

使用 hasOwnProperty() 方法可以检测一个属性是不是私有属性。

存在实例本身中的是私有属性,存在于原型中的是公有属性。这个方法只在给定属性存在于对象实例中时,才返回 true

3.3 hasPubProperty(自定义)

思考题:编写一个方法 hasPubProperty,检测当前属性是否是对象的公有属性。
同时使用 hasOwnproperty()in 操作符

  1. function hasPubProperty(obj, attr) {
  2. return !obj.hasOwnProperty(name) && (attr in obj);
  3. }
  4. //=> 放在 Object 的原型上
  5. Object.prototype.hasPubProperty = function (attr) {
  6. return attr in this && !this.hasOwnProperty(attr);
  7. }

4. 更简单的原型语法

当我们需要给类的原型批量设置属性和方法的时候,一般都是让原型重定向到自己创建的对象中。

  • 简单,减少不必要的输入

  • 从视觉上更好的封装原型的功能

大多数库的封装都会使用一个包含所有属性和方法的对象字面量或者一个实例来重写整个原型对象。

  1. function Person() {
  2. }
  3. Person.prototype = {
  4. name: "Nicholas",
  5. age: 29,
  6. job: "Soft Engineer",
  7. sayName: function () {
  8. alert(this.name);
  9. }
  10. };

本质上完全重写了默认的 prototype 对象

[存在问题]

自己开辟的堆内存中没有 constructor 属性,导致类的原型对象中构造函数确实,解决办法是自己手动在堆内存中增加 constructor 属性。

当原型重定向后,浏览器默认开辟的那个原型堆内存会被释放掉,如果之前已经存储了一些方法或者属性,那么都会丢失(所以内置类的原型不允许虫定向到自己开辟的堆内存,因为内置类原型上自带很多属性和方法,重定向后都没了)

如果 constructor 的值很重要,可以特意设置回适当的值。

  1. function Person() {
  2. }
  3. Person.prototype = {
  4. constructor: Person,
  5. name: "Nicholas",
  6. age: 29,
  7. job: "Soft Engineer",
  8. sayName: function () {
  9. alert(this.name);
  10. }
  11. };

注意: 以这种方式重设的 constructor 属性会导致它的 [[Enumerable]] 特性被设置为 true

也可以通过 Object.defineProperty() 来重设 constructor 属性的值,并设置其不可枚举。

  1. Object.defineProperty(Person.property, "constructor", {
  2. enumerable: false,
  3. value: Person
  4. });

5. 原型的动态性

由于原型中查找值的过程是一次搜索,因此我们对原型对象所做的任何修改都能够立即反映到实例上,即使先创建实例后修改原型。

  1. var friend = new Person();
  2. Person.prototype.sayHi = function() {
  3. console.log('Hi');
  4. }
  5. friend.sayHi(); // 'Hi'

其根本原因在于每个实例上的 __proto__ 属性指向的那个空间与构造函数的 prototype 属性指向的空间是一样的,引用类型的修改相互反映。

但是,如果重写整个原型对象,那么情况就会发生改变。

  1. var friend = new Person();
  2. Person.prototype = {
  3. constructor: Person,
  4. sayHi: function() {
  5. console.log('Hi');
  6. }
  7. }
  8. friend.sayHi(); // 报错,没有这个方法

原因在于,重写构造函数的 prototype 属性,那个它指向的空间发生变化,而在前面定义的实例,其 __proto__ 属性还是指向原来的空间,所以没有这个方法。在重写之后定义的实例,其 __proto__ 属性就会指向新空间

重写实例会切断原型与任何之前已经存在的对象实例之间的联系。

6. 原生对象的原型

原型模式的重要性不仅体现在创建自定义类型方面,而且所有的原生引用类型,都是采用这种模式创建的。

所有原生引用类型的方法都定义在其构造函数的原型上。

通过原生对象的原型,不仅可以取得所有默认方法的引用,而且也可以重新定义自己的方法。

需要注意的是,为了安全性的考虑,原生对象的原型不允许被重写。

在字符串的原型上添加方法:

  1. //=> ES6 中字符串的 startsWith 方法
  2. String.prototype.startsWith = function(text) {
  3. retrun this.indexOf(text) == 0;
  4. }

7. 原型对象的问题

单纯的原型模式,省略了为构造函数初始化参数的环节, 结果所有实例在默认情况下都将取得相同的属性值。

其最大的问题是,由其共享的本性所导致的。
原型中所有属性是被很多实例共享的。对于包含基本类型值或函数的属性,通过实例添加同名属性,可以隐藏原型中的对应属性。

但是对于包含引用类型(除函数外)的属性来说,如果修改了这个引用类型的值上的某个属性,那么就将反映给所有共享的实例上。

  1. function Person() {
  2. }
  3. Person.prototype = {
  4. constuctor: Person,
  5. name: 'aa',
  6. age: 29,
  7. friends: ['aa','bb','cc'],
  8. sayName: function() {
  9. console.log(this.name);
  10. }
  11. }
  12. var person1 = new Person();
  13. var person2 = new Person();
  14. person1.friends.push('dd');
  15. person1.friends; //=> 'aa','bb','cc', 'dd'
  16. person2.friends; //=> 'aa','bb','cc', 'dd'

如果真的需要这个结果,那么这个方法就适用,但是很多时候并不是想要这个结果。

所以,最常见的方式,是组合构造函数模式和原型模式

构造函数模式用于定义实例属性,而原型模式用于定义公有方法和共享属性。这样,每个实例上都会有自己的一份实例属性的副本,但同时共享者对方法的引用,最大限度的节省内存。

  1. function Person(name, age, friends) {
  2. this.name = name;
  3. this.age = age;
  4. this.friends = friends;
  5. }
  6. Person.prototype = {
  7. constructor: Person,
  8. sayName: function() {
  9. console.log(this.name);
  10. }
  11. }