什么是对象?
我们可以把 JavaScript 里的 “对象” 理解成一个能装各种东西的容器
一、对象的 “包容性”
JavaScript 里,除了数字、字符串、布尔值、null、undefined 这些 “简单类型”,数组、函数、正则表达式,甚至 “对象自己” 都是对象。比如数组 [1,2,3]、函数 function(){} 本质上都是对象。
二、对象是 “属性的容器”
可以把对象想成一个带标签的收纳盒:
- 每个 “标签” 就是属性名(可以是任意字符串,甚至空字符串);
- 每个 “标签” 对应的 “东西” 就是属性值(除了
undefined,啥都能装)。比如{ name: "小明", age: 18 },name和age是标签(属性名),“小明” 和18是对应的东西(属性值)。
三、无类别(class-free)的灵活设计
JavaScript 的对象没有 “类别限制”,就像一个不限用途的收纳盒—— 你可以随时给它加新的 “标签 - 东西” 组合(新属性),想装啥就装啥。而且对象里还能装其他对象,比如 { person: { name: "小明", pet: { type: "猫" } } },这样就能轻松表示像 “人养宠物” 这种多层级的结构。
四、原型链:对象的 “继承”
JavaScript 里对象可以通过原型链 “继承” 其他对象的属性。打个比方,有个 “动物” 对象有 “能呼吸” 的属性,“猫” 对象可以继承它,这样 “猫” 就自动有了 “能呼吸” 的属性,不用重复设置,能节省内存和初始化时间。
我们可以把 “对象引用” 理解成对象的 “地址名片” ,通过几个例子来通俗解释:
对象的引用
一、对象引用的本质:共享同一个 “对象实体”
比如:javascript运行
var x = stooge;
x.nickname = 'Curly';
var nick = stooge.nickname;
- 把 stooge 赋值给 x,并不是复制了一个新的 stooge,而是给 x 发了一张指向 stooge 这个对象的 地址名片
- 所以通过 x 给 nickname 赋值为 'Curly',相当于直接修改了 stooge 这个对象本身。
- 最后 stooge.nickname 拿到的就是 'Curly',因为它们指向的是同一个对象。
二、不同引用 vs 同一引用:对象的 “独立性” 与 “共享性”
再看这个例子:
var a = {}, b = {}, c = {};
// a、b、c 各自拿着不同空对象的“地址名片”,所以是三个独立的空对象
a = b = c = {};
// 现在把同一张“地址名片”发给了 a、b、c,所以它们指向同一个空对象
- 一开始 a、b、c 分别引用三个不同的空对象,互相独立。
- 后来让 a = b = c = {},相当于把同一个空对象的 “地址名片” 同时给了 a、b、c,所以它们现在共享同一个空对象。
总结:对象不会被复制,赋值和传递的都是 “引用(地址名片)” 。如果多个变量持有同一个对象的引用,那么修改其中一个,其他变量看到的对象也会跟着变;如果变量持有不同对象的引用,它们就是相互独立的。 我们可以把原型和原型链理解成 “对象的遗传系统”,用通俗的例子来拆解这些概念:
原型
一、原型的本质:对象的 “模板”
每个 JavaScript 对象都有一个 “原型对象”,就像孩子有父母一样,对象可以从原型那里继承属性和方法。比如所有用对象字面量({})创建的对象,原型都是 Object.prototype(JavaScript 里的 “标准模板”)。
二、Object.beget、:创建 “遗传关系” 的工具
为了方便创建有原型的新对象,我们给 Object 加了一个 beget 方法(可以理解为 “生个带遗传的对象”)。它的原理是:
if (typeof Object.beget !== 'function') {
Object.beget = function (o) {
var F = function () {}; // 造一个空函数
F.prototype = o; // 把传入的 o 设为这个函数的原型
return new F(); // 用这个函数创建新对象,新对象的原型就是 o
};
}
// 示例:让 another_stooge 的原型是 stooge
var another_stooge = Object.beget(stooge);
三、原型链的 “遗传规则”
原型的继承不是 “复制”,而是 “委托查找”,分两种情况:
-
修改对象时,不影响原型:比如给
another_stooge加属性:another_stooge['first-name'] = 'Harry'; another_stooge.nickname = 'Moe';这些修改只属于 another_stooge 自己,不会改变它的原型 stooge。
-
查找属性时,会顺着原型链找:如果要找 another_stooge 的某个属性,自己没有的话,就会去它的原型(stooge)里找;原型没有,就去原型的原型(比如 Object.prototype)里找,直到找到或者返回 undefined。这个过程叫委托。
-
原型是 “动态” 的:如果给原型(比如 stooge)新增一个属性,所有基于它创建的对象(比如 another_stooge)会立即看到这个新属性:
stooge.profession = 'actor'; console.log(another_stooge.profession); // 输出 'actor'
简单总结:原型是对象的 “遗传模板”,原型链是 “遗传查找链”。对象自己的修改不影响原型,但查找属性时会顺着原型链委托查找;原型的动态变化也会实时反映到所有 “后代对象” 上。
我们可以把 JavaScript 的 “反射” 理解成 “查看对象内部属性的工具集”,用通俗的例子来拆解:
反射
一、反射的核心:检查对象的属性
反射就是 “看看对象里有什么属性,这些属性是什么类型”。
比如有一个 flight 对象:
var flight = {
number: 123,
status: 'on time',
arrival: new Date()
};
我们可以用 typeof 来检查属性类型:
| 代码 | 结果 | 解释 |
|---|---|---|
| typeof flight.number | number | number 是数字类型 |
| typeof flight.status | string | status 是字符串类型 |
| typeof flight.arrival | object | arrival 是日期对象(属于对象类型) |
| typeof flight.manifest | undefined | manifest 不存在,所以是 undefined |
二、注意原型链的 “干扰”
对象的原型链里也有属性(比如所有对象都继承自 Object.prototype,里面有 toString、constructor 等方法)。这些原型上的属性也会被检测到:
typeof flight.toString // 'function'(来自原型链)
typeof flight.constructor // 'function'(来自原型链)
三、如何只看 “对象自己的属性”?
如果我们只想关注对象自己定义的属性,可以用两个方法:
-
方法 1:过滤函数类型因为反射通常关注 “数据属性”,所以可以把函数类型的属性排除掉(比如上面的 toString、constructor都是函数)。
-
方法 2:用 hasOwnProperty 方法这个方法专门用来判断 “属性是不是对象自己的”,不会去原型链里找。
flight.hasOwnProperty('number') // true(自己的属性) flight.hasOwnProperty('constructor') // false(来自原型链)
简单总结:反射就是 “查看对象属性的工具箱”,typeof 可以看属性类型,但要注意原型链的干扰;如果只想看对象自己的属性,可以用 hasOwnProperty 或者过滤函数类型的属性。