| JavaScript对话面试官系列 | - JavaScript类型

128 阅读16分钟

JavaScript - 类型

参考链接

JavaScript标准参考教材

阮一峰ES6

yayu - JavaScript

起因

当我输入了大量来自优质个人博客文章,经典书籍,名牌讲师的 JavaScript 系统知识之后,我反而变得有些困惑。我明明可以流利的书写八大继承,流利的书写 Underscore 库中的防抖节流,深拷贝……,我也知晓什么是闭包,什么是迭代器,生成器……。但是问题来了,当我试图将一个 JavaScript 核心概念,比如闭包……,介绍给同学,讲解给自己,对话面试官的时候。原本自认为可以脱口而出的语言,到嘴边却显得吞吞吐吐。只能用只言片语,或者东拼西凑的知识点来表达给对方。这无疑让我有了一种,虽然花了大量时间但是却从未拥有过 JavaScript 的感觉。

目的

所以这个系列要尝试解决的问题就是当别人询问或者考察我JavaScript 核心概念的时候,我可以尽可能流畅的,清晰的表达给他人。

期望

希望掘金优秀的前端技术人员和前辈们可以在百忙之中多多补充这篇文章,多多审查这篇文章,多多提问我。我期望可以通过这个系列来解决我当前的问题

类型概述

首先想和面试官聊一聊 JavaScript 当中的类型分类,JavaScript 类型一共可以分为两大类及 8 种数据类型。

第一大类是基本数据类型,一共有 7 种: nullundefinedbooleanstringnumber, 以及 ES6 新增数据类型symbol,ES11 新增数据类型 bigInt

第二大类是引用类型是 object ,引用类型可以细分为很多子类(object 普通对象,function 函数对象,array 数组对象,math 数学对象,date 日期对象,regExp 正则对象, Promise期约对象)。


两种数据类型在内存当中的存储位置和存储的值不同:

  • 原始数据类型的值是栈存储。
  • 引用数据类型的值存储在堆(heap)中,但是引用数据类型在栈中存储了地址,该地址指向堆当中存储的引用类型。

这也是为什么原始数据类型是按值传递,按值访问,浅拷贝的时候,是直接拷贝值。

这也是为什么引用数据类型也是按值传递(因为该值是地址值),按引用访问,浅拷贝的时候,是直接拷贝地址指针。


  • Symbol的应用,主要是为了解决变量命名冲突的问题,比如React源码当中的就使用了大量的Symbol来标识React ElementReact ContextMemoProvider,ForwardRef组件。
  • BigInt是可以表示任意精度格式的整数,使用 BigInt 可以安全地存储和操作大整数,即使这个数已经超出了 Number 能够表示的安全整数范围。

为什么会有 BignInt 的提案

JavaScript 中的最大安全整数是 2 的 53 次方减 1,(64 位双精度浮点数依据 IEEE 754 来看,尾数位是 52 位)一旦超过了这个范围,JavaScript 就会出现计算不准确的情况,在涉及到一些大数的计算,有时候必须得依赖一些第三方库如 math.js等 来解决,因此官方提出了 BigInt 来解决此问题。使用 BigInt 可以安全地存储和操作大整数,即使这个数已经超出了 Number 能够表示的安全整数范围。

类型检测

typeof

typeof 一般检测简单数据类型, 返回值是简单数据类型的字符串如; "number""boolean""string" 检测引用数据类型的时候除了函数类型正常返回"function", 其余都返回"object"

  • typeof 可以检测返回 8 种数据类型也就是typeof 可以返回 8 种数据类型的字符串:undefinedbooleanstringnumberobjectfunctionsymbolbigint
console.log(typeof 1); // 'number'
console.log(typeof "1"); // 'string'
console.log(typeof function () {}); // 'function'
console.log(typeof true); // 'boolean'
console.log(typeof null); // 'object'
console.log(typeof undefined); // 'undefined'
console.log(typeof { name: "ryan" }); // 'object'
console.log(typeof new String("hello")); // 'object'
console.log(typeof new String(1)); // 'object'
console.log(typeof 120n); // 'bigint'
console.log(typeof Symbol()); // 'symbol'
// 由此可见typeof对于基本数据类型的检测是okay的。
  • typeof检测null的时候,也会返回'object', *出现这种情况的原因其实是JavaScript最初是通过 32 位 bit 来存储值,值的低三位或者低一位被当作类型标签来识别数据类型(000:被识别为对象,1:被识别为 31 位的有符号位整数,110:被 识别位布尔值,110:被识别为字符串,010:被识别为双精度的浮点数),而恰巧对象和 null 的低三位都是 000,且000的类型标签是对象,所以 null 也被处理为对象。从技术层面来将,我认为还是挺合理的,因为特殊值null*一直被我们认为是一个空对象的引用。
