读《javaScript高级程序设计-第6章》之封装类

第6章我一共写了3篇总结,下面是相关连接:
读《javaScript高级程序设计-第6章》之理解对象
读《javaScript高级程序设计-第6章》之继承java

工厂模式

所谓的工厂模式就是,把建立具体对象的过程抽象成了一个函数,每次调用这个函数都会返回一个类似的对象。chrome

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("Nicholas", 29, "Software Engineer");
var person2 = createPerson("Greg", 27, "Doctor");
person1.sayName();   //"Nicholas"
person2.sayName();   //"Greg"

工厂模式虽然解决了建立多个类似对象的问题,但却没有解决对象识别的问题(即怎样知道一个对象的类型)。segmentfault

构造函数模式

js里常常如此写var obj=new Object();var arr=new Array();ObjectArray就是构造函数,使用new操做符能够建立相应类型的对象,使用instanceof能够验证对象的类型,例如:
alert(arr instance Array);      //true
构造函数模式就是,自定义像ArrayObject等这样的构造函数,并使用new操做符调用它来建立自定义类型对象的方法。
例如:数组

function Person(name, age, job){
    this.name = name;
    this.age = age;
    this.job = job;
    this.sayName = function(){
        alert(this.name);
    };   
}

var person1 = new Person("Nicholas", 29, "Software Engineer");
var person2 = new Person("Greg", 27, "Doctor");

person1.sayName();   //"Nicholas"
person2.sayName();   //“Greg”
  • new操做符

使用new操做符调用,Person就是一个构造函数
要建立Person的新实例,必须使用new操做符。以这种方式调用构造函数实际上会经历一下4个步骤:
    (1)建立一个新对象
    (2)将构造函数的做用域赋给新对象,即把构造函数的this指向这个新对象
    (3)执行构造函数中的代码(为这个新对象添加属性)
    (4)返回新对象浏览器

若是不使用new,Person就是一个普通的函数,能够正常调用。例如:函数

//做为普通函数在全局做用域下调用
Person("Greg", 27, "Doctor");  //adds to window
window.sayName();   //“Greg"
//做为普通函数在另外一个对象中调用
var o = new Object();
Person.call(o, "Kristen", 25, "Nurse");
o.sayName();    //"Kristen"
  • 检测类型

alert(person1 instanceof Object);  //true
alert(person1 instanceof Person);//truethis

综上,建立自定义的构造函数,意味着未来能够将它的实例标识为一种特定的类型(相似于Array类型,Number类型);而这正是构造函数模式赛过工厂模式的地方。可是构造函数模式也存在缺点。spa

  • 构造函数模式的问题

使用构造函数的主要问题就是,每一个方法都要在每一个实例上从新建立一遍(实例化一次Function对象),浪费内存。例如,person1和person2都有一个sayName()的方法,但建立person1和person2时候,定义sayName这个方法时都实例化了一个函数对象,所以person1.sayName和person2.sayName是不相等的,而事实上它们又是作的一样的事情。或者也能够这么说,person1和person2的sayName()方法作一样的事情,但却在建立对象时被实例化了两次,也就占用了两倍内存。
虽然能够解决,但并不完美,例如:firefox

function Person(name, age, job){
    this.name = name;
    this.age = age;
    this.job = job;
    this.sayName = sayName;
}

function sayName(){
    alert(this.name);
}

