JavaScript 对象(object)详解

248 阅读29分钟

关于 JavaScript ,大家可能经常听到一句话:“JavaScript 万物皆对象”。严格来说,这句话并不完全正确,但是对象在JavaScript确实至关重要。ES6 中为了和其他语言保持一致,引入类(class)的概念,但这也只是语法糖。所以这里学习记录一下JavaScript中的对象和类。

一、什么是对象

对象(Object)是一个属性(property)的集合
一个属性就是一个键值对(“key: value”),其中键(key)是一个字符串(String)符号(Symbol)类型数据(也叫做属性名),值(value)可以是任何值。

对象中的属性可以是命名数据属性命名访问器属性内部属性其中一种:

  • 命名数据属性(namee data property):由一个名称 和 一个ECMAScript语言类型的值和一个Boolean属性集合组成
  • 命名访问器属性(named accessor property):由一个名称 和 一或两个访问器函数,和一个Boolean属性集合组成。访问器函数用于存取一个与该属性相关联的 ECMAScript 语言类型值。
  • 内部属性(internal property):没有名字,且不能通过 ECMAScript 语言操作。内部属性的存在纯粹为了规范的目的。

1.1 命名属性的特性(Attributes)

1.1.1 命名数据属性

命名数据属性由一个名字关联到下表中列出的特性:

