跳转至

对象

字面量

字面量就是指变量的常数值, 字面上看到的值.

例子
let x = 10 // x是变量, 10是字面量
let s = "Hello World" // s是变量, "Hello World"是字面量

对象字面量

对象字面量是被花括号包含, 含有键值对的以逗号分隔的列表.

例子
let person = {
    name: "wenzexu",
    age: "18"
} // name: "wenzexu", age: 18 是对象字面量

属性

访问/新增/删除

访问

访问JS对象的属性有三种方式:

  • [obj].[prop]
  • [obj].[[prop]]
  • [obj].[[expression]]

新增

同访问.

删除

可以使用delete [obj].[prop]删除一个对象的属性或方法.

注意

delete关键字不会删除继承得到的属性, 但是如果你把__prototype__中的某一个属性删除了, 那么所有继承__prototype__的对象中的这个属性都会被删除.

方括号

方括号, 适用于访问, 修改或修改包含特殊字符的属性.

例子

定义:

let user = {}

user["likes birds"] = true
console.log(user["likes birds"])
delete user["like birds"]

执行:

$ node test.js
true

方括号同样提供了一种可以通过任意表达式获取属性名的方式.

例子
let key = "likes birds";

user[key] = true;

计算属性

当创建一个对象的时候, 可以在对象字面量的地方使用方括号, 所表示的属性叫做计算属性. 计算属性名从对象所在的环境中获取.

例子

定义:

let fruit = "apple"
let bag = {
    [fruit]: 5
}

console.log(bag.apple)

执行:

$ node test.js
5

定义:

let fruit = "apple"
let bag = {
    [fruit + "Computers"]: 5
}

console.log(bag.appleComputers)

执行:

$ node test.js
5

属性条目缩写

在实际开发中, 常用已存在的变量当作属性名, 我们可以采取属性条目缩写的方法, 使属性条目变得更短.

例子
function makeUser(name, age) {
    return {
        name, // 与name: name相同
        age, // 与age: age相同
    }
}

属性名称限制

对象的属性名称可以是JS的保留字, 如for, let, return等.

属性名没有任何限制, 可以是任何字符串或者标识符. 其他类型会被自动转换为字符串.

例子

定义:

let obj = {
    0: "test" // 等同于"0": "test"
}

console.log(obj[0])
console.log(obj["0"])

执行:

$ node test.js
test
test

属性存在性测试

可以利用in操作符判断属性是否存在与对象中.

Tip

相比于其他语言, JS的对象能够被访问任何属性, 即使属性不存在也不会报错, 读取不存在的属性会得到undefined.

例子

定义:

let user = {
    name: "wenzexu",
    age: 18
}

console.log("name" in user)
console.log("blabla" in user)

执行:

$ node test.js
true
false
注意

in的左侧必须是属性名, 通常是一个带引号的字符串. 如果忽略引号, 就意味着左边是一个变量, 它应该要包含要跑那段的实际属性名. 否则会报错.

例子

定义:

let user = {
    age: 30
}

let key = "age"
console.log(key in user)

执行:

$ node test.js
true

标识符类型

根据规范, 只有两种原始类型可以用作键:字符串类型和标识符类型, symbol类型.

可以使用Symbol("[description]")来创建一个唯一的标识符. 即使它们的描述相同.

例子

定义:

let id1 = Symbol("id")
let id2 = Symbol("id")

console.log(id1 == id2)

执行:

$ node test.js
false
Tip
  • 如果使用除此之外的类型, 会被自动转换为字符串.

    例子

    obj[1]会自动变为obj["1"]; obj[true]会自动变为obj["true"].

  • 标识符不会被自动转换为字符串

    JS中的大多数值都支持字符串的隐式转换, 但是标识符比较特殊, 不会被自动转换.

    例子

    定义:

    let num = 10
    let id = Symbol("id_name")
    
    console.log(num) // 自动转换为字符串"10"
    console.log(id) // 不会自动转换为描述"id_name"
    

    执行:

    $ node test.js
    10
    Symbol(id_name)
    
  • 如果真的想显示一个标识符的描述, 可以访问标识符的description属性.

    例子

    定义:

    let id = Symbol("id_name")
    console.log(id.description)
    

    执行:

    $ node test.js
    id_name
    
  • 如果要在对象字面量里面使用标识符, 需要使用方括号将其括起来.

    例子
    let id = Symbol("id_name")
    
    let user = {
        name: "wenzexu",
        [id]: 123
    }
    

    我们需要id的值作为键, 而不是字符串'id'.

  • 标识符属性不参与for ... in ...循环

    例子

    定义:

    let id = Symbol("id_name")
    let user = {
        name: "wenzexu",
        age: 18,
        [id]: 123
    }
    
    for (let key in user) {
        console.log(key)
    }
    

    执行:

    $ node test.js
    name
    age
    

    可以看到没有id.

  • Object.assign复制属性会同时复制字符串和标识符属性.

    例子

    定义:

    let id = Symbol("id_name")
    let user = {
        [id]: 123
    }
    
    let clone = Object.assign({}, user)
    console.log(clone[id])
    

    执行:

    $ node test.js
    123
    
