3.1.3 对象成员

JS中的对象是属性包,属性即所谓的对象成员,当我们分别讨论对象实例和类时,属性和类成员是两个概念,由于类本身也是函数类型的对象,所以当我们统一用”对象“这个概念来讲述的时候,其成员仍被称为属性

对象成员有三种性质,称为”可读写“、”可枚举“和”可重置”

对象成员可以是自有的也可以是继承的,所谓继承的就是指对象的父类或者祖先类原型上具有该成员,子类对象可以用相同的名字重新声明该成员,称为“覆盖”或者“重写”

3.1.3.1 成员的列举以及可列举性

对象成员是否可列举被称为成员的可列举性,当某个成员不存在或者不可列举时,对该对象成员调用propertyIsEnumerable()方法将返回false,比较常见的情况是,JS对象的某些特定成员是被设置为隐藏的,因此不能枚举,例如:

var obj = new Object()

// 不存在a所以返回false
console.log(obj.propertyIsEnumerable(a)) // false

// 数组的length也是隐藏的
console.log([].propertyIsEnumerable(length)) // false

这种情况下,可以用in运算来检测到该成员,但是不能用for ... in来列举该成员

一直以来,对propertyIsEnumerable()的设计存在歧义,因为它默认不检查对象的原型链的,但是更合理的方法是让它页检测原型链,因为继承来的成员实际上是可以被for ... in枚举出来的

ES5开始,提供了一些其他的操作对象成员的方法

成员 语法 含义
仅显式成员 for … in 可枚举的成员名(含原型链)
仅显式成员 Object.keys()
Object.values()
Object.entries()
可枚举的、非符号的自有属性名
包含隐式成员 Object.getOwnPropertyNames() 全部的、非符号的自有属性名
包含隐式成员 Object.getOwnPropertySymbols() 全部的、符号键名的自有属性名

Object.values()、Object.entries()可以通过Object.keys().map(key => obj[key])、Object.keys().map(key => [key, obj[key]] )实现

对象可以使用for ... in,通常如果对象的成员插入不是有序的,它的列举也就不是有序的,多数情况下着并不要紧

不要对数组使用for... in遍历使用最基础的循环或者forEachfor...in并不能够保证返回的是按一定顺序的索引,但是它会返回所有可枚举属性,包括继承属性。

或许可以使用for...of遍历,但是不能得到下标,而且该语句将数组视为“集合对象”(Collection objects)并列举其中的“集合成员”,而不是列举“对象成员”。从根本上来说,这些集合公布了它们提供的默认内置的迭代器,而for...of指示调用这些迭代器而已

3.1.3.2 对象及其成员的检查

JS中使用in来检查对象是否具有某个成员(包括显式或者隐式的,也包括符号作为键名的属性)

这种也用来做环境兼容性

if('XMLHttpRequest' in window) {
  // ...
}

但是这并不可靠,因为更早版本的浏览器可能没有in运算

所以建议

if(window.XMLHttpRequest) {
  // ...
}

在JS中,取一个“不存在的属性”并不会导致异常,而是返回undefined,而undefined刚好能被if语句理解为false,效果相当于用in运算

旧版本的浏览器也推荐如下的方法做检测

if(typeof window.XMLHttpRequest !== 'undefined') {
  // ...
}

但是一个属性如果被赋值undefined那也是不好用的,,,在WEB浏览器中,DOM约定,如果一个属性没有初值那么应该将它置null

此外,可以使用instanceof运算符来检测“对象是不是类的一个实例”

console.log(obj instanceof Object)

类可以是一般构造器函数或者类声明,它还会检测类的继承关系,因此一个子类的实例,在进行instanceof运算时,仍会得到true

3.1.3.3 值的存取

多数情况下我们会用对象成员的名字来存取值,使用成员存取运算符.或者[],所不同的是前者右边的操作数必须是一个标识符,后者方括号中的操作数可以是变量、标识符、符号、字面量或者表达式

赋值模板也可以用于将对象的属性值督导变量中,并且它通常用作属性的成批读取,或者按照模式(模板)的规则读取

var obj = {
  'abcd.ef': 1234,
  '1': 4567,
  '.': 7890,
  more: {
    a: 100,
    b: 200,
  }
} 
var {'abcd.ef': x, '.': y, more: {a: z}}

这种声明赋值模板的方法也可以用在函数参数上,这样可以避免在函数内频繁的读取对象成员

无论声明成员还是读取它的值,都可以在[]运算符中使用可计算的成员名,即使用表达式来作为成员名

3.1.3.4 成员的删除

可以使用delete运算符来删除一个对象的指定属性,甚至可以删除全局对象Global的某些成员例如delete isNaN;

不过该运算符不能用于删除

  • 用var/let/const 声明的变量和常量
  • 直接继承自原型的成员

关于delete不能删除“直接继承自原型的成员”有一点例外:如果修改了这个成员的值,仍然可以删除它(并使它恢复到原型的值)例如:

function Myobj() {
  this.name = "instance's name"
}
Myobj.prototype.name = "prototype's name"
var obj = new Myobj()
obj.name // "instance's name"
delete obj.name
obj.name // "prototype's name"

// 如果真的想彻底删除name
delete obj.constructor.prototype.name;
obj.name // undefined
// 但是于此同时,所有实例都将失去这一属性

delete仅在删除一个不能删除的成员的时候才返回false,其余的时候,例如删除不存在的成员,或者删除继承自父类/原型的成员,即使删除不成功也会返回true

delete操作删除宿主对象的成员时,也可能存在问题,删除后仍能用in运算返回true,但是取值会报错

3.1.3.5 方法的调用

JS中可以用两种方式来对类的方法进行调用

