520刚过,今天来教你怎么"驾驭"别人的对象

0 阅读10分钟

520刚过,今天来教你怎么"拿捏"别人的对象

引言

前几天520,大家有没有和心仪的对象甜蜜相伴、共度美好时光,到现在还意犹未尽?

今天这篇文章,就来教你怎么"驾驭"别人的对象。

先别急着义正言辞地拒绝我:"啊?Ricado,你这什么虎狼之词?我对自己的另一半超忠诚,你趁早打消念头!"

别激动,我们说的,其实是 JavaScript 的对象。

在 JS 里,对象可以说是最核心、最万能的数据类型,也是日常开发绕不开的重点。它不像简单的数字、字符串那样单调,既能装属性、存数据,又能绑定方法、实现功能,几乎撑起了整个 JavaScript 的半壁江山。

不管是基础的字面量创建、属性增删改查,还是原型、继承、this 指向这些容易踩坑的知识点,今天一次性讲透,帮你真正吃透 JS 对象。


1.1 创建对象的 3 种方法

想要"驾驭"一个对象,首先得学会怎么"认识"一个对象。JS 里创建对象主要有 3 种方式:

方法一:对象字面量(最常用)

就像相亲时直接拿到对方的全部资料,简单直接:

const girlFriend = {
  name: "小美",
  age: 18,
  hobby: ["追剧", "吃火锅"],
  sayHi: function() {
    console.log("你好呀~");
  }
};

这是最常用的方式,简单明了,想写什么属性直接往里塞。

方法二:new Object()

就像先认识一个陌生人,再慢慢了解她的信息:

const girlFriend = new Object();
girlFriend.name = "小美";
girlFriend.age = 18;
girlFriend.hobby = ["追剧", "吃火锅"];

这种方式比较啰嗦,平时很少用。

方法三:构造函数(批量创建)

就像媒人批量给你介绍对象,每个对象都有相同的"模板":

function GirlFriend(name, age) {
  this.name = name;
  this.age = age;
  this.sayHi = function() {
    console.log(`我是${this.name},今年${this.age}岁~`);
  };
}

const girl1 = new GirlFriend("小美", 18);
const girl2 = new GirlFriend("小丽", 20);

这种方式适合批量创建相似的对象,后面讲原型的时候还会细说。


1.2 访问对象属性:. 和 [] 的正确打开方式

对象创建好了,怎么"打听"她的信息呢?JS 给了我们两种方式:

点运算符 .

这是最常用的方式,就像直接问:"你叫什么名字?"

console.log(girlFriend.name); // "小美"
girlFriend.sayHi(); // "你好呀~"

方括号 []

方括号就比较灵活了,特别是当属性名是变量的时候:

const key = "name";
console.log(girlFriend[key]); // "小美"
// 相当于 girlFriend["name"]

什么时候必须用 []?

  1. 属性名是变量的时候
  2. 属性名有特殊字符或者空格的时候
  3. 属性名是数字的时候
const person = {
  "my name": "张三", // 属性名有空格
  123: "数字属性"    // 属性名是数字
};

console.log(person["my name"]); // "张三"
console.log(person[123]); // "数字属性"

|| 和 && 运算符的妙用

打听别人信息的时候,万一人家不想告诉你呢?这时候就要有"预案"。

|| 运算符:设置默认值

如果属性不存在,就用默认值兜底:

// 如果没有 age 属性,就默认 18 岁
console.log(girlFriend.age || 18);

// 新版写法(更安全,不会把 0、''、false 当成假值)
console.log(girlFriend.age ?? 18);
&& 运算符:避免报错

如果对象可能是 undefined,直接访问属性会报错,这时候用 && 来"防身":

// 如果 girlFriend 是 undefined,不会报错,直接返回 undefined
console.log(girlFriend && girlFriend.name);

// 新版写法:可选链操作符 ?.
console.log(girlFriend?.name);
console.log(girlFriend?.hobby?.[0]); // 还能链式调用

有了这些"防身术",再也不怕对象是 undefined 导致页面白屏了!


1.3 对象的更新:增删改查一条龙

认识久了,对方的信息总会变嘛——年龄涨了,爱好变了,甚至……咳咳,我们来看看怎么操作:

修改属性

直接赋值就行,就像更新你的通讯录:

girlFriend.age = 19; // 过生日,年龄涨一岁
girlFriend.hobby.push("逛街"); // 新增一个爱好

添加属性

如果发现了新信息,直接加进去就行:

girlFriend.phone = "138xxxxxxx"; // 新增手机号

删除属性

有些信息不需要了,可以删掉:

delete girlFriend.exBoyfriend; // 删除前男友信息(手动狗头)

检查属性是否存在

想知道对方有没有告诉你某件事?

// 方式一:in 运算符(会检查原型链)
console.log("name" in girlFriend); // true

// 方式二:hasOwnProperty(只检查自身属性,不会看原型)
console.log(girlFriend.hasOwnProperty("name")); // true

