JavaScript 引用类型的体系关系

118 阅读5分钟

正文

JavaScript 是ECMAScript标准的一种实现,是一门支持面向对象、函数式编程的混合性的动态弱类型、解释型语言,主流的运行时有浏览器和node.js,语法与语言设计参考了c、java、self、Scheme。

在几乎所有的语言中,都有“类型系统”,严格意义上讲“类型系统”包含了类型、类型系统的规则两部分。类型系统是一个编程语言的地基。

类型是对数据的分类,从更细的粒度定义数据的结构、可执行的操作、表示的意义,编程语言提供的类型更底层,抽象层次很低,一般分为基本类型和引用类型(复合类型),开发者可以通过这些基础类型构建复杂的复合类型,或者也可以说数据结构。

一个编程语言典型的“类型系统”一般会有如下基本内容:

  • 基础类型(原始类型、值类型)
  • 复合类型(引用类型)
  • 任意类型
  • 面向对象,即所有具备面向对象特征的类型
  • 接口

从《JavaScript语言精髓与编程》中提到JavaScript有6种类型系统的分类

  1. 8种基本数据类型
  2. 值类型与引用类型(基本数据类型和复杂数据类型)
  3. ECMAScript语言类型
  4. ECMAScript规范类型
  5. 对象类型
  6. 原子对象类型系统

目前网上和主流官方文档的JavaScript的类型分类是2方法:Object和其他7种值类型。

JavaScript中只有一种引用类型(复杂数据类型、复合类型),就是Object,但实际上JavaScript中的很多主要语言特性都是基于其衍生的。如下:

  • object类型
  • Array类型
  • Map类型
  • Set类型
  • Date类型
  • RegExp类型
  • function类型
  • Error类型
  • 基本包装类型(String、Boolean、Number)
  • 单体内置对象(Global、Math)
  • 宿主对象(如Window、Navigator)
  • 引擎扩展对象(如debug)

可以看到除了原始类型外,其他都是对象类型,js是通过原型继承的方式来扩展和构造不同类型的。

原型继承

instanceof 运算符用来检测对象是不是类的一个实例

typeof 返回操作数的类型如下表

类型结果
Undefined"undefined"
Null"object"(原因
Boolean"boolean"
Number"number"
BigInt"bigint"
String"string"
Symbol"symbol"
Function(在 ECMA-262 中实现 [[Call]];classes也是函数)"function"
其他任何对象"object"

可以看到除了值类型外,Function也是一种特殊的顶层类型,而其他均为object类型,null由于历史错误原因是object类型。

另外一个就是所有Object衍生的类型(包括函数)的原型对象的“原型”都指向了基类“Object”的原型对象,即Object.prototype,这些衍生类型有自己的原型对象,定义了特定的属性和方法,并重写了基类的某些方法,如toString方法。

而这个基类的原型对象的原型指向了null

Object.prototype.__proto // null

let a = {b:1}
// undefined
a.toString === Object.prototype.toString
// true
let b = [1,2]
b.toString === Object.prototype.toString
// false
b.toString === Array.prototype.toString
// true
let c = function (){}
c.toString === Function.prototype.toString
// true
let s = '123'
s.toString === String.prototype.toString
// true
const f = function(){}
f.toString === Function.prototype.toString
// true
const date = new Date();
date.toString === Date.prototype.toString
// true
const set = new Set([1,2])
set.toString === Set.prototype.toString
// true

JavaScript中什么会有ƒ () { [native code] }

它是一个预定义的函数,这个函数就是空函数或者空操作,通常表示为 ƒ () { [native code] },这个预定义的函数并不是一个普通的 JavaScript 函数,而是一个由 JavaScript 引擎实现的本地(native)代码函数。

因此,它的实现方式与普通的 JavaScript 代码不同。这个函数并不包含任何实际的代码,它只是一个占位符,用于占据函数对象的位置。这个预定义函数也被称为“空函数”或“占位符函数”。

这个预定义函数的作用是为所有函数对象提供默认的属性和方法。由于所有函数对象都继承自 Function.prototype,因此它们也会继承这个预定义函数的属性和方法,比如 call() 和 apply() 方法等

Function.prototype
//ƒ () { [native code] }
Function.prototype === Function.__proto__
// true 特别的Function构造函数的原型(继承类的原型对象)等于原型对象
Function.prototype.__proto__ === Object.prototype
//true

如何区分Object的子实例(直接实例和构造函数产生的实例(纯粹的对象))和其他衍生类型(Object、Array)的实例呢?

typeof a
// 'object'
typeof b
// 'object'
typeof c
// 'function'
typeof s
// 'string'
typeof date
// 'object'
typeof set
// 'object'

可见,typeof除了function类型外,其他都会得到“object”类型。

分析下两个的区别,构造函数产生的实例,其原型(proto)指向了构造函数的“原型对象”,而构造函数“原型对象”的原型指向了Object基类的“原型对象”。通过这个原型链,最后的实例可以访问Object.prototype,即Object基类的原型对象上的属性和方法。

在对象的衍生的这些构造类型中,Array、Date、Function、RegExp四个构造函数重写了Object的toString方法、其他均没有。构造函数作为一种特殊的产出对象的函数,由其new出来实例(对象),如果不主动设置原型中的属性和方法,默认是会访问到Object.prototype中的所有属性和方法的。

由此,可以用以下来判断是不是一个比较纯粹的对象(非Array、Date、Function、RegExp类型)(object、map、set、error)

foo.toString === Object.prototype.toString && foo.toString.toString().includes('[native code]')

参考

  1. Go语言编程 - 许式伟
  2. JavaScript语言精髓与编程 - 周爱民
  3. TypeScript 中的底层和顶层类型 - 掘金
  4. Rust的类型系统 - RainDavi - 博客园
  5. javascript中15种原生对象类型系统综述 - 小火柴的蓝色理想 - 博客园
  6. javaScript引用类型详解
  7. JavaScript 的内置对象和浏览器对象 - 一抹夏忧☆ - 博客园
  8. typeof - JavaScript | MDN
  9. 🍭 图解原型和原型链 - 掘金
  10. F.prototype