var person1 = new Person("Nicholas", 29, "Software Engineer");
var person2 = new Person("Greg", 27, "Doctor”);
alert(person1.sayName == person2.sayName);  //true 

可是若是共享方法有不少,就须要定义不少个全局函数,那么咱们的自定义的引用类型就丝毫没有封装性可言了。好在,这些问题能够经过使用原型模式解决。prototype

原型模式

(1)理解原型对象

不管何时,只要建立了一个新函数,就会根据一组特定的规则为该函数建立一个prototype属性,这个属性就是该函数的原型对象。每一个函数都有一个原型对象,全部原型对象都会自动得到constructor属性,constructor指向该函数(拥有该prototype属性的函数)。
例如,Person.prototype.constructor指向Person
建立构造函数后,其原型对象默认只会取得constructor属性;至于其余的方法都是从Object继承来的(__proto__)。当调用构造函数建立一个新实例后,该实例内部将包含一个指针(__proto__),指向构造函数的原型对象。(ECMA-262第5版中管这个指针叫[[Prototype]],但在脚本中没有标准的方式访问它。在chrome,safari和firefox中都支持一个属性__proto__,但在其余实现中__proto__对脚本是不可见的)。因此和实例有直接关系的是构造函数的原型对象,而不是构造函数。

上图展现了Person构造函数、Person的原型对象和Person现有的两个实例之间的关系。

(2)实例属性和原型属性:

原型属性即构造函数的原型对象的属性;实例属性即在实例对象上直接添加的属性。
例如:person1.name=“Jone”。
经过点运算符能够访问到实例的实例属性和原型属性。实例访问属性时,脚本会先搜索实例属性,若是找到了,则中止搜索返回实例属性的值;若是没找到就继续搜索原型属性。因此若是实例属性和原型属性同名,那么原型属性就会被屏蔽掉,没法访问到。
须要注意的是:实例没法修改他的原型属性的值,也没法修改原型对象(即不能修改、删除和增长一个原型属性)
(注意:实例不能修改的是原型属性的值,可是若是原型属性指向一个引用类型,原型属性的值是存储这个引用类型的地址,即不能修改原型属性指向另外一个对象,但却能修改原型属性指向的对象里的属性。下面原型对象的问题里还会再讲到)
若是person1.name=“Jone”这样写,脚本只会在实例属性里建立或修改一个name=“Jone”的属性,delete person1.name 只会删除person1的实例属性name(就算实例没有name的实例属性,也不会删除实例的原型属性)。

(3)和原型对象有关的几个方法

  • isPrototypeOf()

alert(Person.prototype.isPrototypeOf(person1));    //true
若是person1[[prototype]]  (即__proto__)指向调用isPrototypeOf的对象即Person.prototype就会返回true
即判断Person.prototype是不是person1[[prototype]]

  • Object.getPrototypeOf()

alert(Object.getPrototypeOf(person1)==Person.prototype);   //true
返回person1这个对象的原型[[prototype]]

  • hasOwnProperty()

person1.hasOwnProperty(“name”);     若是person1.name是来自于person1的实例属性,返回true;若是来自于person1的原型属性,则返回false

(4)原型与in操做符

有两种方式使用in操做符:
单独使用in:alert(“name” in person1);   //true
在经过person1可以访问给定属性是返回true,不管属性是实例属性仍是原型属性。
在for-in循环中使用:返回的是全部可以经过对象访问的、可枚举的属性,其中包括实例属性也包括原型属性。

  • Object.keys()

接受一个对象做为参数,返回一个包含对象的全部可枚举属性的字符串数组。
若是对象是一个实例,则只返回实例的实例属性而不包含原型属性

  • Object.getOwnPropertyNames()
 var keys = Object.getOwnPropertyNames(Person.prototype);
 alert(keys);   //"constructor,name,age,job,sayName”

获得对象的全部实例属性,不管它是否可枚举
  

(5)更简单的原型语法

所谓的更简单的原型写法就是用字面量的形式来定义构造函数的原型对象,以下:

function Person(){
}

Person.prototype = {
    name : "Nicholas",
    age : 29,
    job: "Software Engineer",
    sayName : function () {
        alert(this.name);
    }

};

var friend = new Person();

alert(friend instanceof Object);  //true
alert(friend instanceof Person);  //true
alert(friend.constructor == Person);  //false
alert(friend.constructor == Object);  //true

这样定义完了以后,Person.prototype这个对象就被重写了,致使它的constructor这个属性的指向变成了Object,而不是Person
(解释:Person.prototypeObject的一个实例,因此它有一个原型属性constructor指向ObjectPerson被建立时,它的原型对象Person.prototype自动得到了一个constructor的属性,指向Person,这个属性是对象的实例的实例属性,因此会屏蔽掉对象的原型属性,因此说Person.prototype.constructor是指向Person的。可是用字面量重写了Person.prototype后,Person.prototype还是Object的一个实例,因此它有一个原型属性constructor指向Object,但它没有了指向Person的实例属性constructor,因此在访问Person.prototype.constructor时,就是访问了Person.prototype对象的原型属性,指向了Object)。
但咱们能够再把它定义进这个对象字面量里手动指向Person,即给Person.prototype这个对象的实例加一个实例属性constructor,指向Person。以下:

function Person(){
}

Person.prototype = {
    constructor: Person,
    name : "Nicholas",
    age : 29,
    job: "Software Engineer",
    sayName : function () {
        alert(this.name);
    }
};

咱们知道如此定义对象,对象的属性的[[enumerable]]特性默认是true。而默认状况下,原声的原型对象的constructor属性是不可枚举的,所以若是你使用兼容ES5的javaScript引擎,可使用Object.defineProperty()来设置constructor属性。以下:

//重设构造函数,只适用于ES5兼容的浏览器
Object.difineProperty(Person.prototype,”constructor”,{
    enumerable:false,
    value:Person
});

(6)原型的动态性

简单点来讲,就是实例的[[prototype]]是指向构造函数的原型对象,而不是构造函数。只要你明白这一点,原型的动态性就好理解了。
第一种状况:Person.prototype能够在任意地方增长修改或删除属性,实例能够实时的访问最新的原型属性。由于每次实例访问属性,都是一次搜索的过程,搜索原型属性时是到实例的[[prototype]]指向的对象里查找。实例的[[prototype]]是一个指针,Person.prototype也是一个指针,指向的是同一个地址,也就是说修改和查找都在同一个地方,那么查找到的值天然就是最新实时的了。

function Person(){
}
var friend = new Person();
Person.prototype.sayHi = function(){
    alert("hi");
};
friend.sayHi();   //"hi"

第二种状况:在实例被建立以后,Person.prototype被重写了

function Person(){
}

var friend = new Person();
       
Person.prototype = {
    constructor: Person,
    name : "Nicholas",
    age : 29,
    job : "Software Engineer",
    sayName : function () {
        alert(this.name);
    }
};
friend.sayName();   //error

这种状况是由于:实例一旦被建立,实例的[[prototype]]存储的地址就肯定了,指向的对象地址就肯定了,若是你改变这个地址里的对象,实例均可以访问的到。可是若是在实例被建立以后,重写Person.prototype,就至关因而把Person.prototype指向了一个新的对象,而实例的[[prototype]]仍是指向原来的对象,因此实例访问的原型属性仍是要在原来的对象里查找,原来的对象里并无sayName这个方法,所以会报错。

(7)原生对象的原型

咱们用原型模式建立自定义类型,让自定义类型和原生类型同样使用。其实全部的原生的对象(Object、Array、String,等等)也是采用的原型模式建立的。全部原生的引用类型都在其构造函数的原型上定义了方法。
例如,在Array.prototype中能够找到sort()方法,而在String.prototype中能够找到substring()方法。
经过原生对象的原型,不只能够取得全部默认方法的引用,也能够定义新的方法。能够像修改自定义对象的原型同样修改原生对象的原型,所以能够随时添加方法。可是不建议如此作(在支持该方法的实现中运行代码时会致使命名冲突,或者意外重写了原生方法)

(8)原型对象的问题

**首先,原型模式省略了为构造函数传递参数,初始化实例的环节,使得全部实例默认时都是同样的。
其次,原型模式的共享本性使得全部的实例都能共享它的属性。**
若是属性值是函数或者是基本值时,实例不能修改原型属性的值,只会为该实例增长一个同名属性,而后屏蔽掉同名原型属性,这样其它的实例都不会受到影响,使用的仍然是原型属性原来的值。
若是属性值是引用类型,实例虽不能修改原型属性的值(这个值就是指向的对象的地址),即实例不能让这个原型属性从新指向另外一个对象,可是却能够修改指向的对象的属性,这就会致使其它实例再访问这个对象时,对象已被修改了。
例如:

function Person(){
}

Person.prototype = {
    constructor: Person,
    name : "Nicholas",
    age : 29,
    job : "Software Engineer",
    friends : ["Shelby", "Court"],
    sayName : function () {
        alert(this.name);
    }
};

var person1 = new Person();
var person2 = new Person();

person1.friends.push("Van");

alert(person1.friends);    //"Shelby,Court,Van"
alert(person2.friends);    //"Shelby,Court,Van"
alert(person1.friends === person2.friends);  //true

这样就违反了咱们但愿实例拥有属于本身的所有属性的初衷

组合使用构造函数模式和原型模式

综合前面所说的,咱们发现构造函数模式优势在于能向构造函数传递,定义属于实例本身的实例属性。原型模式优势在于共享着对方法的引用,原型属性是全部实例所共享的。
因此建立自定义类型的最多见方式,就是组合使用构造函数模式与原型模式
例如:

function Person(name, age, job){
    this.name = name;
    this.age = age;
    this.job = job;
    this.friends = ["Shelby", "Court"];
}

Person.prototype = {
    constructor: Person,
    sayName : function () {
        alert(this.name);
    }
};

var person1 = new Person("Nicholas", 29, "Software Engineer");
var person2 = new Person("Greg", 27, "Doctor");

person1.friends.push("Van");

alert(person1.friends);    //"Shelby,Court,Van"
alert(person2.friends);    //"Shelby,Court"
alert(person1.friends === person2.friends);  //false
alert(person1.sayName === person2.sayName);  //true

动态原型模式

这一小节,私觉得了解了解就好,只要你理解了上面所说的构造函数模式和原型模式的原理,那么原型属性的定义你能够为所欲为,只要符合你的预期就好。你高兴就好,代码高兴就好。

寄生构造函数模式

与工厂模式的区别是使用new 调用。不使用new调用,它就是工厂模式。
这一小节,私觉得了解了解就好。

稳妥构造函数模式

与工厂模式的区别是对象定义的方法不使用this,构造函数传进来的参数不向外直接暴露。
这一小节,私觉得了解了解就好。

好了,封装类的几种方式已经介绍完了。个人观点是理解了对象和构造函数模式以及原型模式,就能够随机应变了。不须要记住什么什么各类模式的,无非就是使用对象的场景不一样。要理解对象和构造函数以及原型对象,灵活变换,无招胜有招才好。

这是我读《javaScript高级程序设计》这本书的第6章面向对象的程序设计,作的笔记,在本篇以前还有一篇理解对象的笔记,后面还有一篇继承的笔记。发现问题的小伙伴欢迎指出。

相关文章
相关标签/搜索