WebGL Tutorial
and more

Object

撰写时间:2024-02-11

修订时间:2024-12-01

概述

Object所定义的方法,往往用在prototype链路中,因此只有在涉及到prototype链路的环境中,才能深切体会其各个方法的确切含义。

本章第一部分先从具体用例出发,根据具体需求来调用、讲解其相应方法;第二部分再以参考的形式来列出其全部方法。

遍历对象属性

对象属性分类

JavaScript通过prototype链来访问、获取对象属性。这种方式,类似于其他面向对象编程语言中的类的继承的关系。例如:

let obj = {foo: 'bar'};

obj只有一个foo属性。但我们可以调用:

console.log(obj.toString);

将显示:

function toString() { [native code] }

toString方法是由根对象Object通过其prototype属性来自动传递给obj的,因此,每个对象都可以调用此方法。

对象的属性,可分为两种类型:是否自身的属性(own property),以及是否可列举的属性(enumerable)。

上面的例子较好的说明了foo属性是obj自身的属性;而toString不是obj自身的属性,而是prototype链中存在的属性。

可以调用objhasOwnProperty方法来了解特定属性是否其自身属性:

console.log(obj.hasOwnProperty('foo')); // true console.log(obj.hasOwnProperty('toString')); // false

一个对象的prototype链可能有多级层级,如果只是简单地一概列举prototype链中的所有属性,则会产生较长的列表。因此,如果将某些属性设为不可列举的属性,则当我们调用相应的列举属性的方法时,这些不可列举的属性将不会出现在结果中。而Objectprototype所提供的各个属性,属于不可列举的属性。这是对是否可以列举进行分类的意义所在。可通过调用propertyIsEnumerable方法查看特定属性是否可以列举:

console.log(obj.propertyIsEnumerable('foo')); // true console.log(obj.propertyIsEnumerable('toString')); // false

尽管从方法名称上看不出来,但propertyIsEnumerable方法只有在特定属性同时满足2个条件时,才会返回true:1. 为自身属性;2. 为可列举属性。

有不同的遍历方法,其中一些方法只能遍历自身属性,一些方法只能遍历可列举属性。下面会分类详细说明这些列举方法。

而不管属性是否自身属性,或是否可列举属性,均可通过.操作符或[]操作符来直接访问这些属性。

console.log(obj.foo); console.log(obj.toString); console.log(obj['foo']); console.log(obj['toString']);

遍历自身属性

Object.keys方法

设有以下代码:

function MyObj() { this.name = 'Mike'; this.getName = function() { return this.name; }; } MyObj.prototype.greeting = function() { console.log('Hi'); }; let obj = new MyObj();

prototype链路示意图如下:

  • obj
    • name
    • getName
    • [[Prototype]]: MyObj
      • greeting

即,namegetName这两个属性属于obj的自身属性,greeting属于其构造器MyObjprototype中的属性。

则可通过调用Object的静态方法keys,来返回对象自身的、可列举的属性。

let keys = Object.keys(obj); keys.forEach(key => { console.log(key); }); /* * name * getName */

greeting方法虽然也属于可列举属性,但它不属于obj的自身属性,因此不会出现在结果集合中。

Object的静态方法entries的筛选标准与keys一样,但返回结果中同时包含了属性名以及属性值。

let obj = new MyObj(); let entries = Object.entries(obj); for (let [key, value] of entries) { console.log(key, value); } /* * name - "Mike" * getName - function() { * return this.name; * } */

Object.getOwnPropertyNames方法

Object的静态方法getOwnPropertyNames返回对象的自身的所有属性,包括不可列举的属性。

仍取上面的例子,代码:

console.log(Object.getOwnPropertyNames(obj));

返回:

// ["name", "getName"] (2)

虽然这个例子说明了getOwnPropertyNames确实返回了自身属性,但不足以说明其对不可列举属性的支持情况。

我们取JavScript引擎自带的不可列举属性为例。Object.prototype属性值为一个对象,该对象有众多供子对象继承的属性。

let obj = Object.prototype; console.log(obj);

在Safari中,上面的代码将显示:

{}

一个什么都没有的空对象。

但若使用:

console.dir(obj);

则终端显示为:

  • object
    • __defineGettor__: function()
    • __defineSettor__: function()
    • __lookupGetter__: function()
    • __lookupSetter__: function()
    • constructor: function()
    • hasOwnProperty: function()
    • isPrototypeOf: function()
    • propertyIsEnumerable: function()
    • toLocaleString: function()
    • toString: function()
    • valueOf: function()

没错,这些都是每个对象都会自动获得继承的方法,包括常用的toString方法。