instanceof

instanceof 一般检测引用数据类型,返回值是布尔值,用来判断构造函数的原型对象 prototype 是否存在于实例对象的原型链上。

对于基本数据类型不能准确判断:

111 instanceof Number -> false 同样的还有"xx"true, 因为字面量值不是实例, 通过new构造函数构造出来的才是,new Number()才是

console.log('instanceof 系列');
console.log('instanceof 系列' instanceof String); // false
console.log([23] instanceof Array); // true
console.log(1 instanceof Number); // false
console.log({ name: 'ryan' } instanceof Object); // true
console.log(function () {} instanceof Function); // true
console.log(true instanceof Boolean); // false
console.log(new String('hello') instanceof String); // true
// 由此可以看出instanceof检测简单数据类型都时候都返回false, 除非你将简单值进行类包装如: new String('')
Object.prototype.toString.call()

可以检测 14 个数据类型,检测数据类型的神器

function.call(thisArg, arg1, arg2, ...) // 第一个是可选参数。
console.log(Object.prototype.toString.call(new Number(1)));
console.log(Object.prototype.toString.call('hello'));
console.log(Object.prototype.toString.call(new String('hello')));
console.log(Object.prototype.toString.call(console.log));
console.log(Object.prototype.toString({}));
console.log(Object.prototype.toString.call([]));
console.log(Object.prototype.toString.call(new Date()));
console.log(Object.prototype.toString.call(Math));
console.log(Object.prototype.toString.call(JSON));
console.log(Object.prototype.toString.call(undefined))
console.log(Object.prototype.toString.call(null))
console.log(Object.prototype.toString(null))
console.log(Object.prototype.toString(undefined))
//[object Number]
//[object String]
//[object String]
//[object Function]
//[object Object]
//[object Array]
//[object Date]
//[object Math]
//[object JSON]
//[object Undefined]
//[object Null]
// [object Object]
//[object Object]
//如果非要挑一挑毛病,那就是方法返回的字符串可能没有像typeof一样
// 值得注意的是,Object.prototype.toString(null | undefined) 返回的是 [object Object]
constructor

constructor有两个作用,一是判断数据的类型,二是对象实例通过 constrcutor 对象访问它的构造函数。

  • 需要注意。如果创建一个对象来改变它的原型,constructor就不能用来判断数据类型了。(也非常好理解,因为实例上没有constructor属性,实例找该属性是通过实例的原型对象来找的,所以当原型对象改变了,找到的构造函数就改变了
console.log((2).constructor === Number); // true
console.log((true).constructor === Boolean); // true
console.log(('str').constructor === String); // true
console.log(([]).constructor === Array); // true
console.log((function() {}).constructor === Function); // true
console.log(({}).constructor === Object); // true

类型转换

分为显示类型转换和隐式类型转换

原始值转换为布尔值

  • 原始值转换为布尔值: 只有六种值会被转换为 false, 其他都是 true.

    • 0, NaN, null, '', undefined, false

原始值转换为数字

  • 原始值转换为数字: Number()如果不传值默认返回 0, 如果传了值调用调用 ToNumber(value), ToNumber 是底层规范实现。

    参数类型结果
    undefinedNaN
    Null+0
    Boolean如果参数是 true,返回 1。参数为 false,返回 +0
    Number返回与之相等的值
    String如下段解释
  • 对于 Number(string) 会试着将包含数值字符, 加号,减号的字符串转换成浮点数或者是整数, 也可以识别 8 进制的 0 和 16 进制的 0x,会忽略前导的 0 进行转换, 如果有一个字符不是数字直接返回 NaN, 也就是不符合上述的情况直接返回 NaN.

console.log(Number(undefined)); // NaN
console.log(Number(null)); // 0
console.log(Number(1)); // 1
console.log(Number(true)); // 1
console.log(Number(NaN)); // NaN
console.log(Number("123j")); // NaN
  • 鉴于这种严格的转换数字规则, 我们一般会使用更加灵活的 parseInt()parseFloat()进行转换,他们更专注于字符串当中是否包含数值模式
  • parseInt(string, [radix]); 只解析整数, parseFloat(string, [radix])既可以解析整数也可以解析浮点数, parseIntparseFloat 都会跳过任意数量的前导空格,尽可能解析更多数值字符,并忽略后面的内容。如果第一个非空格字符是非法的数字直接量,将最终返回 NaNparseFloat会专注于浮点数的第一次出现, 第二次就会忽略浮点数
console.log(parseInt("1a2"));
console.log(parseInt("0001a2"));
console.log(parseInt("-0001a2"));
console.log(parseInt("-z0001a2"));
console.log(parseInt("+0.001a2"));
console.log(parseInt("1.001a2"));
console.log(parseFloat("1.001a2.30"));
console.log(parseInt(070));
// 1
// 1
// -1
// NaN
// 0
// 1
// 1.001
// 56

原始值转换为字符串

如果 String 函数不传参数,返回空字符串,如果有参数,调用 ToString(value),而 ToString 也给了一个对应的结果表。

参数类型结果
Undefined"undefined"
Null"null"
Boolean如果参数是 true,返回 "true"。参数为 false,返回 "false"
Numbernum.toString() 括号当中可以选择进制, 默认是 10 进制
String返回与之相等的值

null 和 undefined 没有 toString()方法, 所有我们会使用 String()方法, 如果有参数会调用 toString(), 如果参数是 null 和 undefined 返回字符串, 相当于是 toString()的增强版 -- 注意这里的 toString()和ToString(value) 不一样, 一个是对外暴露的, 一个是不对外暴露的顶层规范

  • 如果 String(对象类型)那么会调用 toPrimitive(input, String)方法,返回一个基本类型的值, 然后在通过ToString对基本类型进行转换

原始值转换为对象

可以通过构造函数 String(), Boolean(), 来将原始值转换为包装对象

对象转化为字符串

console.log(Array.prototype.toString === [1, 1].toString, "布尔值"); // true
({ a: 1 }.toString === Object.prototype.toString); // true

每个类型在调用自己身上的 toString 方法时的一些规则

toString()

从上述代码我们可以看出来,当调用对象的 toString 方法的时候, 他会调用对应对象上的 toString 方法。

  • 数组的 toString 方法将每个数组元素转换成一个字符串,并在元素之间添加逗号后合并成结果字符串。
  • 函数的 toString 方法返回源代码字符串。
  • 日期的 toString 方法返回一个可读的日期和时间字符串。
  • RegExp 的 toString 方法返回一个表示正则表达式直接量的字符串。
  • 如果 String(对象类型)那么会调用 toPrimitive(input, String)方法,返回一个基本类型的值, 然后在通过ToString对基本类型进行转换
console.log(function a() {}.toString());
console.log({ a: "ryan" }.toString());
console.log([1, 3, 34].toString());
console.log(new Date().toString());
console.log(/^a$/.toString());
//function a() {}
//[object Object]
//'1,3,34'
//Sat Jan 08 2022 12:52:09 GMT+0800 (中国标准时间)
//^a$/

valueOf

  • 另一个转换对象的函数是 valueOf,表示对象的原始值。默认的 valueOf 方法返回这个对象本身,数组、函数、正则简单的继承了这个默认方法,也会返回对象本身。日期是一个例外,它会返回它的一个内容表示: 1970 年 1 月 1 日以来的毫秒数。
console.log(function a() {}.valueOf());
console.log({ a: "ryan" }.valueOf());
console.log([1, 3, 34].valueOf());
console.log(new Date().valueOf());
console.log(/^a$/.valueOf());
// [Function: a]
// { a: 'ryan' }
// [ 1, 3, 34 ]
// 1641620212097
// /^a$/

对象转换为数字

  • 如果 Number(对象类型)那么会调用 toPrimitive(input, Number)方法,返回一个基本类型的值, 然后在通过ToNumber对基本类型进行转换

ToPrimitive()

ToPrimitive(input[, PreferredType]) []表示可选参数
  • 第一个参数是 input,表示要处理的输入值。
  • 第二个参数是 PreferredType,非必填,表示希望转换成的类型,有两个值可以选,Number 或者 String
  • 当不传入 PreferredType 时,如果 input是日期类型,相当于传入 String,否则,都相当于传入 Number
  • 如果传入的 inputUndefinedNullBooleanNumberString 类型,直接返回该值。// 如果是原始值直接返回

ToPrimitive(obj, Number)

  • 如果是 ToPrimitive(obj, Number),处理步骤如下:
  1. 如果 obj 为 基本类型,直接返回
  2. 否则,调用 valueOf 方法,如果返回一个原始值,则 JavaScript 将其返回。
  3. 否则,调用 toString 方法,如果返回一个原始值,则 JavaScript 将其返回。
  4. 否则,JavaScript 抛出一个类型错误异常。

ToPrimitive(obj, String)

  • 如果是 ToPrimitive(obj, String),处理步骤如下:
  1. 如果 obj 为 基本类型,直接返回
  2. 否则,调用 toString 方法,如果返回一个原始值,则 JavaScript 将其返回。
  3. 否则,调用 valueOf 方法,如果返回一个原始值,则 JavaScript 将其返回。
  4. 否则,JavaScript 抛出一个类型错误异常。
console.log(Number({})); // NaN
console.log(Number({ a: 1 })); // NaNconsole.log(Number([])); // 0
console.log(Number([0])); // 0
console.log(Number([1, 2, 3])); // NaN
console.log(
  Number(function () {
    var a = 1;
  })
); // NaN
console.log(Number(/\d+/g)); // NaN
console.log(Number(new Date(2010, 0, 1))); // 1262275200000
console.log(Number(new Error("a"))); // NaN

JSON.toStringify()

  • 处理基本类型时,与使用 toString 基本相同,结果都是字符串,除了 undefined
  • 布尔值、数字、字符串的包装对象在序列化过程中会自动转换成对应的原始值。
  • undefined、任意的函数以及 symbol 值,在序列化过程中会被忽略(出现在非数组对象的属性值中时)或者被转换成 null(出现在数组中时)。
// 在对象当中和在数组当中的不同表现
JSON.stringify({ x: undefined, y: Object, z: Symbol("") });
// "{}"
JSON.stringify([undefined, Object, Symbol("")]);
// "[null,null,null]"
  • JSON.stringify 有第二个参数 replacer,它可以是数组或者函数,用来指定对象序列化过程中哪些属性应该被处理,哪些应该被排除。
//数组和函数当中的不同体现
function replacer(key, value) {
  if (key == "name") {
    return undefined;
  }
  return value;
}
JSON.toStringify({ name: "ryan", age: 18 }, replacer);
var foo = { foundation: "Mozilla", model: "box", week: 45, transport: "car", month: 7 };
console.log(JSON.stringify(foo, ["week", "month"]));
// {"week":45,"month":7}
  • 如果一个被序列化的对象拥有 toJSON 方法,那么该 toJSON 方法就会覆盖该对象默认的序列化行为:不是那个对象被序列化,而是调用 toJSON 方法后的返回值会被序列化,例如:
// toJSON返回值被序列化
var obj = {
  foo: "foo",
  toJSON: function () {
    return "bar";
  },
};
JSON.stringify(obj); // '"bar"'
JSON.stringify({ x: obj }); // '{"x":"bar"}'

隐式转换

一元操作符

  • 当一元操作符 + 操作的是原始值 ==== Number(原始值)
  • 当一元操作符 + 操作的是引用类型 ==== ToNumber(引用类型) -> ToPrimitive(引用值, Number) -> 根据 ToNumber 规范求值
console.log(+["1"]); // 1
console.log(+["1", "2", "3"]); // NaN
console.log(+{}); // NaN
console.log(+"1"); // 1

二元操作符

当计算 value1 + value2 时:

  1. 0lprim = ToPrimitive(value1)
  2. rprim = ToPrimitive(value2)
  3. 如果 lprim 是字符串或者 rprim 是字符串或者都是字符串,那么返回 ToString(lprim) 和 ToString(rprim)的拼接结果
  4. 要是不满足那么就返回 ToNumber(lprim) 和 ToNumber(rprim)的运算结果
console.log(null + 1); // 1
console.log([] + []); // ''
console.log([] + {}); // '[object Object]'
console.log(1 + true); // 2
console.log({} + {}); // '[object Object][object Object]'
console.log(new Date(2017, 04, 21) + 1); // 这个知道是数字还是字符串类型就行

x == y 相等

  • 当 x 和 y 是同一类型的时候:

    • x 和 y 是 null 和 undefined 时返回 true
    • x 和 y 是字符串的时候, 只有字符串完全相同的时候返回 true, 否则返回 false
    • x 和 y 是数字的时候, 两个 NaN 在比较的时候, 返回 false, 数字相等的时候返回 true, +0 0 -0 之间的比较是 true 其他是 false
    • x 和 y 是对象的时候, 指向同一对象的时候返回 false
    • x 和 y 是布尔值的时候, 都是 false 或者都是 true 的时候返回 true 其他返回 false
  • 当 x 和 y 是不同的类型的时候

    • x 和 y 一方是 null 另一方是 undefined 的时候返回 true
    • x 和 y 有一方是字符串另一方是数字的时候, 会则会 ToNumber(字符串)来进行比较
    • x 和 y 有一方是布尔值的时候, 将布尔值转换为数字进行比较
    • x 和 y 有一方是数字或者是字符串的时候, 另一方是对象的时候. 会 Toprimitive(对象)进行比较
    • 其他返回 false
console.log(false == "0");
console.log(false == 0);
console.log(false == "");
​
console.log("" == 0);
console.log("" == []);
​
console.log([] == 0);
​
console.log("" == [null]);
console.log(0 == "\n");
console.log([] == 0);
// 都是false要值得注意的是 [null | undefined].toString() 结果是 ''[null, null, null, undefined].toString() j结果是 ',,,'

对于<>比较符

如果两边都是字符串,则比较字母表顺序:

'ca' < 'bd' // false

'a' < 'b' // true

其他情况下,转换为数字再比较:

'12' < 13 // true

false > -1 // true

而引用类型会被ToPrimitive(obj, Number)转换为基本类型再进行转换:

var a = {}

a > 2 // false

NaN 和数字比,比不出来结果

类型相关面试题

你说一下 JavaScript 中为什么 0.1+0.2 ! == 0.3?如何解决?

计算机运算器在做加和的时候,是将 10 进制的 0.1 + 0.2 转换为 2 进制的 0.1 + 0.2 进行加和。由于 0.1 和 0.2 在转换为二进制的时候会无限循环 (0.1转换为二进制,乘以二发现永远都乘不到整数),且 JavaScript Number类型的实现遵循 IEEE 754标准:1 位存符号位 sign ,11 位存指数位 exponent ,52 位使用原码来存储尾数位Fraction。所以 二进制的 0.1 和 0.2 在 52 位之后二进制位会被截掉,即在保存的时候就已经出现了精度的丢失。最后,精度丢失的两个二进制数经过对阶、尾数运算、规格化、舍入处理相加之后再转变为 10 进制也就出现了0.1 + 0.2 = 0.30000000000000004440892098500626的结果。

let n1 = 0.1, n2 = 0.2
console.log(n1 + n2)  // 0.30000000000000004
(n1 + n2).toFixed(2) // 注意,toFixed为四舍五入
//利用误差属性
function numberepsilon(arg1,arg2){                   
  return Math.abs(arg1 - arg2) < Number.EPSILON;        
}        
​
console.log(numberepsilon(0.1 + 0.2, 0.3)); // true

你说一下|| 和 && 和 ?? 操作符的返回值?

|| 和 && 首先会对第一个操作数执行条件判断,如果其不是布尔值就先强制转换为布尔类型,然后再执行条件判断。

  • 对于 || 来说,如果条件判断结果为 true 就返回第一个操作数的值,如果为 false 就返回第二个操作数的值。
  • && 则相反,如果条件判断结果为 true 就返回第二个操作数的值,如果为 false 就返回第一个操作数的值。
  • ?? 控制合并是一个逻辑运算符,当左侧的操作数为 null 或者 undefined 时,返回其右侧操作数,否则返回左侧操作数。

|| 和 && 返回它们其中一个操作数的值,而非条件判断的结果

你说一下 === 和 == 与 Object.is()的区别

  • 对于 ==(双等号) 来说,在隐式转换当中的例子当中我们也可以看到,如果 == 两边的类型不一致会先进行类型转换之后再比较,比如有布尔值就将布尔值转换为数字再进行比较,有字符串和数字就将字符串转换为数字再进行比较,
  • 对于=== (三等号) 来说,如果出现两边类型不一致的情况直接返回 false.
  • Object.is()是再三等号判断的基础上进行增强和完善,处理了特殊情况,比如在Object.is()-0+0 不相等,两个 NaN 是相等的。这在三等号和双等号中 -0+0 相等,两个 NaN 不相等。

你说一下 JavaScript 的包装类型

在 JavaScript 中,基本字面量类型是没有属性和方法的,但是为了便于操作基本类型的原始值,JavaScript 提供了三种包装类型对象,String,Number, Boolean,从而暴露出操作原始值的各种方法。比如我们调用字符串字面量的 subString()方法 (const s1 = "be kind")JavaScript 后台会先创建 String 类型的实例(const s1 = new String("be kind"))。然后调用实例上的 subString()方法供给字面量去消费。最后销毁实例(s1 = null)

object.assign和扩展运算法

两者都是浅拷贝

  • Object.assign()方法接收的第一个参数作为目标对象,后面的所有参数作为源对象。然后会合并源对象的所有属性,包括原型链的继承关系。
  • 扩展操作符(…)使用它时,数组或对象中的每一个值都会被浅拷贝到一个新的数组或对象中。但是不包括原型链的继承关系。(这个也好理解,因为每次展开数组或者展开对象都是用一个新的对象或者数组来接)