注意

标识符键不是字符串, 而是标识符类型. 因此无法使用点.运算符来访问这些属性, 点运算符只能用于访问字符串键.

例子

定义:

let user = {}

let id = Symbol("id_name")
user[id] = "something..."

// 正确地访问方式
console.log(user[id])

// 尝试使用点运算符访问
console.log(user.id)

执行:

$ node test.js
something...
undefined

"隐藏"属性

标识符允许我们创建对象的"隐藏"属性. 这些属性不会被其他模块重写.

例子

目录结构:

.
├── a.js
└── b.js

a.js文件:

let user = {
    name: "wenzexu"
}

// 添加一个隐藏属性
let id = Symbol("id_ajs")
user[id] = "ID from a.js"

// 打印所有属性
console.log(user.name)
console.log(user[id])

// 导出对象
module.exports = user

b.js文件:

let user = require("./a.js")

// 尝试打印一个公开属性
console.log(user.name)

// 尝试打印一个隐藏属性
console.log(user[id])

执行b.js文件:

$ node b.js
wenzexu
ID from a.js
wenzexu
/home/wenzexu/test/b.js:7
console.log(user[id])
                ^

ReferenceError: id is not defined
    at Object.<anonymous> (/home/wenzexu/test/b.js:7:18)
    at Module._compile (node:internal/modules/cjs/loader:1358:14)
    at Module._extensions..js (node:internal/modules/cjs/loader:1416:10)
    at Module.load (node:internal/modules/cjs/loader:1208:32)
    at Module._load (node:internal/modules/cjs/loader:1024:12)
    at Function.executeUserEntryPoint [as runMain] (node:internal/modules/run_main:174:12)
    at node:internal/main/run_main_module:28:49

Node.js v20.15.0

可以看到, 在a.js文件内可以访问隐藏属性id, 但是在另外的模块b.js中无法访问隐藏属性.

如果我们在b.js里面也定义了一个名字为id的标识符并作为导入对象user的属性, 由于上述的特性, a.js中的id属性和b.js中的id属性是完全不同的.

修改后的b.js文件:

let user = require("./a.js")

// 添加一个隐藏属性
let id = Symbol("id_bjs")
user[id] = "ID from b.js"

// 打印所有属性
console.log(user[id])
console.log(user.name)

执行:

$ node b.js
ID from a.js
wenzexu
ID from b.js
wenzexu

注意id属性不是被重写了, 而是新建的.

全局标识符

若要在不同的模块之间共享一个标识符, 可以将其注册用到全局标识符注册表然后在任何地方都可以访问.

全局注册表中, 使用相同的描述访问时, 取回的是相同的标识符. 如果要从注册表中读取(不存在则创建)标识符, 用[symbol].for([key]). 该调用会检查全局注册表, 如果有一个描述为[key]的标识符, 则返回该标识符. 否则将创建一个新的标识符, 描述为[key].

例子

目录结构:

.
├── a.js
└── b.js

a.js文件:

let user = {
    name: "wenzexu"
}

// 将标识符注册到全局标识符注册表
let id = Symbol("id_ajs")
user[id] = "ID from a.js"

// 打印所有属性
console.log(user.name)
console.log(user[id])

// 导出对象
module.exports = user

b.js定义:

let user = require("./a.js")

// 尝试打印一个公开属性
console.log(user.name)

// 尝试从全局标识符注册表中获取一个标识符
let id = Symbol.for("id_ajs")
console.log(user[id])

执行:

$ node b.js
wenzexu
ID from a.js
wenzexu
ID from a.js
[symbol].keyFor()

[symbol].keyFor([symbol])按照标识符返回它在全局注册表中的描述.

例子

定义:

// 注册两个标识符到全局注册表
let sym = Symbol.for("name")
let sym2 = Symbol.for("id")

// 尝试获取这两个标识符的描述
console.log(Symbol.keyFor(sym))
console.log(Symbol.keyFor(sym2))

执行:

$ node test.js
name
id
注意

使用该函数来查找标识符的描述的前提是这个标识符为全局标识符, 否则返回undefined.

例子

定义:

