JS对象分类

2,016 阅读6分钟

这里有一份简洁的前端知识体系等待你查收,看看吧,会有惊喜哦~如果觉得不错,恳求star哈~


概述

我们可以把对象分成几类。

  1. 宿主对象(host Objects):由 JS 宿主环境提供的对象,它们的行为完全由宿主环境决定。
  2. 内置对象(Built-in Objects):由 JS 语言提供的对象。
    • 固有对象(Intrinsic Objects ):由标准规定,随着 JS 运行时自动创建的对象实例。
    • 原生对象(Native Objects):可以由用户通过Array、RegExp等内置构造器或者特殊语法创建的对象。
    • 普通对象(Ordinary Objects):由{}语法、Object构造器或者class关键字定义类创建的对象,它能够被原型继承。

下面讲解普通对象之外的对象类型。


宿主对象

首先我们来看看宿主对象。

JS宿主对象千奇百怪,但是前端最熟悉的无疑是浏览器环境中的宿主了。

在浏览器环境中,我们都知道全局对象是window,window上又有很多属性,如document。

实际上,这个全局对象window上的属性,一部分来自 JS 语言,一部分来自浏览器环境。

JS 标准中规定了全局对象属性,w3c的各种标准中规定了Window对象的其它属性。

宿主对象也分为固有的和用户可创建的两种,比如 document.createElement 就可以创建一些dom对象。

宿主也会提供一些构造器,比如我们可以使用new Image来创建img元素.


内置对象·原生对象

我们把 JS 中,能够通过语言本身的构造器创建的对象称作原生对象

在 JS 标准中,提供了30多个构造器。按照不同应用场景,可以把原生对象分成了以下几个种类。

基本类型 基础功能与数据结构 错误类型 二进制操作 带类型的数组
Boolean Array Error ArrayBuffer Float32Array
String Date EvalError SharedArrayBuffer Float64Array
Number RegExp RangeError DataView Int8Array
Symbol Promise ReferenceError Int16Array
Object Proxy SyntaxError Int32Array
Map TypeError Unit8Array
WeakMap URIError Unit16Array
Set Unit32Array
WeakSet Unit8ClampedArray
Function

通过这些构造器,我们可以用new运算创建新的对象,所以我们把这些对象称作原生对象。

几乎所有这些构造器的能力都是无法用纯JS代码实现的,它们也无法用class/extend语法来继承。

这些构造器创建的对象多数使用了私有字段,例如:

Error: [[ErrorData]]
Boolean: [[BooleanData]]
Number: [[NumberData]]
Date: [[DateValue]]
RegExp: [[RegExpMatcher]]
Symbol: [[SymbolData]]
Map: [[MapData]]

这些字段使得原型继承方法无法正常工作,所以,我们可以认为,所有这些原生对象都是为了特定能力或者性能,而设计出来的“特权对象”。


内置对象·固有对象

固有对象是由标准规定,随着 JS 运行时创建而自动创建的对象实例。

固有对象在任何JS代码执行前就已经被创建出来了,它们通常扮演着类似基础库的角色。我们前面提到的“类”其实就是固有对象的一种。

这里提供代码用来查看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);
    }
}

函数对象与构造器对象

前面介绍了对象的一般分类,在 JS 中,还有一个看待对象的不同视角,这就是用对象来模拟函数和构造器。

事实上,JS 为这一类对象预留了私有字段机制,并规定了抽象的函数对象与构造器对象的概念。

函数对象的定义是:具有[[call]]私有字段的对象,构造器对象的定义是:具有私有字段[[construct]]的对象

任何对象只需要实现[[call]],它就是一个函数对象,可以去作为函数被调用。而如果它能实现[[construct]],它就是一个构造器对象,可以作为构造器被调用。

对于宿主和内置对象来说,它们实现[[call]](作为函数被调用)和[[construct]](作为构造器被调用)不总是一致的。比如内置对象 Date 在作为构造器调用时产生新的对象,作为函数时,则产生字符串,见以下代码:

console.log(new Date); 
console.log(Date())

而浏览器宿主环境中,提供的Image构造器,则根本不允许被作为函数调用。

console.log(new Image);
console.log(Image());//抛出错误

再比如基本类型(String、Number、Boolean),它们的构造器被当作函数调用,则产生类型转换的效果。

值得一提的是,在ES6之后 => 语法创建的函数仅仅是函数,它们无法被当作构造器使用,见以下代码:

new (a => 0) // error

对于用户使用 function 语法或者Function构造器创建的对象来说,[[call]]和[[construct]]行为总是相似的,它们执行同一段代码。

我们看一下示例。

function f () {
  return 1;
}
var v = f (); //把f作为函数调用
var o = new f (); //把f作为构造器调用

我们大致可以认为,它们[[construct]]的执行过程如下:

  1. 以 Object.protoype 为原型创建一个新对象;
  2. 以新对象为 this,执行函数的[[call]];
  3. 如果[[call]]的返回值是对象,那么,返回这个对象,否则返回第一步创建的新对象。

这样的规则造成了个有趣的现象,如果我们的构造器返回了一个新的对象,那么new创建的新对象就变成了一个构造函数之外完全无法访问的对象,这一定程度上可以实现“私有”。

function cls () {
  this.a = 100;
  return {
    getValue: () => this.a,
  };
}
var o = new cls ();
o.getValue (); //100
//a在外面永远无法访问到

特殊行为的对象

除了上面介绍的对象之外,在固有对象和原生对象中,有一些对象的行为跟正常对象有很大区别。

它们常见的下标运算(就是使用中括号或者点来做属性访问)或者设置原型跟普通对象不同,这里简单总结一下。

  1. Array:Array的length属性根据最大的下标自动发生变化。
  2. Object.prototype:作为所有正常对象的默认原型,不能再给它设置原型了。
  3. String:为了支持下标运算,String的正整数属性访问会去字符串里查找。
  4. Arguments:arguments的非负整数型下标属性跟对应的变量联动。
  5. 模块的namespace对象:特殊的地方非常多,跟一般对象完全不一样,尽量只用于import吧。
  6. 类型数组和数组缓冲区:跟内存块相关联,下标运算比较特殊。
  7. bind后的function:跟原来的函数相关联。

总结

在这篇文章中,我们介绍了一些不那么常规的对象,并且还介绍了JS中用对象来模拟函数和构造器的机制。