JavaScript学习笔记(七):对象与对象进阶

  • Post author:
  • Post category:java




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中对象以及一些进阶部分的总结,不足之处欢迎指正!



版权声明:本文为EverglowCP原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。