JavaScript对象

# JavaScript对象

[TOC]

# 一、对象的特征

  1. 对象具有唯一标识性:即使完全相同的两个对象,也并非同一个对象。
  2. 对象有状态:对象具有状态,同一对象可能处于不同状态之下。
  3. 对象具有行为:即对象的状态,可能因为它的行为产生变迁。
    • 关于对象的第二个和第三个特征“状态和行为”,不同语言会使用不同的术语来抽象描述它们,比如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
1
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
      6
    • Object.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))); 
1
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();
1
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;
}
1
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);
  }
}
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
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)
1
2
3
4
5
6
7
8
9
10