我们的日常工作中,接触到的主要API,几乎就是下面这些对象提供的。理解这些对象的性质,才能真正理解我们使用的API的一些特性。

1. JavaScript中的对象全部分类

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

1.1 宿主对象

  1. JavaScript宿主对象千奇百怪,最熟悉的就是浏览器环境中的宿主了。
  2. 在浏览器环境中,全局对象是`window``window`上又有很多属性,如`document`
  3. 实际上,全局对象`window`上的属性,一部分来自JavaScript语言,一部分来自浏览器环境。
  4. JavaScript标准中规定了全局对象属性,w3c的各种标准中规定了window对象的其他属性。
  5. 宿主对象也分为两种:固有的和用户可创建的。比如`document.createElement`就可以创建一些dom对象。
  6. 宿主也会提供一些构造器,比如用new Image来创建img元素,这些会在浏览器的API部分详细讲。

1.2 内置对象·固有对象

  1. 前面说过,固有对象是由标准规定,随着JavaScript运行时创建而自动创建的对象实例。
  2. 固有对象在任何JS代码执行前就已经被创建出来了,他们通常扮演基础库的角色。我们前面提到的“类”就是固有对象的一种。
  3. ECMA提供了一份固有对象表,又150+个固有对象,可以通过[链接](https://www.ecma-international.org/ecma-262/9.0/index.html#sec-well-known-intrinsic-objects)查看。(这个表不完整,在本篇末会有一个实验,获取全部JavaScript的固有对象)。

1.3 内置对象·原生对象

  1. 我们把JavaScript中能够通过语言本身的构造器创建的对象称作原生对象。在JavaScript标准中,提供了30多个构造器。按照笔者的理解,可以把原生对象分为以下几类。
  2. P.S. `WeapSet` => `WeakSet`

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

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

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

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

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

2. 用对象来模拟函数与构造器

2.1 函数对象与构造器对象

  1. 前面介绍了对象的一般分类,在JavaScript中,还有一个看待对象的不同视角,这就是用对象来模拟函数的构造器。
  2. 事实上,JavaScript为这一类对象预留了私有字段机制,并规定了抽象的函数对象与构造器对象的概念。
  • 函数对象的定义:具有[[call]]私有字段的对象。
  • 构造器对象的定义:具有私有字段[[construct]]的对象。

    JavaScript用对象模拟函数的设计代替了一般编程语言中的函数,他们可以像其它语言的函数一样被调用、传参。任何宿主只要提供了“具有[[call]]私有字段的对象”,就可以被JavaScript函数调用语法支持。

[[call]]私有字段必须是一个引擎中定义的函数,需要接受this值和调用参数,并且会产生域的切换,这些内容会在属性访问和执行过程两个章节详细讲述。

  1. 我们可以说,任何对象只要实现[[call]],他就是一个函数对象,可以去作为函数被调用。而如果它能实现[[construct]],它就是一个构造器对象,可以作为构造器被调用。
  2. 对于JavaScript的程序员来说,只要字段符合,我们在上文中提到的宿主对象和内置对象(如Symbol函数)可以模拟函数和构造器。
  3. 当然用户用function关键字创建的函数必定同时是函数和构造器。不过他们表现得行为效果却不相同。
  4. 对宿主和内置对象来说,他们实现[[call]](作为函数被调用)和[[construct]](作为构造器被调用)不总是一直的。比如内置对象Date在作为构造器调用时产生的新的对象,作为函数时,则产生字符串,如下:
  1. console.log(new Date); // 1
  2. console.log(Date());
  3. // ???? 第一个我打出来的是时间

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

  1. console.log(new Image);// <img>
  2. console.log(Image());// 报错

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

ES6之后 => 语法创建的函数仅仅是函数,它们无法被当作构造器使用,如:

  1. new (a => 0)// error

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

  1. function f(){
  2. return 1;
  3. }
  4. var v = f();// 把f作为函数调用
  5. var o = new f();// 把f作为构造器调用
  6. console.log(v);//1
  7. console.log(o);// f{}

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

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

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

  1. function cls(){
  2. this.a = 100;
  3. return {
  4. getValue: () => this.a
  5. }
  6. }
  7. var o = new cls;
  8. o.getValue(); //100
  9. //a在外面无法访问到

3.特殊行为的对象

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

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

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

总结

不用new运算符,尽可能找到活的对象的方法

例子:

  1. var o = {}
  2. var o = function(){}

小实验:

获取全部JavaScript固有对象。

可以来这看总结的全部

评论拾遗:文中[[]]双括号的属性是没法用JS代码访问的私有属性,得用C++代码调用JS引擎才能看到

好的 看不懂 没学明白。