下面代码都在node.js测试
这一次我们来谈谈对象。
什么是对象
对象是一种复合值。
对象是属性的无序集合,每个属性都是一个名/值对。
对象不仅仅是字符串到值的映射,除了可以保持自有的属性,还可以从原型对象继承属性。这种原型式继承是JS的核心特征。
对象是动态的,可以新增属性也可以删除属性。对象是可变,我们通过引用而非值来操作对象,如:
var x = {a:1, b:2};
var y = x;
y; // {a:1, b:2}
y.b = 3;
y; // {a:1, b:3}
x; // {a:1, b:3}
对象常见方法有:创建、设置、查找、删除、检测和枚举它的属性,接下来一一解析这些方法。
对象属性特性(property attribute):可写、可枚举、可配置。
对象自身还拥有三个相关的对象特性(attribute):原型、类、扩展标记。
对象分类:内置对象、宿主对象、自定义对象。
对象属性分类:自有属性、继承属性。
对象的KEY
对象的key为字符串,即使不为字符串,也会转换为字符串类型。如:
var obj = {};
obj[1] = 1;
obj['1'] = 2;
obj; // Object {1: 2}
obj[{}] = true; // {}会转换为字符串'[object Object]'
obj[{x : 1}] = true; // {x: 1}会转换为字符串'[object Object]'
obj; // Object {1: 2, [object Object]: true}
对象中不能存在两个同名的属性,如:
var obj = {a: 1, a: 2};
obj; // {a:2}
对象的结构
对象的属性有五个特征:
writable、enumerable、configurable、value、get/set
对象的创建
可以通过对象直接量、关键字new和Object.create()函数来创建。
字面量创建
var obj1 = {x: 1, y: 2};
在ES5中,保留字可以用作不带引号的属性名;而ES3使用保留字作为属性名必须使用引号引起来。
在ES5中,对象直接量最后一个属性后的逗号将忽略;且在ES3的大部分实现中也可以忽略这个逗号,而在IE中则报错。
注意:所有通过对象直接量创建的对象都具有同一个原型,原型指向Object.prototype
new以及原型链
通过关键字new和构造函数调用创建的对象的原型就是构造函数的prototype属性的值。通过new Array()创建的对象的原型就是Array.prototype,通过new Date()创建的对象原型就是Date.prototype。
所有的内置构造函数(以及大部分自定义的构造函数)都具有一个继承自Object.prototype的原型。因此由new Date()创建的对象同时继承Array.prototype和Object.prototype。这一系列链接的原型对象就是所谓的原型链。
Object.create() ES5定义
直接在花括号里传入所需的原型对象即可:
var obj = Object.create({x : 1});
obj.x // 1
typeof obj.toString // "function"
obj.hasOwnProperty('x');// false
可以通过传入参数null来创建一个没有原型的新对象,但通过这种方法创建的对象不会继承任何东西,甚至包括基础方法,如:
var obj = Object.create(null);
obj.toString // undefined,注意与上面例子的比较
注意:obj原型为{x:1},而{x:1}原型为Object.prototype,所以obj可以访问到toString方法
如果想通过Object.create()来创建一个空对象,可以通过下面方法来创建:(也可以通过{}和new Object()来创建)
Object.create(Object.prototype)
ES3模拟原型继承,不过inherit不能完全替代Object.create(),因为前者不能传入null来创建对象,也不能接受可选的第二个参数,下面为inherit的实现:
function inherit(p){
if(p == null){
throw TypeError();
}
if(Object.create){
return Object.create(p);
}
var t = typeof p;
if (t !== "object" && t !== "function"){
throw TypeError();
}
function f(){};
f.prototype = p;
return new f();
}
对象方法
属性的查询和设置
可以通过.运算符或者方括号[]访问属性。对于.运算符来说,右侧必须是一个以属性名称命名的简单标识符;对于方括号来说,方括号内必须是一个计算结果为字符串的表达式(严格地讲,表达式必须返回字符串或返回一个可以转换为字符串的值),如:
var obj = {'5': "abc", '7': "cdf", c: 100};
obj.c; // 100
obj['c']; // 100
obj[c]; // Error:c is not defined
obj[1]; // undefined,因为1会转换为字符串'1'
obj['5']; // 'abc'
obj[1+4]; // 'abc'
在ES3中,点.后面的标识符不能是保留字,ES5放宽了,可以直接在.运算符后面直接使用保留字。
关联数组的对象
使用.运算符来访问对象的属性时,属性名用一个标识符来表示,由于标识符不是数据类型,因此程序无法修改它们。
而通过[]来访问时,属性名通过字符串来表示,在程序运行时,可以修改和创建他们,如:
var addr = ""
for(i=0; i<4; i++){
addr += customer["address" + i] + '\n';
}
可以看见关联数组的方式来实现更强大、更灵活,推荐。
继承
属性访问细节:
假如要查询对象o的属性x,如果o中不存在x,那么将会继续在o的原型对象中查询属性x。如果原型对象中也没有x,但这个原型也有原型,那么继续在这个原型对象的原型上执行查询,直到找到x或者查找到一个原型是null的对象为止。如:
var o = {};
o.x = 1;
var p = Object.create(o);
p.y = 2;
var q = Object.create(p);
q.z = 3;
q.x + q.y //3 ,x和y分别继承自o和p
属性赋值细节:
1.假如对象o已经有属性x(这个属性不是继承来的),那么这个赋值操作只改变这个已有属性x的值。
2.如果o不存在x,接着判断:
(1)现在如果原型上有属性x且为可写,则会为o创建一个x属性;
(2)如果原型上有属性x且不可写,则赋值失败;
如:
var obj1 = {};
Object.defineProperty(obj1, 'z', {value:1, writable:false, enumerable: true, configurable:true});
obj1.z; // 1
Object.keys(obj1); // ['z']
obj2 = Object.create(obj1);
obj2.z; // 1
obj2.z=2; // 赋值失败,则有下面的结果
obj2.z; // 1
obj2.hasOwnProperty('z'); // false
Object.defineProperty(obj1, 'z', {value:1, writable:true, enumerable: true, configurable:true});
obj2.z=2; // 赋值成功,则有下面的结果
obj2.z; // 2
obj2.hasOwnProperty('z'); // true
注意:如果原型上有属性x,而这个属性是一个具有setter方法的accessor属性,那么这时将调用setter方法而不是给o创建一个属性x。且setter是对象o调用的,因此setter方法的操作只是针对o本身,并不会修改原型链。(此处例子见下面set/get介绍)
注意:只有在查询属性时才会体会到继承的存在,而设置属性则和继承无关。
属性访问错误
查询一个不存在的属性并不会报错,如:
var obj = {x : 1};
obj.y; // undefined
但是,如果对象不存在,那么试图查询这个不存在的对象的属性就会报错,如:
var yz = obj.y.z; // TypeError: Cannot read property 'z' of undefined
obj.y.z = 2; // TypeError: Cannot set property 'z' of undefined
处理方式:
var yz;
if (obj.y) {
yz = obj.y.z;
}
更简练的方式,可以使用&&的短路行为:
var yz = obj && obj.y && obj.y.z
给null和undefined设置属性也会报类型错误。设置属性的失败操作都会抛出一个类型错误异常,除了一些对象例外,如:
Object.prototype = 1;
Object.prototype // {},赋值失败,但上面没有报错
这是一个历史遗留bug,在严格模式下,任何设置属性的失败操作都会抛出一个类型错误异常。
属性的删除
delete可以删除对象的属性,如
var person = {age : 28, title : 'fe'};
delete person.age; // true
delete person['title']; // true
person.age; // undefined
delete person.age; // true
需要注意,delete只是断开属性和宿主对象的联系,而不会去操作属性中的属性,由于已经删除的属性的引用依然存在,可能因为这种不严谨的代码而造成内存泄露,如
var a = {p: {x:1}};
var b = a.p;
b; // {x:1}
delete a.p;
b; // {x:1}
delete只能删除自有属性,而不能删除继承属性。
delete不能删除那些可配置性为false的属性,在严格模式下,删除一个不可配置属性会报一类型错误,在非严格模式下,这些情况下的delete操作会返回一个false:
delete Object.prototype; // false,
var descriptor = Object.getOwnPropertyDescriptor(Object, 'prototype');
descriptor.configurable; // false
另外,用var定义的全局变量或者局部变量也不能删除,全局函数或者局部函数也不能删除,如:
var globalVal = 1;
delete globalVal; // false
(function() {
var localVal = 1;
return delete localVal;
}()); // false
function fd() {}
delete fd; // false
(function() {
function fd() {};
return delete fd;
}()); // false
隐式定义的全局变量可以删除,如
ohNo = 1;
ohNo; // 1
delete ohNo; // true
注意:属性的删除由属性的特性configurable值来控制。
属性的检测
三种方法:in运算符、hasOwnProperty(是否是自有属性)、propertyIsEnumerable(检测是否为自有属性且可枚举),如:
var cat = new Object;
cat.legs = 4; // leg属性自有且可枚举
Object.defineProperty(cat, 'age', {value:1, writable:true, enumerable: false, configurable:true}); // age属性为自有但不可枚举
'legs' in cat; // true
'age' in cat; // true
'abc' in cat; // false
"toString" in cat; // true, inherited property!!!
cat.hasOwnProperty('legs'); // true
cat.hasOwnProperty('age'); // true
cat.hasOwnProperty('toString'); // false
cat.propertyIsEnumerable('legs'); // true
cat.propertyIsEnumerable('age'); // false
cat.propertyIsEnumerable('toString'); // false
Object.keys(cat); //legs
属性设置方法(用defineProperty设置的属性默认是不可枚举):
Object.defineProperty(cat, 'price', {value : 1000});
Object.getOwnPropertyDescriptor(cat, 'price'); // {value: 1000, writable: false, enumerable: false, configurable: false}
cat.propertyIsEnumerable('price'); // false
cat.hasOwnProperty('price'); // true
除了使用in运算符外,另一种更简单的方法是使用“!==”判断一个属性是否是undefined,但当一个属性值刚好为undefined时无效,这时用in运算符才准确,如:
var o = {x:1}
o.x !== undefined; // true
o.y !== undefined; // false
var o = {x: undefined}
o.x !== undefined; // false
o.y !== undefined; // false
"x" in o; // true
"y" in o; // false
注意:!=不是严格等于,undefined与null相等,所以有下面的:
if (cat.legs != undefined) {
// !== undefined, or, !== null
}
属性的枚举
对象继承的内置方法不可枚举,但在代码中给对象添加的属性都是可枚举的(除了defineProperty这类情况),见例子:
var o = {x : 1, y : 2, z : 3};
'toString' in o; // true
o.propertyIsEnumerable('toString'); // false
var key;
for (key in o) {
console.log(key); // x, y, z
}
for..in..会去原型链上枚举所有特征Enumerable为true的属性,如:
var obj = Object.create(o);
obj.a = 4;
var key;
for (key in obj) {
console.log(key); // a, x, y, z
}
只枚举出是自己属性的:
var obj = Object.create(o);
obj.a = 4;
var key;
for (key in obj) {
if (obj.hasOwnProperty(key)) {
console.log(key); // a
}
}
ES5增加了两个用于枚举属性名称的函数,Object.keys()和Object.getOwnPropertyNames(),如:
Object.defineProperty(obj, 'b', {value:1}) // 属性b不可枚举
Object.keys(obj); // ['a'],枚举出特性为可枚举的自有属性
Object.getOwnPropertyNames(obj); // ['a', 'b'],所有自有属性,不管特性是否可枚举
get、set方法
对象的属性分为数据属性和存取器属性,存取器属性就是属性值可以用一个或两个方法替代,这两个方法就是setter、getter
读取存取器属性的值是,调用getter方法(无参数),这个方法的返回值就是存取器属性的值;当程序设置一个存取器属性的值时,JS调用setter方法,将赋值表达式右侧的值当做参数传入setter,可以忽略setter的返回值。
读取只写属性总是返回undefined,如:
var o = { set a(value){}};
o.a; // undefined
先看一个例子:
var man = {
name : 'cauil',
weibo : '@cauil',
get age() {
return new Date().getFullYear() - 1989;
},
set age(val) {
console.log('Age can\'t be set to ' + val);
}
}
console.log(man.age); //
man.age = 100; // Age can't be set to 100
console.log(man.age); // still 27
再来看一个例子:
var man = {
weibo : '@cauil',
$age : null,
get age() {
if (this.$age == undefined) {
return new Date().getFullYear() - 1989;
} else {
return this.$age;
}
},
set age(val) {
val = +val;
if (!isNaN(val) && val > 0 && val < 150) {
this.$age = val;
} else {
throw new Error('Incorrect val = ' + val);
}
}
}
console.log(man.age); // 27
man.age = 100;
console.log(man.age); // 100;
man.age = 'abc'; // error:Incorrect val = NaN
get/set与原型链, 本身对象属性不存在时,会去原型上查找,尝试赋值时,会去读取原型链上的set方法,是原型调用:
function foo(){};
Object.defineProperty(foo.prototype, 'z',
{get: function(){return 1;}});
var obj = new foo();
obj.z; // 1
obj.z = 10;
obj.z; // still 1
Object.defineProperty(obj, 'z',
{value : 100, configurable: true});
obj.z; // 100;
delete obj.z;
obj.z; //back to 1
如果o继承自一个只读属性x,那么赋值操作是不允许的,如果允许属性赋值,它也只是在原始对象上创建属性或对已有的属性赋值,而不会去修改原型链,如:
var o = {};
Object.defineProperty(o, 'x', {value : 1}); // writable=false, configurable=false
var obj = Object.create(o);
obj.x; // 1
obj.x = 200;
obj.x; // still 1, can't change it
Object.defineProperty(obj, 'x', {writable:true, configurable:true, value : 100});
obj.x; // 100
obj.x = 500;
obj.x; // 500
属性特性
数据属性具有四个特性:value、writable、enumerable、configurable
存取器属性具有四个特性:set、get、enumerable、configurable
Object.getOwnPropertyDescriptor()
可以获取某个对象特定属性的属性描述符,如:
var o = {x:1, get y(){}, set y(value){}}
// { value: 1, writable: true, enumerable: true, configurable: true }
Object.getOwnPropertyDescriptor(o, "x");
// { get: [Function: y], set: [Function: y], enumerable: true, configurable: true }
Object.getOwnPropertyDescriptor(o, "y");
对于不存在和继承的属性,返回undefined,如:
Object.getOwnPropertyDescriptor(o, "z"); // undefined
Object.getOwnPropertyDescriptor(o, "toString"); // undefined
Object.defineProperty()
此方法是设置属性的特性,或者让新建对象具有某种特性,使用这种方法设置的属性,如果没有定义的特性,则默认为false,先看一个例子:
Object.getOwnPropertyDescriptor({pro : true}, 'pro');
// Object {value: true, writable: true, enumerable: true, configurable: true}
Object.getOwnPropertyDescriptor({pro : true}, 'a'); // undefined
再看一个例子:
var person = {};
Object.defineProperty(person, 'name', {
configurable : false,
writable : false,
enumerable : true,
value : "Bosn Ma"
});
person.name; // Bosn Ma
person.name = 1;
person.name; // still Bosn Ma
delete person.name; // false,因为name特性为不可配置
Object.keys()
Object.keys(person)获取person的所有可枚举自有属性,如:
Object.defineProperty(person, 'type', {
configurable : true,
writable : true,
enumerable : false,
value : "Object"
});
Object.keys(person); // 只有["name"]
Object.defineProperties()定义多个对象
Object.defineProperties(person, {
title : {value : 'fe', enumerable : true},
corp : {value : 'BABA', enumerable : true},
salary : {value : 50000, enumerable : true, writable : true}
});
Object.getOwnPropertyDescriptor(person, 'salary');
// Object {value: 50000, writable: true, enumerable: true, configurable: false}
特征定义
configurable主要控制是否可以delete、是否可以修改get/set方法,是否可以修改特征值(如writable和configurable)的属性。
writable主要控制属性值是否可以修改值。
注意:有一个特例,当configurable为false时,writable可以从true修改为false,其他情况都不能配置特征的值。
对象本身特征与序列化(待整理修改)
原型
获取对象的原型,使用Object.getPrototypeOf(),只会返回原型,不会返回原型链,如
var o = { x:1, y:2};
var p = new Array();
var q = Object.create(o);
Object.getPrototypeOf(o); // {}
Object.getPrototypeOf(p); // []
Object.getPrototypeOf(q); // {x:1, y:2}
判断x是否为y的原型,使用x.isPrototypeof(y)
var o = {x:1}
var p = Object.create(o);
o.isPrototypeOf(p); // true
Object.prototype.isPrototypeOf(p); // true
class
对象的类属性是一个字符串,用以表示对象的类型信息,如:
var toString = Object.prototype.toString;
function getType(o){return toString.call(o).slice(8,-1);};
toString.call(null); // "[object Null]"
getType(null); // "Null"
getType(undefined); // "Undefined"
getType(1); // "Number"
getType(new Number(1)); // "Number"
typeof new Number(1); // "object"
getType(true); // "Boolean"
getType(new Boolean(true)); // "Boolean"
其中new Number(1)为Object Number
extensible
Object.preventExtensions会设置对象是否不可扩展,如:
var obj = {x : 1, y : 2};
Object.isExtensible(obj); // true
Object.preventExtensions(obj);
Object.isExtensible(obj); // false
obj.z = 1;
obj.z; // undefined, add new property failed
Object.getOwnPropertyDescriptor(obj, 'x');
// Object {value: 1, writable: true, enumerable: true, configurable: true}
Object.seal
会在preventExtensions
基础上添加configurable:false
特征,如:
Object.seal(obj);
Object.getOwnPropertyDescriptor(obj, 'x');
// Object {value: 1, writable: true, enumerable: true, configurable: false}
Object.isSealed(obj); // true
Object.freeze
会在preventExtensions
基础上添加configurable:false,writable:false
特征,如:
Object.freeze(obj);
Object.getOwnPropertyDescriptor(obj, 'x');
// Object {value: 1, writable: false, enumerable: true, configurable: false}
Object.isFrozen(obj); // true
序列化
对象系列化可以使用JSON.stringfy(obj)
,如:
var obj = {x : 1, y : true, z : [1, 2, 3], nullVal : null};
JSON.stringify(obj); // "{"x":1,"y":true,"z":[1,2,3],"nullVal":null}"
序列化的一个属性值为undefined
时,会忽略;时间对象会转化为UTC格式;NaN/infinity
会转化为null
,如:
obj = {val : undefined, a : NaN, b : Infinity, c : new Date()};
JSON.stringify(obj); // "{"a":null,"b":null,"c":"2015-01-20T14:15:43.910Z"}"
JSON转化为javascipt对象,需要用引号引起来,如:
obj = JSON.parse('{"x" : 1}');
obj.x; // 1
自定义:
var obj = {
x : 1,
y : 2,
o : {
o1 : 1,
o2 : 2,
toJSON : function () {
return this.o1 + this.o2;
}
}
};
JSON.stringify(obj); // "{"x":1,"y":2,"o":3}"
定义自己对象上的toString方法和valueOf方法,如:
var obj = {x : 1, y : 2};
obj.toString(); // "[object Object]"
obj.toString = function() {return this.x + this.y};
"Result " + obj; // "Result 3", by toString
+obj; // 3, from toString
obj.valueOf = function() {return this.x + this.y + 100;};
+obj; // 103, from valueOf
"Result " + obj; // still "Result 3"
如果运算的对象的valueof
和toString
都存在,当进行一元‘+’和二元‘+’运算的时候,会进行转换字符串的操作;
如果valueOf
方法存在返回的是基本类型的值,以valueOf
的值为结果,反之,valueOf不存在或者返回的是对象,则会去找toString
方法,如果两者都不存在或者返回对象,就会报错。
不过一般都会继承Object.prototype.toString
的方法。