细读 JS | 数据类型详解

简介: 细读 JS | 数据类型详解

前言


今天又又又...又整理了一下,那些 JavaScript 里不清不楚的知识点。


正文


一、数据类型的分类


截止发文日期,ECMAScript 标准的数据类型仅有 8 种(ECMAScript Language Types)。可以分为两类:


  • 原始类型(Primitives),我们也称作基本数据类型
  • Undefined
  • Null(一种特殊的原始类型,typeof(instance) === 'object'
  • Boolean
  • String
  • Symbol(typeof(instance) === 'symbol'
  • Number
  • BigInt(typeof(instance) === 'bigint'
  • 引用类型(Objects)
  • Object(包括从 Object 派生出来的结构类型,如 Object、Array、Map、Set、Date 等)


关于使用 typeof 判断以上数据类型的话题,老生常谈了


原始类型的比较的是值,只有两者的值相等,那么它们被认为是相等的,否则不相等。而引用类型比较的是地址,当两者的标识符同时指向内存的同一个地址,则被认为是相等的,否则不相等。

console.log({} == {}) // false
console.log([] == []) // false


二、原始类型与原始值


所有基本类型的值(即原始值,Primitive Values)都是不可改变(immutable)的,而且不含任何属性和方法的。


到这里可能会有小伙伴打问号了???


Q1:原始类型与原始值有什么区别?

原始类型的值称为原始值。例如原始类型 Boolean 有两个(原始)值 truefalse。同样的原始类型 Undefined(Null),只有一个原始值 undefinednull)。其他的就有很多个了...

Q2:原始值不可改变?这样不是改变了吗?

var foo = true
foo = false
console.log(foo) // false


其实不然,以上示例是原始类型和一个赋值为原始类型的变量的区别。变量会被赋予一个新值,而原值不能像数组、对象以及函数那样被改变。


基本类型值可以被替换,但不能被改变。

// 使用字符串方法不会改变一个字符串
var foo = 'foo'
foo.toUpperCase()
console.log(foo) // "foo"
// 赋值行为可以给基本类型一个新值,而不是改变它
foo = foo.toUpperCase() // "FOO"


再有示例:

var num = 1
function add(num) {
  num += 1
  console.log(num)
}
add(num) // 2
console.log(num) // 1
// ************************** 华丽的分割线 **************************
// 如果没有看上面的一些概念,单纯地看上面的例子,我相信百分百都能得到正确答案。
// 但看完上面一些的概念之后,再结合例子,不知道会不会有人对 “原始类型的值不可改变” 这句话产生怀疑?
// 如果有怀疑就继续往下看 ,否则可直接跳到 Q3 了。
// ************************** 华丽的分割线 **************************
// JS 运行的三个步骤:词法分析、预编译、解析执行。
// 其中预编译,不仅仅发生在 script 代码块执行之前,还发生在函数执行之前。
// 
// 函数预编译的过程大致是这样的:
// 1. 首先查找形参和变量声明(此时并赋予值 undefined)
// 2. 接着将实参赋值给形参
// 3. 接着查找函数体内的函数声明(赋予函数本身)。
//
// 函数 add 在实参赋值给形参的过程,会将传递进来的参数(基本类型的值)复制一份,
// 创建一个本地副本,该副本只存在于该函数的作用域中。(原本的值与副本是完全独立,互不干扰的)


Q3:原始值没有任何属性和方法?那这个是怎么回事?

var foo = 'foo'
console.log(foo.length) // 3
console.log(foo.toUpperCase()) // "FOO"
// 试图改变 length 属性
foo.length = 4
console.log(foo.length) // 3


其实这是 JavaScript 包装类的内容了。


在 JavaScript 中除了 nullundefined 之外,所有的基本类型都有其对应的包装对象(Wrapper Object)。因此,访问 nullundefined 的任何属性和方法都会抛出错误。


  • String 为字符串基本类型。
  • Number 为数值基本类型。
  • BigInt 为大整数基本类型。
  • Boolean 为布尔基本类型。
  • Symbol 为字面量基本类型。


这些包装对象的 valueOf方法返回其对应的原始值。


再次明确一点,原始值是没有任何属性和方法的。


不是说好的,原始值不含任何的属性和方法吗?那 foo.lengthfoo.toUpperCase() 是咋回事啊???


其实它内部是这样实现的:当字符串字面量调用一个字符串对象才有的方法或属性时,JavaScript 会自动将基本字符串转化为字符串对象并且调用相应的方法或属性。(Boolean 和 Number 也同样如此)。


我们尝试在控制台上打印一下 new String('foo'),可以看到该实例对象有一个 length 属性,其值为 3,实例对象本身没有 toUpperCase() 方法,所以接着往原型上查找,果然找到了。(由于原型上方法太多,截图里没有展开,否则影响文章篇幅)

8.webp.jpg

因此

var foo = 'foo'
console.log(foo.length) // 3
console.log(foo.toUpperCase()) // "FOO"
// 相当于
var foo = 'foo'
console.log(new String(foo).length) // 3
console.log(new String(foo).toUpperCase()) // "FOO"


可下面为什么 length 还是 3 呢?

foo.length = 4
console.log(foo.length) // 3
// 怎样理解呢?
//
//
// 执行第一行代码
// foo.length = 4 可以拆分成两部分去理解:
var temp = new String(foo) // 在内存中创建了一个对象,只是没有一个标识符(变量)指向它而已(为了便于理解,我这里假装有一个 temp 变量指向它)
temp.length = 4 // 修改包装对象的 length 属性,其实是修改成功的
// 由于该对象并没有被引用,所以在执行下一句代码之前就被回收销毁了
//
//
// 2. 执行第二行代码
// console.log(foo.length) 相当于
console.log(new String(foo).length) // foo 还是 "foo",自然结果就是 3 了。


三、对象


在 JavaScript 中,除了以上的原始值,其余都属于对象。

与原始类型不同的是,对象是可变(mutable)的。

1. 对象的分类


我们可以将对象划分为普通对象(ordinary object)和函数对象(function object)。

那怎样区分呢?我们先定义一些 Function 实例和 Object 实例:

// Function 实例
function fn1() {}
var fn2 = function() {}
var fn3 = new Function('console.log("Hi, everyone")') // 一般不使用 Function 构造器去生成 Function 对象,相比函数声明或者函数表达式,它表现更为低效。
// Object 实例
var obj1 = {}
var obj2 = new Object()
var obj3 = new fn1()


我们来打印一下结果:

typeof Object     // "function"
typeof Function   // "function"
typeof fn1        // "function"
typeof fn2        // "function"
typeof fn3        // "function"
typeof obj1       // "object"
typeof obj2       // "object"
typeof obj3       // "object"


ObjectFunction 本身就是 JavaScript 中自带的函数对象。其中 obj1obj2obj3 为普通对象(均为 Object 的实例),而 fn1fn2fn3 为函数对象(均是 Function 的实例)。


记住以下这句话:


所有 Function 的实例都是函数对象,而其他的都是普通对象


2. 对象的原型


接着,引入两个很容易让人抓狂、混淆的两兄弟 prototype (原型对象)和 __proto__(原型)。这俩兄弟的主要是为了构造原型链而存在的。


对象类型 prototype __proto__
普通对象
函数对象


因此有以下结论:


所有对象都有 __proto__ 属性,而只有函数对象才具有 prototype 属性。


再上几个菜,请慢慢品尝:

// 每个对象都有一个 constructor 属性,该属性指向实例对象的构造函数
Object.prototype.constructor === Object // true
Function.prototype.constructor === Function // true
// (全局对象)Object 是 (构造器)Function 的实例
// (全局对象)Function 也是 (构造器)Function 的实例
Object.__proto__ === Function.prototype // true
Function.__proto__ === Function.prototype // true
// (构造器)Function 也是(构造器)Object 的实例
Function.prototype.__proto__ === Object.prototype // true
// 从原型上查找属性,不可能无终止地查找下去,那原型的尽头在哪呢?
// 站在原型顶端的男人,是它。
// 假设我们访问一个对象的属性或者方法,如若前面的原型上均无法查找到,最终会止步于此,并返回 undefined。
Object.prototype.__proto__ // null


在 JavaScript 中访问一个对象属性,它在原型上是怎样查找的呢?

function Person() {} // 构造函数
var person = new Person() // 实例化对象
console.log(person.name);  // undefined
// 过程如下:
person // 是对象,可以继续
person['name'] // 不存在属性 name,继续查找
person.__proto__ // 是对象,可以继续
person.__proto__['name'] // 不存在属性 name,继续查找
person.__proto__.__proto__ // 是对象,可以继续
person.__proto__.__proto__['name'] // 不存在属性 name,继续查找
person.__proto__.__proto__.__proto__ // 不是对象,是 null 值。停止查找,返回 undefined


需要注意的是,Object.prototype.__proto__ 从未被包括在 ECMAScript 语言规范中标准化,但它被大多数浏览器厂商所支持。该特性已从 Web 标准中删除,详情可看 Object.prototype.__proto__

在标准中,几乎(例外是 Object.create(null) ,下面有说明)每个实例对象内部都有一个 [[Prototype]] 属性,该属性指向对象的原型,而且该属性值只会是对象或者 null

在非标准下,可以通过 Object.prototype.__proto__ 访问(或设置)实例对象内部的 [[Prototype]],这种方式其实是不被推荐使用的。现在更被推荐使用的方式是 Objec.getPrototypeOf()/Object.setPrototypeOf()

请注意,以上(包括下文)所指对象均不是通过 Object.create(null) 实例化的(除特意说明外)。Object.create(null) 实例化的对象比较特殊,它内部没有 [[Prototype]] 属性,也没有任何其他内部属性。(Object.create()

var obj = Object.create(null)
var obj1 = Object.create(null)
var obj2 = {}
obj.__proto__ === undefined // true
obj.getPrototypeOf() // 抛出错误 TypeError: obj.getPrototypeOf is not a function


我们可以在控制台打印一下,看下两者的区别。

3.webp (1).jpg


JavaScript 常被描述为一种基于原型的语言 —— 每个对象拥有一个原型([[Prototype]]),对象以其原型为模板、从原型继承方法和属性。原型对象也可能拥有原型,并从中继承方法和属性,一层一层、以此类推。这种关系常被称为原型链(prototype chain)。


3. 继承


关于继承内容,可看另外一篇文章:深入 JavaScript 继承原理。


4. 对象的内部属性(Internal properties)


在规范中,对象的内部方法和内部插槽使用双方括号 [[]] 中包含的名称标识,且首字母为大写。例如 [[Prototype]][[Class]][[Extensible]][[Call]][[Scopes]][[FunctionLocation]] 等等。


下面挑几个来讲一下:


4.1 [[Class]]


[[Class]] 是对象的一个内部属性,其值为以下字符串之一:

  • 常见的有:FunctionObjectArrayBooleanNumberStringSymbolRegExpJSONDateMathErrorArguments 等。
  • 比较少用的有:BigIntSetWeakSetMapWeakMapReflectPromiseGeneratorFunctionAsyncFunctionWindowIntlWebAssembly,以及派生于 HTMLElement 的(如 HTMLScriptElement )等等。
  • 几乎所有标准内置对象,都有特定的类型。实在太多了...


我们都知道 typeof 无法判断对象的具体类型,无论是 typeof {}typeof []、还是 typeof Math  都返回 "object"。但有了 [[Class]] 属性之后,我们就可以利用它来判断对象的类型了。访问 [[Class]] 的唯一方法是通过默认的 toString() 方法(该方法是通用的):


Object.prototye.toString()

  • 如果参数 undefined,则返回 [object Undefined] 字符串;
  • 如果参数 null,则返回 [object Null] 字符串;
  • 如果参数是一个对象,则返回 "[object " + obj.[[Class]] + "]" 字符串,例如 [object Array]
  • 如果参数是一个原始值,则会先将其转换为相应的对象,然后按照以上的规则输出。


以下封装了获取对象类型的方法:

function getClass(x) {
  const { toString } = Object.prototype
  const str = toString.call(x)
  return /^\[object (.*)\]$/.exec(str)[1]
}
getClass(null) // "Null"
getClass(undefined) // "Undefined"
getClass({}) // "Object"
getClass([]) // "Array"
getClass(JSON) // "JSON"
getClass(() => {}) // "Function"
;(function() { return getClass(arguments) })() // "Arguments"


4.2 [[Construct]]


一个对象里,如若没有 [[construct]] 属性,是无法使用 new 关键字进行构造的。


四、类型转换


在 JavaScript 中,我们会经常使用相等运算符(==)去比较两个操作数是否相等。当两个操作数一个是引用类型,另一个是原始类型的时候,前者会先转换为原始类型,再比较。


那么,引用类型是如何转换为原始类型的呢?


关于 JavaScript 类型转换的内容,已经单独写了一篇文章详细地介绍了


未完待续...


参考


目录
相关文章
|
8天前
|
JavaScript 前端开发
js变量的作用域、作用域链、数据类型和转换应用案例
【4月更文挑战第27天】JavaScript 中变量有全局和局部作用域,全局变量在所有地方可访问,局部变量只限其定义的代码块。作用域链允许变量在当前块未定义时向上搜索父级作用域。语言支持多种数据类型,如字符串、数字、布尔值,可通过 `typeof` 检查类型。转换数据类型用 `parseInt` 或 `parseFloat`,将字符串转为数值。
18 1
|
8天前
|
JavaScript
JS 获取对象数据类型的键值对的键与值
JS 获取对象数据类型的键值对的键与值
|
8天前
|
存储 JavaScript 前端开发
JavaScript数据类型详解
【4月更文挑战第4天】JavaScript有七种基本数据类型(Number, String, Boolean, Null, Undefined, Symbol, BigInt)和一种复杂数据类型(Object)。Number用于数字,String是不可变的文本,Boolean表示逻辑值,Null为空,Undefined表示未赋值,Symbol是唯一标识,BigInt处理大整数。Object用于复杂数据结构,如数组和函数。此外,`typeof`操作符检测数据类型,但有特殊行为,如`typeof null === "object"`。
23 2
|
8天前
|
JavaScript
typeof 和 instanceofJS数据类型(js的问题)
typeof 和 instanceofJS数据类型(js的问题)
|
8天前
|
JavaScript 前端开发
JavaScript 中有哪些数据类型?
JavaScript 中有哪些数据类型?
19 3
|
8天前
|
JavaScript
JS常用数据类型转换
JS常用数据类型转换
19 1
|
8天前
|
JavaScript
数据类型转换(js的问题)
数据类型转换(js的问题)
10 0
|
8天前
|
存储 JavaScript 前端开发
【JavaScript技术专栏】JavaScript基础入门:变量、数据类型与运算符
【4月更文挑战第30天】本文介绍了JavaScript的基础知识,包括变量(var、let、const)、数据类型(Number、String、Boolean、Undefined、Null及Object、Array)和运算符(算术、赋值、比较、逻辑)。通过实例展示了如何声明变量、操作数据类型以及使用运算符执行数学和逻辑运算。了解这些基础知识对初学者至关重要,是进阶学习JavaScript的关键。
|
5天前
|
存储 前端开发 JavaScript
JavaScript数据类型归纳,架构师花费近一年时间整理出来的前端核心知识
JavaScript数据类型归纳,架构师花费近一年时间整理出来的前端核心知识
JavaScript数据类型归纳,架构师花费近一年时间整理出来的前端核心知识
|
8天前
|
存储 JavaScript 前端开发
JavaScript引用数据类型
JavaScript引用数据类型