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
遍历使用最基础的循环或者forEach
,for...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 使用对象自身
有一些操作对象是仅针对对象自身而非对象成员的,典型的例子就是instanceof
和typeof
,还有一些运算或者语句是直接作用于对象自身的
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()
因为正常情况下没办法枚举,所以也没必要去隐藏它们,例如设置enumerable
为false
,无论如何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 = {};
因此,对象的构建过程可以被简单的理解为“对原型的复制”
原型的含义是指,如果构造器有一个原型对象,则由该构造器创建的实例都必然复制自该原型对象。换言之,所谓“原型”就是由构造器用于生成实例的模板。而这样的“复制”就存在多种可能性,由此引出动态绑定和静态绑等问题。
假如不考虑“复制”如何被实现,至少我们可以关注到,由于实例复制自原型,所以它必然有(或者说继承了)后者——原型对象——的所有属性、方法和其他性质
这也就是所谓继承性的实现