let globalSymbol = Symbol.for("name")
let localSymbol = Symbol("name")

console.log(Symbol.keyFor(globalSymbol))
console.log(Symbol.keyFor(localSymbol))

执行:

$ node test.js
name
undefined

for ... in ...循环

可以使用for ... in ...循环遍历对象所有的属性.

例子

定义:

let user = {
    name: "John",
    age: 30,
    isAdmin: true
}

for (let key in user) {
    console.log(key)
    console.log(user[key])
}

执行:

$ node test.js
name
John
age
30
isAdmin
true

遍历的顺序

使用for ... in ...循环遍历属性的顺序如下:

  • 若为整数属性, 按照从小到大顺序遍历
  • 若为非整数属性, 按照创建的顺序遍历
例子

定义:

let codes = {
    "49": "Germany",
    "41": "Switzerland",
    "44": "Great Britain",
    // ..,
    "1": "USA"
};

for(let code in codes) {
    console.log(code)
}

执行:

$ node test.js
1
41
44
49

定义:

let codes = {
    "+49": "Germany",
    "+41": "Switzerland",
    "+44": "Great Britain",
    // ..,
    "+1": "USA"
};

for (let code in codes) {
    console.log(code)
}

执行:

$ node test.js
+49
+41
+44
+1

方法

方法简写

在对象字面量中, 有一种更短的方法的语法.

???+ example "例子'

```js
let user = {
    sayHi() {
        console.log("Hello")
    }
}
```

this的值

详情见这里.

引用和复制

引用

对象变量存储的是对对象的引用.

例子

定义:

let a = {}
let b = a;

console.log(a == b)
console.log(a === b)

执行:

$ node test.js
true
true

复制和克隆

复制属性

可以使用Object.assign([dest], [[src1], [src2], [src3], ...])实现复制属性.

  • [dest]: 目标对象
  • [src1], [src2], [src3], ...: 多个源对象

该方法会将所有源对象的属性拷贝到目标对象中.

例子

定义:

let user = {
    name: "wenzexu"
}
let permission1 = {
    canView: true
}
let permission2 = {
    canEdit: true
}

Object.assign(user, permission1, permission2)

console.log(user)

执行:

$ node test.js
{ name: 'wenzexu', canView: true, canEdit: true }
注意

若被拷贝的对象的属性的属性名已经存在, 那么它会被覆盖.

例子

定义:

let user = {
    name: "wenzexu"
}

Object.assign(user, {
    name: "ricolxwz"
})

console.log(user.name)

执行:

$ node test.js
ricolxwz

克隆对象

Object.assign([dest], [[src1], [src2], [src3], ...])方法的返回值为目标对象, 我们设置目标对象初始值为{}实现克隆.

例子

定义:

let user = {
    name: "wenzexu",
    age: 18
}

let clone = Object.assign({}, user)
console.dir(clone)

执行:

{ name: 'wenzexu', age: 18 }

或者使用...语法.

例子

定义:

let user = {
    name: "wenzexu",
    age: 18
}

let clone = {...user}
console.dir(clone)

执行:

{ name: 'wenzexu', age: 18 }
深层克隆

对象的属性可以是其他对象的引用.

例子

定义:

let user = {
    name: "wenzexu",
    sizes: {
        height: 180,
        width: 50
    }
}

let clone = {...user}

clone.name = "ricolxwz"
clone.sizes.width++

console.dir(user)

执行:

$ node test.js
{ name: 'wenzexu', sizes: { height: 180, width: 51 } }

因此, 为了让拷贝之后的对象成为真正独立的对象, 应该使用过一个拷贝循环来检查源对象的每一个属性, 如果值为一个对象, 也应该复制它的结构. 这就是所谓的"深拷贝". 可以使用lodash库的_.cloneDeep(obj).

创建对象

在JS中, 有很多中不同的方式来创建对象, 大致为:

  • 使用对象字面量
  • 使用new关键字搭配构造函数

使用对象字面量

使用{}创建对象.

例子
let user = {
    name: "wenzexu",
    age: 18
}

使用构造函数

如果有许多属性和方法是所有用户都要用到的, 可以使用构造函数创建对象.

构造函数在技术上是常规函数, 但是有两个特殊的地方:

  • 命名以大写字母开头(非强制)
  • 只能由new关键字执行
例子

定义:

function User(name) {
    this.name = name
    this.isAdmin = false
}

let user = new User("wenzexu")

console.log(user.name)
console.log(user.isAdmin)

执行:

$ node test.js
wenzexu
false
笔记

当一个构造函数使用new操作符执行的时候, 它按照以下步骤执行:

  1. 一个新的空对象被创建并分配给this
  2. 构造函数体执行. 通常会修改this所指向的对象
  3. 返回this所指的对象

