ECMA-262 把对象定义为
无序属性的集合,其属性可以包含基本值、对象或者函数。
即对象是一组没有特定顺序的值。对象的每个属性或方法都有一个名字,而每个名字都映射到一个值。正因为这样,我们可以把 ECMAScript 的对象想象成散列表:无非就是一组名值对,其中值可以是数据或函数。
ECMAScript 第 5 版 在定义只有内部采用的特性(attribute)时, 描述了属性(property)的各种特征。ECMAScript 定义这些特性是为了实现 JavaScript 引擎用的,因此在 JavaScript 中不能直接访问它们。为表示特性是内部值,该规范把它们放在两对方括号中,例如 [[Enuermerable]]。
ECMAScript 中只有两种属性:数据属性和访问器属性。
[[value]]:包含这个属性的数据值。读取属性值的时候,从这个位置读;写入属性值的时候,把新值保存在这个位置。
要修改属性默认的特性,必须用 ECMAScript 5 的 Object.defineProperty()
方法。这个方法接受三个参数`: 属性所在的对象,属性名,和一个描述符对象。其中,描述符(descriptor)对象的属性必须是:configurable、enumerable、writable 和 value。
js
var person = {}; Object.defineProperty(person,"name",{ writable: false, value: "Nicholas", configurable: false }); alert(person.name);//"Nicholas" person.name = "PaddingMe"; alert(person.name);//"Nicholas" delete person.name; alert(person.name);//"Nicholas"
一旦把属性定义为不可配置特性就不能再把它变为可配置。
在调用 Object.defineProperty()
方法时,如果不指定, configurable, enumerable 和 writable 特性的默认值为 false。
访问器属性
访问器属性不包含数据值,他们包含一对 getter 和 setter 函数(不过不是必须的),在读取访问器属性时,会调用 getter 函数,这个函数负责返回有效的值。在写入访问器属性时,会调用 setter 函数并传入新值,此函数负责决定如何处理数据。访问器也有4个特性:
[[set]]: 在写入属性时调用的函数。默认值为 undefined。
访问器不能直接定义,必须使用 object.defineProperty()
来定义。
var book = { _year: 2004, edition: 1 }; Object.defineProperty(book, "year", { get: function() { return this._year; }, set: function() { if (newValue > 2004) { this._year = newValue; this.edition += newValue -2004; } } }); book.year = 2005; alert(book.edition); //2
_year 前面的下划线用于表示只能通过对象方法访问的属性。book.edition 变为 2 这是使用访问器属性的常见方式。即设置一个属性的值会导致其他属性发生变化。
Object.definePorperties()
接受两个对象参数:第一个要添加或修改其属性的对象,第二个对象的属性 与第一个对象中要添加或修改的属性一一对应。
Object.getOwnPropertyDescriptor()
方法获取给定属性的描述符。
此方法接受2个参数:属性所在的对象和要读取其描述符的属性名称。
返回值是一个对象。
js
var book = {}; Object.defineProperties(book,{ _year: {value: 2004}, edition: {value: 1}, year: { get: function() { return this._year; }, set: function() { if (newValue > 2004) { this._year = newValue; this.edition += newValue -2004; } } } }); 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
最简单的可以用 Object 构造函数,或者对象字面量来创建单个对象,但这样使用一个接口创建很多对象,会产生大量的重复代码。
js
var paddingme = new Object(); //用 Object 构造函数 创建对象 var paddingme = {}; //对象字面量创建对象
工厂模式是用函数来封装以特定接口创建对象的细节。
js
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 person = createPerson("PaddingMe",25,"front-end developer");
工厂模式虽然解决了创建多个相似对象的问题,但没有解决对象识别的问题(即怎样知道一个对象的类型)。
js
function Person(name, age, job) { this.name = name; this.age = age; this.job = job; this.sayName = function() { alert(this.name); }; } var person1 = new Person("PaddingMe",25,"front-end developer"); alert(person1.constructor == Person);//true alert(person1 instanceof Person);//true alert(person1 instanceof Object);//true
构造函数模式与工厂模式不同的是:
- 没有显式地创建对象;
- 没有 return 语句;
- 直接将属性和方法赋给了 this 方法。
另按照惯例,构造函数首字母都应该大写。
创建 Person 新实例,经过了以下4个新步骤:
创建自定义的构造函数意味着将来可以将它的实例标识为一种特殊的类型,而这正是构造函数模式胜过工厂模式的地方。
js
//当作构造函数来使用 var person = new Person("paddingme",25,"F2ER"); person.sayName();//"paddingme" //作为普通函数来使用; Person("paddingme",25,"F2ER"); window.sayName();//"paddingme" //**当在全局作用域中调用一个函数时,this 对象总是指向 Global 对象。** //在另一个对象的作用域中调用 var o = new Object(); Person.call(o,"paddingme",25,"F2ER"); o.sayName();// "paddingme"
构造函数创建对象的问题在于:每个方法都要在每个实例上重新创建一遍,会导致不同的作用域链和标识符解析。不同实例上的同名函数是不相等的。
我们创建的每个函数都有一个 prototype(原型) 属性,这个属性是一个指针,指向一个对象,而这个对象的用途是包含可以由特定类型的所有实例共享的属性和方法。 按照字面意思即 prototype 就是调用构造函数而创建的那个对象实例的原型对象。使用原型对象的好处就是可以让所有对象实例共享它所包含的属性和方法。换言之,不必在构造函数中定义实例的信息,而是可以将这些信息直接添加到原型对象中。
js
function Person() {} Person.prototype.name = "PaddingMe"; Person.prototype.age = 29; Person.prototype.job = "Front-end Engineer"; Person.prototype.sayName = function () { alert(this.name); }; var person1 = new Person(); person1.sayName(); //"PaddingMe" var person2 = new Person(); person2.sayName(); //"PaddingMe"
理解原型对象
无论什么时候,只要创建一个新函数,就会根据一组特定的规则为该函数创建一个 prototype 属性,这个属性指向函数的原型对象。在默认情况下,所有原型对象都会自动获得一个 constructor 属性。这个属性包含一个指向所在函数的指针。 创建了自定义的构造函数之后,其原型对象默认只会取得 constructor 属性;至于其他方法,都是从 Object 继承而来的。当调用构造函数创建一个新实例后,该实例的内部将包含一个指针(内部属性),指向构造函数的原型对象。ECMAScript 管此指针叫 [[prototype]]
Person 构造函数、 Person 的原型属性以及 Person 现有的两个实例之间的关系。在此,Person.prototype 指向了原型对象,而 Person.prototype.constructor 又指回了 Person。 原型对象中除了包含 constructor 属性之外,还包括后来添加的其他属性。 Person 的每个实例都包含一个内部属性,该属性指向了 Person.prototype;换句话说,它们与构造函数没有直接的关系。
js
alert(Person.prototype.isPrototype(person1));//true alert(Person.prototype.isPrototype(person2));//true
ECMASript 5 增加了 Object.getPrototypeOf()
,在所有支持的实现中,这个方法返回[[Prototype]] 的值。
js
alert(Object.getPrototypeOf(person1) == Person.prototype); // true alert(Object.getPrototypeOf(person1).name); // "PaddingMe"
每当代码读取某个对象的某个属性时,都会执行一次搜索,目标是具有给定名字的属性。搜索首先从对象实例本身开始。若在实例中找到了具有给定名字的属性,则返回该属性的值。若没有,则继续搜索指针指向的原型镀锡i昂,在原型对象中查找是否具有给定名字的属性。若在原型对象中找到此属性,则返回该属性的值。
原型最初只包括 constructor 属性,而该属性也是共享的,因此可以通过对象实例访问
当为对象实例添加一个属性时,这个属性就会 屏蔽 原型对象中同名属性,换句话说,添加这个属性只会阻止我们访问原型对象中的属性,而不会修改那个属性。
使用 hasOwnProperty()
方法可以检测一个属性是存在于实例中,还是存在于原型中。
这个方法(它继承于 Object)只在给定属性存在于对象实例中,才会返回 true。
js
function Person() {} Person.prototype.name = "PaddingMe"; Person.prototype.age = 29; Person.prototype.job = "Front-end Engineer"; Person.prototype.sayName = function () { alert(this.name); }; var person1 = new Person(); var person2 = new Person(); alert(person1.hasOwnProperty("name"));//false person2.name = "hhb"; alert(person2.hasOwnProperty("name"));//true delete person2.name; alert(person2.hasOwnProperty("name"));//false
原型与 in 操作符
在单独使用时,in 操作符会在通过对象能够访问给定属性时返回 true,无论该属性是在实例中还是原型中。与 hasOwnProperty()
一起使用可以确定该属性到底是在对象中,还是存在于原型中。
js
function hasPrototypeProperty(){ return !object.hasOwnProperty(name) && (name in object); }
在使用 for-in 循环时, 返回的是所有能够通过对象访问的、可枚举(enumerated)属性,其中既包括存在于实例中的属性,也包括存在于原型中的属性。屏蔽了原型中不可枚举属性的实例属性也会返回,因为根据规定,所有开发人员定义的属性都是可枚举的——只有在 IE8 以及更早版本中例外。
要去的对象上所有可枚举的实例属性,可使用 ECMAScript 5 中的 Object.keys() 方法。此方法要接受一个对象作为参数,返回一个包含所有可枚举属性的字符串组。
js
function Person() {} Person.prototype.name = "PaddingMe"; Person.prototype.age = 29; Person.prototype.job = "Front-end Engineer"; Person.prototype.sayName = function () { alert(this.name); }; var keys = Object.keys(Person.prototype); alert(keys);// "name,age,job,sayName" var p1 = new Person(); p1.name = "hhb"; p1.age = 25; var p1keys = Object.keys(p1); alert(p1keys);// "name,age"
想要得到所有的实例属性,无论它是否可枚举,可以使用 Object.getOwnPropertyNames()
方法。
js
var keys = Object.getOwnPropertyNames(Person.prototype); alert(keys);// "constructor,name,age,job,sayName"
更简单的原型方法用对象自变量来重写真哥哥原型对象。
js
function Person(){ } Person.prototype = { name : "PaddingMe", age : 25, job : "F2ER", sayName : function() { alert(this.name); } };
注意 constructor 属性不再指向 Person,而是指向了 Object 构造函数。
js
function Person(){ } Person.prototype = { constructor: Person, name : "PaddingMe", age : 25, job : "F2ER", sayName : function() { alert(this.name); } };
原型的动态性
对原型对象所在的任何修改都能够立即从实例上反映出来—— 即使是先创建了实例后修改原型也是一样的。但若是重写整个原型对象,情况就不一样了。
调用构造函数时会为实例添加一个指向最初原型的 [[prototype]] 的指针,而把原型修改为另外一个对象就等于切断了构造函数于最初原型之间的联系。 实例中的指针只指向原型,而不指向构造函数。
js
function Person() { } var friend = new Person(); Person.prototype = { constuctor : Person, name : "PaddingMe", age : 25, sayName : function(){ alert(this.name); } }; friend.sayName(); //error
原生对象的原型
js
String.prototype.startsWith = function (text) { return this.indexOf(text) == 0; } var msg = "Hello world!"; alert(msg.startsWith("Hello")); //true
原型对象的问题所有实例在默认情况下都将取得相同的属性值。原型模式的最大问题是由其共享的本性所导致。
js
funtion Person(){ } Person.prototype = { constuctor: Person, name: "PaddingMe", age: 26, job: "F2ER", friends: ['hw','wjj'], sayName: function() { alert(this.name); } }; var person1 = new Person(); var person2 = new Person(); Person1.friends.push("ex"); alert(person1.friends);//"hw,wjj,ex" alert(person2.friends);//"hw,wjj,ex" alert(person1.friends == person2.friends);//true
构造函数模式用于定义实例属性,而原型模式用于定义方法和共享的属性。
js
function Person(name,age,job) { this.name = name; this.age = age; this.job = job; this.friends = ["hw","wjj"]; } Person.prototype = { constructor: Person, sayName: function() { alert(this.name); } } var person1 = new Person("winter",59,"Full-Stack Enginner"); var person2 = new Person("PaddingMe",25,"F2ER"); person1.friends.push("ex"); alert(person1.friends); // "hw,wjj,ex" alert(person2.friends); // "hw,wjj" alert(person1.friends == person2.friends); //false alert(person1.sayName == person2.sayName); //true
js
function Person(name,age,job) { this.name = name; this.age = age; this.job = job; if(typeof this.sayName != "funtion") { Person.prototype.sayName = function(){ alert(this.name); }; } } var friend = new Person("PaddingMe",25,"F2ER"); friend.sayName();
寄生(parasitic)构造函数模式的基本思想是:创建一个函数,该函数的作用仅仅是封装创建对象的代码,然后再返回新创建的对象;但从表面上看,有很像典型的构造函数。
js
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("PaddingMe",25,"F2ER"); friend.sayName;// "PaddingMe"
js
function SpecialArray() { var values = new Array(); values.push.apply(values,arguments); values.toPipedString = function() { return this.join("|"); } } var colors = new SpecialArray("red","blue","green"); alert(colors.toPipedString());//"red|blue|green"
关于寄生构造函数模式需要说明的是:返回的对象与构造函数或者与构造函数的原型属性之间没有关系,亦即构造函数返回的都喜爱那个与在构造函数外部建立的对象没什么不同。所以不能依赖 instanceof 操作符来确定对象模型。
所谓稳妥对象是指没有公共属性,而且其方法也引用 this 的对象。稳妥对象最适合在一些安全的环境中(这些环境中会禁止使用 this 和 new),或者在防止数据被其他应用程序(如 Mashup 程序)改动时使用。
稳妥构造函数模式与寄生构造函数模式不一样的是:
- 新创建对象的实例方法不引用 this;
- 不使用 new 操作符调用构造函数。
js
function Person(name,age,job) { var o = new Object(); o.sayName = function (){ alert(name); }; return o; }