ECMA-262对对象的定义:无序属性的集合,其属性可以包含基本值、对象或者函数。
6.1 理解对象
创建对象
//早起创建对象方法
var person = new Object();
person.name = "Greg";
person.age = 29;
person.sayName = function(){
console.log(this.name);
};
//字面量方法
var person = {
name:"Greg",
age:29,
sayName:function(){
console.log(this.name);
}
};
6.1.1属性类型
只有内部才用的特性。规范:例如[[Enumerable]]
数据属性
- 数据属性包含一个数据值的位置,在这个位置可以读取和写入值。
特性 | 描述 |
---|---|
[[Enumerable]] | 能否通过for-in循环返回属性,例子中直接在对象上定义的属性,默认true |
[[Configurable]] | 能否通过delete删除属性从而重新定义属性,能否修改属性特性,能否把属性修改为访问器属性,例子中直接在对象上定义的属性,默认true |
[[Writable]] | 能否修改属性的值,例子中直接在对象上定义的属性,默认true |
[[Value]] | 包含这个属性的数据值,默认undefined |
例如,前面person对象的name属性的数据属性分别是true,true,true,“Greg”。
* 修改属性默认的特性 Object.defineProperty(object,property,descriptor)
该函数接收三个参数:属性所在的对象、属性的名字和一个描述符对象。描述符对象的属性必须是enumerable、configurable、writable和value中的一个或多个。
var person = new Object();
Object.defineProperty(person,"name",{
writable:false,
value:"Jane"
});
person.age = 29;
person.sayName = function(){
console.log(this.name);
};
person.sayName(); //Jane
person.name = "Greg";
person.sayName(); //Jane
如果尝试为name属性指定新值,非严格模式下,赋值被忽略;严格模式下,抛出错误。
注:可以多次修改同一属性,但是修改Configurable特性后,只能再修改writable特性。
访问器属性
- 访问器属性不包含数据值,有一对getter和setter函数
特性 | 描述 |
---|---|
[[Enumerable]] | 能否通过for-in循环返回属性,例子中直接在对象上定义的属性,默认true |
[[Configurable]] | 能否通过delete删除属性从而重新定义属性,能否修改属性特性,能否把属性修改为访问器属性,例子中直接在对象上定义的属性,默认true |
[[Get]] | 读取属性时候调用的函数,默认undefined |
[[Set]] | 写入属性时候调用的函数,默认undefined |
* 访问器属性的定义:Object.defineProperty(object,property,descriptor)
//使用访问器属性常见方式:设置一个属性的值,导致其他属性发生变化
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
6.1.2 定义多个属性
Object.defineProperties(object,properties)
该函数接收两个参数,第一个是要添加和修改其属性的对象;第二个对象的属性与要添加的修改的对象的属性一一对应。
var book = {};
Object.defineProperties(book,{
//数据属性
_year:{value:2004},
edition:{value:1},
//访问器属性
year:{
get: function () {
return this._year;
},
set: function (newValue) {
if (newValue > 2004) {
this._year = newValue;
this.edition += newValue - 2004;
}
}
}
});
6.1.3 读取属性的特性
Object.getOwnPropertyDescription()接收两个参数:属性所在的对象和要读取其描述符的属性名称。
var descriptor = Object.getOwnPropertyDescriptor(book,"_year");
console.log(descriptor.value); //2004
console.log(descriptor.configurable); //false
6.2 创建对象
6.2.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("Jane",29,"teacher");
弊端:没有解决对象识别问题(怎样知道一个对象的类型)
6.2.2 构造函数模式
function Person(name,age,job){
this.name = name;
this.age = age;
this.job = job;
this.sayName = function(){
console.log(this.name);
};
}
var person1 = new Person("Jack",23,"worker");
var person2 = new Person("Tom",23,"actor");
与上面一种方法比较:
* 没有显式地创建对象,
* 直接将属性和方法赋给this
* 没有return语句
* 创建实例的时候需要用new操作符
弊端:每个方法都要在每个实例上创建一遍,比如上面的person1和person2的sayName是不相等的。有this属性在,没必要在执行代码前面就把函数绑定到特定对象上面。如果把函数定义转移到构造函数外部,又影响封装性。
6.2.3 原型模式
创建的每个函数都有一个prototype属性,该属性是一个指针,指向一个对象,这个对象的用途是包含可以由特定类型的所有实例共享的属性和方法。
function Person(){}
Person.prototype.name = "Tom";
Person.prototype.age = 23;
Person.prototype.sayName = function(){
console.log(this.name);
};
var person1 = new Person();
person1.sayName(); //Tom
var person2 = new Person();
person2.sayName(); //Tom
console.log(person1.sayName == person2.sayName); //true
- 原型对象
任何时候创建一个新函数时,就会为该函数创建一个prototype属性,这个属性指向函数的原型对象。默认情况下,所有原型对象都会自动获得一个constructor(构造函数)属性,这个属性包含一个指向prototype属性所在函数的指针。调用函数创建一个新的实例,该实例的内部将包含一个指针(内部属性),指向构造函数的原型对象。ES5称之为[[Prototype]]。
①Person.prototype指向原型对象
②Person.prototype.constructor指回Person
③Person的每个实例的内部属性只指向Person.prototype,实例与构造函数没有直接关系
确定对象和原型之间是否有关系,有以下两种方法
console.log(Person.prototype.isPrototypeOf(person1)); //true
console.log(Object.getPrototypeOf(person1) == Person.prototype); //true
每当代码读取一个对象的某个属性时,先搜索对象实例有没有这个属性,有就返回该属性的值;如果没找到,继续搜索指针指向的原型对象,若在原型对象找到该属性,则返回该属性的值。
注:不能通过对象实例重写原型中的值,实例中与原型对象重名的属性,会屏蔽原型的属性。
person1.name = "Jack";
person1.sayName(); //Jack——来自实例
person2.sayName(); //Tom——来自原型
使用delete可以完全删除实例属性,同时原型属性不受影响
delete person1.name;
person1.sayName(); //Tom——来自原型
检测一个属性是否存在于实例中:hasOwnProperty()
console.log(person1.hasOwnProperty("name")); //false
- in操作符
单独的in操作符,property in object,通过对象能访问到属性,返回true。不管属性是存在于实例中,还是原型中。
person1.name = "Jack";
console.log("name" in person1); //true
delete person1.name;
console.log("name" in person1); //true
使用for-in循环:返回所有能通过对象访问的、可枚举的(enumerated)属性,包括存在在实例中、原型中的属性。屏蔽了原型中不可枚举的属性([[Enumerable]]标记为false)的实例属性也会在for-in中返回,因为开发人员定义的属性都是可枚举的。【此处IE有bug】
ES5中的Object.keys()方法接受一个参数对象,返回一个包含所有可枚举属性的字符串数组,Object.getOwnPropertyNames()获取所有属性。
function Person(){}
Person.prototype.name = "Tom";
Person.prototype.age = 23;
Person.prototype.sayName = function(){
console.log(this.name);
};
var p1 = new Person();
p1.name = "Jack";
var key1 = Object.keys(Person.prototype); //[ 'name', 'age', 'sayName' ]
var key2 = Object.keys(p1); //[ 'name' ]
var key3 = Object.getOwnPropertyNames(Person.prototype); //[ 'constructor', 'name', 'age', 'sayName' ]
//包括不可枚举的constructor
- 更简单的原型属性
对象字面量重写Person原型对象:
function Person(){}
Person.prototype = {
name:"Tom",
age:29,
sayName:function(){
console.log(this.name);
}
};
注意,constructor不再指向Person,这里的语法本质上重写了prototype属性,constructor属性变成新的对象的constructor属性(指向Object)。instanceof操作符还能正确返回结果。
var friend = new Person();
console.log(friend instanceof Person); //true
console.log(friend instanceof Object); //true
console.log(friend.constructor == Object); //true
console.log(friend.constructor == Person); //false
可以让constructor重新指向Person:
function Person(){}
Person.prototype = {
/************看这里*************/
constructor:Person,
name:"Tom",
age:29,
sayName:function(){
console.log(this.name);
}
};
以上方法会使constructor的[[Enumerable]]特性被设置为true。ES5兼容的浏览器可以这么干:
//重设构造函数
Object.defineProperty(Person.prototype,"constructor",{
enumerable:false,
value:Person
});
- 原型的动态性
在原型中查找值的过程是一次搜索,对原型对象作出的任何修改能立刻从实例中反映出来。
* 原生对象的原型
所有原生引用类型(Object、Array、String…)都在其构造函数的原型上定义的方法。可以为原生对象的原型添加方法
- 原型对象的问题
有时候共享属性不是我们想要的。比如向前文的Person.prototype添加:
friend:["Jane","Tod"];
对person1执行 person.friend.push(“Andy”); 结果同样反映到person2上。
解决方法:组合使用构造函数模式和原型模式
不希望(不需要)共享的属性,在构造函数中定义;constructor和共享的方法在原型中定义。
* 动态原型模式
function Person(name,age,job){
//属性
this.name = name;
this.age = age;
this.job = job;
//方法
if(typeof this.sayName != "function"){
Person.prototype.sayName = function(){
console.log(this.name);
};
}
}
方法部分的代码只在初次调用构造函数时执行
* 寄生构造函数模式
基本思想:创建一个函数,函数的作用仅仅是封装创建对象的代码,然后返回新创建的对象。
function Person(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;
}
- 稳妥构造函数模式
稳妥对象:没有公共属性,其方法也不引用this。适合在一些安全的环境中(禁用new和this)或者在防止数据被改动时使用。
function Person(name,age,job){
var o = new Object();
o.name = name;
o.age = age;
o.job = job;
o.sayName = function(){
console.log(name);
};
return o;
}
以上例子,除了sayName方法,没有其他办法访问name的值。
6.3 继承
ES只支持实现继承。
6.3.1 原型链
function SuperType(){
this.property = true;
}
SuperType.prototype.getSuperValue = function(){
return this.property;
};
function SubType(){
this.prototype = false;
}
SubType.prototype = new SuperType();
SubType.prototype.getSubValue = function(){
return this.property;
};
var instance = new SubType();
console.log(instance.getSuperValue()); //true
关系图:
①实现的本质是重写原型对象。原本存在于SuperType的实例中的所有属性和方法,现在也存在于SubType.prototype中。
②property属性从SuperType“变到”SubType.prototype中。因为property是一个实例属性,getSuperValue是一个原型属性。SubType.prototype是SuperType的实例,那么property就位于该实例中。
③instance.constructor现在指向SuperType
④不能使用字面量方法重写SubType.prototype,会切断原型链
* 默认的原型
所有引用类型默认继承Object
* 确定原型和实例的关系
instanceof操作符、isPrototypeOf()方法:只要是原型链中出现过的原型,结果都返回true。
console.log(instance instanceof Object); //true
console.log(SuperType.prototype.isPrototypeOf(SubType.prototype)); //true
- 原型链的问题:SuperType的实例属性会变成SubType.prototype的属性,同样有共享原型属性的问题。
6.3.2 借用构造函数
在子类型构造函数的内部调用超类型构造函数。使用apply()/call()在(将来)新创建的对象上执行构造函数。
function Product(name,price){
this.name = name;
this.price = price;
}
function Food(name,price){
Product.call(this,name,price);
this.category = "food";
}
//等同于
function Food(name, price) {
this.name = name;
this.price = price;
this.category = 'food';
}
var f1 = new Food("apple",5.8);
var f2 = new Food("corn",3);
console.log(f1); //Food { name: 'apple', price: 5.8, category: 'food' }
console.log(f2); //Food { name: 'corn', price: 3, category: 'food' }
6.3.3 组合继承
使用原型链实现对原型属性和方法的继承,通过借用构造函数来实现对实例属性的继承。【最常用】
function Product(name,price){
this.name = name;
this.price = price;
}
Product.prototype.printName = function(){
console.log(this.name);
};
function Food(name,price){
Product.call(this,name,price);//继承属性
this.category = "food";
}
Food.prototype = new Product();//继承方法
Food.prototype.constructor = Product;
Food.prototype.printCategory = function(){
console.log(this.category);
};
6.3.4 原型式继承
ES5的Object.create()
6.3.5 寄生式继承
创建一个仅用于封装继承过程的函数,在该函数内部以某种方式增强对象,最后返回对象。
function createAnother(original){
var clone = object(original); //通过调用函数创建一个新对象
clone.sayHi = function(){ //以某种方式增强这个对象
console.log("Hi";)
};
return clone; //返回对象
}
6.3.6 寄生组合式继承
组合继承中,调用了两次超类型构造函数,第二次调用时重写了实例属性。
寄生组合:借用构造函数继承属性,通过原型链的混成形式来继承方法。
function Product(name,price){
this.name = name;
this.price = price;
}
Product.prototype.printName = function(){
console.log(this.name);
};
function Food(name,price){
Product.call(this,name,price);//继承属性
this.category = "food";
}
inheritPrototype(Food,Product);
Food.prototype.printCategory = function(){
console.log(this.category);
};
function inheritPrototype(subType,superType){
var prototype = Object(superType.prototype); //创建对象
prototype.constructor = superType; //增强对象
subType.prototype = prototype; //指定对象
}