JavaScript对象
# JavaScript对象
[TOC]
# 一、对象的特征
- 对象具有唯一标识性:即使完全相同的两个对象,也并非同一个对象。
- 对象有状态:对象具有状态,同一对象可能处于不同状态之下。
- 对象具有行为:即对象的状态,可能因为它的行为产生变迁。
- 关于对象的第二个和第三个特征“状态和行为”,不同语言会使用不同的术语来抽象描述它们,比如C++中称它们为“成员变量”和“成员函数”,Java中则称它们为“属性”和“方法”。
- 在 JavaScript中,将状态和行为统一抽象为“属性”,考虑到 JavaScript 中将函数设计成一种特殊对象,所以 JavaScript中的行为和状态都能用属性来抽象。
- 在实现了对象基本特征的基础上, JavaScript中对象独有的特色是:对象具有高度的动态性,这是因为JavaScript赋予了使用者在运行时为对象添改状态和行为的能力。
- 比如,JavaScript 允许运行时向对象添加属性,这就跟绝大多数基于类的、静态的对象设计完全不同。
# 二、对象的两类属性
为了提高抽象能力,JavaScript的属性被设计成比别的语言更加复杂的形式,它提供了数据属性和访问器属性(getter/setter)两类。
对JavaScript来说,属性并非只是简单的名称和值,JavaScript用一组特征(attribute)来描述属性(property)。
# 数据属性
四个特征
- value:就是属性的值。
- writable:决定属性能否被赋值。(默认true)
- enumerable:决定for in能否枚举该属性。(默认true)
- configurable:决定该属性能否被删除或者改变特征值。(默认true)
使用内置函数
Object.getOwnPropertyDescripter
来查看var o = { a: 1 }; o.b = 2; //a和b皆为数据属性 Object.getOwnPropertyDescriptor(o,"a") // {value: 1, writable: true, enumerable: true, configurable: true} Object.getOwnPropertyDescriptor(o,"b") // {value: 2, writable: true, enumerable: true, configurable: true}
1
2
3
4
5**
Object.defineProperty
**改变属性的特征,或者定义访问器属性
var o = { a: 1 };
Object.defineProperty(o, "b", {value: 2, writable: false, enumerable: false, configurable: true});
//a和b都是数据属性,但特征值变化了
Object.getOwnPropertyDescriptor(o,"a"); // {value: 1, writable: true, enumerable: true, configurable: true}
Object.getOwnPropertyDescriptor(o,"b"); // {value: 2, writable: false, enumerable: false, configurable: true}
o.b = 3;
console.log(o.b); // 2
2
3
4
5
6
7
# 访问器属性
四个特征
存取描述符
getter:函数或undefined,在取属性值时被调用。
setter:函数或undefined,在设置属性值时被调用。
与数据描述符(value、writable)不能共存。
enumerable:决定for in能否枚举该属性。
configurable:决定该属性能否被删除或者改变特征值。
访问器属性使得属性在读和写时执行代码,它允许使用者在写和读属性时,得到完全不同的值,它可以视为一种函数的语法糖。
在创建对象时,也可以使用 get 和 set 关键字来创建访问器属性,代码如下所示:
var o = { get a() { return 1 } }; console.log(o.a); // 1
1
2访问器属性跟数据属性不同,每次访问属性都会执行getter或者setter函数。这里我们的getter函数返回了1,所以o.a每次都得到1。
实际上JavaScript 对象的运行时是一个“属性的集合”,属性以字符串或者Symbol为key,以数据属性特征值或者访问器属性特征值为value。
# 三、基于原型的面向对象
# JavaScript的原型
直接地访问操纵原型
Object.create
根据指定的原型创建新对象,原型可以是null;var cat = { say(){ console.log("meow~"); }, jump(){ console.log("jump"); } } var tiger = Object.create(cat, { say:{ writable:true, configurable:true, enumerable:true, value:function(){ console.log("roar!"); } } }) var anotherCat = Object.create(cat); anotherCat.say();//meow~ var anotherTiger = Object.create(tiger); anotherTiger.say();//roar!
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28可以用
Object.create
来创建另外的猫和虎对象,通过“原始猫对象”和“原始虎对象”来控制所有猫和虎的行为。Object.getPrototypeOf
获得一个对象的原型;//给定对象的原型。如果没有继承属性,则返回 null 。 const prototype1 = {}; const object1 = Object.create(prototype1); console.log(Object.getPrototypeOf(object1) === prototype1); // expected output: true
1
2
3
4
5
6Object.setPrototypeOf
设置一个对象的原型。警告: 由于现代 JavaScript 引擎优化属性访问所带来的特性的关系,更改对象的
[[Prototype]]
在***各个***浏览器和 JavaScript 引擎上都是一个很慢的操作。其在更改继承的性能上的影响是微妙而又广泛的,这不仅仅限于obj.__proto__ = ...
语句上的时间花费,而且可能会延伸到***任何**代码,那些可以访问任何***[[Prototype]]
已被更改的对象的代码。如果你关心性能,你应该避免设置一个对象的[[Prototype]]
。相反,你应该使用Object.create()
来创建带有你想要的[[Prototype]]
的新对象。
唯一可以访问[[class]]属性的方式是
Object.prototype.toString
var o = new Object;//"[object Object]"
var n = new Number;//"[object Number]"
var s = new String;//"[object String]"
var b = new Boolean;//"[object Boolean]"
var d = new Date;//"[object Date]"
var arg = function(){ return arguments }();//"[object Arguments]"
var r = new RegExp;//"[object RegExp]"
var f = new Function;//"[object Function]"
var arr = new Array;//"[object Array]"
var e = new Error;//"[object Error]"
console.log([o, n, s, b, d, arg, r, f, arr, e].map(v => Object.prototype.toString.call(v)));
2
3
4
5
6
7
8
9
10
11
在
ES5
开始,[[class]] 私有属性被Symbol.toStringTag
代替,Object.prototype.toString
的意义从命名上不再跟 class 相关。我们甚至可以自定义Object.prototype.toString
的行为,以下代码展示了使用Symbol.toStringTag
来自定义Object.prototype.toString
的行为:var o = { [Symbol.toStringTag]: "MyObject" } console.log(o + "");//[object MyObject]
1
2这里创建了一个新对象,并且给它唯一的一个属性
Symbol.toStringTag
,我们用字符串加法触发了Object.prototype.toString
的调用,发现这个属性最终对Object.prototype.toString
的结果产生了影响。new操作
new 运算接受一个构造器和一组调用参数,实际上做了几件事:
- 以构造器的 prototype 属性(注意与私有字段[[prototype]]的区分)为原型,创建新对象;
- 将 this 和调用参数传给构造器,执行;
- 如果构造器返回的是对象,则返回,否则返回第一步创建的对象。
它客观上提供了两种方式,一是在构造器中添加属性,二是在构造器的 prototype 属性上添加属性。
用构造器模拟类的两种方法:
//第一种:直接在构造器中修改this,给this添加属性
function c1(){
this.p1 = 1;
this.p2 = function(){
console.log(this.p1);
}
}
var o1 = new c1;
o1.p2();
//第二种:修改构造器的prototype属性指向的对象,它是从这个构造器构造出来的所有对象的原型
function c2(){
}
c2.prototype.p1 = 1;
c2.prototype.p2 = function(){
console.log(this.p1);
}
var o2 = new c2;
o2.p2();
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
我们甚至可以用它来实现一个Object.create的不完整的pollyfill,见以下代码:
Object.create = function(prototype){
var cls = function(){}
cls.prototype = prototype;
return new cls;
}
2
3
4
5
这段代码创建了一个空函数作为类,并把传入的原型挂在了它的prototype,最后创建了一个它的实例,根据new的行为,这将产生一个以传入的第一个参数为原型的对象。
这个函数无法做到与原生的Object.create一致,一个是不支持第二个参数,另一个是不支持null作为原型,所以放到今天意义已经不大了。
# 四、ES6中的类
ES6中引入了class关键字,并且在标准中删除了所有[[class]]相关的私有属性描述,类的概念正式从属性升级成语言的基础设施,从此,基于类的编程方式成为了JavaScript的官方编程范式。
类的基本写法:
class Rectangle { // 数据型成员最好写在构造器里 constructor(height, width) { this.height = height; this.width = width; } // Getter 通过get/set关键字来创建getter get area() { return this.calcArea(); } // Method 通过括号和大括号来创建方法 calcArea() { return this.height * this.width; } }
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15类的继承性
class Animal { constructor(name) { this.name = name; } speak() { console.log(this.name + ' makes a noise.'); } } //使用extends关键字自动设置了constructor,并且会自动调用父类的构造函数 //这是一种更少坑的设计 class Dog extends Animal { constructor(name) { super(name); //super关键字用于访问和调用一个对象的父对象上的函数。 //在派生的类中, 在你可以使用'this'之前, 必须先调用super()。忽略这, 这将导致引用错误。 } speak() { console.log(this.name + ' barks.'); } } let d = new Dog('Mitzie');
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
d.speak(); // Mitzie barks.
**当我们使用类的思想来设计代码时,应该尽量使用class来声明类,而不是用旧语法,拿函数来模拟对象。**
## 五、对象的分类
- 宿主对象(host Objects):由JavaScript宿主环境提供的对象,它们的行为完全由宿主环境决定。
- 内置对象(Built-in Objects):由JavaScript语言提供的对象。
- 固有对象(Intrinsic Objects ):由标准规定,随着JavaScript**运行时创建而自动创建**的对象实例。
- 原生对象(Native Objects):可以由用户通过Array、RegExp等**内置构造器或者特殊语法创建**的对象。
- 普通对象(Ordinary Objects):由**{}语法、Object构造器或者class关键字定义类创建**的对象,它能够被原型继承。
### 宿主对象
在浏览器环境中,我们都知道全局对象是window,window上又有很多属性,如document。
实际上,这个全局对象window上的属性,一部分来自JavaScript语言,一部分来自浏览器环境。
JavaScript标准中规定了全局对象属性,w3c的各种标准中规定了Window对象的其它属性。
宿主对象也分为固有的和用户可创建的两种,**比如document.createElement就可以创建一些dom对象**。
宿主也会提供一些构造器,比如我们可以**使用new Image来创建img元素**。
### 内置对象-固有对象
固有对象在任何JS代码执行前就已经被创建出来了,它们通常扮演者类似基础库的角色。
ECMA标准为我们提供了一份固有对象表,里面含有150+个固有对象。
- 获取全部JavaScript固有对象
从JavaScript标准中可以找到全部的JS对象定义。JS语言规定了全局对象的属性。
三个值:
Infinity、NaN、undefined。
九个函数:
- eval
- isFinite
- isNaN
- parseFloat
- parseInt
- decodeURI
- decodeURIComponent
- encodeURI
- encodeURIComponent
一些构造器:
Array、Date、RegExp、Promise、Proxy、Map、WeakMap、Set、WeapSet、Function、Boolean、String、Number、Symbol、Object、Error、EvalError、RangeError、ReferenceError、SyntaxError、TypeError
URIError、ArrayBuffer、SharedArrayBuffer、DataView、Typed Array、Float32Array、Float64Array、Int8Array、Int16Array、Int32Array、UInt8Array、UInt16Array、UInt32Array、UInt8ClampedArray。
四个用于当作命名空间的对象:
- Atomics
- JSON
- Math
- Reflect
我们使用广度优先搜索,查找这些对象所有的属性和Getter/Setter,就可以获得JavaScript中所有的固有对象。
```js
//类似于数组,但是成员的值都是唯一的,没有重复的值。
var set = new Set();
var objects = [
eval,
isFinite,
isNaN,
parseFloat,
parseInt,
decodeURI,
decodeURIComponent,
encodeURI,
encodeURIComponent,
Array,
Date,
RegExp,
Promise,
Proxy,
Map,
WeakMap,
Set,
WeakSet,
Function,
Boolean,
String,
Number,
Symbol,
Object,
Error,
EvalError,
RangeError,
ReferenceError,
SyntaxError,
TypeError,
URIError,
ArrayBuffer,
SharedArrayBuffer,
DataView,
Float32Array,
Float64Array,
Int8Array,
Int16Array,
Int32Array,
Uint8Array,
Uint16Array,
Uint32Array,
Uint8ClampedArray,
Atomics,
JSON,
Math,
Reflect];
objects.forEach(o => set.add(o));
for(var i = 0; i < objects.length; i++) {
var o = objects[i]
for(var p of Object.getOwnPropertyNames(o)) {
var d = Object.getOwnPropertyDescriptor(o, p)
if( (d.value !== null && typeof d.value === "object") || (typeof d.value === "function"))
if(!set.has(d.value))
set.add(d.value), objects.push(d.value);
if( d.get )
if(!set.has(d.get))
set.add(d.get), objects.push(d.get);
if( d.set )
if(!set.has(d.set))
set.add(d.set), objects.push(d.set);
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
# 内置对象-原生对象
通过语言本身的构造器创建的对象称作原生对象。
JavaScript标准中,提供了30多个构造器。
通过这些构造器,我们可以用new运算创建新的对象,所以我们把这些对象称作原生对象。 几乎所有这些构造器的能力都是无法用纯JavaScript代码实现的,它们也无法用class/extend语法来继承。
这些构造器创建的对象多数使用了私有字段,例如:
- Error: [[ErrorData]]
- Boolean: [[BooleanData]]
- Number: [[NumberData]]
- Date: [[DateValue]]
- RegExp: [[RegExpMatcher]]
- Symbol: [[SymbolData]]
- Map: [[MapData]]
这些字段使得原型继承方法无法正常工作,所以,我们可以认为,所有这些原生对象都是为了特定能力或者性能,而设计出来的“特权对象”。
# 用对象来模拟函数与构造器:函数对象与构造器对象
事实上,JavaScript为这一类对象预留了私有字段机制,并规定了抽象的函数对象与构造器对象的概念。
- 函数对象的定义是:具有[[call]]私有字段的对象。
- 构造器对象的定义是:具有私有字段[[construct]]的对象。
JavaScript用对象模拟函数的设计代替了一般编程语言中的函数,它们可以像其它语言的函数一样被调用、传参。任何宿主只要提供了“具有[[call]]私有字段的对象”,就可以被 JavaScript 函数调用语法支持。
- [[call]]私有字段必须是一个引擎中定义的函数,需要接受this值和调用参数,并且会产生域的切换。
任何对象只需要实现[[call]],它就是一个函数对象,可以去作为函数被调用。而如果它能实现[[construct]],它就是一个构造器对象,可以作为构造器被调用。
用户用function关键字创建的函数必定同时是函数和构造器。不过,它们表现出来的行为效果却并不相同。
对于宿主和内置对象来说,它们实现[[call]](作为函数被调用)和[[construct]](作为构造器被调用)不总是一致的。比如内置对象 Date 在作为构造器调用时产生新的对象,作为函数时,则产生字符串,见以下代码:
console.log(new Date); // Sun Apr 28 2019 12:50:27 GMT+0800 (中国标准时间) console.log(Date()); //// Sun Apr 28 2019 12:50:27 GMT+0800 (中国标准时间)
1
2而浏览器宿主环境中,提供的Image构造器,则根本不允许被作为函数调用。
console.log(new Image); //<img> console.log(Image());//Uncaught TypeError: Failed to construct 'Image': Please use the 'new' operator, this DOM object constructor cannot be called as a function.
1
2再比如基本类型(String、Number、Boolean),它们的构造器被当作函数调用,则产生类型转换的效果。
值得一提的是,在ES6之后 => 语法创建的函数仅仅是函数,它们无法被当作构造器使用,见以下代码:
new (a => 0) // error
1对于用户使用 function 语法或者Function构造器创建的对象来说,[[call]]和[[construct]]行为总是相似的,它们执行同一段代码。
我们看一下示例。
function f(){ return 1; } var v = f(); //把f作为函数调用 var o = new f(); //把f作为构造器调用
1
2
3
4
5我们大致可以认为,它们[[construct]]的执行过程如下:
- 以 Object.protoype 为原型创建一个新对象;
- 以新对象为 this,执行函数的[[call]];
- 如果[[call]]的返回值是对象,那么,返回这个对象,否则返回第一步创建的新对象。
这样的规则造成了个有趣的现象,如果我们的构造器返回了一个新的对象,那么new创建的新对象就变成了一个构造函数之外完全无法访问的对象,这一定程度上可以实现“私有”。
function cls(){ this.a = 100; return { getValue:() => this.a } } var o = new cls; o.getValue(); //100 o.a; //undefined //a在外面永远无法访问到
1
2
3
4
5
6
7
8
9
10
# 特殊行为的对象
除了上面介绍的对象之外,在固有对象和原生对象中,有一些对象的行为跟正常对象有很大区别。
它们常见的下标运算(就是使用中括号或者点来做属性访问)或者设置原型跟普通对象不同,这里我简单总结一下。
- Array:Array的length属性根据最大的下标自动发生变化。
- Object.prototype:作为所有正常对象的默认原型,不能再给它设置原型了。
- String:为了支持下标运算,String的正整数属性访问会去字符串里查找。
- Arguments:arguments的非负整数型下标属性跟对应的变量联动。
- 模块的namespace对象:特殊的地方非常多,跟一般对象完全不一样,尽量只用于import吧。
- 类型数组和数组缓冲区:跟内存块相关联,下标运算比较特殊。
- bind后的function:跟原来的函数相关联。
# 六、小练习
1.不使用new运算符,尽可能找到获得对象的方法。
// 1. 利用字面量
var a = [], b = {}, c = /abc/g
// 2. 利用dom api
var d = document.createElement('p')
// 3. 利用JavaScript内置对象的api
var e = Object.create(null)
var f = Object.assign({k1:3, k2:8}, {k3: 9}) //将所有可枚举属性的值从一个或多个源对象复制到目标对象,返回目标对象
var g = JSON.parse('{}') //将一个JSON字符串转换为对象
// 4.利用装箱转换
var h = Object(undefined), i = Object(null), k = Object(1), l = Object('abc'), m = Object(true)
2
3
4
5
6
7
8
9
10