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"]
什么时候必须用 []?
- 属性名是变量的时候
- 属性名有特殊字符或者空格的时候
- 属性名是数字的时候
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、''、false、null、undefined 这些假值,会误判。
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 最核心也是最难理解的概念之一。别慌,我们用"遗传学"来理解就简单多了。
三句箴言,记住就行
-
每个对象都有
__proto__(隐式原型)- 就像每个人都有自己的基因
-
每个函数都有
prototype(显式原型)- 就像每个父母都有自己的遗传物质
-
对象的
__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 会这样找:
- 先看对象自己有没有这个属性 → 有就用
- 没有就去
__proto__上找 → 有就用 - 还没有就去
__proto__.__proto__上找 - 一直找到
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...in、in 运算符:
// 检查属性
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 的区别
| 对比项 | Object | Reflect |
|---|---|---|
| 定位 | 偏向操作对象 | 纯反射 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 对象可以,别真的去驾驭别人的对象啊!(狗头保命)