// 基于运算符.
obj.method();
// 基于运算符[]
obj.['method']()

3.1.4 使用对象自身

有一些操作对象是仅针对对象自身而非对象成员的,典型的例子就是instanceoftypeof,还有一些运算或者语句是直接作用于对象自身的

3.1.4.1 与基础类型数据之间的运算

对象可以直接与其他基础类型的数据进行运算

// 尝试进行数值运算
console.log(new Object * 1)

虽然返回的是NaN但是这个操作本身是有意义的,表明Object的实例,包括任何对象实例,都可以被先转换成基础类型,再与值”1“进行数值运算,转换过程与Object.prototype.valueOf()有关

3.1.4.2 默认对象的绑定

在JS中,脚本引擎可以很容易的区分用户代码是在访问值、对象还是对象的成员

with是使用对象自身的另一种方法,他的作用在于存取对象的成员

3.1.5 符号

符号作为数据类型,使用Symbol()函数来创建新值
var aSymbol = Symbol()
这种情况下,symbol()被称为”符号类型“而不是”符号类“,而值aSymbol直接被称为符号,在语义上是指”aSymbol是Symbol()类型的一个值”

符号可以作为对象的成员名使用,且这种对象成员仍然被称为属性,也具有一般属性具有的全部性质,也可以继承或者基于原型访问等。唯一不同的是,它通常需要特殊的方式猜能列举、存取和使用

3.1.5.1 列举符号属性

当符号用作对象的属性名时,我们称该属性为“符号属性”,唯一能有效列举符号属性的方法是Object.getOwnPropertySymbols()

因为正常情况下没办法枚举,所以也没必要去隐藏它们,例如设置enumerablefalse,无论如何Object.getOwnPropertySymbols()都可以获取一个对象全部的、自有的符号属性列表

默认情况下对象没有自有的符号属性,所以Object.getOwnPropertySymbols(new Object)会返回一个空数组

3.1.5.2 改变对象内部行为

由于JS对内部行为的约定,所有对象的行为都受到一些“与内部行为相关”的符号属性的影响。这些符号定义在Symbol类型中,更详细的去参考”3.6.1.2可能被符号影响的行为“

3.1.5.3 全局符号表

使用字符串作为属性名可以随处访问,例如:

// 模块A
var propName = 'myProp'
export var obj = {[propName]: 100};
// 模块B
import {obj} from 'module_a';
console.log(obj['propName']);

然而使用符号作为属性名时,例如

var symbolPropName = Symbol()
export var obj = {[symbolPropName]: 100}

就必须导出symbolPropName来访问对应的属性

但是JS提供了一个新的手段

Symbol.for(keyName)

JS能确保即使用户代码在多个地方调用了Symbol.for(keyName)也只有第一次会返回(并创建)符号,而此后的调用都将直接返回该符号,这种内建的机制保证了这些符号全局唯一,也意味着Symbol在全局建立了一个”符号名-符号“的对照表。因此,这个符号表也可以反过来查找它的KeyName,例如:console.log(Symbol.keyFor(s));显式符号s注册的名字是:’symbolPropName’

3.2 JavaScript的原型继承

一个对象系统的继承特性有三种实现方案,包括基于类(class-based)、基于原型(prototype-based)和基于元类(metaclass-based)。这三种对象模型各具特色,也各有应用。在这其中,JS使用了原型继承来实现对象系统,并基于原型继承实现了具备类继承特征的对象系统。

3.2.1 空(null)与空白对象(empty)

JS中,空白对象是整个原型继承体系的根基,但是我们从空(null)对象说起

JS中,空(null),是作为一个保留字存在的,代表一个”属于对象类型的空值“。因为它属于对象类型,所以也可以用for ... in去列举它,又因为它是空值,所以没有任何方法和属性,因而枚举不到内容,另一方面,多数对象相关的方法都将null作为特殊值处理,例如不能使用Object.keys()来列举值

null也可以参与运算,例如+-运算,但是由于它不是创建自Object()构造器及其子类,因此instanceof运算会返回false

null不是空白对象,空白对象(也称为裸对象),是一个标准的,通过Object()构造的对象实例,例如:

obj = new Object()
obj1 = {}

由于对象的字面量声明也会隐式的调用Object()来构造实例,所以两者都是空白对象

空白对象具有对象的一切特性,因此可以使用对象的内置属性和方法,而且instanceof运算符也会返回true

默认情况下空白对象只具有原生对象的一些内置成员,for...in语句并不列举它们

3.2.1.1 空白对象是所有对象的基础

我们用下边的代码来考察以下最基本的Object()构造器:

// 列举原型对象成员并计数
var num = 0
for (var n in Object.prototype) {
  num++;
}
// 显示计数0
console.log(num)

说明Object()构造器的原型就是一个空白对象,这就意味着,下边的两行代码,无非就是从Object.prototype上复制出一个“对象”的映像来——它们也是空白对象

obj1  = new Object();
obj2 = {};

因此,对象的构建过程可以被简单的理解为“对原型的复制”

原型的含义是指,如果构造器有一个原型对象,则由该构造器创建的实例都必然复制自该原型对象。换言之,所谓“原型”就是由构造器用于生成实例的模板。而这样的“复制”就存在多种可能性,由此引出动态绑定和静态绑等问题。

假如不考虑“复制”如何被实现,至少我们可以关注到,由于实例复制自原型,所以它必然有(或者说继承了)后者——原型对象——的所有属性、方法和其他性质

这也就是所谓继承性的实现



语言   js      js 基础 绿宝书

本博客所有文章除特别声明外,均采用 CC BY-SA 3.0协议 。转载请注明出处!