但这些方法都是不可列举的。使用代码:

let desc = Object.getOwnPropertyDescriptor(obj, 'toString'); console.log(desc);

终端显示:

  • object
    • configurable: true
    • enumerable: false
    • value: function()
    • writable: true

上面的enumerable属性值显示,toString方法是不可列举的。

我们也可以使用getOwnPropertyDescriptors一下子全部列出特定对象所有属性的情况:

let descs = Object.getOwnPropertyDescriptors(obj); console.log(descs);

Safari的理解是,既然这些属性都是不可列举的,因此对它们调用console.log就返回一个空对象;只有对它们调用console.dir,才会列出这些不可列举的属性。

而在Chrome中,对不可列举对象调用console.logconsole.dir的效果一样,都会列出这些不可列举的属性。并且,Chrome所列出的属性中还多出以下属性:

  • object
    • ...
    • __proto__: null
    • get __proto__: __proto__()
    • set __proto__: __proto__()

__proto__属性引用了prototype链路中父级的prototype对象,这里其值为null,说明prototype链路到此终止。

__proto__属性是一个gettersetter,对其调用getOwnPropertyDescriptors

let obj = Object.prototype; console.log(Object.getOwnPropertyDescriptor(obj, '__proto__'));

则显示:

  • object
    • configurable: true
    • enumerable: false
    • get: function()
    • set: function()

即,属于访问器的属性不再有value属性,而是多了get属性及set属性,类型均为函数。因此在遍历属性值时需注意此点。

因为是不可列举的对象,如果我们使用for...in语句来遍历:

let obj = Object.prototype; for (let propName in obj) { console.log(propName); }

则不管是Safari,还是Chrome,终端都不会显示任何内容。(for...in语句详见下节)

现在,Object.getOwnPropertyNames就派上用场了:

let obj = Object.prototype; console.log(Object.getOwnPropertyNames(obj));

终端显示:

  • Array (12)
    • " 0 toString"
    • " 1 toLocaleString"
    • " 2 valueOf"
    • " 3 hasOwnProperty"
    • " 4 propertyIsEnumerable"
    • " 5 isPrototypeOf"
    • " 6 __defineGettor__"
    • " 7 __defineSettor__"
    • " 8 __lookupGetter__"
    • " 9 __lookupSetter__"
    • "10 __proto__"
    • "11 constructor"

此时,在Safari中,__proto__属性也出现了。

精准控制遍历流程

由于Object.getOwnPropertyNames返回自身所有属性名,我们往往需要精准控制整个流程。如,是否需要将不可列举的属性加入到结果集中,判断属性值类型,区别对待访问器属性等等。

function MyObj(name) { this.name = name; this.getName = function() { return this.name; }; Object.defineProperty(this, "_age", { value: 0, enumerable: false, configurable: false, writable: true }); Object.defineProperty(this, "age", { get() { return this._age; }, set(value) { this._age = value; }, enumerable: true, configurable: true }); } MyObj.prototype.greeting = function() { console.log('Hi'); }; let obj = new MyObj('Mike');

上面,实例变量_age为不可列举的变量,我们为它定义了gettersetter访问器。

这样定义的好处是,使用for...in语句将自动屏蔽已另行定义访问器的变量_age:

for (let propName in obj) { console.log(propName); } /* name getName age greeting */

下面代码在遍历所有属性的过程中,对不可列举的属性、属性值为函数、属性值为访问器等情况均予以了区别对待。

let propNames = Object.getOwnPropertyNames(obj); for (let propName of propNames) { const propertyDescriptor = Object.getOwnPropertyDescriptor(obj, propName); if (!propertyDescriptor.enumerable) { console.log(`${propName}: non-enumerable`); continue; } if (propertyDescriptor.hasOwnProperty('value')) { if (typeof propertyDescriptor.value === 'function') { console.log(`${propName}: ${typeof propertyDescriptor.value}`); } else { console.log(`${propName}: ${propertyDescriptor.value}`); } } else { let label = propertyDescriptor.get ? 'get(), ' : ''; label += propertyDescriptor.set ? 'set()' : ''; console.log(`${propName}: ${label}`); } }

终端显示:

name: Mike getName: function _age: non-enumerable age: get(), set()

通过调用Object.getOwnPropertyDescriptor方法,我们取得了每个属性的细节。

对于不可列举的属性,在输出时,在属性名后面加上non-enumerable的标注。

如果有value属性,对于类型为非函数的属性,可直接打印其属性值;对于类型为函数的属性,则不打印其值,而是直接标注function