可以表示为:

例子
function User(name) {
    // this = {}; 隐式创建
    this.name = name
    this.isAdmin = false
    // return this; 隐式返回
}
Tip

如果构造函数没有参数, 可以省略new后面的构造函数的括号.

例子
let user = new User;
let user = new User();

构造函数模式测试

在一个函数内部, 可以使用new.target检查它是否被使用new进行调用.

  • 对于常规调用, 它为undefined
  • 对于使用new的调用, 等于该函数
例子

定义:

function User() {
    console.log(new.target)
}

User()
new User()

执行:

$ node test.js
undefined
[Function: User]
Tip

甚至可以让常规调用和new调用的效果相同.

例子

定义:

function User(name) {
    if (!new.target) { // 若为常规调用, 则重定向为`new`调用
        return new User(name)
    }
    this.name = name
}

let wenzexu = User("wenzexu")
console.log(wenzexu.name)

执行:

$ node test.js
wenzexu

构造函数的return

构造函数通常没有return语句, 有return语句会根据返回内容产生不同的结果.

return对象, 则返回该对象; 否则, 返回this所指的对象.

例子

定义:

function BigUser() {
    this.name = "wenzexu"
    return {
        name: "Godzilla"
    }
}

console.log(new BigUser().name)

执行:

$ node test.js
Godzilla

定义:

function SmallerUser() {
    this.name = "wenzexu"

    return;
}

console.log(new SmallerUser().name)

执行:

$ node test.js
wenzexu

可选链

可选链?.是一种访问嵌套对象属性的安全的方式. 即使中间的属性不存在, 也不会出现错误.

"不存在的属性"问题

这个问题出现的原因在于:

  • 无法访问undefined的属性
  • 无法访问null的属性
例子

定义:

let user = {}
console.log(user.address)
console.log(user.address.street)

执行:

$ node test.js
undefined
/Users/wenzexu/test/test.js:3
    console.log(user.address.street)
                            ^

TypeError: Cannot read properties of undefined (reading 'street')
    at Object.<anonymous> (/Users/wenzexu/test/test.js:3:30)
    at Module._compile (node:internal/modules/cjs/loader:1369:14)
    at Module._extensions..js (node:internal/modules/cjs/loader:1427:10)
    at Module.load (node:internal/modules/cjs/loader:1206:32)
    at Module._load (node:internal/modules/cjs/loader:1022:12)
    at Function.executeUserEntryPoint [as runMain] (node:internal/modules/run_main:135:12)
    at node:internal/main/run_main_module:28:49

Node.js v20.12.0

user.addressundefined, 我们尝试访问undefinedstreet属性, 所以报错. 而正常情况下, 我们希望得到的仍然是undefined.

定义:

let html = document.querySelector('.elem').innerHTML;

在浏览器环境中执行:

VM371:1 Uncaught TypeError: Cannot read properties of null (reading 'innerHTML')
    at <anonymous>:1:43

document.querySelector('.elem')的结果为null, 因为不存在这个元素. 尝试访问nullinnerHTML属性, 所以报错. 而正常情况下, 我们希望得到的仍然是null.

可选链

可选链?.的语法有三种形式:

  1. [obj]?.[prop]: 如果[obj]存在则返回[obj].[prop], 否则直接返回undefined
  2. [obj]?.[[prop]]: 如果[obj]存在则返回[obj].[[prop]], 否则直接返回undefined
  3. [obj].[method]?.(): 如果[obj].[method]存在在调用[obj].[method](), 否则直接返回undefined

简洁来说, 就是?.会检查左边部分是否为undefined/null, 如果不是则继续运算.

例子

定义:

let user = {}
console.log(user?.address?.street)

执行:

$ node test.js
undefined

定义:

let html = document.querySelector('.elem')?.innerHTML;

在浏览器环境中执行:

undefined

定义:

let user = null

console.log(user?.address)
console.log(user?.address.street)

执行:

$ node test.js
undefined
undefined
短路效应

?.左边的部分不存在, 即为undefined/null, 就会立即停止运算.

例子
let user = null;
let x = 0

user?.sayHi(x++) // 没有user, 所以代码执行到user即终止, 没有调用sayHi
console.log(x)

  1. 对象. (n.d.). Retrieved June 22, 2024, from https://zh.javascript.info/object 

  2. 构造器和操作符 “new.” (n.d.). Retrieved June 22, 2024, from https://zh.javascript.info/constructor-new 

  3. 可选链 “?.” (n.d.). Retrieved June 22, 2024, from https://zh.javascript.info/optional-chaining 

评论