@guoxs
2015-09-04T12:23:47.000000Z
字数 16292
阅读 3658
JavaScript高级程序设计
JavaScript中的OO编程是一大难点,这里参考《JavaScript高级程序设计》第六章总结了一些笔记。
ECMAScript的对象就像是散列表,是一组名值对,其中的值可以是数据也可以是函数。每个对象都是基于一个引用类型创建的。
ECMAScript有两种属性:数据属性与访问器属性。
JavaScript中不能直接访问对象属性,为了表示特性是内部值,使用两对方括号表示,如:[[Enumerable]]
1、数据属性
以下默认值是针对直接在对象上定义的属性。
| 数据属性特性 | 描述 | 默认值 |
|---|---|---|
| [[Configurable]] | 定义是否可通过delete删除属性从而重定义属性, 能否修改属性特征,能否把属性修改为访问器属性 |
true |
| [[Enumerable]] | 定义能否通过for-in 循环返回属性 | true |
| [[Writable]] | 定义能否修改属性的值 | true |
| [[Value]] | 包含这个属性的数据值,读取属性值时从这个位置读, 写入属性把新值保存于此 |
undefined |
要修该属性默认特性,必须使用
Object.defineProperty()方法。该方法接受三个参数:属性所在的对象,属性的名字以及一个描述符对象。描述符对象必须是以上四个值中的一个或多个。
var person = ();Object.defineProperty(person,"name",{configurable: false,value: "Nicholas"});alert(person.name); //"Nicholas"delete person.name; //无效,严格模式下报错。alert(person.name); //"Nicholas",把configurable设置为false,delete失效。
在调用
Object.defineProperty()方法时,如果不指定,configurable、enumerable和writable默认值都会变成false。
2、访问器属性
访问器属性不包含数据值,包含一对getter和setter方法(皆非必需)。
以下默认值是针对直接在对象上定义的属性。
| 访问器属性特性 | 描述 | 默认值 |
|---|---|---|
| [[Configurable]] | 定义是否可通过delete删除属性从而重定义属性, 能否修改属性特征,能否把属性修改为访问器属性 |
true |
| [[Enumerable]] | 定义能否通过for-in 循环返回属性 | true |
| [[Get]] | 读取属性时调用 | undefined |
| [[Set]] | 写入属性时调用 | undefined |
访问器属性必须通过
Object.defineProperty()来定义。
var book = {_year: 2004, //加下划线表示只能通过对象方法访问的属性edition: 1};Object.defineProperty(book,"year",{get:function(){return this._year;},set:function(newValue){if(newValue>2004){this._year = newValue;this.edition += newValue - 2004;}}});book.year = 2005;alert(book.edition); //2
只指定getter意味着属性是不能写的,尝试写入属性时会被忽略,严格模式下抛出错误;只指定setter函数的属性不能读,尝试读取会返回undefined,严格模式下抛出错误。
兼容性:IE9+(IE8部分实现),Firefox 4+,Safari 5+,Opera 12+ 和chrome。在这个方法之前,使用两个非标准方法_defineGetter_()和_defineSetter_()。
定义多个属性:
Object.defineProperties(),该方法接受两个参数:第一个属性是要添加和要修改其属性的对象,第二个对象的属性与第一个对象中要添加或要修改的属性一一对应。
兼容性:IE9+,Firefox 4+,Safari 5+,Opera 12+ 和chrome。
读取属性的特性:
Object.getOwnPropertyDescriptor(),取得给定对象描述符。该方法接受两个参数:属性所在的对象,要读取其描述符的属性名称。返回值是一个对象,若是访问器属性,则该对象的属性有:configurable、enumerable、get和set;若是数据属性,则:configurable、enumerable、writable和value。
在JavaScript中,可以针对任何对象——包括DOM或BOM对象使用该方法。兼容性:IE9+,Firefox 4+,Safari 5+,Opera 12+ 和chrome。
Object构造函数或对象字面量都可以用来创造单个函数,但是使用同一个接口创建很多对象时会产生大量重复代码,故开始有以下其中变体。
工厂模式抽象了创建具体对象的过程,由于ECMAScript无法创建类,开发人员就发明了一种函数,用函数来封装以特定接口创建对象的细节。
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");
工厂模式解决了创建多个相似对象的问题,但是却没有解决对象识别的问题(即怎样知道一个对象的类型)。
构造函数可以用来创建特定类型的对象,像Object和Array这样的原生对象,在运行时会自动出现在执行环境中。此外,也可以创建自定义的构造函数。
function Person(name,age,job){this.name = name;this.age = age;this.job = job;this.sayName = function(){alert(this name);};}var person1 = createPerson("Nicholas",29,"Software Engineer");var person2 = createPerson("Greg",27,"Doctor");
注意到,Person()与工厂模式的createPerson()函数存在以下不同:
按惯例构造函数以一个大写字母开头,如Person();非构造函数应该以小写字母开头。
要创建Person的新实例,必须使用new操作符,任何函数,只要通过new操作符调用,那它就可以作为构造函数。这种方式调用构造函数会经历以下四个步骤:
创建一个新对象—→ 将构造函数的作用域赋给新对象(因此this指向了这个新对象) —→ 执行构造函数中的代码(为这个新对象添加属性)—→ 返回新对象
例子中,person1和person2分别保存着不同的实例,但是他们的constructor(构造函数)属性都指向Perosn,该属性最初是用来标识对象类型的。
instanceof 操作符可以用来检测对象类型。
创建自定义的构造函数意味着将来可以为它的实例标识为一种特定的类型,而这正是构造函数胜于工厂模式的地方。
以这种方式定义的构造函数是定义在global/window 对象上的。构造函数若不是用new操作符调用,则属性和方法会添加到global/window上。
每个方法都要在每个实例中重新创建一遍。
function Person(name,age,job){this.name = name;this.age = age;this.job = job;this.sayName = new Function(){alert(this name); //与声明函数在逻辑上是等价的};}
每个Person实例都会包含一个不同的Function实例,以这种方法创建函数,会导致不同作用域链和标识符解析,但创建Function新实例的机制任然是相同的,不同实例上的同名函数是不等价的。
alert(person1.sayName == person2.sayName); //false
解决方案:
function Person(name,age,job){this.name = name;this.age = age;this.job = job;this.sayName = sayName; // 全局sayName}function sayName(){alert(this.name);} //将sayName()函数转移到构造函数外部。
在全局作用域中定义的函数实际上只能被某个对象调用,若对象需要定义很多方法,就要创建很多个全局函数,而无任何封装可言。解决方法:原型模式。
理解prototype:指向一个对象的指针。通过调用构造函数而创建的那个对象实例的原型对象。
function Person(){}Person.prototype.name = "Nicholas";Person.prototype.age = 29;Person.prototype.job = "Software Engineer";Person.prototype.sayName = function(){alert(this.name);};var person1 = new Person();person1.sayName(); //"Nicholas"var person2 = new Person();person2.sayName(); //"Nicholas"alert(person1.sayName == person2.sayName); //true
将sayName()方法和所有属性直接添加到了Person 的prototype属性中,构造函数变成了空函数。
1、理解原型对象
无论什么时候,只要创建了一个新函数,就会根据一组特定的规则为该函数创建一个prototype属性,这个属性指向函数的原型对象。在默认情况下,所有原型对象都会自动获得一个constructor(构造函数)属性,这个属性包含一个指向prototype 属性所在函数的指针。

从图中可以看出,构造函数与其实例无直接关系。
创建自定义构造函数之后,其原型默认只取得constructor属性,其他方法都是从Object继承而来。[[prototype]]指针无标准访问方式,在Firefox、Safari、chrome中支持一个_proto_属性可以实现。可以通过isPrototypeOf()方法来确定对象之间是否存在这种关系:
alert(Person.prototype.isPrototypeOf(person1)); //truealert(Person.prototype.isPrototypeOf(person2)); //true
ECMAScript 5 新增的Object.getPrototypeOf()方法也可以返回[[Prototype]]的值。支持这个方法的浏览器有IE9+、Firefox 3.5+、Safari5+、Opera 12+和Chrome。
当为对象实例添加一个属性时,这个属性就会屏蔽原型对象中保存的同名属性,即使将这个属性设置为null,也只会在实例中设置这个属性,而不会恢复其指向原型的连接。不过,使用delete 操作符则可以完全删除实例属性,从而让我们能够重新访问原型中的属性。
使用
hasOwnProperty()方法可以检测一个属性是存在于实例中,还是存在于原型中。这个方法(从Object继承来的)只在给定属性存在于对象实例中时,才会返回true.
function Person(){}Person.prototype.name = "Nicholas";Person.prototype.age = 29;Person.prototype.job = "Software Engineer";Person.prototype.sayName = function(){alert(this.name);};var person1 = new Person();var person2 = new Person();alert(person1.hasOwnProperty("name")); //falseperson1.name = "Greg";alert(person1.name); //"Greg"——来自实例alert(person1.hasOwnProperty("name")); //truealert(person2.name); //"Nicholas"——来自原型alert(person2.hasOwnProperty("name")); //falsedelete person1.name;alert(person1.name); //"Nicholas"——来自原型alert(person1.hasOwnProperty("name")); //false
关系图:

ECMAScript 5 的
Object.getOwnPropertyDescriptor()方法只能用于实例属性,要取得原型属性的描述符,必须直接在原型对象上调用Object.getOwnPropertyDescriptor()方法。
2、原型与in操作符
有两种方式使用in 操作符:单独使用和在
for-in循环中使用。
① 在单独使用时,in操作符会在通过对象能够访问给定属性时返回true,无论该属性存在于实例中还是原型中。同时使用hasOwnProperty()方法和in 操作符,就可以确定该属性到底是存在于对象中,还是存在于原型中:
function hasPrototypeProperty(object, name){return !object.hasOwnProperty(name) && (name in object);}
由于in 操作符只要通过对象能够访问到属性就返回true,hasOwnProperty()只在属性存在于实例中时才返回true,因此只要in 操作符返回true,而hasOwnProperty()返回false,就可以确定属性是原型中的属性。
② 在使用for-in 循环时,返回的是所有能够通过对象访问的、可枚举的(enumerated)属性,其中既包括存在于实例中的属性,也包括存在于原型中的属性。屏蔽了原型中不可枚举属性(即将[[Enumerable]]标记为false 的属性)的实例属性也会在for-in 循环中返回。只有IE8以及更早期版本例外。
要取得对象上所有可枚举的实例属性,可以使用ECMAScript 5 的
Object.keys()方法。这个方法接收一个对象作为参数,返回一个包含所有可枚举属性的字符串数组。
如果你想要得到所有实例属性,无论它是否可枚举,都可以使用Object.getOwnPropertyNames()方法。
var keys = Object.getOwnPropertyNames(Person.prototype);alert(keys); //"constructor,name,age,job,sayName"
Object.keys()和Object.getOwnPropertyNames()方法都可以用来替代for-in 循环。支持这两个方法的浏览器有IE9+、Firefox 4+、Safari 5+、Opera12+和Chrome。
3. 更简单的原型语法
function Person(){}Person.prototype = { //避免重复输入prototypename : "Nicholas",age : 29,job: "Software Engineer",sayName : function () {alert(this.name);}};
var friend = new Person();alert(friend instanceof Object); //truealert(friend instanceof Person); //truealert(friend.constructor == Person); //falsealert(friend.constructor == Object); //true
function Person(){}Person.prototype = {name : "Nicholas",age : 29,job : "Software Engineer",sayName : function () {alert(this.name);}};//重设构造函数,只适用于ECMAScript 5 兼容的浏览器Object.defineProperty(Person.prototype, "constructor", {enumerable: false,value: Person});
4. 原型的动态性
var friend = new Person();Person.prototype.sayHi = function(){alert("hi");};friend.sayHi(); //"hi"(没有问题!)
先实例化对象,后修改构造函数原型方法,也能在实例化对象中正常调用该方法。但是:
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
5. 原型对象的问题型
原型模式省略了为构造函数传递初始化参数使得所有实例在默认情况下都将取得相同的属性值。
原型模式的最大问题:共享性。改变原型对象属性或方法,会在所有实例化对象中体现。
创建自定义类型的最常见方式:组合使用构造函数模式与原型模式。构造函数模式用于定义实例属性,而原型模式用于定义方法和共享的属性。结果,每个实例都会有自己的一份实例属性的副本,但同时又共享着对方法的引用,最大限度地节省了内存。另外,这种混成模式还支持向构造函数传递参数;可谓是集两种模式之长。
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,Count,Van"alert(person2.friends); //"Shelby,Count"alert(person1.friends === person2.friends); //falsealert(person1.sayName === person2.sayName); //true
这种构造函数与原型混成的模式,是目前在ECMAScript 中使用最广泛、认同度最高的一种创建自定义类型的方法。可以说,这是用来定义引用类型的一种默认模式。
把所有信息都封装在了构造函数中,而通过在构造函数中初始化原型(仅在必要的情况下),又保持了同时使用构造函数和原型的优点。换句话说,可以通过检查某个应该存在的方法是否有效,来决定是否需要初始化原型。
function Person(name, age, job){//属性this.name = name;this.age = age;this.job = job;//方法 这段代码只会在初次调用构造函数时才会执行。if (typeof this.sayName != "function"){Person.prototype.sayName = function(){alert(this.name);};}}var friend = new Person("Nicholas", 29, "Software Engineer");friend.sayName();
if 语句检查的可以是初始化之后应该存在的任何属性或方法——不必用一大堆if 语句检查每个属性和每个方法;只要检查其中一个即可。对于采用这种模式创建的对象,还可以使用instanceof 操作符确定它的类型。
使用动态原型模式时,不能使用对象字面量重写原型,切断现有实例与新原型之间的联系。
基本思想:创建一个函数,该函数的作用仅仅是封装创建对象的代码,然后再返回新创建的对象。从表面上看,这个函数很像是典型的构造函数。
function Person(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 friend = new Person("Nicholas", 29, "Software Engineer");friend.sayName(); //"Nicholas"
除了使用new 操作符并把使用的包装函数叫做构造函数之外,这个模式跟工厂模式其实是一模一样的。构造函数在不返回值的情况下,默认会返回新对象实例。而通过在构造函数的末尾添加一个return语句,可以重写调用构造函数时返回的值。
这个模式可以在特殊的情况下用来为对象创建构造函数。假设我们想创建一个具有额外方法的特殊数组。由于不能直接修改Array构造函数,因此可以使用这个模式。
function SpecialArray(){//创建数组var values = new Array();//添加值values.push.apply(values, arguments);//添加方法values.toPipedString = function(){return this.join("|");};//返回数组return values;}var colors = new SpecialArray("red", "blue", "green");alert(colors.toPipedString()); //"red|blue|green"
道格拉斯·克罗克福德(Douglas Crockford)发明了JavaScript中的稳妥对象(durable objects)这个概念。所谓稳妥对象,指的是没有公共属性,而且其方法也不引用this 的对象。稳妥对象最适合在一些安全的环境中(这些环境中会禁止使用this 和new),或者在防止数据被其他应用程序(如Mashup程序)改动时使用。稳妥构造函数遵循与寄生构造函数类似的模式,但有两点不同:一是新创建对象的实例方法不引用this;二是不使用new 操作符调用构造函数。
function Person(name, age, job){//创建要返回的对象var o = new Object();//可以在这里定义私有变量和函数//添加方法o.sayName = function(){alert(name);};//返回对象return o;}
在以这种模式创建的对象中,除了使用sayName()方法之外,没有其他办法访问name 的值。与寄生构造函数模式类似,使用稳妥构造函数模式创建的对象与构造函数之间也没有什么关系,因此
instanceof操作符对这种对象也没有意义。
继承分为接口继承和实现继承。接口继承只继承方法签名,而实现继承则继承实际的方法。由于函数没有签名,在ECMAScript 中无法实现接口继承。ECMAScript只支持实现继承,而且其实现继承主要是依靠原型链来实现的。
基本思想:利用原型让一个引用类型继承另一个引用类型的属性和方法。
基本模式:
function SuperType(){this.property = true;}SuperType.prototype.getSuperValue = function(){return this.property;};function SubType(){this.subproperty = false;}//继承了SuperType,重写原型对象SubType.prototype = new SuperType();SubType.prototype.getSubValue = function (){return this.subproperty;};var instance = new SubType();alert(instance.getSuperValue()); //true
加上默认的原型,其图示为:
SubType 继承了SuperType,而SuperType继承了Object。当调用instance.toString()时,实际上调用的是保存在Object.prototype 中的那个方法。
1. 确定原型和实例的关系
使用instanceof 操作符,只要用这个操作符来测试实例与原型链中出现过的构造函数,结果就会返回true:
alert(instance instanceof Object); //truealert(instance instanceof SuperType); //truealert(instance instanceof SubType); //true
使用isPrototypeOf()方法,只要是原型链中出现过的原型,都可以说是该原型链所派生的实例的原型,因此isPrototypeOf()方法也会返回true:
alert(Object.prototype.isPrototypeOf(instance)); //truealert(SuperType.prototype.isPrototypeOf(instance)); //truealert(SubType.prototype.isPrototypeOf(instance)); //true
2. 谨慎地定义方法
给原型添加方法的代码一定要放在替换原型的语句之后:
function SuperType(){this.property = true;}SuperType.prototype.getSuperValue = function(){return this.property;};function SubType(){this.subproperty = false;}//继承了SuperTypeSubType.prototype = new SuperType(); //之前//添加新方法SubType.prototype.getSubValue = function (){ //之后return this.subproperty;};//重写超类型中的方法SubType.prototype.getSuperValue = function (){return false;};var instance = new SubType();alert(instance.getSuperValue()); //false
在通过原型链实现继承时,不能使用对象字面量创建原型方法。因为这
样做就会重写原型链。
3. 原型链的问题
function SuperType(){this.colors = ["red", "blue", "green"];function SubType(){}//继承了SuperTypeSubType.prototype = new SuperType();var instance1 = new SubType();instance1.colors.push("black");alert(instance1.colors); //"red,blue,green,black"var instance2 = new SubType();alert(instance2.colors); //"red,blue,green,black"
实际上,应该说是没有办法在不影响所有对象实例的情况下,给超类型的构造函数传递参数。有鉴于此,实践中很少会单独使用原型链。
基本思想: 在子类型构造函数的内部调用超类型构造函数,通过使用apply()和call()方法可以在(将来)新创建的对象上执行构造函数。
function SuperType(){this.colors = ["red", "blue", "green"];}function SubType(){//继承了SuperTypeSuperType.call(this); //在(未来将要)新创建的SubType 实例的环境下调用了SuperType 构造函数}var instance1 = new SubType();instance1.colors.push("black");alert(instance1.colors); //"red,blue,green,black"var instance2 = new SubType();alert(instance2.colors); //"red,blue,green"
通过使用call()/apply()方法,在(未来将要)新创建的SubType 实例的环境下调用了SuperType 构造函数。这样一来,就会在新SubType 对象上执行SuperType()函数中定义的所有对象初始化代码。结果,SubType 的每个实例就都会具有自己的colors 属性的副本了。
1. 传递参数
function SuperType(name){this.name = name;}function SubType(){//继承了SuperType,同时还传递了参数SuperType.call(this, "Nicholas");//实例属性this.age = 29;}var instance = new SubType();alert(instance.name); //"Nicholas";alert(instance.age); //29
。为了确保SuperType 构造函数不会重写子类型的属性,可以在调用超类型构造函数后,再添加应该在子类型中定义的属性。
2. 借用构造函数的问题
方法都在构造函数中定义,因此函数复用就无从谈起了。而且,在超类型的原型中定义的方法,对子类型而言也是不可见的,结果所有类型都只能使用构造函数模式。
基本原理:使用原型链实现对原型属性和方法的继承,而通过借用构造函数来实现对实例属性的继承。
function SuperType(name){this.name = name;this.colors = ["red", "blue", "green"];}SuperType.prototype.sayName = function(){alert(this.name);};function SubType(name, age){//继承属性SuperType.call(this, name);this.age = age;}//继承方法SubType.prototype = new SuperType();SubType.prototype.constructor = SubType;SubType.prototype.sayAge = function(){alert(this.age);};var instance1 = new SubType("Nicholas", 29);instance1.colors.push("black");alert(instance1.colors); //"red,blue,green,black"instance1.sayName(); //"Nicholas";instance1.sayAge(); //29var instance2 = new SubType("Greg", 27);alert(instance2.colors); //"red,blue,green"instance2.sayName(); //"Greg";instance2.sayAge();
组合继承避免了原型链和借用构造函数的缺陷,融合了它们的优点,成为JavaScript 中最常用的继承模式。而且,
instanceof和isPrototypeOf()也能够用于识别基于组合继承创建的对象。
道格拉斯·克罗克福德提出,基本思想是:借助原型可以基于已有的对象创建新对象,同时还不必因此创建自定义类型。
function object(o){function F(){}F.prototype = o;return new F();}
在object()函数内部,先创建了一个临时性的构造函数,然后将传入的对象作为这个构造函数的原型,最后返回了这个临时类型的一个新实例。从本质上讲,object()对传入其中的对象执行了一次浅复制。
var person = {name: "Nicholas",friends: ["Shelby", "Court", "Van"]};var anotherPerson = object(person);anotherPerson.name = "Greg";anotherPerson.friends.push("Rob");var yetAnotherPerson = object(person);yetAnotherPerson.name = "Linda";yetAnotherPerson.friends.push("Barbie");alert(person.friends); //"Shelby,Court,Van,Rob,Barbie"
ECMAScript 5 通过新
增Object.create()方法规范化了原型式继承。这个方法接收两个参数:一个用作新对象原型的对象和(可选的)一个为新对象定义额外属性的对象。在传入一个参数的情况下,Object.create()与object()方法的行为相同。
寄生式(parasitic)继承是与原型式继承紧密相关的一种思路,并且同样也是由克罗克福德推而广之的。寄生式继承的思路与寄生构造函数和工厂模式类似,即创建一个仅用于封装继承过程的函数,该函数在内部以某种方式来增强对象,最后再像真地是它做了所有工作一样返回对象。
function createAnother(original){var clone = object(original); //通过调用函数创建一个新对象clone.sayHi = function(){ //以某种方式来增强这个对象alert("hi");};return clone; //返回这个对象}var person = {name: "Nicholas",friends: ["Shelby", "Court", "Van"]};var anotherPerson = createAnother(person);anotherPerson.sayHi(); //"hi"
使用寄生式继承来为对象添加函数,会由于不能做到函数复用而降低效率;这一点与构造函数模式类似。
组合继承是JavaScript最常用的继承模式,不过,它也有自己的不足。组合继承最大的问题就是无论什么情况下,都会调用两次超类型构造函数:一次是在创建子类型原型的时候,另一次是在子类型构造函数内部。
function SuperType(name){this.name = name;this.colors = ["red", "blue", "green"];}SuperType.prototype.sayName = function(){alert(this.name);};function SubType(name, age){SuperType.call(this, name); //第二次调用SuperType()this.age = age;}SubType.prototype = new SuperType(); //第一次调用SuperType()SubType.prototype.constructor = SubType;SubType.prototype.sayAge = function(){alert(this.age);};
解决方法:寄生组合式继承
基本思想:通过借用构造函数来继承属性,通过原型链的混成形式来继承方法。
本质上,就是使用寄生式继承来继承超类型的原型,然后再将结果指定给子类型的原型。寄生组合式继承的基本模式如下所示:
function inheritPrototype(subType, superType){var prototype = object(superType.prototype); //创建对象prototype.constructor = subType; //增强对象subType.prototype = prototype; //指定对象}
YUI 的YAHOO.lang.extend()方法采用了寄生组合继承,从而让这种模式首次出现在了一个应用非常广泛的JavaScript 库中。