JS不要太简单(六):对象?原型?原型链?继承?太难了

对象基础

什么是对象

        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这个不会重复的值,作为对象的键

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值