Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

细读 JS | 数据类型详解 #239

Open
toFrankie opened this issue Feb 26, 2023 · 0 comments
Open

细读 JS | 数据类型详解 #239

toFrankie opened this issue Feb 26, 2023 · 0 comments
Labels
2021 2021 年撰写 JS 与 JavaScript、ECMAScript 相关的文章 前端 与 JavaScript、ECMAScript、Web 前端相关的文章 尚未完结 未完成的、未完结的文章

Comments

@toFrankie
Copy link
Owner

toFrankie commented Feb 26, 2023

今天又又又...又整理了一下,那些 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 判断以上数据类型的话题,老生常谈了。例如,为什么 typeof null === 'object'typeof(() => {}) === 'function' 呢?这里不展开赘述了,请移步:JavaScript 的迷惑行为大赏

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

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() 方法,所以接着往原型上查找,果然找到了。(由于原型上方法太多,截图里没有展开,否则影响文章篇幅)

因此

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

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

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 类型转换的内容,已经单独写了一篇文章详细地介绍了,请看 👉 Type Conversion 详解

未完待续...

参考

@toFrankie toFrankie added 前端 与 JavaScript、ECMAScript、Web 前端相关的文章 JS 与 JavaScript、ECMAScript 相关的文章 尚未完结 未完成的、未完结的文章 labels Feb 26, 2023
@toFrankie toFrankie added the 2021 2021 年撰写 label Apr 26, 2023
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
2021 2021 年撰写 JS 与 JavaScript、ECMAScript 相关的文章 前端 与 JavaScript、ECMAScript、Web 前端相关的文章 尚未完结 未完成的、未完结的文章
Projects
None yet
Development

No branches or pull requests

1 participant