注意: 不要用 if (girlFriend.name) 来判断属性是否存在!因为如果属性值是 0''falsenullundefined 这些假值,会误判。


1.4 遍历对象:把对方的底细摸清楚

想知道对方都告诉你了哪些信息?那就遍历一下,把所有键值对都拿出来看看。

方法一:for...in 循环

这是最经典的遍历方式:

for (let key in girlFriend) {
  console.log(key + ": " + girlFriend[key]);
  // name: 小美
  // age: 18
  // ...
}

注意: for...in 会把原型链上的属性也遍历出来。如果只想遍历自身属性,要加一层判断:

for (let key in girlFriend) {
  if (girlFriend.hasOwnProperty(key)) {
    console.log(key + ": " + girlFriend[key]);
  }
}

方法二:Object.keys()

获取所有自身属性的键名,返回一个数组:

const keys = Object.keys(girlFriend);
console.log(keys); // ["name", "age", "hobby", "sayHi"]

keys.forEach(key => {
  console.log(key + ": " + girlFriend[key]);
});

方法三:Object.values()

获取所有自身属性的值,返回一个数组:

const values = Object.values(girlFriend);
console.log(values); // ["小美", 18, ["追剧", "吃火锅"], function]

方法四:Object.entries()

获取所有键值对,每个键值对是一个 [key, value] 数组:

const entries = Object.entries(girlFriend);
console.log(entries);
// [
//   ["name", "小美"],
//   ["age", 18],
//   ["hobby", ["追剧", "吃火锅"]],
//   ["sayHi", function]
// ]

想把对象转成 Map 也很方便:

const map = new Map(Object.entries(girlFriend));

1.5 对象的引用:这是个大坑!

这是 JS 对象最容易踩坑的地方,没有之一。很多新手在这里栽了无数跟头。

基本类型 vs 引用类型

先看一个例子,你猜结果是什么?

let a = 10;
let b = a;
a = 20;
console.log(b); // ?

没错,b 还是 10。这就是值传递——a 把自己的值"拷贝"了一份给 b,从此以后 a 和 b 井水不犯河水。

但对象不一样!

let obj1 = { name: "张三" };
let obj2 = obj1;
obj1.name = "李四";
console.log(obj2.name); // ?

答案是 "李四"!惊不惊喜?意不意外?

这就是引用传递——obj1 并没有把对象拷贝给 obj2,而是把"对象的地址"告诉了 obj2。从此以后,obj1 和 obj2 指向的是同一个对象!

就像你把家里的钥匙给了朋友,你们俩都能打开同一扇门,朋友在你家乱翻,你回家一看肯定会傻眼。

记住:对象永远不会被自动拷贝,拷贝的只是引用。

浅拷贝和深拷贝

那我就是想真的拷贝一个对象怎么办?

浅拷贝(只复制一层)
// 方式一:Object.assign()
const obj3 = Object.assign({}, obj1);

// 方式二:扩展运算符 ...
const obj4 = { ...obj1 };

但是! 浅拷贝只复制第一层。如果对象里嵌套了对象,内层对象还是引用传递:

const obj1 = { 
  name: "张三",
  friend: { name: "李四" } // 嵌套对象
};

const obj2 = { ...obj1 };
obj2.friend.name = "王五";

console.log(obj1.friend.name); // "王五" —— 外层对象没影响,但内层还是被改了!老王还是一如既往的防不胜防啊。。。
深拷贝(彻底复制)

简单粗暴的方法:

const obj2 = JSON.parse(JSON.stringify(obj1));

但是这个方法有局限性:

  • 无法复制函数
  • 无法复制 undefined
  • 无法复制循环引用的对象
  • 日期对象会变成字符串

更完善的深拷贝需要自己写递归函数,或者用 lodash 的 _.cloneDeep()

引用相等判断

判断两个对象是不是"同一个人":

const obj1 = { name: "张三" };
const obj2 = { name: "张三" };

console.log(obj1 === obj2); // false —— 长得再像也不是同一个人
console.log(obj1.name === obj2.name); // true —— 但属性值是一样的

const obj3 = obj1;
console.log(obj1 === obj3); // true —— 同一个引用,当然相等

记住:=== 判断的是"是不是同一个对象",不是"长得像不像"。


1.6 原型(Prototype):对象的"遗传基因"

这是 JS 最核心也是最难理解的概念之一。别慌,我们用"遗传学"来理解就简单多了。

三句箴言,记住就行

  1. 每个对象都有 __proto__(隐式原型)

    • 就像每个人都有自己的基因
  2. 每个函数都有 prototype(显式原型)

    • 就像每个父母都有自己的遗传物质
  3. 对象的 __proto__ 指向构造函数的 prototype

    • 就像孩子的基因来自父母
function Person(name) {
  this.name = name; // 自身属性
}

Person.prototype.sayHi = function() { // 原型属性
  console.log(`我是${this.name}`);
};

