第六章 面向对象的程序设计
属性类型
1.数据属性特性
[[Configurable]](可配置):能否通过delete删除属性重新定义属性,能否修改属性的特性,或者能否把属性修改为访问器属性。
[[Enumerable]](可枚举):能否通过for-in循环返回属性。
[[Writable]](可写):能否修改属性的值。
[[Value]](值):包含这个属性的数据值。读写属性值都再这,默认为undefined。
修改属性默认的特性,必须使用Object.defineProperty()方法。
Object.defineProperty(): 接受三个参数:属性所在的对象,属性的名字,描述符对象。属性描述符对象必须是 configurable、enumberable、writable、value。其中的一或多个。
var person = {}; Object.defineProperty(person, "name", { writable: false, //设置为不可写的。(不能修改name的值) configurable: false, //设置为不可配置的, value: "Nicholas" }); console.log(person.name);
// Nicholas
delete person.name;
console.log(person.name);
// Nicholas
person.name = "Gray";
//修改了name的值
console.log(person.name); //修改的值无效
// Nicholas
在调用Object.defineProperty(),如果不指定,configurable,wirtable,enumerable默认值为false.
2、访问器属性
访问器属性不包含数据值。包含一对getter函数和setter函数。
[[Configurable]](可配置):能否通过delete删除属性重新定义属性,能否修改属性的特性,或者能否把属性修改为访问器属性。
[[Enumerable]](可枚举):能否通过for-in循环返回属性。
[[Get]]:读取属性时调用的函数。
[[Set]]: 写入属性时调用的函数。
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; console.log(book.edition); // 2
IE9之前创建访问器属性的方法:
var book = { _year: 2004, edition: 1 } book.__defineGetter__("year", function(){ return this._year; }); book.__defineSetter__("year", function(newValue) { if(newValue > 2004) { this._year = newValue; this.edition +=newValue - 2004; } }); book.year = 2005; console.log(book.edition); // 2
Object.defineProperties(): 通过描述符一次定义多个属性。接受2个对象参数:第一个对象是要添加和修改其属性的对象,第二个对象的属性与第一个对象中要添加或修改的属性对应。
var book = {}; Object.defineProperties(book, { _year: { writbale: true, value: 2004 }, edition: { writable: true, value: 1 }, year: { get: function() { return this._year; }, set: function(newValue) { if(newValue > 2004) { this._year = newValue; this.edition += newValue - 2004; } } } }); book.year = 2007; console.log(book.edition); var descriptor = Object.getOwnPropertyDescriptor(book,"_year"); console.log(descriptor.value); // 4 // 2004 // false
创建对象
1.工厂模式
工厂模式解决了创建多个相似对象的问题。没有解决对象识别的问题(怎样指定一个对象的类型)。
function createPerson(name, age, job) { var o = new Object(); o.name = name; o.age = age; o.job = job; o.sayName = function() { console.log(this.name); }; return o; } var person1 = createPerson("zhangsan", 28, "Software Engineer"); var person2 = createPerson("李四", 25, "Docter"); console.log(person1.name); console.log(person2.name); // zhangsan // 李四
2.构造函数模式
构造函数始终都应该以一个大写字母开头,非构造函数以小写字母开头。以这种方式定义的构造函数是定义再Global对象,再浏览器即window对象下。
function Person(name, age, job) { this.name = name; this.age = age; this.job = job; this.sayName = function() { console.log(this.name); } } var person3 = new Person("Nicholas", 27, "Software Enginner"); var person4 = new Person("Gray", 25, "Docter"); console.log(person3.name); console.log(person4.name);
console.log(person3 instanceof Object);
console.log(person3 instanceof Person);
console.log(person4 instanceof Object);
console.log(person4 instanceof Person);
// Nicholas
// Gray
// true
// true
// true
// true
调用 Person构造函数经历了以下4个步奏:
1.创建一个新对象。
2、将构造函数的作用域赋给新对象(这里的新对象是person3,this指向person3)
3、执行构造函数中的代码。
4.返回新对象。
构造函数的优点:将来可以将它的实例标识为一直特定的类型。
构造函数的缺点:每个方法都要再每个实例上重新创建一遍。函数也是对象,每定义一个函数,即实例化了一个对象。
原型模式
创建的每个函数都有一个prototype属性,这个属性是一个指针,指向一个对象。即原型对象。作用是让所有对象实例共享它包含的属性和方法。
function Person() { } Person.prototype.name = "Nicholas"; Person.prototype.age = 27; Person.prototype.job = "Software Enginner"; Person.prototype.sayName = function() { console.log(this.name); }; //将所有属性和方法直接添加到prototype原型中。这里的Person.prototype.constructor指向他的构造函数,即Person var person1 =new Person(); person1.sayName(); // Nicholas var person2 = new Person(); person2.sayName(); // Nicholas console.log(person1.sayName === person2.sayName); // true person1实例和person2实例访问的都是同一组属性和同一个sayName()函数
原型对象自动获得一个constructor属(构造函数)性,constructor包含一个指向prototype属性所在的函数的指针。
EC5 把这个指针叫[[prototype]]。这个链接存在于实例与构造函数的原型对象之间。
可以用isPrototyOf():确认实例和构造函数之间是否存在这种关系。
console.log(Person.prototype.isPrototypeOf(person1)); console.log(Person.prototype.isPrototypeOf(person2)); // true 用原型对象测试person1和person2.它们内部都有一个指向Person.prototype的指针。 // true
Object.getPrototypeOf(): 返回[[prototype]]值。
console.log(Object.getPrototypeOf(person1) == Person.prototype); console.log(Object.getPrototypeOf(person1).name); // true // Nicholas
通过对象的实例可以访问原型中的值,不能通过对象实例重写原型中的值,再实例中定义的同名属性的值,会屏蔽原型中的值。但是不会修改原型中的值。
function Person() { } Person.prototype.name = "Nicholas"; Person.prototype.age = 27; Person.prototype.job = "Software Enginner"; Person.prototype.sayName = function() { console.log(this.name); }; var person1 =new Person(); person1.sayName(); var person2 = new Person(); person2.name = "张三"; console.log(person1.name); console.log(person2.name); // Nicholas // Nicholas 来着原型 // 张三 来自实例
hasOwnProperty():用于检测一个属性是存在于实例还是原型中。从Object继承而来。在实例中返回true,在原型中返回false;
console.log(person1.hasOwnProperty("name")); console.log(person2.hasOwnProperty("name")); // false 来自原型 // true 来自实例
Object.keys(): 取的对象中所有可枚举的实例属性,接受一个参数:一个对象。返回一个包含所有可枚举属性的字符串数组。
function Person() { } Person.prototype.name = "张三"; Person.prototype.age = 25; Person.prototype.job = "Software Enginner"; Person.prototype.sayName = function() { console.log(this.name); }; var keys = Object.keys(Person.prototype); console.log(keys); var p1 = new Person(); p1.name = "Rob"; p1.age = 27; var p1keys = Object.keys(p1); console.log(p1keys); // ["name", "age", "job", "sayName"] // ["name", "age"] //如果要得到所有的实例属性不论是否可枚举,可使用Object.getOwnPropertyNames() var keys1 = Object.getOwnPropertyNames(Person.prototype); console.log(keys1); //[”constructor“, "name", "age", "job", "sayName"]
更简介的原型语法
function Person() {} Person.prototype = { name: "张三", age: 27, job: "Software Enginner", sayName: function() { console.log(this.name); } };
这种形式的原型语法和之前的原型语法结果一样,但是constructor不在指向原型对象的函数。可以通过显式的设置,让它指向正确。
但是Enumerable特性被设置为:true;
function Person() {} Person.prototype = { constructor: Person, //显式的设置constructor值 name: "张三", age: 27, job: "Software Enginner", sayName: function() { console.log(this.name); } };
Object.defineProperty(Person.prototype,"constructor", { enumerable: false, value: Person });
原型模式的问题
1、所有实例默认情况下都将取的相同的值。
2、在其中一个实例中的修改,会影响其他的实例的属性。像一个实例中添加属性,不是同名的字符串,都是再原型中添加。
function Person() {} Person.prototype = { name: "张三", age: 27, job: "Software Enginner", friends: ["Shelby", "Court"], sayName: function() { console.log(this.name); } }; Object.defineProperty(Person.prototype,"constructor", { enumerable: false, value: Person }); var person3 = new Person(); var person4 = new Person(); person3.friends.push("Van"); console.log(person3.friends); console.log(person4.friends); // ["Shelby", "Court", "Van"] // ["Shelby", "Court", "Van"]
构造函数与原型混成的模式,是最广泛,认同度最高的一直创建自定义类型的方法。
function Person(name, age, job) { this.name = name; this.age = age; this.job = job; this.friends = ["Shely", "Court"]; } Person.prototype = { constructor: Person, sayName: function() { console.log(this.name); } }; var person1 = new Person("张三", 24, "Docter"); var person2 = new Person("李四", 25, "后台"); person1.friends.push("刘五"); console.log(person1.friends); console.log(person2.friends); console.log(person1.friends === person2.friends); // ["Shely", "Court", "刘五"] // ["Shely", "Court"] // false
继承
EC只支持实现继承,实现继承依靠原型链来实现。利用原型让一个引用类型继承另一个引用类型的属性和方法。
实现原型链的基本模式
function SuperType() { this.property = true; } SuperType.prototype.getSuperValue = function() { return this.property; }; //在SuperType的原型中定义一个方法 function SubType() { this.subproperty = false; } SubType.prototype = new SuperType(); //把superType当作实例,赋给SubType的原型,(实例对象包含一个指针[[prototype]],指向构造函数的原型对象),SubType继承了 SuperType SubType.prototype.getSubValue = function() { //为SubType添加了一个新方法 return this.subproperty; };
//重新父原型中的方法 代码需放在替换原型的语句之后 SuperType.prototype.getSuperValue = function() {
return false;
}
var instance = new SubType(); console.log(instance.getSuperValue()); // false
原型链继承的缺点:
1.包含引用类型值的原型属性会被所有实例共享;
2.没有办法在不影响所有对象实例的情况下,给父原型对象的构造函数传递参数。
function SuperType() { this.color = ["yellow", "blue", "green", "red"]; } function SubType() {} SubType.prototype = new SuperType(); var instance1 = new SubType(); instance1.color.push("black"); console.log(instance1.color); var instance2 = new SubType(); console.log(instance2.color); // ["yellow", "blue", "green", "red", "black"] // ["yellow", "blue", "green", "red", "black"]
借用构造函数
即在子类型构造函数的内部调用超类型构造函数。函数只不过是在特定环境中执行代码的对象,通过使用call()或apply()方法在新创建的对象上执行构造函数。
function colorsItem() { this.color = ["red", "blue", "green"]; } function colorsItem2 () { colorsItem.call(this); //借调“colorsItem”构造函数,实际上是在colorItem2的实例下调用“colorsItem”构造函数 } var instance3 = new colorsItem2(); // 实例化一个对象 instance3.color.push("black"); // 在 colorsItem2的其中一个实例中执行colorsItem2的初始化代码,顺便把一个值推进副本中去。每个实例化对象都拥有各自的color属性的副本 console.log(instance3.color); var instance4 = new colorsItem2(); console.log(instance4.color); // ["red", "blue", "green", "black"] // ["red", "blue", "green"]
组合继承
使用原型链实现原型属性和方法的继承,通过借用构造函数实现对实例属性的基础。
在原型上定义方法实现函数复用,又能够保证每个实例有自己的属性。
function SuperType1(name) { this.name = name; this.colors = ["red", "blue", "yellow"]; } SuperType1.prototype.sayName = function () { console.log(this.name); } function SubType1(name, age) { SuperType1.call(this, name); this.age = age; } SubType1.prototype = new SuperType1(); SubType1.prototype.consctructor = SubType1; SubType1.prototype.sayAge = function() { console.log(this.age); } var instances = new SubType1("张三", 27); instances.colors.push("black"); console.log(instances.colors); instances.sayName(); instances.sayAge(); // ["red", "blue", "yellow", "black"] // 张三 // 27 var instances1 = new SubType1("李斯", 25); console.log(instances.colors); instances1.sayName(); instances1.sayAge(); // ["red", "blue", "yellow", "black"] // 李斯 // 25
优点: 可以让不同的实例分别拥有自己的属性又可以使用不同的方法。instanceof和isPrototypeOf()也能识别基于组合继承创建的对象。也是最常用的继承模式。
缺点: 会调用俩次超类型函数,一次是创建子类型函数,另一次在子类型函数内部。
原型式继承
EC5的Object.create()方法:接受2个参数:用作新对象原型的对象和(可选)一个为心对定义额外属性的对象;
var person = { name: "Nicholas", friends: ["Shelby", "Court", "Van"] }; var anotherPerson = Object.create(person, { name: { value: "Greg" } }); console.log(anotherPerson.name); // Greg
寄生式继承
创建一个用于封装继承过程的函数, 该函数在内部以某种方式来增强对象,最后再像针对是它做了所有工作一样返回对象。
function createAnother(original) { var clone = object(original); clone.sayHi = function() { console.log("Hi"); }; return clone; } var person = { name: "Nicholas", friends: ["Shelby", "Court", "Van"] }; var anotherPerson = createAnother(person); anotherPerson.sayHi(); // Hi
寄生组合继承
组合继承会调用俩次超类型函数,一次是创建子类型函数,另一次在子类型函数内部。
function SuperType (name) { this.name = name; this.colors = ["red", "blue", "green"]; } SuperType.prototype.sayName = function() { console.log(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 () { console.log(this.age); };
第一次调用,SubType得到俩个属性,SuperType构造函数的color 和name属性。调用SubType又会再一次调用SuperType构造函数。这一次在新对象上创建实例属性name和color,这俩个同名属性屏蔽了原型中的同名属性。
寄生组合式继承: 通过借用构造函数来继承属性,通过原型链的混成形式来继承方法。
思路:使用寄生式继承来继承超类型的原型,将结果指定给子类型的原型。
寄生组合式继承的模式 是引用类型最理想的继承范式
function inheritPrototype(SubType, SuperType) { var prototype = Object(SuperType.prototype); //创建对象,创建超类型原型的一个副本 prototype.consctructor = SubType; //增强对象 为创建的副本添加constructor属性 SubType.prototype = prototype; //指定对象 将新创建的对象赋值给子类型的原型。 } function SuperType(name) { this.name = name; this.colors = ["red", "blue", "yellow"]; } SuperType.prototype.sayName = function() { console.log(this.name); }; function SubType(name, age) { SuperType.call(this, name); this.age = age; } inheritPrototype(SubType, SuperType); Subtype.prototype.sayAge = function () { console.log(this.age); };