对象基础
什么是对象
JavaScript 中的对象是一种无序的键值对集合,键(Key)是字符串或 Symbol,值(Value)可以是任何类型(基本类型或引用类型);它是 JS 中除了基本类型(number、string、boolean、null、undefined、symbol、bigint)以外的“引用类型”的代表
说人话,就是你想报存的内容类型多,内容多,但是都是一个东西的描述,那就用对象;例如,一个人的信息,包含自己的名字,性别,出生年月等,你使用let 声明变量不得一个一个搞?所以不如直接使用对象
const user={
name: "akin",
age:25,
since:1999
}
是不是,看起来是不是很简洁?其中,{}里的我们一般称为对象的属性,name,age,since是属性的键,akin,25,1999都是属性的值,他的值可以是多种类型的东西,函数,对象,数组等等,是的,你没有看错,他的属性值还可以是对象,嵌套型的,哈哈哈,也可以是函数,这时候我们不叫他为对象属性了,而是对象的方法,至于为啥,母鸡
对象的定义
我们曾经说过,我们JS有拆封箱的操作,原因就是因为有包装类型(一般都是使用构造函数加new的方式)和字面量方式两个表达的方式
字面量方式还是最简单的,也不会出现什么问题,语义简单
//字面量方式
const user = {
username: "jack",
score: 100
};
使用内置的object构造函数的方式,还是算了,他会出现很多问题,例如,你就像创建一个空对象,但是其实创建了一个包装对象;其次他影响JS的性能,能一句话的事,你非要墨墨迹,图什么?
//构造函数方式
const user = new Object();
user.name = "jack";
你看自定义构造函数的方式,主要是为了实现高度的自定义,以及复用
//自定义构造函数方式
function Person(name, age) {
this.name = name;
this.age = age;
}
const tom = new Person("Tom", 30);
使用工厂函数的方式,也是一样的
//工厂函数的方式
function createUser(name, age) {
return {
name,
age
};
}
const user = createUser("Mike", 22);
使用自身的crteat方式,主要就是依据一个对象为模板(原型对象),进行创建的,常用于“继承”或手动构建原型链结构
const baseUser = { isAdmin: false };
const user = Object.create(baseUser);
user.name = "Tom";
访问对象属性
有人问了,我们声明了,然后呢?怎么访问属性?
使用点操作符
const user = { name: "Ann" };
console.log(user.name); // "Ann"
使用[],支持动态属性名,就是你可以建立一个key,[key]就是你最后要查的属性,当然,你可以随意的变化key不是?
let key = "name";
console.log(user[key]); // "akin"
key="age"
console.log(user[age]); //age的值
使用可选链式符,就是你担心一个属性不存在的时候,使用的方式,例如:看看用户信息里有没有没profile?有的话,就输出avatar属性内容,没有就undefined
const user = {};
console.log(user.profile?.avatar); // undefined,不报错
添加,修改,删除属性
一个对象,如果你直接写访问代码,如果他有这个属性你就是访问或修改了,没有就是添加
let user ={
name:"akin",
age:25
}
//添加
user.sex=1;
//修改
user.age = 20;
删除的话,需要使用delete 关键字,(只会删除自身属性,原型不受影响)
注意,删除只能用于对象,你要是使用变量或数组元素,那么会留一个empty孔
const arr = [1, 2, 3];
delete arr[1];
console.log(arr); // [1, empty, 3]
对象进阶
对象属性遍历
我们遍历对象的属性可以使用的方式有很多
使用for in,你可能听过for或for of,你不知道还有for in吧,是的,for in就是转为为object
设计的,他遍历的是对象的键(可枚举的),什么是可枚举的?就是你可以遍历的,有的属性它配置了,说不参与遍历的属性,等下,我们在object的defineProperty中介绍哈
const obj = { name: "JS", age: 25 };
for (const key in obj) {
console.log(key, obj[key]);
}
// 输出:
// name JS
// age 25
使用object.keys()--获取键,object.values()--获取值,object.entries()--获取键值对(二维数组方式),这些访问的,都是可以参与枚举的属性哦
//遍历键
const object1 = {
a: 'somestring',
b: 42,
c: false,
};
console.log(Object.keys(object1));
// Expected output: Array ["a", "b", "c"]
//遍历值
const object1 = {
a: 'somestring',
b: 42,
c: false,
};
console.log(Object.values(object1));
// Expected output: Array [ 'somestring', 42, false ]
//键值对
const object1 = {
a: 'somestring',
b: 42,
c: false,
};
console.log(Object.entries(object1));
// Expected output: Array [ [ 'a', 'somestring' ], [ 'b', 42 ], [ 'c', false ] ]
使用Reflect.ownKeys()方法,返回他自己的可枚举和不可枚举的所有属性
const person = { name: 'Alice', age: 25 };
const sym = Symbol('id');
person[sym] = 123;
const keys = Reflect.ownKeys(person);
console.log(keys); // 输出: ['name', 'age', Symbol(id)]
对象的深浅拷贝
首先说明一下,你复制对象的时候,要注意;有可能你只是把对象的内容拷贝过来了,你修改,原来的不会受影响,但是有的时候你是把对象的指针复制过来了,你修改了,原来的也会变化
浅拷贝:拷贝的是第一层属性的引用,嵌套对象仍然共享内存地址,也就是说你修改了复制新对象,那么就对象e内容也会变化,所以注意了;怎么实现浅拷贝呢?使用object.assgin()方法,...展开操作符等
const obj1 = { name: "JS", info: { age: 10 } };
const obj2 = { ...obj1 }; // 浅拷贝
obj2.info.age = 20;
console.log(obj1.info.age); // 20,原对象被影响
深拷贝:拷贝的是整个对象结构的所有层级,内部对象重新分配内存,不共享引用,就是说你修改复制的新对象的内容,原来的对象内容没有变化,这里使用了原生的structuredClone方法,还有其他的方式:使用JSON.parse(JSON.stringify(obj)),这种最好别用,全是毛病;structuredClone(obj)原生方法,自己手写一个递归,在每一层都copy一下;或者使用第三方的库,例如Lodash
const obj1 = { name: "JS", info: { age: 10 } };
const obj2 = structuredClone(obj1); // 深拷贝
obj2.info.age = 20;
console.log(obj1.info.age); // 10,不受影响
//自己写一个递归函数
function deepClone(value, hash = new WeakMap()) {
// 1. 处理原始类型(string, number, boolean, null, undefined, symbol)
if (value === null || typeof value !== 'object') {
return value;
}
// 2. 处理循环引用
if (hash.has(value)) {
return hash.get(value);
}
// 3. 特殊对象类型处理
if (value instanceof Date) {
return new Date(value);
}
if (value instanceof RegExp) {
return new RegExp(value);
}
// 4. 初始化拷贝的容器:数组或普通对象
const result = Array.isArray(value) ? [] : {};
hash.set(value, result); // 设置缓存,防止循环引用
// 5. 递归拷贝每个属性
for (const key in value) {
if (Object.prototype.hasOwnProperty.call(value, key)) {
result[key] = deepClone(value[key], hash);
}
}
return result;
}
对象的扩展性
什么是对象的扩展性?就是一句话,我让你添加,你才能添加,我说不行就是不行
怎么控制呢?有多种方式
我们冻结对象!意思就是这个对象,被我冰冻了,你无法添加或修改删除,就是只读了,理解了吧,哪怕我给他配置了你可以读写的配置,你也无法修改,我们使用object 的freeze方法冻结,还有一个Object.isFrozen()方法,可以判断一个对象是不是被冻结,哈哈哈,还有,要注意我们这种是浅冻结,至于怎么深度冻结,我确实没用过, 和深拷贝估计原理差不多呢,自己了解一下
const obj = {
name: 'JS',
info: {
version: 'ES6'
}
};
Object.freeze(obj);
obj.name = 'JavaScript'; // ❌ 修改失败(静默或报错,取决于是否是严格模式)
obj.age = 10; // ❌ 添加失败
delete obj.name; // ❌ 删除失败
console.log(obj); // { name: 'JS', info: { version: 'ES6' } }
第二种,我们密封对象!什么是密封,就是也是半只读的,你可以修改已有的可写的属性,我们使用对象的seal方法,密封对象;巧了,我们有一个Object.isSealed()方法,可以用来判断是不是被密封了,哈哈
const obj = { name: 'JS' };
Object.seal(obj);
obj.age = 10; // ❌ 无法添加
delete obj.name; // ❌ 无法删除
obj.name = 'ES6'; // ✅ 可修改
还有一个,那就是Object.preventExtensions()方法,他只是不可以添加新的属性,但是你可以修改,删除已有的属性,你也可以使用Object.isExtensible看看,是不是可以扩展的
const obj = { name: 'JS' };
Object.preventExtensions(obj);
obj.age = 25; // ❌ 添加失败
obj.name = 'Vue'; // ✅ 修改成功
delete obj.name; // ✅ 删除成功
console.log(obj); // {}
还有最后一个,Object.defineProperty(),这玩意,可是牛逼了,它可以往一个对象里添加或修改属性,并且可以配置对象是否只读,是否可以修改,属性是否可枚举当然了
还有一个访问器/拦截器(get和set,)别小看了他,他可是vue2的响应式的核心机制,大家自己看一下,其实挺简单的,就是你访问的时候,我是用get方法,给你我想给你的值,你修改的时候,我使用set给你修改后我想给你的值
(Vue2使用该方法的弊端:就是后续添加的属性不具备响应式,因为他没有自己的get和set,所以他自己有一个$set方法,其实还是,再次调用Object.defineProperty()方法,他自己深层的属性无法实现监听也是因为这样的)
这是一个对象的一个属性,你还可以使用Object.defineProperties(),添加多个属性
const o = {}; // 创建一个新对象
// 通过 defineProperty 使用数据描述符添加对象属性的示例
Object.defineProperty(o, "a", {
value: 37, //他的值
writable: true, //是否可以写
enumerable: true, //是否参与枚举
configurable: true, //是否可以删除,修改等操作
});
// 'a' 属性存在于对象 o 中,其值为 37
// 通过 defineProperty 使用访问器属性描述符添加对象属性的示例
let bValue = 38;
Object.defineProperty(o, "b", {
get() {
return bValue;
},
set(newValue) {
bValue = newValue;
},
enumerable: true,
configurable: true,
});
o.b; // 38
// 'b' 属性存在于对象 o 中,其值为 38。
// o.b 的值现在始终与 bValue 相同,除非重新定义了 o.b。
// 数据描述符和访问器描述符不能混合使用
Object.defineProperty(o, "conflict", {
value: 0x9f91102,
get() {
return 0xdeadbeef;
},
});
// 抛出错误 TypeError: value appears only in data descriptors, get appears only in accessor descriptors
Object.defineProperties(obj, {
prop1: { descriptor1 },
prop2: { descriptor2 },
...
})
那有人问了,为啥不直接定义的时候写进去不就完了,还搞得这么复杂,是的,这是必须的,因为你一开始不知道他又那么多东西,所以你需要他可以扩展或非扩展,所以,你就需要上述API,精准的控制一个对象的所有东西
对象原型
原型
其实,不管怎么创建一个对象哈,他都会自动关联另一个对象,这个对象就是它的“原型” —— 即 [[Prototype]]
,在代码中通过 __proto__
访问
原型([[Prototype]]
)就是一个对象所继承的“备用查找模板;就是他不是凭空出来的,你理解吗?你如果使用的构造函数方式,那他一定有参考的原型不是吗?如果你使用的字面量方式,他会偷偷的自动转为new 的方式,你还记的我说的拆封箱操作吧,所以她一定有一个参考的模板出来.
你看,我在浏览器的控制台,写一个对象,使用字面量方式吧,但是他就是有一个隐藏的Prototype属性,是不是?这里是我们看到的,你发现没,其他的就是他的实例方法,也就是说,你创建了,虽然没有见到new,但其实你也是底层给你new出来,你可以直接使用实例方法,那还有一个是什么?就是_proto_,这就是我们在代码中可是访问的,他好比一个独有的令牌,就和你说,我是参考"他"的,不信你看看
原型链
结果你一看,_proto_顺着这个线,找到了啥?是的,找到了object这个顶级原型,而这个查找的过程就是原型链
所以就是一个逻辑:先自己身上(Prototype)看看,没有用_proto_去看看亲爹身上(Prototype)看看,周而复始,理解了吧
顶级原型
我为什么说object是顶层了,?因为你所有的object类型到顶他不就是object?
你看看我点开object的_proto_他是不是null?这就到顶了孩子
原型链的作用
那你说,我有一个原型,我也有一个原型链,它有什么用?
你还记得我说的作用域吗?你自身找不到就去外边找,外边找不到没了,是吧;有了原型链啊,也是的,一个对象上有没有这个属性?没有,那就去他爹哪里看看,他爹没有,就去他爷爷哪里看看,就这样,所以你为什么新建一个对象,对象上就有实例方法等属性,你自己又没写,原因就是他是去他爹,爹的爹哪里找的,最后如果到了 Object.prototype
都没找到,就返回 undefined
const obj = {};
console.log(obj.toString); // 找不到 toString,会去 obj.__proto__ 找(即 Object.prototype)
继承
什么是继承,就是让一个对象通过原型链访问另一个对象的属性或方法;
意思就是实现属性的共享!!!!
你看,我在Animal对构造函数,我往他的prototype上添加一个sayHi方法是吧,然后,我们后续的时候创建一个dog的构造函数,我创建Animal的实例,并把prototype给了他,就是找,最后找到了Animal身上,就有了sayHi方法,这就是原型链继承,继承的方式有很多种
function Animal(name) {
this.name = name;
}
Animal.prototype.sayHi = function () {
console.log(`I'm ${this.name}`);
};
function Dog(name) {
Animal.call(this, name); // 构造函数继承属性
}
Dog.prototype = Object.create(Animal.prototype); // 原型继承方法
Dog.prototype.constructor = Dog;
const d = new Dog('Lucky');
d.sayHi(); // ✅ I'm Lucky
关于原型的方法
所以,你知道了原型对象了,object给你很多看看原型的方法
Object.getOwnPropertyDescriptor()静态方法返回一个对象,该对象描述给定对象上特定属性(即直接存在于对象上而不在对象的原型链中的属性)的配置
Object.getPrototypeOf()静态方法返回指定对象的原型(即内部 [[Prototype]]
属性的值);
使用 in
操作符可以检查对象是否包含指定属性。它会返回一个布尔值,表示对象是否具有该属性,检查对象属性时,会沿着原型链查找,自身属性和继承属性都会查找
主流继承方式
原型链继承
就是按照原型链一直往上查找,继承,这就是原型链继承
function Parent() {
this.name = "Parent";
}
Parent.prototype.sayHi = function () {
console.log("Hi from parent");
};
function Child() {}
Child.prototype = new Parent();
const c = new Child();
c.sayHi(); // Hi from parent
构造函数继承
就是我们使用构造函数的创建方式,然后让他从构造函数的prototype上去查找,有一个缺点就是方法不能复用
function Parent() {
this.names = ["Tom", "Jerry"];
}
function Child() {
Parent.call(this); // 只继承属性,不继承方法
}
const c1 = new Child();
c1.names.push("Spike");
const c2 = new Child();
console.log(c2.names); // ["Tom", "Jerry"]
组合继承
就是原型链继承+构造函数的继承
function Parent(name) {
this.name = name;
}
Parent.prototype.sayHi = function () {
console.log("Hi from", this.name);
};
function Child(name) {
Parent.call(this, name); // 继承属性
}
Child.prototype = new Parent(); // 继承方法
Child.prototype.constructor = Child;
const c = new Child("Child");
c.sayHi();
class 继承
这是ES6提出的,我们需要使用extends关键字,constructor就是一个对象在new实例化的时候,自带的一些东西;super就是,我们继承的时候需要从父类上拿到一些属性自己用的,你看,我的父类,创建的时候,有一个name属性哦,然后子类的话,继承自父类,然后,使用父类中的name熟属性,所以写在super中
class Parent {
constructor(name) {
this.name = name;
}
sayHi() {
console.log(`Hi ${this.name}`);
}
}
class Child extends Parent {
constructor(name, age) {
super(name); // 必须调用 super
this.age = age;
}
}
const child = new Child("Tom", 18);
child.sayHi(); // Hi Tom
注意:还有很多方式的继承,暂时就不说了,下次,我会以附言的方式,补充和重新讲解这些继承的方法,以及其他的继承方式
Symbol
提一嘴,我们可以使用symbol这个不会重复的值,作为对象的键