JavaScript中的对象
一、对象的基本概念
JavaScript对象的特征
- 对象是JavaScript的基本数据类型,一种复合值,可以看做是属性的无序集合,每个属性都是一个键值对,属性名是字符串,因此可以把对象看成是从字符串到值的映射
- 对象除了可以保持自身的属性,还可以从一个原型对象继承属性(原型继承是JS的核心特征)
- 对象是动态的,可以增加或删除属性
- 在JavaScript中,除了string、布尔值、null和undefined,其他值都是对象。
对象最常见的用法是对其属性进行创建、设置、查找、删除、检测和枚举等操作。
- 属性值可以是任意的JavaScript中的值,或者是一个getter或setter函数
-
属性的特征分为四种
– 可写(writable):表名是否可以设置属性的值
– 可枚举(enumerable):表明是否可以通过for/in获取属性
– 可配置(configurable):表明是否可以删除或修改属性
– 属性的值(value):包含属性实际的值,默认为undefined -
每个对象还拥有三个相关的对象特性
– 对象的原型(prototype)原型指向另一个对象,当前对象的属性会继承该原型对象的属性或方法
– 对象的类(class)一个标识对象类型的字符串
– 对象的扩展标记(extensible flag)指明了是否可以向该对象添加新属性
二、创建对象及其应用
创建对象
{
// 通过new构造函数Object创建对象
let person = new Object();
person.name = 'Tom';
person.age = 18;
// 通过对象字面量创建对象
let person = {
"name":"Tom",
"age":18,
5:true
}
// 通过构造函数创建对象
function Person(name,age){
this.name = name;
this.age = age;
}
let tom = new Person("Tom",18);
console.log(tom); // {name:"Tom",age:18}
}
通过这两种方式创建的对象是一样的,对象的属性名可以是数值,属性名最后都会被自动转为字符串。更推荐使用字面量的形式创建对象,因为这样代码更少更简洁。
Object.create()
使用Object.create()方法创建的对象会继承参数里的对象,也就是说通过这个方法可以创建指定原型的对象
{
let person = Object.create({name:"Tom","age":18});
let person2 = Object.create(null);
let person3 = Object.create(Object.prototype);
}
可以看出,第一个对象是继承了给定对象属性的对象,也就是说person的原型就是参数的那个原型对象,然后参数对象的原型就是Object对象。第二个对象是没有任何属性和原型的空对象,因此需要创建一个完全空的对象时可以使用这个方法。第三个对象是一个普通的空对象,会继承Object对象。
关于属性的访问
属性值一般是通过点语法来获取的,但也可以使用中括号来获取属性的值,中括号内需要使用属性名的字符串形式
{
let person = {
"name":"Tom",
"age":18,
5:true
}
console.log(person.name == person["name"]) // 结果为true
}
如果对象中的属性名包含导致语法错误的字符,或者关键字/保留字时,这时就需要使用中括号语法,比如person[“a name”]。
属性访问错误
当我们访问一个不存在的属性时并不会报错,比如一个对象如果自身的属性或继承的属性均未找到某一个属性,那么访问属性时就会返回undefined。
{
let person = {}
console.log(person.name); // undefined,因为没有定义
// 这个语法表示传导运算符,如果前面的没找到会一直往后访问,最后的结果是undefined
console.log(person?.one?.two?.three);
}
如果对象不存在的话,那么访问这个不存在的对象的属性就会报错,null和undefined值都没有属性,因此访问这些值的属性也会报错。
还有一些情况下对属性的设置也会错误,比如:
- 对象中的某个属性是只读的,不能给只读属性重新赋值,可以使用Object.defineProperty()方法重新配置
- 对象中的属性是继承属性,且它是只读的,因此不能通过同名自由属性覆盖继承的只读属性
删除属性
delete运算符可以删除对象的属性,但只能删除自有的属性,不能删除继承而来的属性,要删除继承属性必须从定义这个属性的原型对象上删除它,并且这样会影响所有继承自这个原型的对象
{
let person = {name:"Tom",age:18};
delete person.age;
console.log(person); // 成功删除了age属性
delete person.toString;
console.log(person) // 不能删除
}
更改属性的默认特性
要想更改属性的默认特性,必须使用Object.defineProperty()方法,该方法有三个参数:要给其添加属性的对象、属性的名称和一个描述符对象,描述符对象就包括那四个特性。
{
let person = {};
Object.defineProperty(person,"name",{
writable:false,
configurable:false,
value:"Tom"
})
console.log(person.name); // 结果为Tom
person.name = "Jack";
delete person.name;
console.log(person.name); // 依然为Tom,因为该属性不可设置且不可配置,因此不能从该对象上删除该属性
}
一般情况下,configurable、enumerable、writable都默认为false
如果要同时修改或创建多个属性,则使用Object.defineProperties(),第一个参数是要修改的对象,第二个参数是一个映射表,包括要新建或修改的属性的名称,以及它们的属性描述符。
{
let p = Object.defineProperties(
{},
{
x: { value: 1, writable: true, enumerable: true, configurable: true },
y: { value: 1, writable: true, enumerable: true, configurable: true }
}
);
console.log(p); //{x:1,y:2}
}
通过属性描述符获取和设置属性的特性
Object.getOwnPropertyDescriptor(obj,propKey)
{
console.log(Object.getOwnPropertyDescriptor(Object.prototype,"toString"))
}
检测属性
判断某个属性是否存在于某个对象中,可以通过in运算符、hasOwnProperty()和propertyIsEnumerable()方法来判断
{
let person = {name:"Tom",age:20};
console.log("x" in person); // false
console.log("name" in person); // true
console.log("toString" in person); // true
// 是否有自己的属性,toString是继承的Object原型方法
console.log(person.hasOwnProperty("x")); // false
console.log(person.hasOwnProperty("name")); // true
console.log(person.hasOwnProperty("toString")); // false
// 是否有可枚举的属性
console.log(person.propertyIsEnumerable("x")); // false
console.log(person.propertyIsEnumerable("name")); // true
console.log(person.propertyIsEnumerable("toString")); // false
}
枚举属性
使用for in循环可以遍历对象中所有可枚举的属性(包括自有属性和继承的属性)把属性名称赋值给循环变量。
{
let obj = Object.create({a:100,b:200});
obj.c = 300;
for(let o in obj){
// 输出c,a,b
console.log(o);
}
}
Object.keys() 它返回一个数组,这个数组由对象中可枚举的自有属性的名称组成。
Object.getOwnPropertyNames() 和Object.keys()类似,只是它返回对象的所有自有属性的名称,而不仅仅是可枚举的属性。
{
let obj = Object.create({a:10,b:20});
obj.x = 1;obj.y = 2;
console.log(Object.keys(obj)); // ["x","y"]
console.log(Object.getOwnPropertyNames(obj)); // ["x","y"]
}
访问器属性
属性getter和setter,对象属性是由名字、值和一组特性组成的,也就是上面的属性的四个默认特性,由getter和setter定义的属性称作存取器属性,它不同于数据属性,数据属性只有一个简单的值。
当程序查询存取器属性的值时,JavaScript调用getter方法,这个方法的返回值就是属性存取表达式的值。
当程序设置一个存取器属性时,JavaScript就会调用setter方法,将赋值表达式右侧的值当做参数传入setter,这个方法用来设置属性值,且setter方法的返回值可以忽略。
{
let obj = {
get foo(){
return 'getter';
}
set foo(value){
console.log('setter:'+value)
}
}
obj.foo = 'hello'; // 输出setter:hello
console.log(obj.foo); // 输出getter
}
当访问属性时会getter访问器,当给属性设置值时,会触发setter,因此输出setter:hello。
一个小例子:
{
let circle = {
r : 10,
get round(){
return 2 * this.r * Math.PI;
},
set round(v){
this.r = v / 2 / Math.PI
},
get area(){
return Math.PI * this.r ** 2
},
}
// 获取圆的周长和面积
console.log(circle.round,circle.area);
circle.round = 60; // 把周长设置为60,会重新计算r
console.log(circle.r,circle.area);
// 继承circle对象,存取器属性也是可以继承的
let circle1 = Object.create(circle);
circle1.r = 20;
console.log(circle1.round);
circle1.round = 500;
console.log(circle.r, circle1.r, circle1.area);
console.log(Object.getOwnPropertyDescriptor(circle,"r"));
console.log(Object.getOwnPropertyDescriptor(circle1,"round")); //只获得自身的属性,继承属性返回undefined
}
对象原型
-
每一个JavaScript对象(null除外)都和另一个对象相关联,这另一个对象就是原型对象,每一个对象都从原型继承属性或方法。
- 所有通过对象字面量创建的对象都具有同一个原型对象,通过Object.prototype获得原型对象的引用
- 通过new和构造函数创建的对象的原型就是构造函数的prototype属性引用的对象
- 所有的内置构造函数都具有一个继承自Object.prototype的原型,比如Array.prototype的属性继承自Object.prototype
{
let obj = {value:10};
Object.prototype.label = 'top';
console.log(obj.label); // 输出 top
let arr = [1,2,3];
console.log(arr.label); // 也输出 top
}
每个对象的顶级原型都是Object,用Object.prototype添加一个label属性,这样每个继承自Object的对象都会具备这个属性,arr是一个数组,数组的原型对象是Array,然后Array的原型才是Object,如果Array没有label属性那么会再往上层去找,Object具有label属性,因此也会输出top,这种通过层级的原型继承形成的链接,就被称为原型链。
原型属性与特殊属性__proto__
原型属性是在实例对象创建之初就设置好的。
- 通过对象字面量创建的对象使用Object.prototype作为它们的原型
- 通过new关键字创建的对象使用构造函数(constructor属性)的prototype属性作为它们的原型
- 通过Object.create()创建的对象使用第一个参数作为它们的原型
Object.getPrototypeOf(obj)获取对象的原型
Object.isPrototypeOf(obj)检查一个对象是否是另一个对象的原型
{
let person = {name:"Tom",age:20};
let obj = Object.create(person);
console.log(Object.getPrototypeOf(obj)) // 就是person对象
console.log(person.isPrototypeOf(obj)) // true
// __proto__ 是JS的特殊属性,可以获取和设置对象的原型
console.log(person.__proto__ === Object.prototype) // true
}
复制对象
为了创建一个对象完全相同的拷贝需要确保:
- 拷贝必须具有与原对象相同的原型
- 拷贝必须具有与原对象相同的属性和特性
这里有个相关的高频面试题,关于深拷贝和浅拷贝
深拷贝:深拷贝是指会拷贝多层,嵌套的对象也会被拷贝出来,相当于开辟一个新的内存地址用于存放拷贝的对象。
浅拷贝:如果拷贝基本类型,那么就等于赋值一样,会直接拷贝其本身;但如果拷贝的是引用类型(例如对象),就只会拷贝一层,如果原对象发生改变,那么拷贝对象也会发生改变。
{
/* 利用es6中Object.assign()这个方法接收一个目标对象和一个或多个源对象作为参数,
然后将每个源对象中可枚举和自有属性赋值到目标对象。
*/
let person = {name:'Tom',age:20}
let obj = {};
// 方法1,Object.assign()
Object.assign(obj,person);
obj.sex = "男";
console.log(person); // { name: 'Tom', age: 20 }
console.log(obj) // { name: 'Tom', age: 20, sex: '男' }
// 方法2,展开语法
let obj2 = {...person};
obj2.name = "Jack"
console.log(obj2); // { name: 'Jack', age: 20 }
// 方法3,循环
let obj3 = {};
for(const key in person){
obj3[key] = person[key]
}
obj3.name = "Bob";
console.log(obj3); // { name: 'Bob', age: 20 }
}
序列化对象
对象序列化是指将对象的状态转换为字符串,也可以将字符串还原为对象。
- ES5提供了内置函数JSON.stringify()和JSON.parse()用来序列化和反序列化JavaScript对象
- JSON的全称是“JavaScript Object Notation”即JavaScript对象表示法
- 函数、RegExp、Error对象和undefined值不能序列化和反序列化
- JSON.stringify()只能序列化对象可枚举的自有属性
{
let person = {name:'Tom',age:20}
let obj2=JSON.parse(JSON.stringify(person))
obj2.name="Jack"
console.log(person,person.name) // { name: 'Tom', age: 20 } Tom
console.log(obj2,obj2.name) // { name: 'Jack', age: 20 } Jack
}
序列化和反序列化也可以用来实现深拷贝。
三、Object构造函数的方法
方法 | 功能 |
---|---|
Object.assign() | 通过复制一个或多个对象来创建一个新对象 |
Object.create() | 使用指定的原型对象和属性创建一个新对象 |
Object.defineProperty() | 给对象添加多个属性并分别指定它们的配置 |
Object.entries() | 返回给定对象自身可枚举属性的[key,value]数组 |
Object.freeze() | 冻结对象,其他代码不能删除或修改任何属性 |
Object.getOwnPropertyDescriptor() | 返回对象指定的属性配置 |
Object.getOwnPropertyNames() | 返回一个数组,包含了指定对象所有的可枚举或不可枚举的属性名 |
Object.keys() | 返回一个包含所有给定对象自身可枚举属性名称的数组 |
Object.values() | 返回给定对象自身可枚举值的数组 |
Object.getPrototypeOf() | 返回指定对象的原型对象 |
Object.setPrototypeOf() | 设置对象的原型 |
Object.is() | 比较两个值是否相同 |
一个小例子
{
let obj = Object.create({m:10,n:20})
obj.x = 1
obj.y = 2
obj.z = 3
console.log(Object.keys(obj)); //只获得自身的属性,不获得继承的属性
console.log(Object.values(obj)); //只获得自身的属性值,不会获得继承
console.log(Object.entries(obj)); //获得键值对,每个键值对存在一个数组中,在一个数组中返回
for(let p in obj){
console.log(obj[p]); //会遍历所有的属性,包括继承的
}
console.log(Object.getOwnPropertyNames(obj)); //它返回对象的所有自有属性的名称,而不仅仅是可枚举的属性。
}
四、ES6中关于对象的扩展
1.对象简洁法表示
{
// 1.对象简洁法表示
// es5
let name = "Tom";
let age = 20;
let person1 = {name:name,age:age};
console.log(person1); // { name: 'Tom', age: 20 }
// es6
let person2 = {name,age};
console.log(person2); // 与上面是一样的结果
// 关于方法的简写,es5
let es5_fun = {
hello:function(){
console.log('hello');
}
}
// es6
let es6_fun = {
hello(){
console.log('hello');
}
}
console.log(es5_fun.hello(),es6_fun.hello()); // 都是hello
}
2.新增的API
{
// 新增API,Object.is()判断两个是否相等,跟全等于类似
console.log('string',Object.is('abc','abc'),'abc' === 'abc');
console.log('array',Object.is([],[]),[] === []);
// Object.assign()是一个浅拷贝,只拷贝自身对象的属性,不会拷贝继承的属性和不可枚举的属性
console.log("拷贝",Object.assign({a:10},{b:20})) // 会把第二个参数的对象拷贝到第一个参数的对象里
// Object.entries()
let obj = {a:100,b:200};
for(let [key,value] of Object.entries(obj)){
console.log([key,value]); // [ 'a', 100 ] [ 'b', 200 ]
}
}
3.扩展运算符
跟数组的扩展运算符是一致的,在解构赋值中,多余的属性都会被放在扩展运算符的对象
{
let {a,b,...c} = {a:100,b:200,c:300,d:400,e:500}
console.log(a,b,c) // 100 200 { c: 300, d: 400, e: 500 }
}
以上就是关于JavaScript中对象以及一些进阶部分的总结,不足之处欢迎指正!