JS基础学习——对象
什么是对象
对象object是JS的一种基本数据类型,除此之外还包括的基本数据类型有string、number、boolean、null、undefined。与其他数据类型不同的是,对象是一种复合值,由多个键值对组成,这些键值对也可以看成对象的属性集合,键为属性名,值为属性值(任意数据类型)。
object又可以分成多种子类型,称为JS的内置对象,包括String、Number、Boolean、Function、Array、Data、RegExp(regular expressions)、Error。这些内置对象其实都是构造函数,也可通过new关键字创建新的对应对象。
可以看到内置对象中的String、Number、Boolean在基本数据类型中都存在同名类型,它们是不一样的,但也存在联系。
比如说string类型只是一串原始字符串,我们无法操作它无法改变它,但是String对象除了包含存储值的value属性外,还有很多方便实用的方法,比如检查字符串、访问字符串局部。幸运的是,JS提供了对象自动转换的功能,string类型可以直接调用String方法,如code 1所示,好像是string类型自动转成String对象一样,但其实JS引擎只是临时根据string类型创建类一个同值的String对象,最终执行方法的是这个新创建的String对象,所以code 1在调用String方法后再查看strPrimitive对象,显示的还是string。number和Numbe、boolean和Boolean之间存在类似的转换关系。
/*-----------code 1----------*/
var strPrimitive = "I am a string";
console.log( strPrimitive.length ); // 13
console.log( strPrimitive.charAt( 3 ) ); // "m"
console.log(typeof strPrimitive);//string
创建对象
JS有三种创建对象的语法,包括new构造函数、对象字面量、create函数构造函数。
new构造函数
使用构造函数调用的方法创建对象,构造函数可以是Object()或是其他对象子类型构造函数,如String(),或是自定义的构造函数。
/*-----------code 2----------*/
var person = new Object();
person.name = 'bai'
person.age = 29;
var person2 = new String('person.name = bai');
function Person()
{
this.name = 'bai'
this.age = 29;
}
var person3 = new Person();
对于Object构造函数,code 2中是它的无参构造形式;当构造函数中传入的是原始类型的值,则返回对象是该值的包装对象,code 2中的person2创建方式等价于code 3;当传入参数为对象时,则直接返回这个对象,如code 4所示;当传入参数为null或是undefined时,则创建一个空对象。
/*-----------code 3----------*/
var person2 = new Object('person.name = bai');
/*-----------code 4----------*/
var person = new Object({name:'bai',age:29});
var o1 = {a: 1};
var o2 = new Object(o1);
对象字面量
对象字面量较构造函数更为简单,是大多原生对象的创建方式,它是若干键值对构成的映射表,键值之间用冒号隔开,键值对之间用逗号隔开,如code 5所示。
/*-----------code 5----------*/
var person = {
name : 'bai',
age : 29,
5 : true
};
create函数
ES5引入了一个Object.create()方法,用于创建对象,第一个参数是继承对象,第二个参数是新增属性的描述。当第二个参数不使用时,create就是用来创建一个新方法,当第二个参数使用时,则可以实现对原型对象的继承和扩展。
/*-----------code 6----------*/
var o1 = Object.create({z:3},{
x:{value:1,writable: false,enumerable:true,configurable:true},
y:{value:2,writable: false,enumerable:true,configurable:true}
});
var o2 = Object.create(parents,{t:{value:2},k:{value:3}});//第二个参数必须以属性描述的形式写,不能写t:2,k:3;
Object.create(null)会创建一个没有任何继承的对象,所以这个对象不能调用任何基础方法,比如toString()或valueOf()。
对象的属性访问
JS有两种对象属性访问形式:myObject.a;
和myObject['a'];
,
这两种形式的主要区别是,myObject.a;
格式对属性名有格式要求,属性名必须满足标识符的命名规范要求,而[]的形式不需要,适用性更广,比如一个名为‘hello word’的属性,就只能以[]的形式访问。同时[]也可以接收一个字符串变量,比如myobject[a],其中a是一个变量名,这样我们就能以编程的方式动态得到属性名了。因为对象属性只能是string类型,所以任何不适string类型的变量传入[]都会自动转换成string。
用[]访问对象属性时,特别注意数组对象,因为数组对象根据索引值访问数组内容时,用的也是[],容易出现错误,所以对象的属性名最好不要用数字直接命名。
ES6还增加了可计算的属性名,即[]里可以包含运算符,如code 7所示。
/*-----------code 7----------*/
var prefix = "foo";
var myObject = {
[prefix + "bar"]: "hello",
[prefix + "baz"]: "world"
};
myObject["foobar"]; // hello
myObject["foobaz"]; // world
当访问对象属性时,引擎实际上会调用的内部缺省值[[Get]]操作([[Put]]用于设置值),它不仅会在对象上查找属性,而且还会遍历对象的原型链,直到找到属性,但如果找不到该属性,[[Get]]操作会返回undefined,而不是报错。
属性描述符
对象属性描述符的类型有两种:数据描述符和访问描述符。
数据描述符
【1】 writable:表示属性value值是否可修改,默认为true。当writable:false时,在非严格模式下,任何修改操作将会失效,在严格模式下会报TypeError;
【2】 configurable:表示数据描述符是否可修改,以及属性是否可删除(delete myObject.property),默认为true。当configurable:false时,除了writable:true可以修改,writable:false、configurable、enumerable都不可修改,修改的话会报TypeError,同时删除对象delete myObject.property在非严格模式下会返回false,在严格模式下会报TypeError;
【3】enumerable:表示属性是否可枚举,默认为true。当enumerable:false时,Object.keys( myObject );返回的对象属性列表中将不出现该属性,属性也不会出现在for-in循环中。可以用myObject.propertyIsEnumerable(property);方法查看对象该属性的可没举性。
【4】 value:属性的值,可以为任意类型的数据,当数据为Function类型时,该属性称为对象的方法,默认值为undefined。[[Get]]和[[Put]]操作的就是这个属性。
访问描述符
【5】get:访问属性值的时候自动会调用的方法,默认为undefined。get属性定义之后将覆盖属性的[[Get]]操作,因此属性的值不再由value值决定,而是由get方法决定。
【6】set:设置属性值时自动调用的方法,默认为undefined。get属性定义之后将覆盖属性的[[Put]]操作,且writable:false将失效,属性值永远可修改。
注意:get和set方法只要定义了其中一个,默认的[[Get]]和[[Put]]操作就会同时失效,所以如果只定义了get方法,就无法对属性值进行修改了,如果定义了set方法,访问到的属性值都只能为undefined。
查询或设置属性描述符的方法
【1】Object.defineProperty(o,name,desc):创建或修改属性的描述符,需要注意,如果用这个方法直接创建一个不存在的属性,则描述符的默认值为false。如code 9所示。
/*-----------code 9----------*/
var obj = {};
//{a:1}
console.log(Object.defineProperty(obj,'a',{
value:1,
writable: true
}));
//由于没有配置enumerable和configurable,所以它们的值为false
//{value: 1, writable: true, enumerable: false, configurable: false}
console.log(Object.getOwnPropertyDescriptor(obj,'a'));
【2】Object.defineProperty(o,descriptors):创建或修改对象的多个属性的描述符,如code 10所示。
/*-----------code 10----------*/
var obj = {
a:1
};
//{a: 1, b: 2}
console.log(Object.defineProperties(obj,{
a:{writable:false},
b:{value:2}
}));
//{value: 1, writable: false, enumerable: true, configurable: true}
console.log(Object.getOwnPropertyDescriptor(obj,'a'));
//{value: 2, writable: false, enumerable: false, configurable: false}
console.log(Object.getOwnPropertyDescriptor(obj,'b'));
【3】Object.create(proto,descriptors):使用指定的原型和属性来创建一个对象,如code 11所示。
var o = Object.create(Object.prototype,{
a:{writable: false,value:1,enumerable:true}
});
//{value: 1, writable: false, enumerable: true, configurable: true}
console.log(Object.getOwnPropertyDescriptor(obj,'a'));
【4】Object.getOwnPropertyDescriptor(myObject,property):可以查询指定属性的描述符。如code 8所示。
/*-----------code 8----------*/
var obj = {a:1};
//Object {value: 1, writable: true, enumerable: true, configurable: true}
console.log(Object.getOwnPropertyDescriptor(obj,'a'));
//undefined
console.log(Object.getOwnPropertyDescriptor(obj,'b'));
【5】myObject.hasownproperty(property):判断对象是否拥有指定属性名的属性,不会查找原型链,因为对象继承的是Object的方法,而有的对象没有链接到Object对象上,无法执行hasownproperty方法,因此Object.hasownproperty.call(myObject,property)的写法更为健壮。如code 9所示。
【6】in关键词:判断对象是否拥有指定属性名的属性,当对象没有时,它会在对象的原型链中继续查找。如code 9所示。
/*-----------code 9----------*/
var myObject = {
a: 2
};
("a" in myObject); // true
("b" in myObject); // false
myObject.hasOwnProperty( "a" ); // true
myObject.hasOwnProperty( "b" ); // false
Object.hasOwnProperty.call(myObject,"b")//false
【7】for...in循环:遍历对象的所有可枚举的属性名,但如果对象正好是数组,则会先遍历数组中的所有值,然后遍历数组对象可枚举的属性名,如code 10所示。如果你指向访问可枚举的属性名,则利用Object.keys(myObject)方法,如code 11所示,如果想访问所有属性名,则利用Object.getOwnPropertyNames(myObject)方法,如code 12所示。
/*-----------code 10----------*/
var myArray = [1, 2, 3];
myArray.a = 2;
// 1 2 3 a
for(var t in myArray){
console.log(t);
}
var keysOfArray = object.keys(myArray);
for(var key in keysOfArray){
console.log(t);
}
对象拷贝
对象拷贝有浅拷贝和深拷贝两种,浅拷贝中对象包含的对象属性只是引用拷贝,因此浅拷贝中原对象和拷贝对象的对象属性是指向相同内存地址的,深拷贝中对象的对象属性是进行值拷贝的,所以拷贝对象和原对象的修改不会相互发生影响。
而对于非对象的基本类型,发生的拷贝都是值拷贝,拷贝对象和元对象之间的修改都不会相互影响。
浅拷贝的方法
【1】for循环拷贝属性引用,如code 11所示。
/*-----------code 11----------*/
function simpleClone(obj){
if(typeof obj != 'object'){
return false;
}
var cloneObj = {};
for(var i in obj){
cloneObj[i] = obj[i];
}
return cloneObj;
}
var obj1={a:1,b:2,c:[1,2,3]};
var obj2=simpleClone(obj1);
console.log(obj1.c); //[1,2,3]
console.log(obj2.c); //[1,2,3]
obj2.c.push(4);
obj2.a = 5;
console.log(obj2.c); //[1,2,3,4]
console.log(obj1.c); //[1,2,3,4]
console.log(obj1.a); //1;
【2】使用属性描述符,如code 12所示。
/*-----------code 12----------*/
function simpleClone2(orig){
var copy = Object.create(Object.getPrototypeOf(orig));//create创建的是一个空对象,如果直接用orig的话,copy将是继承orig,得到的结果和复制目标不一致。
Object.getOwnPropertyNames(orig).forEach(function(propKey){
var desc = Object.getOwnPropertyDescriptor(orig,propKey);//得到属性值,prokey是属性名
Object.defineProperty(copy,propKey,desc);
});
return copy;
}
var obj1={a:1,b:2,c:[1,2,3]};
var obj2=simpleClone1(obj1);
console.log(obj1.c); //[1,2,3]
console.log(obj2.c); //[1,2,3]
obj2.c.push(4);
console.log(obj2.c); //[1,2,3,4]
console.log(obj1.c); //[1,2,3,4]
【3】Object.assign(decObj,srcObj);assign方法不能赋值属性的get和set方法,此时只能用getOwnPropertyNames和defineProperty方法进行复制。如code 13所示。
/*-----------code 13----------*/
var obj1={a:1,b:2,c:[1,2,3]};
var obj2=Object.assign({},obj1);
console.log(obj1.c); //[1,2,3]
console.log(obj2.c); //[1,2,3]
obj2.c.push(4);
obj2.a = 5;
console.log(obj2.c); //[1,2,3,4]
console.log(obj1.c); //[1,2,3,4]
console.log(obj1.a); //1;
深拷贝的方法
【1】自定义拷贝函数,需要注意,在JS中基本数据类型除了Object类型都是值拷贝,不是引用拷贝。如code 14所示。
/*-----------code 14----------*/
function deepClone1(obj,cloneObj){
if(typeof obj != 'object'){
return false;
}
var cloneObj = cloneObj || {};
for(var i in obj){
if(typeof obj[i] === 'object'){
cloneObj[i] = (obj[i] instanceof Array) ? [] : {};
arguments.callee(obj[i],cloneObj[i]);//递归函数,相当于deepClone1(obj[i],cloneObj[i])
}else{
cloneObj[i] = obj[i];
}
}
return cloneObj;
}
var obj1={a:1,b:2,c:[1,2,3]};
var obj2=deepClone1(obj1);
console.log(obj1.c); //[1,2,3]
console.log(obj2.c); //[1,2,3]
obj2.c.push(4);
console.log(obj2.c); //[1,2,3,4]
console.log(obj1.c); //[1,2,3]
【2】Json,只能正确处理的对象只有Number、String、Boolean、Array、扁平对象,即那些能够被json直接表示的数据结构,但是复制结果和原对象只有值是一致的。如code 15所示。
/*-----------code 15----------*/
function jsonClone(obj){
return JSON.parse(JSON.stringify(obj));
}
var obj1={a:1,b:2,c:[1,2,3]};
var obj2=jsonClone(obj1);
console.log(obj1.c); //[1,2,3]
console.log(obj2.c); //[1,2,3]
obj2.c.push(4);
console.log(obj2.c); //[1,2,3,4]
console.log(obj1.c); //[1,2,3]
var a = new String('aaaa');
var b = jsonClone(a);
console.log(a);
console.log(b);
控制对象状态
属性描述符控制的是对象属性的状态,下面这些方法是用来控制对象整体的状态:
【1】 Object.preventExtensions(myObject):禁止对象扩展,不能再添加新的属性。
【2】Object.seal(myObject):禁止对象扩展,同时禁止对对象属性进行配置。
【3】Object.freeze(myObject):禁止对象扩展,同时禁止对对象属性进行配置,同时禁止对象属性进行修改。
数组对象的遍历
前面在for...in循环中介绍到,for...in循环对同时遍历数组中的值和数组对象属性,也介绍了该如何只访问对象的属性,下面将一下怎样只遍历数组存在的数据集合,而非属性。
最简单的方式就是标准for循环,如code 16所示。
/*-----------code 16----------*/
var myArray = [1, 2, 3];
for (var i = 0; i < myArray.length; i++) {
console.log( myArray[i] );
}
// 1 2 3
或者是ES6引入的for...of循环,如code 17所示。
/*-----------code 17----------*/
var myArray = [ 1, 2, 3 ];
for (var v of myArray) {
console.log( v );
}
// 1 2 3
同时ES5还提供了一些数组遍历方法,包括forEach、every、some方法,这三个函数都是顺序遍历数组中的每个值执行回调函数,调用格式为functtion(回调函数,this指向)
,不同的是forEach方法对数组中所有数都执行回调函数不管回调函数是否return false,every方法当遇到某个数使得回调函数返回false时,停止执行,不再对该数后面的数调用回调函数。some方法则正好相反,它在遇到某个值使回调函数返回true时停止执行。
上面几种遍历方法都是顺序依次访问数据中的每个对象,那么是不是可以自定义数组的遍历顺序呢?
其实数组对象有一个名为Symbol.iterator方法,它就是数组的一个迭代器,通过重复调用Symbol.iterator方法里的next方法,就可以不断向前访问数组,如code 17所示。上面几种循环遍历背后执行的就是类似code 18的过程。
/*-----------code 18----------*/
var myArray = [ 1, 2, 3 ];
var it = myArray[Symbol.iterator]();
it.next(); // { value:1, done:false }
it.next(); // { value:2, done:false }
it.next(); // { value:3, done:false }
it.next(); // { done:true } //done等于true的时候,遍历结束
因此只要重写数组对象的Symbol.iterator方法,我们就可以自定义数组的遍历顺序,可用用defineProperty方法或是字面量的格式进行重写,如code 19所示。
/*-----------code 19----------*/
var myObject = {
a: 2,
b: 3
};
Object.defineProperty( myObject, Symbol.iterator, {
enumerable: false,
writable: false,
configurable: true,
value: function() {
var o = this;
var idx = 0;
var ks = Object.keys( o );
return {
next: function() {
return {
value: o[ks[idx++]],
done: (idx > ks.length)
};
}
};
}
} );
//或是
var myObject = {
a: 2,
b: 3,
[Symbol.iterator]: function() {
return {
next: function() {
var o = this;
var idx = 0;
var ks = Object.keys( o );
return {
next: function() {
return {
value: o[ks[idx++]],
done: (idx > ks.length)
};
}
};
}
};
// iterate `myObject` manually
var it = myObject[Symbol.iterator]();
it.next(); // { value:2, done:false }
it.next(); // { value:3, done:false }
it.next(); // { value:undefined, done:true }
// iterate `myObject` with `for..of`
for (var v of myObject) {
console.log( v );
}
// 2
// 3
参考资料:
[1] You don't know js -- this & Prototypes
[2] 深入理解javascript对象系列第一篇——初识对象
[3] 深入理解javascript对象系列第二篇——属性操作
[4] 深入理解javascript对象系列第三篇——神秘的属性描述符
[5] 对象拷贝