const zhangsan = new Person("张三");
console.log(zhangsan.__proto__ === Person.prototype); // true

自身属性 vs 原型属性

  • 自身属性this.xxx,每个实例独立
    • 就像每个人自己的名字、年龄,你改你的,不影响别人
  • 原型属性构造函数.prototype.xxx,所有实例共享
    • 就像人类都会说话、会走路,这是"人类"这个"类"的共性,不需要每个人都单独学一遍
const lisi = new Person("李四");

zhangsan.sayHi(); // "我是张三"
lisi.sayHi(); // "我是李四"

// 两个人用的是同一个 sayHi 方法!
console.log(zhangsan.sayHi === lisi.sayHi); // true

这就是原型的好处——节省内存!如果把方法都写在构造函数里,每个实例都要新建一份,太浪费了。

原型链:找属性的"族谱"

当你访问对象的一个属性时,JS 会这样找:

  1. 先看对象自己有没有这个属性 → 有就用
  2. 没有就去 __proto__ 上找 → 有就用
  3. 还没有就去 __proto__.__proto__ 上找
  4. 一直找到 Object.prototype.__proto__(也就是 null)为止

这就是原型链,是不是很像查族谱?

console.log(zhangsan.toString());
// zhangsan 自己没有 toString
// 去 zhangsan.__proto__(Person.prototype)找,也没有
// 去 Person.prototype.__proto__(Object.prototype)找,找到了!

1.7 反射(Reflect):对象自己"照镜子"

简单说:反射就是程序自己查看、操作自身对象的结构,就像照镜子一样——看看自己长什么样,还能给自己"化妆"。

比如查属性、删属性、判断有没有属性、调用方法等,这些都属于反射操作。

JS 里主要用两个东西:

旧版反射方式

以前我们都是用 Object 上的方法 + for...inin 运算符:

// 检查属性
console.log("name" in girlFriend);

// 获取所有键
console.log(Object.keys(girlFriend));

// 删除属性
delete girlFriend.age;

新版:Reflect 对象(ES6 标准)

ES6 给了我们一套更标准、更统一的反射 API:

// 检查属性(会走原型链)
console.log(Reflect.has(girlFriend, "name")); // true

// 获取所有自身属性的键(不看原型)
console.log(Reflect.ownKeys(girlFriend)); // ["name", "age", ...]

// 获取属性值
console.log(Reflect.get(girlFriend, "name")); // "小美"

// 设置属性值
Reflect.set(girlFriend, "age", 20);

// 删除属性(返回布尔值表示是否成功)
Reflect.deleteProperty(girlFriend, "age");

// 调用方法
Reflect.apply(girlFriend.sayHi, girlFriend, []);

Reflect 和 Object 的区别

对比项ObjectReflect
定位偏向操作对象纯反射 API
返回值有些返回 undefined,有些返回对象返回布尔值,更规范
和 Proxy 配合不太行天生一对!

划重点:

  • Reflect.has() 会走原型链
  • Reflect.ownKeys() 只看自身属性,不看原型

1.8 对象的特性:你需要知道的小细节

键名默认是字符串

不管你写什么当键名,最后都会变成字符串:

const obj = {
  123: "数字",
  true: "布尔值",
  null: "null"
};

console.log(Object.keys(obj)); // ["123", "true", "null"] —— 全变成字符串了!

同名属性会覆盖

const obj = {
  name: "张三",
  name: "李四" // 后面的会覆盖前面的
};

console.log(obj.name); // "李四"

this 指向:谁调用就指向谁

这又是一个经典大坑!简单记:

const person = {
  name: "张三",
  sayHi: function() {
    console.log(this.name); // 这里的 this 是谁?
  }
};

person.sayHi(); // "张三" —— person 调用,this 指向 person

const fn = person.sayHi;
fn(); // undefined —— 没人调用,this 指向全局对象(浏览器里是 window,Node 里是 global)

记住:this 不是在定义的时候决定的,是在调用的时候决定的!

谁调用这个函数,this 就指向谁。没人调用,严格模式下是 undefined,非严格模式下是全局对象。


写在最后

好了,关于 JS 对象,从入门到"入坑",今天都讲得差不多了。

回顾一下我们今天"驾驭"的内容:

  • 3 种创建对象的方式
  • .[] 访问属性,以及 ||&&?. 这些实用技巧
  • 对象的增删改查
  • 4 种遍历对象的方法
  • 最坑的引用传递、浅拷贝 vs 深拷贝
  • 原型和原型链(对象的"遗传基因")
  • 反射(自己"照镜子")
  • 对象的一些小特性和 this 指向

JS 对象就像一个活生生的人——有自己的属性、自己的方法、自己的"遗传基因",还会和其他对象"打交道"。真正理解了对象,你才算真正入门了 JavaScript。

最后提醒一句:驾驭 JS 对象可以,别真的去驾驭别人的对象啊!(狗头保命)