读书时间《JavaScript高级程序设计》二:面向对象
接着上次的进度,开始看第6章。
第6章 面向对象的程序设计
理解对象
创建自定义对象最简单的方式就是创建一个 Object 的实例,然后为它添加属性和方法。
var person = new Object(); person.name = 'xxx'; person.say = function(){ alert(this.name); } //等同于 对象字面量 var person = { name: 'xxx', say: function(){ alert(this.name); } }
ECMAScript中有两种属性:数据属性 访问器属性
数据属性包含一个数据值的位置,在这个位置可以读写。要修改属性默认的特性,必须使用 ECMAScript 5定义的 Object.defineProperty() 方法。这个方法接收3个参数:属性所在的对象、属性的名称、一个描述符对象(configurable、enumerable、writable、value).
var person = {}; Object.defineProperty(person, "name", { writable: false, value: "Nicholas" }); alert(person.name); //Nicholas person.name = "Michael"; alert(person.name); //Nicholas // Object.defineProperty(person, "name", { configurable: false, value: "Nicholas" });
访问器属性不包含数据值,包含一对 getter setter。访问器属性不能直接定义,必须使用 defineProperty() 来定义。
读取属性的特性
ECMAScript 5中的 getOwnPropertyDescriptor() 方法可以取得属性的描述符。这个方法接收2个参数:属性所在的对象、要读取描述符的属性名称。
创建对象
虽然 Object 构造函数或对象字面量都可以创建单个对象,但这些方式有缺点:使用同一个接口创建很多对象,会产生大量的重复代码。
1. 工厂模式
//工厂模式 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"
2. 构造函数模式
//构造函数模式 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 操作符。
一般经历4个步骤:
1). 创建一个新对象
2). 将构造函数的作用域赋给新对象(这时this指向了这个新对象)
3). 执行构造函数中的代码(为新对象添加属性、方法)
4). 返回新对象
创建的新对象都有一个 constructor (构造函数)属性,这个属性指向 那个new实例的构造函数。
instanceof 操作符用来检测对象类型。
//constructor alert(person1.constructor == Person); //true alert(person2.constructor == Person); //true //instanceof alert(person1 instanceof Object); //true alert(person1 instanceof Person); //true alert(person2 instanceof Object); //true alert(person2 instanceof Person); //true
构造函数也是普通的函数, 任何函数通过new操作符调用,那它就可以作为构造函数。
//new操作符 当作构造函数调用 var person = new Person("Nicholas", 29, "Software Engineer"); person.sayName(); //"Nicholas" //作为普通函数调用 Person("Greg", 27, "Doctor"); //adds to window window.sayName(); //"Greg" //在另一个对象作用域调用 var o = new Object(); Person.call(o, "Kristen", 25, "Nurse"); o.sayName();
3. 原型模式
新创建的每个函数都有一个 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
理解原型对象
无论什么时候,只要创建一个新函数,就会根据一组特定的规则为该函数创建一个 prototype 属性,这个属性指向函数的原型对象。 在默认情况下,所有原型对象都会自动获得一个 constructor 属性,这个属性包含一个指向 prototype 属性所在函数的指针。
创建了自定义构造函数后,其原型对象默认只会取得 constructor 属性;其他方法,则是从 Object 继承而来。 当调用构造函数创建一个新实例,该实例的内部将包含一个指针(内部属性[[prototype]])指向构造函数的原型对象。( 在一些浏览器中这个属性为 __proto__ )
Person的实例:person1, person2,这2个实例都包含一个内部属性( 某些浏览器中:__proto__ ),这个属性指向了 Person.prototype。( 换句话说这个内部属性和构造函数没有直接关系 )
可以通过 isPrototypeOf() 方法来确定对象之间是否存在这种关系(实例内部属性[[prototype]]指向构造函数原型)
alert(Person.prototype.isPrototypeOf(person1)); //true alert(Person.prototype.isPrototypeOf(person2)); //true
使用 hasOwnProperty() 方法可以检测一个属性是存在于实例中,还是存在于原型中。
//检测属性 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")); //false alert("name" in person1); //true person1.name = "Greg"; alert(person1.name); //"Greg" – from instance alert(person1.hasOwnProperty("name")); //true alert("name" in person1); //true alert(person2.name); //"Nicholas" – from prototype alert(person2.hasOwnProperty("name")); //false alert("name" in person2); //true delete person1.name; alert(person1.name); //"Nicholas" - from the prototype alert(person1.hasOwnProperty("name")); //false alert("name" in person1); //true
//判断原型属性 function hasPrototypeProperty(object, name){ return !object.hasOwnProperty(name) && (name in object); } var person = new Person(); alert(hasPrototypeProperty(person, "name")); //true person.name = "Greg"; alert(hasPrototypeProperty(person, "name")); //false
在使用for-in循环时,返回是所有能够通过对象访问的、可枚举的属性,包括实例的属性,原型的属性。要取得对象所有可枚举的实例属性,可以使用 ECMAScript 5 中的 Object.keys() 方法。这个方法接收一个对象,返回一个包含可枚举属性的字符串数组。
//属性枚举 function Person(){} Person.prototype.name = "Nicholas"; Person.prototype.age = 29; Person.prototype.job = "Software Engineer"; Person.prototype.sayName = function(){ alert(this.name); }; var keys = Object.keys(Person.prototype); alert(keys); //"name,age,job,sayName" var person1 = new Person(); person1.name = 'xxx'; person1.age = 50; var person1keys = Object.keys(person1); alert(person1keys); //"xxx,50"
可以这样扩展:
if(!Object.create){ Object.create = function(o){ function F(){} F.prototype = o; return new F(); } } if(!Object.keys){ Object.keys = function(o){ var k = [], p; for(p in o){ if(Object.prototype.hasOwnProperty.call(o,p)){ k.push(p); } } return k; } }
更简单的原型语法
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
原型的动态性
由于在原型中查找值的过程是一次搜索,因此对原型对象所做的任何改变都能够立即从实例上反映出来,即时是先创建实例后改变原型。
//原型的动态性 function Person(){} Person.prototype = { constructor: Person, name : "Nicholas", age : 29, job : "Software Engineer", sayName : function () { alert(this.name); } }; var friend = new Person(); Person.prototype.sayHi = function(){ alert("hi"); }; friend.sayHi(); //"hi"
如果重写整个原型对象,会切断实例中的指针( 调用构造函数会为实例添加一个指向最初原型的指针[[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
原生对象的原型
原型模式的重要性不仅体现在创建自定义类型上,就是所有原生的引用类型,也都是采用原型模式创建的。所有原生引用类型(Object、Array、String...)都是在其构造函数的原型上定义了方法。
//原生对象的原型 alert(typeof Array.prototype.sort); //"function" alert(typeof String.prototype.substring); //"function" String.prototype.startsWith = function (text) { return this.indexOf(text) == 0; }; var msg = "Hello world!"; alert(msg.startsWith("Hello")); //true
原型模式的问题
原型模式省略了为构造函数传递初始化参数这一环节,会造成所有实例在默认情况下都取得相同的属性值。原型中所有的属性是被很多实例共享的,实例一般都有属于自己的属性。
//原型问题 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
4. 组合使用构造函数模式和原型模式
创建自定义类型的最常见方式,就是组合使用构造函数模式和原型模式。构造函数用于定义实例属性,原型模式用于定义方法和共享属性。 这样的组合,每个实例都有自己的一份实例属性,又共享对方法的引用,最大限度的节省了内存。
//组合构造函数模式和原型模式 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
这种构造函数与原型混合的模式,是目前是用最广泛、认同度最高的一种创建自定义类型的方法。这是用来自定义类型的一种默认模式。
5. 动态原型模式
动态原型模式把所有信息都封装在构造函数中,而通过在构造函数中初始化原型,又保持同时使用构造函数和原型的优点。
//动态原型模式 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();
使用动态原型模式,不能使用对象字面量重写原型。
6. 寄生构造函数模式
7. 稳妥构造函数模式
继承
许多OO语言支持两种继承:接口继承、实现继承。接口继承只继承方法签名,实现继承则继承实际的方法。在ECMAScript中无法实现接口继承,因为函数没有签名。所以ECMAScript只支持实现继承,实现继承主要是依靠原型链来实现的。
1. 原型链
原型链的基本思想是利用原型让一个引用类型继承另一个引用类型的属性和方法。
构造函数、原型、实例的关系:每个构造函数都有一个原型对象,原型对象都包含一个指向构造函数的指针( constructor ),实例都包一个指向原型对象的内部指针( [[prototype]] __proto__ )。
如果让原型对象等于另一个类型的实例,此时的原型对象将包含一个指向另一个原型的指针,相应的,另一个原型中也包含着一个指向另一个构造函数的指针。假如另一个原型又是另一个类型的实例,这种关系依然成立,如此层层递进,就构成了实例与原型的链条。这就是所谓原型链的基本概念。
//原型链 //SuperType function SuperType(){ this.property = true; } SuperType.prototype.getSuperValue = function(){ return this.property; }; //SubType function SubType(){ this.subproperty = false; } //继承了SuperType SubType.prototype = new SuperType(); SubType.prototype.getSubValue = function (){ return this.subproperty; }; //instance var instance = new SubType(); alert(instance.getSuperValue()); //true //原型和实例的关系 1 alert(instance instanceof Object); //true alert(instance instanceof SuperType); //true alert(instance instanceof SubType); //true //原型和实例的关系 2 alert(Object.prototype.isPrototypeOf(instance)); //true alert(SuperType.prototype.isPrototypeOf(instance)); //true alert(SubType.prototype.isPrototypeOf(instance)); //true
//原型链关系图
所有的引用类型都继承了 Object,这个继承也是通过原型链实现的。
SubType继承了SuperType, SuperType继承了Object. 当调用instance.toString()时,实际上调用的是保存在Object.prototype中的那个toString()方法.
//谨慎的定义方法
子类型有时候需要重写超类型中的某个方法,或者添加超类型中不存在的方法,给原型链加方法的代码一定要放在替换原型的语句之后。
//原型链的问题
//原型链的问题 function SuperType(){ this.colors = ["red", "blue", "green"]; } function SubType(){} SubType.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"
SuberType构造函数定义了一个colors属性,SubType通过原型链继承SuperType的一个实例,因此SubType.prototype变成了SuperType的一个实例,也拥有一个自己的colors属性, 所有的SubType实例都共享这个属性。对instance1.colors的修改也会通过instance2.colors反映出来。
创建子类型的实例时,不能向超类型的构造函数传递参数。实际上是没有办法在不影响所有对象实例的情况下,给超类的构造函数传递参数。
2. 借用构造函数
3. 组合继承
4. 原型式继承 - ‘现代’无类继承模式
function object(o){ function F(){} F.prototype = o; return new F(); } if(!Object.create){ Object.create = function(o){ function F(){} F.prototype = o; return new F(); } }
ECMAScript新增了 Object.create() 方法规范化了原型式继承。这方法介绍两个参数:一个用作新对象原型的对象、一个为新对象定义额外属性的对象。在传入一个参数的情况下,Object.create() 和 object()方法类似。
5. 寄生式继承
6. 寄生组合式继承
function object(o){ function F(){} F.prototype = o; return new F(); } function inheritPrototype(subType, superType){ var prototype = object(superType.prototype); //create object prototype.constructor = subType; //augment object subType.prototype = prototype; //assign object } 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; } inheritPrototype(SubType, SuperType); 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(); //29 var instance2 = new SubType("Greg", 27); alert(instance2.colors); //"red,blue,green" instance2.sayName(); //"Greg"; instance2.sayAge(); //27
在这个例子中,它只调用了一次 SuperType 构造函数,避免了在 SubType.prototype 上创建不必要的属性。原型链也保持不变,还能正常使用 instanceof 和 isPrototypeOf()。普遍认为寄生组合式继承是引用类型最理想的继承方式。
第6章主要介绍了面向对象相关知识。创建对象:工厂模式、构造函数模式、原型模式、组合使用构造函数模式和原型模式... 实现继承:原型式、寄生式、寄生组合式... 这一章需要深刻理解,是JS能否走得更远的重要基础。