本章内容数组
ECMA-262 把对象定义为:“无序属性的集合,其属性能够包含基本值、对象或者函数。”严格来说,这就至关于说对象是一组没有特定顺序的值。app
每一个对象都是基于一个引用类型建立的,既能够是原生类型,也能够是开发人员定义的类型。函数
建立对象最简单的方式就是建立一个 Object 的实例,而后为它添加属性和方法。测试
var person = { name: "Jack", age: 29, sayName: function() { alert(this.name); } }
这些属性在建立时都带有一些特征值(characteristic),JS 经过这些特征值来定义它们的行为。this
ECMAScript 中有两种属性:数据属性和访问器属性。prototype
数据属性包含一个数据值的位置。在这个位置能够读取和写入值。数据属性有4个描述其行为的特性。指针
对于直接在对象上定义的属性,它们的 [[Configurable]]、[[Enumerable]]、[[Writable]] 特性都被设置为 true,而 [[Value]] 特性被设置为指定的值。code
要修改属性默认的特性,必须使用 ECMAScript 5 的 Object.defineProperty() 方法。接收3个参数:属性所在的对象、属性名字、一个描述符对象。其中描述符(descriptor)对象的属性必须是:configurable/enumerable/writable/value。设置其中的一个或多个值,能够修改对应的特性值。对象
var person = {} Object.defineProperty(person, "name", { writable: false, configurable: false, value: "Nick" }) alert(person.name); // Nick person.name = Jack; alert(person.name); // Nick delete person.name; alert(person.name); // Nick
注意:一旦把属性定义为不可配置的,就不能再把它变回可配置了。也就是说,能够屡次调用 Object.defineProperty()方法修改同一个属性,但在把 configurable 设置为 false 后,就不能了。继承
在调用 Object.defineProperty() 时,若是不指定,则 configurable/writable/enumerable 都为 false。
访问器属性不包含数据值;它们包含一对 getter/setter 函数(不过,这两个函数都不是必须的)。在读取访问器属性时,会调用 getter 函数,这个函数负责返回有效的值;在写入访问器属性时,会调用 setter 函数并传入新值,这个函数负责决定如何处理数据。特性以下:
访问器属性不能直接定义,必须使用 Object.defineProperty() 。
var book = { _year: 2004, edition: 1 }; Object.defineProperty(book, "year", {// IE9+ 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。只指定 getter 表示属性是不能写,反之则表示属性不能读。
Object.defineProperties() 能够经过描述符一次性定义多个属性。接收2个参数:一、第一个对象是要添加和修改其属性的对象;二、第二个对象的属性与第一个对象中要添加或修改的属性一一对应。
var book = {}; Object.defineProperties(book, { // IE9+ _year: { writable: true, value: 2004 }, edition: { writable: true, value: 1 }, year: { get: function() { return this._year; }, set: function(newValue) { this._year = newValue; this.edition++; } } })
使用 ECMAScript 5 中的 Object.getOwnPropertyDescriptor() IE9+ 方法,能够取得给定属性的描述符。这个方法接收两个参数:一、属性所在的对象;二、要读取器描述符的属性名称。返回值是一个对象,若是是数据属性,这个对象的属性有 configurable/enumerable/writable/value,若是是访问器属性,则这个对象的属性有 configurable/enumerable/get/set
// 使用前面的例子 var descriptor = Object.getOwnPropertyDescriptor(book, "_year"); alert(descriptor.value); // 2004 alert(descriptor.configurable); //false alert(typeof descriptor.get); //"undefined" var descriptor = Object.getOwnPropertyDescriptor(book, "year"); alert(descriptor.value); //undefined alert(descriptor.enumerable); //false alert(typeof descriptor.get); //"function"
问题:使用同一个接口建立不少对象,会产生大量的重复代码。
工厂模式抽象了建立具体对象的过程。用函数来封装以特定接口建立对象的细节。
function createPerson(name, age) { var o = new Object(); o.name = name; o.age = age; o.sayName = function() { alert(this.name); }; return o; } var person1 = createPerson("Jack", 29); var person2 = createPerson("Nick", 22);
工厂模式虽然解决了建立多个类似对象的问题,可是没有解决对象识别的问题(即怎样知道一个对象的类型)。
能够建立自定义的构造函数,从而定义自定义对象类型的属性和方法。
function Person(name, age) { this.name = name; this.age = age; this.sayName = function() { alert(this.name); } } var person1 = new Person("Jack", 23); var person2 = new Person("Nick", 22);
构造函数模式有如下几个特色:
要建立 Person 的新实例,必须使用 new 操做符。以这种方式调用构造函数实际上会经历如下4个过程:
使用 instanceof 检测对象类型:
alert(person1 instanceof Object); // true alert(person1 instanceof Person); // true alert(person2 instanceof Object); // true alert(person2 instanceof Person); // true
建立自定义的构造函数意味着未来能够将它的实例标识为一种特定的类型;而这正是构造函数模式赛过工厂模式的地方。
// 当作构造函数来使用 var person = new Person("Nick", 29); person.sayName(); // "Nick" // 当作普通函数调用 Person("Nick", 29); // 添加到 window 对象 window.sayName(); // "Nick" // 在另外一个对象做用域中调用 var o = new Object(); Person.call(o, "Nick", 29); o.sayName(); // "Nick"
每一个方法都要在每一个实例上从新建立一遍。在前面的例子中,person1 和 person2 的 sayName() 方法并非同一个 Function 的实例。由于函数是对象,因此每定义一个函数,也就实例化了一个对象。(new Function())。
解决的办法,能够把函数定义移到构造函数外部。
function Person(name, age) { this.name = name; this.age = age; this.sayName = sayName; } function sayName() { alert(this.name); } var person1 = new Person("Jack", 23); var person2 = new Person("Nick", 22);
但新问题是:在全局做用域定义的函数实际上只能被某个对象调用,这让全局做用域名存实亡。并且,若是对象须要定义不少方法,那么就要定义多个全局函数,因而这个自定义的引用类型就没有丝毫封装性可言。
每一个函数都有一个 prototype(原型)属性,这个属性是一个指针,指向一个对象,这个对象的用途是包含能够由特定类型的全部实例共享的属性和方法。
也能够说 prototype 就是经过调用构造函数而建立的对象实例的原型对象。使用原型对象的好处是可让全部“对象实例”共享“原型对象”所包含的属性和方法。
function Person() {} Person.prototype = { constructor: Person, friends: ["Jack"] }; var person1 = new Person(); var person2 = new Person(); person1.friends.push("Nick"); alert(person1.friends); // "Jack, Nick" alert(person2.friends); // "Jack, Nick" alert(person1.friends === person2.friends); // true
实例通常都是要有本身的所有属性的,然而因为 person1.friends 和 person2.friends 都指向同一个数组,致使修改其中一个,就会在另外一个上同步共享。
构造函数模式用于定义实例属性,原型模式用于定义方法和共享的属性。
function Person(name, age) { this.name = name; this.age = age; this.friends = ["Jack"]; } Person.prototype = { constructor: Person, sayName: function() { alert(this.name); } } var person1 = new Person("Nick", 22); var person2 = new Person("Mike", 21); person1.friends.push("Jane"); alert(person1.friends); // "Jack, Jane" alert(person2.friends); // "Jack" alert(person1.friends === person2.friends); // false alert(person1.sayName === person2.sayName); // true
混成模式中,不一样实例引用了不一样的数组,所以原型对象的问题解决了。
function Person(name, age) { // 属性 this.name = name; this.age = age; // 方法 if (typeof this.sayName != "function") { Person.prototype.sayName: function() { alert(this.name); } } } var person1 = new Person("Nick", 22); person1.sayName();
if 语句检查的能够是初始化以后应该存在的任何属性或方法——没必要用一大堆 if 语句检查每一个属性和每一个方法,只要检查其中一个便可。
对于采用这种模式建立的对象,可使用 instanceof 操做符肯定它的类型。
使用动态原型模式时,不能使用对象字面量重写原型,若是重写,则会切断现有实例与新原型之间的联系。
因为函数没有签名,在ECMAScript 中没法实现【接口继承】。ECMAScript 只支持【实现继承】,并且其实现继承主要依靠【原型链】来实现。
基本思想
利用原型让一个引用类型继承另外一个引用类型的属性和方法。
构造函数、原型、实例之间的关系:
实现原型链的基本模式:
function A() { this.aproperty = true; } A.prototype.getAValue = function() { return this.property; }; function B() { this.bproperty = false; } // 继承了 A,建立了 B 的实例,并将实例赋给 B.prototype B.prototype = new A(); B.prototype.getBValue = function() { return this.bproperty; } var instance = new B(); alert(instance.getAValue); // true
实现的本质是重写原型对象,代之以一个新实例的类型。原来存在于 A 的实例中的全部属性和方法,如今也存在于 B.prototype 中。
全部应用类型默认都继承了 Object,而这个继承也是经过原型链实现的。全部函数的默认原型都是 Object 的实例,所以默认原型都会包含一个内部指针,指向 Object.prototype。这也是全部自定义类型都会继承 toString()valueOf() 等默认方法的根本缘由。
能够经过两种方式来肯定原型和实例之间的关系。
方法一:instanceof,只要用这个操做符来测试实例和原型链中出现过的构造函数,结果就会返回 true。
alert(instance instanceof Object); // true alert(instance instanceof A); // true alert(isntance instanceof B); // true
因为原型链的关系,instance 是 Object、A、B 中任何一个类型的实例。
方法二:isPropertyOf,只要是原型链中出现过的原型,均可以说是该原型链所派生的实例的原型,所以该方法也会返回 true。
alert(Object.prototype.isPropertyOf(instance)); // true
子类型有时候须要覆盖超类型中的某个方法,或者须要添加超类型中不存在的某个方法。给原型添加方法的代码必定要放在替换原型的语句以后。
function A() { this.property = true; } A.prototype.getAValue = function() { return this.property; }; function B() { this.bproperty = false; } // 继承了 A B.prototype = new A(); // 添加新方法 B.prototype.getBValue = function() { return this.bproperty; } // 重写超类型方法 B.prototype.getAValue = function() { return false; }
注意,经过 A 的实例调用 getAValue() 方法时,仍然继续调用原来的方法。
在经过原型链实现继承时,不能使用对象字面量建立原型方法。由于这样作会重写原型链。
function A() { this.property = true; } A.prototype.getAValue = function() { return this.property; }; function B() { this.bproperty = false; } // 继承了 A B.prototype = new A(); // 添加新方法 B.prototype = { getBValue: function() { return this.bproperty; } }; var instance = new B(); alert(instance.getAValue); // error!
又叫“伪造对象”或“经典继承”。
基本思想
在子类型构造函数的内部调用超类型构造函数。函数只不过是在特定环境中执行代码的对象,所以经过使用 apply() 和 call() 也能够在(未来)新建立的对象上执行构造函数。
function A() { this.colors = ["red"]; } function B() { // 继承了 A A.call(this); } var instance1 = new B(); instance1.colors.push("blue"); alert(instance1.colors); // "red, blue" var instance2 = new B(); alert(instance2.colors); // "red"
相对于原型链而言,借用构造函数有一个很大的有时,能够在子类型构造函数中向超类型构造函数传递参数。
function A(name) { this.name = name; } function B() { // 继承了 A A.call(this, "Jack"); } var instance1 = new B(); alert(instance1.name); // "Jack"
为了确保 A 构造函数不会重写子类型的属性,能够在调用超类型构造函数后,再添加应该在子类型中定义的属性。
又叫“伪经典继承”,组合了原型链继承和借用构造函数继承。既经过在原型上定义方法实现了函数服用,又能保证每一个实例都拥有本身的属性。
function A(name) { this.name = name; this.colors = ["red"]; } A.prototype.sayName = function() { alert(this.name); }; function B(name, age) { // 继承属性 A.call(this, name); // 第二次调用 A this.age = age; } // 继承方法 B.prototype = new A(); // 第一次调用 A B.prototype.constructor = B; B.prototype.sayAge = function() { alert(this.age); } var instance1 = new B("Jack", 22); instance1.colors.push("blue"); alert(instance1.colors); // "red, blue" instance1.sayName(); // "Jack" instance1.sayAge(); // 22 var instance2 = new B("Nick", 21); alert(instance2.colors); // "red" instance2.sayName(); // "Nick" instance2.sayAge(); // 21
组合继承避免了原型链和借用构造函数的缺陷,融合了它们的优势,成为JS中最经常使用的继承模式。并且,instanceof 和 isPropertyOf() 也可以用于识别基于组合继承建立的对象。
组合模式的问题
不管什么状况下,都会调用两次超类型构造函数:一次是在建立子类型原型的时候,一次是在子类型构造函数内部。
function object(o) { function F(){}; F.prototype = o; return new F(); }
原型式继承要求必须有一个对象做为另外一个对象的基础。
ECMAScript 5 中新增了 Object.create() 来规范原型式继承。接收2个参数:一、一个用作新对象原型的对象;二、(可选)一个为新对象定义额外属性的对象。在传入一个参数的状况下,Object.create() 和 object() 的行为相同。
var person = {}; var anotherPerson = Object.create(person);
若是只想让一个对象与另外一个对象保持相似的状况下,原型式继承彻底能够胜任。可是包含引用类型值的属性始终都会共享相应的值,这点与原型模式同样。
它的思路与寄生构造函数和工厂模式类似,即建立一个仅用于封装继承过程的函数,该函数在内部以某种方式来加强对象,最后再像真地是它作了全部工做同样返回对象。
function createAnother(original) { var clone = object(original); // 经过调用函数建立一个新对象 clone.sayHi = function() { // 以某种方式加强对象 alert("Hi"); }; return clone; // 返回对象 } var person = { name: "Jack", friends: ["Nick", "Tony"] }; var anotherPerson = createAnother(person); anotherPerson.sayHi(); // "Hi"
新对象不只具备 person 的全部属性和方法,还有本身的方法。
在主要考虑“对象”而不是“自定义类型”和“构造函数”的状况下,寄生式继承也是一种有用的模式。object() 并非必需的;任何可以返回新对象的函数都适用于该模式。
注意:使用寄生式继承来为对象添加函数,会因为不能作到函数复用而下降效率;这一点与构造函数模式相似。
本质上,就是使用“寄生式继承”来继承超类型的原型,再将结果指定给子类型的原型。
function inheritPrototype(sub, super) { var prototype = Object(super); // 建立对象 prototype.constructor = sub; // 加强对象 sub.prototype = prototype; // 指定对象 }
修改以前的例子:
function A(name) { this.name = name; } A.prototype.sayName = function() { alert(this.name); }; function B(age) { A.call(this, "Jack"); this.age = age; } inheritPrototype(B,A); B.prototype.sayAge = function() { alert(this.age); }
该模式的高效率体如今它只调用了一次 A 构造函数,而且所以避免了在 B 的 prototype 上面建立没必要要的、多余的属性。与此同时,原型链还能保持不变;所以,还可以正常使用 instanceof 和 isPrototypeOf() 方法。开发人员广泛认为寄生组合式继承是引用类型最理想的继承范式。
ECMAScript 支持面向对象(OO)变成,但不使用类或者接口。对象能够在代码执行过程当中建立和加强,所以具备动态性而非严格定义的实体。在没有类的状况下,能够采用下列模式建立对象:
JS 主要经过原型链实现继承。原型链的构建是经过将一个类型的实例赋值给另一个构造函数的原型实现的。这样,子类型就能够继承超类型的属性和方法,这一点与基于类的继承很类似。
原型链的问题是:对象实例共享全部继承的属性和方法,所以不适宜单独使用。解决这个问题的技术是借用构造函数,即在子类型构造函数的内部调用超类型构造函数。这样就能够作到每一个实例都具备本身的属性,同时还能保证只使用构造函数模式来定义类型。
使用最多的继承模式是组合继承,这种模式使用原型链继承共享的属性和方法,经过借用构造函数继承实例属性。
此外,还存在下列可供选择的继承模式: