面试准备-JS类型

237 阅读16分钟

JS类型

类型分类

首先基于MDN的文档介绍,目前最新的ES中所包含官方数据类型共有8种分别是

  • Undefined
  • Boolean
  • Null
  • String
  • Symbol
  • Number
  • BigInt
  • Object

ES官方文档中,Number跟BigInt都是属于数字类型,所以在不同文章中可能也会有作者不主动区分这两种类型。

类型介绍

Undefined类型

官方大致意思Undefined 类型只有一个值那就是undefined, 任何没有赋值的变量都是undefined,我们平常获取undefined除了直接使用以外还有一个方式 就是 通过void 0获取。

void 0

首先·void,delete,typeof,+,-,~,!都是属于JS中的一元运算符, void后主要是要接一个表达式,无论后续的表达式返回什么最终都只会得到undefined

举个例子就比如:

// 箭头函数
(() => 1)() // 1
// 加上void运算符
void (() => 1) // undefined
// 常用操作
void 0 // undefined

那么为什么要用void 0来代替undefined, 我在网上查到的一些说法是因为undefined是一个可以用来使用的ReservedWord(保留字) 是可以在函数作用域下被声明的。 我个人尝试了一下,首先确实是在函数作用域下undefined是可以作为变量名进行声明并且修改

// 1
(function () {
    var undefined = 1;
    console.log(undefined);
})()

但是我在ES官方文档中却没有找到undefined作为ReservedWord(保留字) 的内容,我的个人理解是undefined实际上在ES中作为一个跟 0,1一样的值的存在,只是这个存在会根据语法的位置来进行判断是作为变量还是作为值使用,使用void 0方案从全局来看,只是为了方式代码undefined的这个特殊变量被声明,当然个人觉得通常情况下直接使用undefined应该是不会有什么问题的,除非是哪个大哥手欠哈哈哈哈

Boolean类型

官方大致意思Boolean 类型的数据代表的是一个逻辑实体,具有两种值 true 和 false, 除去truefalse的使用以外我们还会经常的直接使用1, '1', {...}, [...]来代替true以及null, undefined, '', 0来代替false, 这里其实就会涉及到数据类型的类型转换,下面给大家列举一些常见的转Boolean的场景

类型转换
// true
Boolean(1)         // true 同理BigInt
Boolean(2)         // 不为0的数字都是true
Boolean(Infinity)  // true
Boolean(-Infinity) // true
Boolean(-Infinity) // true
Boolean(() => {})  // true
Boolean([])        // true
Boolean([1])       // true
Boolean({})        // true
Boolean('123')     // true
Boolean(Symbol())  // true

// false
Boolean(NaN)      // false
Boolean(0)        // false
Boolean('')       // false
Boolean()         // false
Boolean(undefined)// false
Boolean(null)     // false

Null类型

官方大致意思Null类型只有只有一个值那就是null, 我个人的场景是有时候我们在设置默认值的时候会用到null用来表示变量还没有赋值或者在清除变量值时会用到,其他场景较少使用

String类型

官方大致意思String 是一个由一个由0或多个无符号的元素组成的有序序列,最长不超过2^53 - 1,每个元素通常会当作UTF-16 代码单元值(后面的就不翻译了,很复杂 有兴趣自己看),其中也包含了如果混乱的情况下,String会如何解析字符串,以及normalize(...), localeCompare(...), indexOf(...)函数运行介绍。

Symbol类型

官方大致意思Symbol类型可以做Object类型的Key值,每个Symbol都是唯一的,每个Symbol都有一个不可变的的[[Description]]属性,这个属性可以是undefined 或者 字符串,并且Symbol也挂载了一些操作的接口来让大家可以需求对部分语言行为进行调整,详情可以看这里

Number类型

官方大致意思 Number 类型采用双精度 64 位格式 IEEE 754-2019 值,不在IEEE标准下的会被标记为NaN,NaN跟NaN之间是无法辨别的,当然其中还包含一些运算规则,感兴趣的小伙伴可以看这里

BigInt类型

官方大致意思BigInt 类型表示一个整数值。该值可以是任何大小,并且不限于特定的位宽。通常,除非另有说明,否则操作旨在返回精确的基于数学的答案。对于二进制运算,BigInts 充当二进制补码字符串,负数被视为具有无限向左设置的位。

Object类型

官方大致意思:

属性

Object是一个属性集合体,存在两个不同的属性。一种是数据属性,一种是访问器属性。

  • 数据属性是ES所有数据类型以及数据值跟跟KEY的关联
  • 访问器属性包括两个函数跟一些boolean的叙述属性,来控制Object上的数据属性的存储,访问,检索等。

Objectd的Key值可以是String 或者 Symbol类型,空的字符串也可以作为一个有效的Key值,字符型的Key也叫属性名。

Key可以用来访问对象的属性值,访问会给予两个不同类型的访问属性的限制set(...)get(...),分别会限制数据属性的获取设置,通过set(...)get(...)既可以访问属性自身的数据属性,也可以访问原型上的数据属性,对象自己的属性一定要有Key,每个Key都是唯一的。

  • 对象上属性的默认属性(或者特征更加合适?)
属性名所属的属性数据类型默认值叙述
[[Value]]数据属性所有ES的数据类型undefined由访问器属性GET可以访问到的数据
[[Writable]]数据属性Booleanfalse如果为false的情况下,Set访问器属性修改[[value]]将不会成功
[[Get]]访问器属性Object(function) 或者 undefinedundefined如果当前访问器属性存在,拿它一定是函数对象,当前函数对象会使用他的[[Call]]属性(也就是正常调用), 调用是没有参数的。每一次的属性访问都会经过这个访问器属性
[[Set]]访问器属性Object(function) 或者 undefinedundefined如果当前访问器属性存在,拿它一定是函数对象,当前函数对象会使用他的[[Call]]属性(也就是正常调用),有一个唯一的参数(设置值),[[Set]]属性可能会对[[Get]]属性产生一定的影响
[[Enumerable]]访问器属性 或者 数据属性Booleanfalse如果为true那么这个属性就可以通过for-in进行枚举
[[Configurable]]访问器属性 或者 数据属性Booleanflase如果为false的话, 删除,修改属性的访问器属性,将数据属性变成访问器属操作都会失败
内部方法以及插槽

对象的行为等都是由内部方法的算法所定义的行为所决定的,这些内部方法并不属于ES语言中的内容,是属于叙述规范也就是官方文档的内容。但是ES中的对象必须严格的执行这些内部方法

内部方法是多态,当前对象就是调用内部方法的target,如果当前对象没有办法作为内部方法的调用对象,就会抛出一个TypeError的报错

内部插槽是用来定义跟对象相关的内部状态的并部署对象的属性,并不可以被继承或者是动态修改,它既可以是ES的数据类型,也可以是ES的叙述数据类型,内部插槽是在对象初始化的时候自动分配的,通常都是undefined, 在ES的语言规范中,并没有提供直接操作内部插槽的方法。

所有的对象都有一个[[PrivateElements]]的内部插槽,属于叙述类型中的[[List]],这个列表用来定义私有的属性,方法还有对象访问器。 初始化是一个空列表。

对于一个对象中的普通对象定义如下:

  • 对于下列表格的内部方法使用,通过10.1定义使用
  • 对于具有[[Call]]的属性,使用10.2.1的定义使用
  • 对于具有[[Construct]], 使用10.2.2的定义使用

一个怪异对象它是一个对象,但是不是普通对象

本规范用内部对象来识别不同的怪异对象,比如一些行为类似数组或绑定对象的函数,虽然行为怪异,但是他们内部方法没有变,所以他们也不是怪异对象

  • 基本的内部方法
方法名声明叙述
[[GetPrototypeOf]]( )  Object | Null用来获取继承的属性,如果为空就是没有继承属性
[[SetPrototypeOf]](Object | Null)  Boolean把当前对象设置为另一个对象的字对象,如果放回true就是成功继承
[[IsExtensible]]( )  Boolean查询当前对象拓展新的属性,false为不可拓展
[[PreventExtensions]]( )  Boolean用来控制对象是否允许拓展新的属性,true为不可拓展
[[GetOwnProperty]](propertyKey Undefined | Property Descriptor用来获取对象中属性的叙述属性如果不存在就返回 undefined
[[DefineOwnProperty]](propertyKeyPropertyDescriptor Boolean为对象创建,更改他的叙述属性
[[HasProperty]](propertyKey Boolean判断自身属性或者继承属性中是否存在当前属性
[[Get]](propertyKeyReceiver any返回这个对应的属性值,如果要执行ES代码来获取数据那么Receiver在评估代码时作为this存在(怀疑是不是跟function有关)
[[Set]](propertyKeyvalueReceiver Boolean设置这个propertyKeyvalue,如果设置过程执行任何ES的代码,那么Receiver在评估代码时作为this存在(怀疑是不是跟function有关)
[[Delete]](propertyKey Boolean移除对象上对应的属性值
[[OwnPropertyKeys]]( )  List of property keys返回一个对象自身key的列表

函数对象拥有内部方法[[Call]],构造对象拥有内部方法[[Construct]], 需要支持[[Construct]]就必须要支持[[Call]],所以每个构造对象都必须是一个函数对象,构造对象也可以叫构造函数对象。

  • 基础的函数对象内部方法
方法名声明叙述
[[Call]](any, a List of any any执行与此对象关联的代码。通过函数调用表达式调用。内部方法的参数是 this 和 以及参数列表。实现此内部方法的对象是可调用的。
[[Construct]](a List of any, Object)  Object创建一个对象。通过 new 或 super 调用。第一个参数是构造函数入参数或者是super的如参。第二个参数是最初应用 new 运算符的对象。实现此内部方法的对象称为构造函数。函数对象不一定是构造函数,并且此类非构造函数对象没有 [[Construct]] 内部方法。

普通对象跟标准怪异对象的定义在这里

内部方法的常量

本规范中的普通 ECMAScript 对象以及所有标准怪异对象都保持这些常量。 ECMAScript 代理对象通过运行时检查在 [[ProxyHandler]] 对象上调用的 invoked 的结果来维护这些常量。

标准怪异对象也必须维护这些常量,如果违反这些常量很可能会导致不安全的问题出现。但是无论怎么违反常量,都不会引起内存安全问题

详细内容

常见固有对象

(确实太硬核有点啃不动)

内在名称全剧名称ES介绍
%AggregateError%AggregateErrorThe AggregateError constructor (20.5.7.1)
%Array%ArrayThe Array constructor (23.1.1)
%ArrayBuffer%ArrayBufferThe ArrayBuffer constructor (25.1.3)
%ArrayIteratorPrototype%The prototype of Array iterator objects (23.1.5)
%AsyncFromSyncIteratorPrototype%The prototype of async-from-sync iterator objects (27.1.4)
%AsyncFunction%The constructor of async function objects (27.7.1)
%AsyncGeneratorFunction%The constructor of async iterator objects (27.4.1)
%AsyncIteratorPrototype%An object that all standard built-in async iterator objects indirectly inherit from
%Atomics%AtomicsThe Atomics object (25.4)
%BigInt%BigIntThe BigInt constructor (21.2.1)
%BigInt64Array%BigInt64ArrayThe BigInt64Array constructor (23.2)
%BigUint64Array%BigUint64ArrayThe BigUint64Array constructor (23.2)
%Boolean%BooleanThe Boolean constructor (20.3.1)
%DataView%DataViewThe DataView constructor (25.3.2)
%Date%DateThe Date constructor (21.4.2)
%decodeURI%decodeURIThe decodeURI function (19.2.6.2)
%decodeURIComponent%decodeURIComponentThe decodeURIComponent function (19.2.6.3)
%encodeURI%encodeURIThe encodeURI function (19.2.6.4)
%encodeURIComponent%encodeURIComponentThe encodeURIComponent function (19.2.6.5)
%Error%ErrorThe Error constructor (20.5.1)
%eval%evalThe eval function (19.2.1)
%EvalError%EvalErrorThe EvalError constructor (20.5.5.1)
%FinalizationRegistry%FinalizationRegistryThe FinalizationRegistry constructor (26.2.1)
%Float32Array%Float32ArrayThe Float32Array constructor (23.2)
%Float64Array%Float64ArrayThe Float64Array constructor (23.2)
%ForInIteratorPrototype%The prototype of For-In iterator objects (14.7.5.10)
%Function%FunctionThe Function constructor (20.2.1)
%GeneratorFunction%The constructor of Generators (27.3.1)
%Int8Array%Int8ArrayThe Int8Array constructor (23.2)
%Int16Array%Int16ArrayThe Int16Array constructor (23.2)
%Int32Array%Int32ArrayThe Int32Array constructor (23.2)
%isFinite%isFiniteThe isFinite function (19.2.2)
%isNaN%isNaNThe isNaN function (19.2.3)
%IteratorPrototype%An object that all standard built-in iterator objects indirectly inherit from
%JSON%JSONThe JSON object (25.5)
%Map%MapThe Map constructor (24.1.1)
%MapIteratorPrototype%The prototype of Map iterator objects (24.1.5)
%Math%MathThe Math object (21.3)
%Number%NumberThe Number constructor (21.1.1)
%Object%ObjectThe Object constructor (20.1.1)
%parseFloat%parseFloatThe parseFloat function (19.2.4)
%parseInt%parseIntThe parseInt function (19.2.5)
%Promise%PromiseThe Promise constructor (27.2.3)
%Proxy%ProxyThe Proxy constructor (28.2.1)
%RangeError%RangeErrorThe RangeError constructor (20.5.5.2)
%ReferenceError%ReferenceErrorThe ReferenceError constructor (20.5.5.3)
%Reflect%ReflectThe Reflect object (28.1)
%RegExp%RegExpThe RegExp constructor (22.2.3)
%RegExpStringIteratorPrototype%The prototype of RegExp String Iterator objects (22.2.7)
%Set%SetThe Set constructor (24.2.1)
%SetIteratorPrototype%The prototype of Set iterator objects (24.2.5)
%SharedArrayBuffer%SharedArrayBufferThe SharedArrayBuffer constructor (25.2.2)
%String%StringThe String constructor (22.1.1)
%StringIteratorPrototype%The prototype of String iterator objects (22.1.5)
%Symbol%SymbolThe Symbol constructor (20.4.1)
%SyntaxError%SyntaxErrorThe SyntaxError constructor (20.5.5.4)
%ThrowTypeError%function object that unconditionally throws a new instance of %TypeError%
%TypedArray%The super class of all typed Array constructors (23.2.1)
%TypeError%TypeErrorThe TypeError constructor (20.5.5.5)
%Uint8Array%Uint8ArrayThe Uint8Array constructor (23.2)
%Uint8ClampedArray%Uint8ClampedArrayThe Uint8ClampedArray constructor (23.2)
%Uint16Array%Uint16ArrayThe Uint16Array constructor (23.2)
%Uint32Array%Uint32ArrayThe Uint32Array constructor (23.2)
%URIError%URIErrorThe URIError constructor (20.5.5.6)
%WeakMap%WeakMapThe WeakMap constructor (24.3.1)
%WeakRef%WeakRefThe WeakRef constructor (26.1.1)
%WeakSet%WeakSetThe WeakSet constructor (24.4.1)

类型判断

在大多数场景下,类型判断是我们经常要处理的事情,在这里我也回顾一下几个常见判断方法

typeof

typeof作为官方的一元运算符,基本的使用方式如下:

typeof 1 // 'number'
typeof(1) // 'number'

那么typeof的全部执行结果如下:

TargetResult
Undefined"undefined"
Null"object"
Boolean"boolean"
Number"number"
String"string"
Symbol"symbol"
BigInt"bigint"
Object (does not implement [[Call]])"object"
Object (implements [[Call]])"function"

我们可以看到总共8种结果,其中Null 跟 Object 的结果是一样的,使用typeof运算符存在以下的2个问题(个人提出的):

  1. 如果直接使用typeof 是没有办法直接区分Null类型跟Object类型的,可能是需要再去判断是否跟null相等
  2. 许多Object的固有对象是没有办法直接判断的,比如Array, Map, Set等

Object.prototype.toString

这个是对象上的字符串方法,但是可以通过修改不同的this指向,来达到类型判断的效果。toString(...)方法的内部实现是通过判断this指向,以及所包含的内部插槽的类型来进行类型判断,如果没有特定的内部插槽再根据对象预制的symbol.toString()实现来获取类型最终生成一个"[object , * ]"的字符串。

简单案例:

Object.prototype.toString.call([]) // '[object Array]'
Object.prototype.toString.call(function() {}) // '[object Function]'
Object.prototype.toString.call(new Map()) // '[object Map]'
Object.prototype.toString.call(new Set()) // '[object Set]'

当然为了使用方便也可以通过以下方式进行封装

const getValType = (val) => {
   return Object.prototype.toString.call(val)
   .replace(/\[object|\]|\s/g, '')
   .toLowerCase()
}
getValType([]) // 'array'

instanceof

instanceof是ES中的操作符,主要是用来判断某个构造函数是否在对象的原型链上,使用如下

({} instanceof Object) // true
(() => {}) instanceof Function // true

使用 instanceof 也存在几个问题

  1. instanceof操作符的适用基本只适终于对象的类型判断, 针对基本类型的判断是有一些小的问题的,比如以下情况
1 instanceof Number // false
'' instanceof String // false
Object(1) instanceof Number // true
Object('') instanceof String // true

所以我们常见的一些场景可能会有所限制(当然可能是我垃圾导致的)

  1. instanceof因为判断的是原型链上是否存在当前对象的构造函数,所以针对大部分的Object的固有对象判断优先级就非常重要了
(new Map()) instanceof Map // true
(new Map()) instanceof Object// true

constructor

通过判断对象的构造函数来进行溯源,因为对象的数据属性是可以获取到继承来的数据属性的,再加上引用类型的特性,可以通过===来进行判断

// false
(new Map()).constructor === Object
// true
(new Map()).constructor === Map
// true
''.constructor === String
// true
(1).constructor === Number

但是这种方法的缺点也还是比较明显的,就是针对undefined,null还是不能判断,所以如果要使用这种方式进行封装就需要有限判断empty值,然后根据需要来进行不同构造函数的判断

=====

目前我们大部分的工作场景下都会使用 ===!==,但是这里还是把场景都列出来,毕竟不怕一万就怕问到

a === b实际上针对两边的变量进行了类型以及值的判断,其大致分为4个阶段

  1. 判断 a 的类型是否跟 b的类型一致 否则返回 false

  2. 如果是Number类型 那么如下判断

    2.1 a 或者 b 任意为NaN那么返回false

    2.2 如果两个值相等 返回true

    2.3 如果 a和b分别为 -0 跟 +0 那么返回 true(无关顺序)

    2.4 否则返回false

  3. 如果是BigInt 那么直接判断两个值是否相等

  4. 如果是String就判断序列中的所有节点是否相等

  5. 其他类型就是判读是否是同一个值

这里多说一句,引用类型的是否为同一个值指的是是否为同一个引用

a == b的整体步骤就会长的多,如下所示

  1. 如果ab的类型相等,直接使用严格相等判断也就是===的结果

  2. 如果 ab 任意为 nullundefined 也是 true

  3. 如果 ab 任意为 Object 并且当前 Object具有[[IsHTMLDDA]]的内部插槽 ,另外一个值任意为 nullundefined 返回 true

  4. 如果 abNumberString类型, 那么把String转成Number类型,然后再进行a == b的比较

  5. 如果 abBigIntString类型, 那么把String转成BigInt类型,然后再进行比较

  6. 如果 ab 为 任意为 Boolean类型, 那么把 Boolean 转换成 Number类型,然后再进行比较

  7. 如果 abObject[String, Number, BigInt, or Symbol]其中一个,那么Object的值先去做获取获取初始值,再进行对比。

    取初始数据的过程:

    7.1 如果有用Symbol.toPrimitive进行定义,那么就是定义的函数返回结果

    7.2 如果是用原始定义的的转换,就是先调用valueOf(..)然后调用toString(...)当然也有反过来的场景,总之理论上最终得到的就是一个字符串

  8. 如果 abBigIntNumber类型, 如果Number类型为NaN, +∞𝔽, -∞𝔽, 那么就返回false,否则如果两个的值相等就返回true

  9. 返回 false

那么看完了这个过程我们就来看一个经典的面试题目

[] == ![] // true

这个过程是怎么样的呢?整个步骤列在下面

  1. 首先!先对[]进行布尔取值,然后取反,得到false
  2. 然后根据上面第6条任何的Boolean==都会吧Boolean转换为Number, 这里我们得到数字0
  3. 然后根据上述7条,获取[]的初始值,无论是[].valueOf().toString()还是[].toString().valueOf()都得到''
  4. 然后根据上面的第4条,把''转成数字类型 得到0
  5. 根据第一条进行严格对比, 0 === 0 所以为true

完美!

下一章: 面试准备-声明