注意,判断是否存在value属性时,不要使用if (!propertyDescriptor.value),否则,当其值为0false""时,将出现误判。具体详见Falsy及Truthy。因此上面的代码调用了hasOwnProperty方法。

而对于访问器属性,如上所述,它没有value属性,而只有get属性及set属性。因此,也直接标注为get(), set()

遍历prototype链中所有属性

for...in语句用于遍历对象的所有可遍历的属性名称,包括prototype链中的可遍历的属性名称。

function MyObj() { this.name = 'Mike'; this.getName = function() { return this.name; }; } MyObj.prototype.greeting = function() { console.log('Hi'); }; let obj = new MyObj(); for (let propName in obj) { console.log(propName); } /* name getName greeting */

所列出的属性名中,不仅有obj自有的namegetName属性名,还包括其prototype链中的greeting函数名。

  • obj
    • name
    • getName
    • [[Prototype]]: MyObj
      • greeting

ObjectprototypetoString等众多方法,虽也处在prototype链中,但由于它们属于不可列举的属性,因此不会被遍历。

console.log(Object.prototype.propertyIsEnumerable('toString')); // false

如果我们希望在for...in语句中只提取对象自身属性,可通过hasOwnProperty方法来筛选。

for (let propName in obj) { if (obj.hasOwnProperty(propName)) { console.log(propName); } } /* * name * getName */

objhasOwnProperty方法,也可通过调用Object的静态方法来实现:

console.log(Object.hasOwn(obj, 'getName')); // true

方法

entries

将一个对象的所有自有属性都打包为一个二维数组。

最里层的每一个数组由每个属性的属性名称与属性值构成,最外层的数组由最里层的数组组成。

语法

[string, any][]entries
  • Objectobj
obj
包含属性及方法的对象。

例子

let obj = { name: 'sarkuya', age: 25, sayHello: function() { console.log('Hello'); } }; let entries = Object.entries(obj); console.log(entries);

显示:

[ ["name", "sarkuya"], ["age", 25], ["sayHello", function] ]

而对于嵌套的属性值,只提取第一级的属性名称,后续的属性名值均作为属性值:

let obj = { name: 'sarkuya', age: 25, phones: [ 123, 456 ], hobbies: { sport: 'basketball', reading: 'History' }, sayHello: function() { console.log('Hello'); } }; let entries = Object.entries(obj); console.log(entries);

显示:

[ ["name", "sarkuya"], ["age", 25], ["phones", [123, 456]], ["hobbies", {sport: "basketball", reading: "History"}], ["sayHello", function] ]

因为数组也是Object的实例,因此entries方法也可用于数组:

let arr = [7, 8, 9]; console.log(Object.entries(arr));

显示:

[ ["0", 7], ["1", 8], ["2", 9], ]

数值为数组索引值的字符串将成为属性名称。

entries方法所返回的值,最外层是一个数组,因此可以使用for ... of语句来遍历;最里层是一个只有2个元素、且元素数量固定的数组,因此可使用数组解包语句[key, value]来直接解包。因此可以方便地编写如下代码:

let user = { name: 'Mike', age: 25 }; let entries = Object.entries(user); for (let [key, value] of entries) { console.log(key, value); } /* * name - "Mike" * age - 25 */

参见

  1. keys
  2. values

keys

将一个对象的所有自有属性的属性名称都打包为一个数组。

语法

[string]keys
  • Objectobj
obj
包含属性及方法的对象。

例子

let obj = { name: 'sarkuya', age: 25, phones: [ 123, 456 ], hobbies: { sport: 'basketball', reading: 'History' }, sayHello: function() { console.log('Hello'); } }; let keys = Object.keys(obj); console.log(keys);

显示:

["name", "age", "hobbies", "sayHello"]

应用

获取属性数量

当我们想知道一个对象有多少属性名称,不用keys方法时:

let propNums = 0; for (let propName in obj) { propNums++; } console.log(propNums); // 5

使用keys方法:

console.log(Object.keys(obj).length); // 5

参见

  1. entries
  2. values

values

将一个对象的所有自有属性的属性值都打包为一个数组。

语法

[any]values
  • Objectobj
obj
包含属性及方法的对象。

例子

let obj = { name: 'sarkuya', age: 25, phones: [ 123, 456 ], hobbies: { sport: 'basketball', reading: 'History' }, sayHello: function() { console.log('Hello'); } }; let values = Object.values(obj); console.log(values);

显示:

["sarkuya", 25, [123, 456], {sport: basketball, reading: "History"}, function]

参见

  1. entries
  2. keys

参考资源

  1. Enumerability and ownership of properties (MDN)