特性名称取值范围描述
[[Value]]任何ECMAScript语言类型包含属性实际的值,即读取和写入属性值的位置。
[[Writable]]Boolean表示:属性的值是否可被修改
如果为 false,试图通过 ECMAScript 代码[[Put]]去改变该属性的  [[Value]] ,将会失败。
[[Enumerable]]Boolean表示:属性是否可被枚举
如果为 true,则该属性可被 for-inObject.keys() 枚举出来,否则,该属性不可枚举。
[[Configurable]]Boolean表示:
(1)是否可删除该属性
(2)是否可改变该属性为访问器属性
(3)是否可改变该属性的特性(除了  [[Value]] 和 [[writable]]单向改为false
如果为 false,上述操作都会失败。

针对上述描述,我们写个例子来理解一下命名数据属性的特性。

let obj = {
    a: 0,
    b: 0
}
// 查看属性 a 的特性
console.log(Object.getOwnPropertyDescriptor(obj, 'a'));  // {value: 0, writable: true, enumerable: true, configurable: true}

/************************************** writable *************************************/
// writable: true 时可以修改属性的 value
obj.a = 1;
console.log(obj.a)  // 1
// writable: false 时属性的 value 不可修改 
Object.defineProperty(obj, 'a', {writable: false});
obj.a = 2;  // 不报错,但不起作用
console.log(obj.a);  // 1

/************************************** enumerable *************************************/
// enumerable: true 时属性可以被 for-in 和 Object.keys() 枚举
for (property in obj) {
    console.log(property);  // a b
}
console.log(Object.keys(obj));  // ['a', 'b']
// enumerable: false 时属性不可被 for-in 和 Object.keys() 枚举
Object.defineProperty(obj, 'a', {enumerable: false});
for (property in obj) {
    console.log(property);  // b
}
console.log(Object.keys(obj));  // ['b']

/************************************** configurable *************************************/
// configurable: true 时可以删除属性
delete obj.a
console.log(obj.a)  // undefined

// 修改属性b的特性 configurable: false
Object.defineProperty(obj, 'b', {configurable: false});

// configurable: false 时,value 仍然可以修改
obj.b = 1;
console.log(obj.b);  // 1
// configurable: false 时
// 若 writable 为 true,则可修改
Object.defineProperty(obj, 'b', {writable: true});
Object.defineProperty(obj, 'b', {writable: false});
console.log(Object.getOwnPropertyDescriptor(obj, 'b'));  // {value: 1, writable: false, enumerable: true, configurable: false}
// 若 writable 为 false,则不可修改
Object.defineProperty(obj, 'b', {writable: true});   // TypeError: Cannot redefine property: b

// configurable: false 时,configurable、enumerable都不能修改
Object.defineProperty(obj, 'b', {configurable: true, enumerable: true});   // TypeError: Cannot redefine property: b

// configurable: false 时,不能改变属性为访问器属性
Object.defineProperty(obj, 'b', {get: undefined});  //  Cannot redefine property: b

// configurable: false 时,不能删除属性
delete obj.b;  // 不报错,但不起作用
console.log(obj.b)  // 1

注意\color{red}{注意}

  • configurable: false 时,并非属性的所有特性都不能修改:
    • value 仍然可以修改
    • writable 在值为 true 的情况下可以修改为 true 或者 false都行,但是值为 false 时修改该属性就会报错。
  • 当 configurable: false 时,configurable也无法修改成 true了。
  • {writable: false} 时,尝试修改属性的值,在严格模式下会报错。。

1.1.2 命名访问器属性

特性名称取值范围描述
[[Get]]Object 或 Undefined如果该值为一个 Object 对象,那么它必须是一个函数对象。每次有对属性进行 get 访问时,该函数的内部方法 [[Call]] 会伴随着一个空参数列表被调用,并返回该属性值。
[[Set]]Object 或 Undefined如果该值为一个 Object 对象,那么它必须是一个函数对象。每次有对属性进行 set 访问时,该函数的内部方法 [[Call]]会伴随着一个参数列表被调用,这个参数列表包含了指定值作为唯一的参数。属性的内部方法  [[Set]]  可能会对随后的 属性 内部方法  [[Get]]  的调用返回结果产生影响。
[[Enumerable]]Boolean表示:属性是否可被枚举
如果为 true,则该属性可被 for-inObject.keys() 枚举出来,否则,该属性不可枚举。
[[Configurable]]Boolean表示:
(1)是否可删除该属性
(2)是否可改变该属性为访问器属性
(3)是否可改变该属性的特性
如果为 false,上述操作都会失败。

个人理解,访问器属性其实还是对数据属性的访问,只是在获取或设置某个数据属性之前添加一些处理。例如下面代码中的year_中的下划线常用来表示该属性并不希望在对象方法的外部被访问。而另一个属性year 定义为访问器属性,获取函数简单返回year_的值,设置函数则会要求设置的year_不能大于2022.

let obj = {
    year_: 2000
};

Object.defineProperty(obj, "year", {
    get() {
        return this.year_;
    },
    set(newValue) {
        if(newValue > 2022)
            this.year_ = 2022;
        else
            this.year_ = newValue;
    }
})
console.log(Object.getOwnPropertyDescriptor(obj, 'year'))
obj.year = 2034;
console.log(obj.year)

此外获取函数(get)和设置函数(set)不一定需要定义,默认为undefined

  • 若只定义获取函数,则意味属性是只读的,尝试修改属性会被忽略
  • 若只定义设置函数,则意味着属性是不可读的,尝试读取属性返回 undefined

命名属性特性的默认值

名称默认值
[[Value]]undefined
[[Get]]undefined
[[Set]]undefined
[[Writable]]false
[[Enumerable]]false
[[Configurable]]false

上表是 ECMAScript 中规定的命名属性特性的默认值。但需要注意以下几种情况

(1)对象字面量、构造函数中定义的属性
通过对象字面量 {} 定义的对象 或 构造函数创建的对象,其默认属性的默认特性为:
{value: undefined, writable: true, enumerable: true, configurable: true}

// 对象字面量
let obj = {
    a: 0
}
// 查看属性 a 的特性
console.log(Object.getOwnPropertyDescriptor(obj, 'a'));  // {value: 0, writable: true, enumerable: true, configurable: true}

// 构造函数
function Foo(name) {
    this.name = name;
}
let foo = new Foo();
console.log(Object.getOwnPropertyDescriptor(foo, 'name'));  // {value: undefined, writable: true, enumerable: true, configurable: true}

其实通过字面量 {} 初始化的对象,其内部会先通过new Object()创建对象,然后再去设置每个属性的属性描述对象为 {[[Value]]: propValue, [[Writable]]: true, [[Enumerable]]: true, [[Configurable]]: true}。具体步骤可参考:ES5 表达式 (2)通过点(.)表示法或者括号([])表示法添加的属性
默认特性为:{value: undefined, writable: true, enumerable: true, configurable: true}

let obj = new Object();
obj.b = 0;
obj['c'] = 0;
// 查看属性 a 的特性
console.log(Object.getOwnPropertyDescriptor(obj, 'b'));  // {value: 0, writable: true, enumerable: true, configurable: true}
console.log(Object.getOwnPropertyDescriptor(obj, 'c'));  // {value: 0, writable: true, enumerable: true, configurable: true}

(3)通过 Object.create() 第二个参数指定的对象自身属性
默认特性为:{value: undefined, writable: false, enumerable: false, configurable: false}

let obj = Object.create(null, {'a': {}})
console.log(Object.getOwnPropertyDescriptor(obj, 'a'));  // {value: undefined, writable: false, enumerable: false, configurable: false}

(4)通过Object.definePropertyObject.defineProperties 定义的属性
默认特性为:{value: undefined, writable: false, enumerable: false, configurable: false}

let obj = {};
Object.defineProperty(obj, 'a', {});
Object.defineProperties(obj, {b:{}});
// 访问器属性
Object.defineProperty(obj, 'c', {set: undefined});

console.log(Object.getOwnPropertyDescriptor(obj, 'a'));  // {value: undefined, writable: false, enumerable: false, configurable: false}
console.log(Object.getOwnPropertyDescriptor(obj, 'b'));  // {value: undefined, writable: false, enumerable: false, configurable: false}
console.log(Object.getOwnPropertyDescriptor(obj, 'c'));  // {get: undefined, set: undefined, enumerable: false, configurable: false}

综上所述:\color{red}{综上所述:}

  • Object.definePropertyObject.definePropertiesObject.create() 中定义的属性特性默认值与规范相同,即:
    • 数据属性默认特性:{value: undefined, writable: false, enumerable: false, configurable: false}
    • 访问器属性默认特性:{get: undefined, set: undefined, enumerable: false, configurable: false}
  • 自定义构造函数、对象字面量中的默认属性,以及点(.)表示法或者括号([])表示法添加的属性,属性特性的默认值为:{value: undefined, writable: true, enumerable: true, configurable: true} 但是对于一些JavaScript内置构造函数生成的对象,属性的默认特性也会出现与上述例外的情况。例如通过new String('12')创建的对象中默认的属性的特性。

1.2 对象相关操作

1.2.1 对象初始化

可以通过new Object() Object.create()方法,或者使用字面量标记(初始化标记)初始化对象。

(1)对象字面量 {}

一个对象初始化器,由花括号/大括号 ({}) 包含的一个由零个或多个对象属性名和其关联值组成的一个逗号分隔的列表构成。

  • 属性的键(标识符)和属性的值由冒号 : 隔开,多个属性之间由逗号 , 隔开。
let user = {
    // 一个对象 
    name: "John", // 键 "name",值 "John" 
    age: 30 // 键 "age",值 30 
};
  • 多词属性标识符必须加上引号""''(双引号、单引号都可)
let user = { 
    "likes birds": true // 多词属性名必须加引号 
};

(2)Object() 构造函数

Object 构造函数将给定的值包装为一个新对象。

  • 如果未指定值或给定的值是 null 或 undefined, 它会创建并返回一个空对象。
  • 否则,它将返回一个和给定的值相对应的类型的对象。
  • 如果给定值是一个已经存在的对象,则会返回这个已经存在的值(相同地址)。
console.log(new Object(undefined));  // {}
console.log(new Object(1));  // Number {1}
console.log(new Object({name: "dali"}));  // {name: 'dali'}

(3)Object.create()

Object.create() 方法创建一个新对象,使用现有的对象来提供新创建的对象的__proto__。

Object.create(proto,[propertiesObject])
  • proto:新创建对象的原型对象。(不是 null 或非原始包装对象,则抛出一个 TypeError 异常。)
  • propertiesObject:可选。如果该参数被指定且不为 undefined,该传入对象的自有可枚举属性将为新创建的对象添加指定的属性值和对应的属性描述符。 该方法主要作用在于指定新创建对象的原型

(4)Object.fromEntries()

补充一种初始化对象的方法,主要用于Array和Map 与 Object 直接的转换。
Object.fromEntries()  方法把键值对列表转换为一个对象。

Object.fromEntries(iterable);
  • iterable:类似 Array、 Map 或者其它实现了可迭代协议的可迭代对象
// Map 转换成对象
const map = new Map([ ['foo', 'bar'], ['baz', 42] ]);
const obj = Object.fromEntries(map);
console.log(obj); // { foo: "bar", baz: 42 }

// 数组转换成对象
const arr = [ ['0', 'a'], ['1', 'b'], ['2', 'c'] ];
const obj = Object.fromEntries(arr);
console.log(obj); // { 0: "a", 1: "b", 2: "c" }

与之对应的互逆操作Object.entries()在后文中会介绍。

1.2.2 属性访问

(1)点.表示法

一般对象的属性可以通过 . 获取,但是点符号要求 key 是有效的变量标识符

obj.a

(2)方括号[]表示法

当属性的key并非有效的标识符,即:包含空格、数字开头、包含特殊字符的情况下,点表示法就不可用了。例如下面代码中使用点表示法就会报语法错误。

let obj = {
    "a b": 2000
};
console.log(obj.a b);  // SyntaxError

这种情况下就可以使用方括号来访问对象的属性:

let obj = {
    "a b": 2000
};
console.log(obj["a b"]);  // 2000

此外,方括号还提供了通过表达式获取属性名的方法:

let obj = {
    "a b": 2000
};
let key = "a b";
console.log(obj[key]);  // 2000

1.2.3 属性定义

let obj = {
  a: 1
};
obj.b = 0;
obj["c"] = 0

当属性中出现了同名的属性时,后面的属性会覆盖前面的属性(严格模式下会报语法错误)

var a = {x: 1, x: 2};
console.log(a); // { x: 2}

1.2.4 属性删除(delete)

delete 操作符用于删除对象的某个属性;如果没有指向这个属性的引用,那它最终会被释放。

delete object.property
delete object['property']

1.2.5 方法定义

存储在对象属性中的函数被称为“方法”。对象属性也可以是一个函数、getter、setter方法。 一般如下定义:

var o = {
  property: function ([parameters]) {},
  get property() {},
  set property(value) {},
};

1.2.5 对象语法增强

  1. 属性值简写 给对象添加属性时,属性名和变量名一样时,只写变量名即可。如下:
let name = "dali";
let obj = {
    name: name
}
console.log(obj)  // {name: 'dali'}

// 可以简写为
let name = "dali";
let obj = {
    name
}
console.log(obj)  // {name: 'dali'}
  1. 可计算属性 即:通过表达式设置属性,但是表达式需要放在[]中。例如:
let nameKey = "name";
let obj = {
    [nameKey]: "dali"
}
console.log(obj)  // {name: 'dali'}

obj[nameKey] = "haha"
console.log(obj)  // {name: 'haha'}

function foo() {
    return "foo";
}
// 也可以是一个函数
obj[foo()] = "lala"
console.log(obj)  // {name: 'haha', foo: 'lala'}
  1. 简写方法名 通常给对象定义方法时,都是 方法名:匿名函数表达式 的形式,例如:
let obj = {
    foo: function() {
    }
}

但是可以简写成如下方式:

let obj = {
    foo() {
    }
}

此外,简写方法名对获取函数(get)和设置函数(get)也适用:

let person = {
    name_: '',
    get name() {
        return this.name_;
    },
    set name(name) {
        this.name_ = name;
    }
}
person.name = 'dali';
console.log(person.name);  // dali

1.2.7 对象解构

对象解构:使用与对象匹配的解构来实现对象属性的赋值

(1)一般解构

let person= {
    name: 'dali',
    age: 12
}
// 保证和对象结构匹配
let {name: personName, age: personAge} = person;

console.log(personName, personAge);  // dali 12

(2)一般解构简写

变量名和属性名相同时,那么就可以如下方式简写:

let person= {
    name: 'dali',
    age: 12
}
// 保证和对象结构匹配
let {name, age} = person;

console.log(name, age);  // dali 12

(3)嵌套解构

let person= {
    name: 'dali',
    age: 12,
    hobby: {
        title: 'basketball'
    }
}
// 保证和对象结构匹配
let {name, age, hobby: {title}} = person;

console.log(name, age, title);  // dali 12 basketball

// 注意:hobby在上述解构赋值中是当作属性名存在的,并非变量
console.log(hobby)  // ReferenceError: hobby is not defined

(4)部分解构

涉及多个属性的解构赋值不需要保证顺序,且不需要对象中的属性全部列出,只要保证属性名匹配即可。

let person= {
    name: 'dali',
    age: 12,
}
// 保证和对象结构匹配
let {age} = person;

console.log(age);  // 12 

二、Object 的相关方法

2.1 定义属性

(1)定义单个属性 —— Object.defineProperty()

Object.defineProperty()  方法会直接在一个对象上定义一个新属性,或者修改一个对象的现有属性,并返回此对象。

Object.defineProperty(obj, prop, descriptor)
  • obj:要定义属性的对象。
  • prop:要定义或修改的属性的名称或 Symbol 。
  • descriptor:要定义或修改的属性描述符。
let obj = {};
Object.defineProperty(obj, "name", {
    value: 'dali', 
    writable: true, 
    enumerable: true, 
    configurable: true});
console.log(obj)  // {name: 'dali'}

(2)定义多个属性 —— Object.defineProperties()

Object.defineProperties() 方法直接在一个对象上定义新的属性或修改现有属性,并返回该对象。

Object.defineProperties(obj, props)
  • obj:在其上定义或修改属性的对象。
  • props:要定义其可枚举属性或修改的属性描述符的对象。对象中存在的属性描述符主要有两种:数据描述符和访问器描述符
let obj = {};
Object.defineProperties(obj, {
  'property1': {
    value: true,
    writable: true
  },
  'property2': {
    value: 'Hello',
    writable: false
  }
});
console.log(obj);  // {property1: true, property2: 'Hello'}

注意\color{red}{注意}:如1.1中有关特性默认值的描述,该两种方法定义属性特性默认值:对象特性为 undefined ,布尔特性为false

2.2 读取属性的特性

(1)`Object.getOwnPropertyDescriptor() —— 读取某个属性的特性

Object.getOwnPropertyDescriptor()  方法返回指定对象上一个自有属性对应的属性描述符。(自有属性指的是直接赋予该对象的属性,不需要从原型链上进行查找的属性)

Object.getOwnPropertyDescriptor(obj, prop)
  • obj:需要查找的目标对象
  • prop:目标对象内属性名称
  • 返回值:如果指定的属性存在于对象上,则返回其属性描述符对象(property descriptor),否则返回 undefined

(2)`Object.getOwnPropertyDescriptors() —— 读取所有属性的特性

Object.getOwnPropertyDescriptors()  方法用来获取一个对象的所有自身属性的描述符。

Object.getOwnPropertyDescriptors(obj)
  • obj:任意对象
  • 返回值:所指定对象的所有自身属性的描述符,如果没有任何自身属性,则返回空对象。
let obj = {};
Object.defineProperties(obj, {
  'property1': {
    value: true,
    writable: true
  },
  'property2': {
    value: 'Hello',
    writable: false
  }
});

// {value: true, writable: true, enumerable: false, configurable: false}
console.log(Object.getOwnPropertyDescriptor(obj, 'property1'));  

// {property1: {…}, property2: {…}}
console.log(Object.getOwnPropertyDescriptors(obj));  

2.3 合并对象

Object.assign()  方法用于将所有可枚举属性的值从一个或多个源对象分配到目标对象(目标对象属性会发生改变,源对象不会)。它将返回目标对象。

  • 如果目标对象中的属性具有相同的键,则属性将被源对象中的属性覆盖。后面的源对象的属性将类似地覆盖前面的源对象的属性
  • Object.assign 方法只会浅拷贝源对象自身的并且可枚举的属性到目标对象。
  • 该方法使用源对象的[[Get]]和目标对象的[[Set]],所以它会调用相关 getter 和 setter
  • String类型和 Symbol 类型的属性都会被拷贝。
  • 原始数据类型会被包装成对象
Object.assign(target, ...sources)
  • target:目标对象。
  • sources:源对象。
  • 返回值:目标对象。
    (1)合并对象
let obj1 = {a: 1};
let obj2 = {b: 2};

// 合并对象
console.log(Object.assign(obj1, obj2));  // {a: 1, b: 2}
// 目标对象属性发生改变
console.log(obj1);  // {a: 1, b: 2}
// 源对象不发生改变
console.log(obj2);  // {b: 2}

(2)合并具有相同属性的对象

let obj1 = {a: 1}
let obj2 = {a: 2, b: 2};
let obj3 = {b: 3, c: 3};

// 合并对象
console.log(Object.assign(obj1, obj2, obj3));  // {a: 2, b: 3, c: 3}

(3)浅复制

let obj1 = {a: 1}
let obj2 = {b: {name: 'haha'}};
// 合并对象
console.log(Object.assign(obj1, obj2));  // {a: 1, b: {name: 'haha'}}

// 浅复制只复制了对象的引用,所以obj1.b 和 obj2.b 指向的是同一对象
obj1.b.name = "dali";
console.log(obj2)  // {name: 'dali'}

(4)只拷贝源对象自身的并且可枚举的属性

let obj1 = {a: 1}

// 原型上的属性 b
let obj2 = Object.create({b: 1})
// 不可迭代属性 c
Object.defineProperty(obj2, 'c', {value: 3, enumerable: false});

// 非自身属性和不可迭代属性都未复制
console.log(Object.assign(obj1, obj2));  // {a: 1}

(5)符号(Symbol)类型会被拷贝

let obj1 = {a: 1}
// 符号属性
let obj2 = {[Symbol('b')]: 2}

// 符号属性也被拷贝
console.log(Object.assign(obj1, obj2));  // {a: 1, Symbol(b): 2}

(6)原始数据类型会被包装

  • null 和 undefined 会被忽略
  • 原始类型会被包装,但只有字符串的包装对象才可能有自身可枚举属性
const obj = Object.assign({}, undefined, null, true, 1, 1n, 'str', Symbol('key'));

// 原始类型会被包装,null 和 undefined 会被忽略。
// 注意,只有字符串的包装对象才可能有自身可枚举属性。
console.log(obj); // {0: 's', 1: 't', 2: 'r'}

(7)拷贝访问器属性 拷贝访问器属性时属性的值是调用对应 setter 方法的返回值。

const obj = {
    foo: 1,
    get bar() {
      return 2;
    }
  };
  
  let copy = Object.assign({}, obj);
  console.log(copy); // { foo: 1, bar: 2 } copy.bar的值来自obj.bar的getter函数的返回值

有关于对象的合并,ES6中提供了新的扩展操作符...也可用来合并对象。

2.4 获取对象属性标识符

(1)Object.getOwnPropertyNames()

Object.getOwnPropertyNames() 方法返回一个由指定对象的所有自身属性的属性名(包括不可枚举属性但不包括Symbol值作为名称的属性)组成的数组。

Object.getOwnPropertyNames(obj)
  • obj: 一个对象,其自身的可枚举和不可枚举属性的名称被返回。
  • 返回值:在给定对象上找到的自身属性对应的字符串数组。
// a : 原型上的属性
let obj = Object.create({a: 1});
Object.defineProperties(obj, {
    // 可迭代属性
    b: {value:2, enumerable: true},
    // 不可迭代属性
    c: {value:2, enumerable: false},
    // 符号属性
    [Symbol('key')]: {value:2, enumerable: true},
})
console.log(Object.getOwnPropertyNames(obj))  // ['b', 'c']

(2)Object.getOwnPropertySymbols()

Object.getOwnPropertySymbols() 方法返回一个给定对象自身的所有 Symbol 属性(无论是否可迭代)的数组。

Object.getOwnPropertySymbols(obj)
  • obj:要返回 Symbol 属性的对象。
  • 返回值:在给定对象自身上找到的所有 Symbol 属性的数组。
let obj = Object.create({a: 1});
Object.defineProperties(obj, {
    // 可迭代属性
    b: {value:2, enumerable: true},
    // 不可迭代属性
    c: {value:2, enumerable: false},
    // 符号属性
    [Symbol('key1')]: {value:2, enumerable: true},
    [Symbol('key2')]: {value:2, enumerable: false},
})
console.log(Object.getOwnPropertySymbols(obj))  // [Symbol(key1), Symbol(key2)]

2.4 对象自身属性枚举

下述代码基于该对象:

let obj = Object.create({a: 1});
Object.defineProperties(obj, {
    // 可迭代属性
    b: {value:2, enumerable: true},
    c: {value:3, enumerable: true},
    // 不可迭代属性
    d: {value:2, enumerable: false},
    // 符号属性
    [Symbol('key1')]: {value:2, enumerable: true},
    [Symbol('key2')]: {value:2, enumerable: false},
})

(1)Object.entries()

Object.entries() 方法返回一个给定对象自身可枚举属性的 键 和 值对数组,即:
[ [键1,值1], [键1,值1], ... ]

Object.entries(obj)
  • obj:可以返回其可枚举属性的键值对的对象。
  • 返回值:给定对象自身可枚举属性的键值对数组
console.log(Object.entries(obj))  // [ ['b', 2], ['c', 3]]

(2)Object.keys()

Object.keys() 方法会返回一个由一个给定对象的自身可枚举属性标识符组成的数组,数组中属性名的排列顺序和正常循环遍历该对象时返回的顺序一致 。

Object.keys(obj)
  • obj:要返回其枚举自身属性的对象。
  • 返回值:一个表示给定对象的所有可枚举属性的字符串数组
console.log(Object.keys(obj))  // ['b', 'c']

(3)Object.values()

Object.values()方法返回一个给定对象自身的所有可枚举属性值的数组

Object.values(obj)
  • obj:被返回可枚举属性值的对象。
  • 返回值:一个包含对象自身的所有可枚举属性值的数组
console.log(Object.values(obj))  // [2, 3]

注意\color{red}{注意}:上述三种方法值的顺序与使用 for...in 循环的顺序相同, 但区别在于 for-in 循环枚举原型链中的属性

属性枚举顺序

JavaScript 中对象的属性是无序的,所以不同的方法(例如:for-in、Object.keys()、Object.getOwnPropertyNames()、Object.getOwenPropertySymbols()、Object.assign()等)枚举属性的顺序是有区别的。

  • for-in、Object.entries()、Object.keys()、Object.values() 枚举顺序不确定,由JavaScript引擎决定
  • Object.getOwnPropertyNames()、Object.getOwenPropertySymbols()、Object.assign()枚举顺序确定。即:先以升序枚举数值键,然后以插入顺序枚举字符串和符号键

2.5 对象相等判断 —— Object.is()

Object.is()  方法判断两个值是否为同一个值。

Object.is(value1, value2);

有关Object.is()需要注意两点:

  • 在比较两个值之前不会进行强制类型转换
  • Object.is() 认为 +0-0不相等,认为 NaNNaN 相等。
console.log(Object.is(-0, +0))  // false
console.log(Object.is(NaN, NaN))  // true

2.6 修改对象的状态

(1)可扩展性(Extension)

  1. Object.preventExtensions() 方法让一个对象变的不可扩展,也就是永远不能再添加新的属性。
Object.preventExtensions(obj)

一个不可扩展对象:

  • 不能添加新的属性,若添加了新属性会被忽略,严格模式下会报错 TypeError。此外,通过Object.defineProperty定义新属性也会报错TypeError
  • 不能修改对象的 [[Class]] 和 [[prototype]] 内部属性,若修改了则会报错 TypeError
  • 可以修改属性、删除属性以及添加、删除、修改原型的属性
  • 一个对象一旦设置成不可扩展,就不能修改成可扩展了(与内部属性[[Extensible]]相关)
let obj = {
    a: 1
}
// 对象设置为不可扩展
Object.preventExtensions(obj);

// 添加新属性
obj.b = 2;
console.log(obj); // {a: 1}

// 属性可以改变
obj.a = 2;
console.log(obj);  // {a: 2}

// 属性可以删除
delete obj.a
console.log(obj);  // {}

console.log(Object.getPrototypeOf(obj));
Object.setPrototypeOf(obj, new Array());  // TypeError: #<Object> is not extensible
  1. Object.isExtensible()  方法判断一个对象是否是可扩展的(是否可以在它上面添加新的属性)。
Object.isExtensible(obj)

(2)密封(seal)

  1. Object.seal() 方法封闭一个对象
    • 阻止添加新属性
    • 将所有现有属性标记为不可配置(即:configurable: false)。当前属性的值只要原来是可写的就可以改变。
Object.seal(obj)
  1. Object.isSealed()  方法判断一个对象是否被密封。
Object.isSealed(obj)

(3)冻结(freeze)

  1. Object.freeze()  方法可以冻结一个对象。一个被冻结的对象再也不能被修改;
    • 不能添加新的属性
    • 不能删除已有属性
    • 不能修改该对象已有属性的可枚举性、可配置性、可写性
    • 不能修改已有属性的值
    • 对象的原型也不能被修改。
Object.freeze(obj)
  1. Object.isFrozen() 方法判断一个对象是否被冻结
Object.isFrozen(obj)

对象的几种状态个人总结如下:

基于知识表示学习的整体框架图 (1).png

2.7 获取和设置对象原型

(1)获取对象原型 —— Object.getPrototypeOf()

Object.getPrototypeOf() 方法返回指定对象的原型(内部[[Prototype]]属性的值)。如果没有继承属性,则返回 null。若是原始数据类型,则返回对应的包装对象原型。

Object.getPrototypeOf(object)

(2)设置对象原型 —— Object.setPrototypeOf()

Object.setPrototypeOf() 方法设置一个指定的对象的原型 ( 即, 内部[[Prototype]]属性)到另一个对象或 null

Object.setPrototypeOf(obj, prototype)

注意:官方不推荐使用Object.setPrototypeOf()修改对象的原型

由于现代 JavaScript 引擎优化属性访问所带来的特性的关系,更改对象的 [[Prototype]]各个浏览器和 JavaScript 引擎上都是一个很慢的操作。如果你关心性能,你应该避免设置一个对象的 [[Prototype]]。可使用 Object.create()来创建带有你想要的[[Prototype]]的新对象。—— MDN

2.8 Object 原型上的方法 —— Object.prototype.

(1) Object.prototype.hasOwnProperty()

hasOwnProperty()  方法会返回一个布尔值,指示对象自身属性是否具有指定的属性(也就是,是否有指定的键)。

obj.hasOwnProperty(prop)

注意该方法与in操作符的区别。

(2)Object.prototype.isPrototypeOf()

isPrototypeOf()  方法用于测试一个对象是否存在于另一个对象的原型链上。

prototypeObj.isPrototypeOf(object)

注意该方法与 instanceof 运算符的区别。

(3)Object.prototype.propertyIsEnumerable()

propertyIsEnumerable()  方法返回一个布尔值,表示指定的属性是否可枚举

obj.propertyIsEnumerable(prop)

(4)Object.prototype.toString()

toString()  方法返回一个表示该对象的字符串。若该方法未被覆盖,默认返回 "[object type]"其中 type 是对象的类型。

obj.toString()

(5)Object.prototype.valueOf()

valueOf()  方法返回指定对象的原始值

obj.valueOf()
对象返回值
Array返回数组对象本身。
Boolean布尔值。
Date存储的时间是从 1970 年 1 月 1 日午夜开始计的毫秒数 UTC。
Function函数本身。
Number数字值。
Object对象本身。这是默认情况。
String字符串值。
 Math 和 Error 对象没有 valueOf 方法。

三、Object 的内部属性及方法(Object Internal Properties and Methods)

上述有关对象的基础知识可能大都了解得很透彻了,个人主要是在学习过程中记录一下。接下来继续聊一聊Object中隐藏的内部属性及方法。
ECMAScript 规范使用各种内部属性来定义对象值的语义。这些内部属性不是 ECMAScript 语言的一部分。本规范中纯粹是以说明为目的定义它们。内部属性的名字用闭合双方括号“ [[ ]] ”括起来

3.1 所有对象共有的内部属性

下表中列出了ECMAScript 规范中适用于所有 ECMAScript 对象的内部属性。所有对象必须实现下表中列出的所有内部属性其中:

  • any:指值可以是任意的 ECMAScript 语言类型

  • primitive:指的是原始值类型(Undefined、Null、Boolean、String、Number、BigInt、Symbol)

  • SpecOp():指对应的内部属性是一个内部方法。括号中是参数列表,→后面指的是返回值类型。 内部属性| 值的类型范围| 说明| | ------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------- | | [[Prototype]] | Object 或 Null | 对象的原型 | | [[Class]] | String | 说明规范定义的对象分类的一个字符串值 | | [[Extensible]] | Boolean | 可扩展性,如果是 true,可以向对象添加自身属性。如果为false,不仅不能添加自身属性,而且不能修改对象的[[Class]] 和 [[prototype]] 的值。一旦Extensible 修改成 false,就不能修改成true了。 | | [[Get]] | SpecOp(propertyName) → any | 返回命名属性的值 | | [[GetOwnProperty]] | SpecOp(propertyName) → Undefined 或 PropertyDescriptor | 返回此对象的自身命名属性的属性描述,如果不存在返回 undefined | | [[GetProperty]] | SpecOp(propertyName) → Undefined 或 PropertyDescriptor | 返回此对象的完全填入的自身命名属性的属性描述,如果不存在返回 undefined | | [[Put]] | SpecOp(propertyNameanyBoolean) | 设置命名属性的值,将指定命名属性设为第二个参数的值。flag 控制失败处理。 | | [[CanPut]] | SpecOp(propertyName) → Boolean | 返回一个 Boolean 值,说明是否可以在 propertyName 上执行 [[Put]]操作。| | [[HasProperty]] | SpecOp(propertyName) → Boolean | 返回一个 Boolean 值,说明对象是否含有给定名称的属性。| | [[Delete]] | SpecOp(propertyNameBoolean) → Boolean | 从对象上删除指定的自身命名属性。flag 控制失败处理。 | | [[DefaultValue]] | SpecOp(Hint) → primitive| Hint 是一个字符串。返回对象的默认值 (某些对象的该方法可以简单抛出 TypeError)| | [[DefineOwnProperty]] | SpecOp(propertyNamePropertyDescriptorBoolean) → Boolean | 创建或修改自身命名属性为属性描述里描述的状态。flag 控制失败处理。

  • 每种内置对象都定义了 [[Class]] 内部属性的值。宿主对象的 [[Class]] 内部属性的值可以是除了  "Arguments""Array""Boolean""Date""Error""Function""JSON""Math""Number""Object""RegExp""String""BigInt""SymBol"  的任何字符串。[[Class]]内部属性的值用于内部区分对象的种类。

只能通过 Object.prototype.toString() 访问[[Class]]内部属性的值。这也解释了为什么在判断数据类型时可以用Object.prototype.toString.call()准确判断。

3.2 某些对象的内部属性

内部属性值的类型范围说明
[[PrimitiveValue]]原始类型与此对象的内部状态信息关联。对于标准内置对象只能用 BooleanDateNumberStringBigIntSymbol 对象实现  [[PrimitiveValue]]
[[Construct]]SpecOp(a list of any) → Object通过 new 运算符调,创建对象。SpecOp的参数是通过 new 运算符传的参数。实现了这个内部方法的对象叫做构造器
[[Call]]SpecOp(any, a list of any) → any 或 Reference运行与此对象关联的代码。通过函数调用表达式调用。SpecOp 的参数是一个 this 对象和函数调用表达式传来的参数组成的列表。实现了这个内部方法的对象是可调用的。只有作为宿主对象的可调用对象才可能返回引用值。
[[HasInstance]]SpecOp (any) → Boolean返回一个表示参数对象是否可能是由本对象构建的布尔值。在标准内置 ECMAScript 对象中只有 Function 对象实现  [[HasInstance]]
[[Scope]]词法环境一个定义了函数对象执行的环境的词法环境。在标准内置 ECMAScript 对象中只有 Function 对象实现  [[Scope]]
[[FormalParameters]]Strings 的列表一个包含 Function 的 FormalParameterList 的标识符字符串的可能是空的列表。在标准内置 ECMAScript 对象中只有 Function 对象实现  [[FormalParameterList]]
[[Code]]ECMAScript 代码函数的 ECMAScript 代码。在标准内置 ECMAScript 对象中只有 Function 对象实现  [[Code]]
[[TargetFunction]]Object使用标准内置的 Function.prototype.bind 方法创建的函数对象的目标函数。只有使用 Function.prototype.bind 创建的 ECMAScript 对象才有  [[TargetFunction]]  内部属性。
[[BoundThis]]any使用标准内置的 Function.prototype.bind 方法创建的函数对象的预绑定的 this 值。只有使用 Function.prototype.bind 创建的 ECMAScript 对象才有  [[BoundThis]]  内部属性。
[[BoundArguments]]list of any使用标准内置的 Function.prototype.bind  方法创建的函数对象的预绑定的参数值。只有使用 Function.prototype.bind 创建的 ECMAScript 对象才有  [[BoundArguments]]  内部属性。
[[Match]]SpecOp(Stringindex) → MatchResult测试正则匹配并返回一个 MatchResult 值。在标准内置 ECMAScript 对象中只有 RegExp 对象实现  [[Match]]
[[ParameterMap]]Object提供参数对象的属性和函数关联的形式参数之间的映射。只有参数对象(arguments objects) 才有  [[ParameterMap]]  内部属性。

四、引用规范类型(The Reference Specification Type)

通常,我们将对象赋值给一个变量的时候,该变量的得到的实际时对象的引用。那么什么是引用类型呢?

一个引用是个已解析的命名绑定。它由三部分组成:值、引用名称和一个严格引用标志(布尔值)。 值是 undefinedObjectBooleanStringNumber、环境记录项 中的任意一个。值是 undefined 表示此引用不可以解析为一个绑定。引用名称是一个字符串。 —— ES5

4.1 相关操作

  • GetBase(V) :返回引用值 V 的值部分。
  • GetReferencedName(V) :返回引用值 V 的引用名称部分。
  • IsStrictReference(V) :返回引用值 V 的严格引用部分。
  • HasPrimitiveBase(V) :如果值是 BooleanStringNumber,那么返回 true
  • IsPropertyReference(V) :如果值是个 Object 或 HasPrimitiveBase(V) 是 true,那么返回 true;否则返回 false
  • IsUnresolvableReference(V) :如果值是 undefined 那么返回 true,否则返回 false

4.2 操作引用

(1)GetValue(V)

  1. 如果 Type(V) 不是引用,返回V。反之,令 base 为调用 GetBase(V) 的返回值
  2. 如果 IsUnresolvableReference(V),即基值为undefined,抛出 ReferenceError
  3. 如果 IsPropertyReference(V)
    1. 如果 HasPrimitiveBase(V) 是 false,那么令 get 为 base 的 [[Get]] 内部方法,否则令get 为下面定义的方法。
    2. 将 base 作为 this 值,传递 GetReferencedName(V) 为参数,调用 get 内部方法,返回结果。
  4. 否则,base 必须是一个环境记录项
  5. 传递 GetReferencedName(V) 和 IsStrictReference(V) 为参数调用 base 的 GetBindingValue具体方法,返回结果。 GetValue 中的 V 是原始基值的属性引用时使用下面的  [[Get]]  内部方法。它用 base 作为他的 this 值,其中属性 P 是它的参数。采用以下步骤:
  6. 令 O 为 ·ToObject(base)`。
  7. 令 desc 为用属性名 P 调用 O 的 [[GetProperty]]内部方法的返回值。
  8. 如果 desc 是 undefined,返回 undefined
  9. 如果 IsDataDescriptor(desc) 是 true,返回 desc。[[Value]]。
  10. 否则 IsAccessorDescriptor(desc) 必须是 true,令 getter 为 desc。[[Get]]。
  11. 如果 getter 是 undefined,返回 undefined
  12. 提供 base 作为 this 值,无参数形式调用 getter 的 [[Call]]内部方法,返回结果。

(2)PutValue(V, W)

  1. 如果 Type(V) 不是引用,抛出一个 ReferenceError。
  2. base 为调用 GetBase(V) 的返回值
  3. 如果 IsUnresolvableReference(V)
    1. 如果 IsStrictReference(V)true,那么抛出一个 ReferenceError
      1. 用 GetReferencedName(V)Wfalse 作为参数调用全局对象的 [[Put]] 内部方法。
  4. 否则如果 IsPropertyReference(V),那么
    1. 如果 HasPrimitiveBase(V) 是 false,那么令 put 为 base 的 [[Put]] 内部方法,否则令 put 为下面定义的特殊的  [[Put]]  内部方法。
    2. 用 base 作为 this 值,用 GetReferencedName(V)WIsStrictReference(V) 作为参数调用 put 内部方法。
  5. 否则 base 必定是环境数据作为 base 的引用。所以,
    1. 用 GetReferencedName(V)WIsStrictReference(V) 作为参数调用 base 的 SetMutableBinding 具体方法。
  6. 返回。 PutValue 中的 V 是原始基值的属性引用时使用下面的  [[Put]]  内部方法。用 base 作为 this 值,用属性 P,值 W,布尔标志 Throw 作为参数调用它。采用以下步骤:
  7. 令 O 为 ToObject(base)
  8. 如果用 P 作为参数调用 O 的 [[CanPut]] 内部方法的结果是 false,那么
    1. 如果 Throw 是 true,那么抛出一个 TypeError 异常。
    2. 否则返回。
  9. 令 ownDesc 为用 P 作为参数调用 O 的 [[GetOwnProperty]] 内部方法的结果。
  10. 如果 IsDataDescriptor(ownDesc) 是 true,那么
    1. 如果 Throw 是 true,那么抛出一个 TypeError 异常。
    2. 否则返回。
  11. 令 desc 为用 P 作为参数调用 O 的 [[GetProperty]]内部方法的结果。这可能是一个自身或继承的访问器属性描述或是一个继承的数据属性描述。
  12. 如果 IsAccessorDescriptor(desc) 是 true,那么
    1. 令 setter 为 desc。[[Set]],他不会是 undefined
    2. 用 base 作为 this 值,用只由 W 组成的列表作为参数调用 setter 的 [[Call]] 内部方法。
  13. 否则,这是要在临时对象 O 上创建自身属性的请求。
    1. 如果 Throw 是 true,抛出一个 TypeError 异常。
  14. 返回。

JavaScript 中,对象是个非常重要的概念。以上也是个人从各大官方文档中搬运来的知识,存在不全面的地方,可以参考以下

主要参考:
[1] ES5 类型
[2] 《JavaScript 高级程序设计(第四版)》
[3] MDN Object