对JavaScript对象的深度解析

68 阅读6分钟

什么是对象?

我们可以把 JavaScript 里的 “对象” 理解成一个能装各种东西的容器

一、对象的 “包容性”

JavaScript 里,除了数字、字符串、布尔值、nullundefined 这些 “简单类型”,数组、函数、正则表达式,甚至 “对象自己” 都是对象。比如数组 [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.numbernumbernumber 是数字类型
typeof flight.statusstringstatus 是字符串类型
typeof flight.arrivalobjectarrival 是日期对象(属于对象类型)
typeof flight.manifestundefinedmanifest 不存在,所以是 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 或者过滤函数类型的属性。