前端面试——JavaScript面经(持续更新中)

112 阅读13分钟

一、数据类型

1. JavaScript用哪些数据类型、它们有什么区别?

JavaScript共有八种数据类型,分别包括5种基本数据类型和3种非基本数据类型。

  • 基本数据类型:UndefinedNullBooleanNumberString
  • 非基本数据类型:ObjectSymbolBigInt

其中SymbolBigInt是ES6新增的数据类型:

  • Symbol代表创建后独一无二且不可变的数据类型,它主要是为了解决可能出现的全局变量冲突的问题。
  • BigInt是一种数字类型的数据,它可以表示任意精度格式的整数,使用BigInt可以安全地存储和大整数,即使这个数超出了Number的安全整数范围。

区别一:分为原始(基本)数据类型和引用数据类型

  • 原始数据类型:UndefinedNullBooleanNumberString
  • 引用数据类型:Object。另外还有数组Array和函数Function

原始(基本)数据类型和引用数据类型有一个很明显的区别是:引用类型有自己内置的方法,也可以自定义其他方法来操作数据,而基本数据类型不能像引用类型那样有自己的内置方法对数据进行更多的操作。因此,为了操作基本类型值,ES提供了3个特殊引用类型,也就是基本包装类型:Number、Boolean、String,关于包装类型将在本章第8节详细讲解。

区别二:存储位置不同

  • 原始数据类型直接存储在栈(stack) 中,往往占据空间小、大小固定、属于被频繁使用数据,所以放在栈中
  • 引用数据类型存储在堆(heap) 中,往往占据空间大、大小不固定,如果存在栈中将会影响程序运行的性能。因此,引用数据类型在栈中存储了指针,指针指向堆中该实体的起始地址。当解释器寻找引用值时,会先检索其在栈中的地址,再根据地址从堆中获得实体

扩展知识:堆与栈

堆和栈的概念存在于数据结构和操作系统内存中。

  • 在数据结构中:

    • 栈:先进后出
    • 堆:先进先出
  • 在操作系统中分为堆区和栈区:

    • 栈区内存由编译器自动分配释放,存放函数的参数值,局部变量的值等。操作方式类似于数据结构中的栈。
    • 堆区内存一般由开发者分配释放,若开发者不释放,程序结束时可能由垃圾回收机制回收。

2. 数据类型的检测方式有哪些?详细讲讲其原理

2.1 typeof

 // 1.typeof 数组、对象和null都会被视为object 其他类型都判定正确
 console.log(typeof {}); // object
 console.log(typeof []); // object
 console.log(typeof null); // object
 console.log(typeof function () {}); // function
 console.log(typeof 1); // number
 console.log(typeof true); // boolean
 console.log(typeof "str"); // string
 console.log(typeof undefined); // undefined
 console.log(typeof Symbol()); // symbol
 console.log(typeof NaN)  // number

根据上面的结果可以看到数组、对象和null都会被视为object,其他类型都能判定正确。

2.2 instanceof

它的原理是:判断在其原型链中能否找到该类型的原型

 console.log(2 instanceof Number); // false
 console.log(true instanceof Boolean); // false
 console.log("str" instanceof String); // false
 ​
 console.log([] instanceof Array); // true
 console.log(function () {} instanceof Function); // true
 console.log({} instanceof Object); // true
 console.log(null instanceof Object); // false  这是因为null是原型链的尽头 它没有后续原型链 更不可能找到Object类型的原型

根据上面的结果可以看到 instanceof只能正确判断引用类型,基本数据类型无法判定。

需要注意的是:null instanceof Object结果是false,因为null是原型链的尽头,它没有后续原型链,更不可能找到Object类型的原型。

instanceof原理的代码如下【手搓instanceof源码】:

 function isInstance(data, type) {
   // 获取变量的原型
   let proto = Object.getPrototypeOf(data);  // Object.getPrototypeOf()此静态方法返回对象的原型
   // 获取对应类型的构造函数的 prototype 对象
   let prototype = type.prototype;
 ​
   // 判断构造函数的 prototype 对象是否在变量的原型链上
   while (true) {
     // 变量的原型链已经找到尽头了还没有找到与变量原型相匹配的对应类型prototype对象 返回false
     if (!proto) return false;
     // 找到了匹配的prototype对象 返回true
     if (proto === prototype) return true;
     // 继续查找原型链
     proto = Object.getPrototypeOf(proto);
   }
 }
 ​
 console.log(
   isInstance({}, Object), // true
   isInstance(new Date(), Date), // true
   isInstance("str", String), // true
   isInstance(true, Boolean) // true
 );

2.3 constructor

它的原理是:除了null之外,任何对象都会在其prototype/__proto__上有一个constructor属性,而constructor属性返回一个引用,这个引用指向创建该对象的构造函数,而NumberBooleanStringArray都属于构造函数。 因此通过construct和构造函数就能判断类型是否符合。

 console.log((2).constructor);  // ƒ Number() { [native code] }  Number构造函数
 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
 // console.log((null).constructor === Object);  // 会报错 原因是null不存在constructor
 // console.log((undefined).constructor === Object);  // 会报错 原因是undefined不存在constructor
 // console.log((null).constructor); // 会报错
 // console.log((undefined).constructor); // 会报错

从上面的结果看到,constructor除了不能判断nullundefined外,其它类型都能判断

需要注意的是,如果创建的对象的原型被改变了,constructor就不能用来判断数据类型了。

 function Fn() {}
 console.log(Fn.prototype.constructor); // f Fn() {}
 Fn.prototype = new Array()
 console.log(Fn.prototype.constructor); // ƒ Array() { [native code] }
 let f = new Fn();
 console.log(f.constructor); // ƒ Array() { [native code] }
 console.log(f.__proto__); // ƒ Array() { [native code] }
 console.log(f.constructor === Fn); // false
 console.log(f.constructor === Array); // true

扩展知识:constructor的两个作用:

  1. 判断数据类型。
  2. 对象实例通过construct对象访问它的构造函数。

2.4 Object.prototype.toString.call()

它的原理是:对象原型上的toString方法会获取当前对象的类型然后返回[object Type]字符串,由于部分内置对象对toString重写了,因此需要调用.call()来利用原本的toString函数,.call(args)方法实现让调用call方法的对象的this指向传的参数args

 let a = Object.prototype.toString;
 console.log(a.call(2)); // [object Number]
 console.log(a.call(2) == Number); // false
 console.log(a.call(2) == "[object Number]"); // true
 ​
 console.log(a.call(true)); // [object Boolean]
 console.log(a.call(true) == Boolean); // false
 console.log(a.call(true) == "[object Boolean]"); // true
 ​
 console.log(a.call("str")); // [object String]
 console.log(a.call("str") == String); // false
 console.log(a.call("str") == "[object String]"); // true
 ​
 console.log(a.call(new Date())); // [object Date]
 console.log(a.call(new Date()) == Date); // false
 console.log(a.call(new Date()) == "[object Date]"); // true
 ​
 console.log(a.call([])); // [object Array]
 console.log(a.call(function () {})); // [object function]
 console.log(a.call({})); // [object Object]
 console.log(a.call(undefined)); // [object undefined]
 console.log(a.call(null)); // [object Null]

通过上面代码可以看到,Object.prototype.toString.call()可以验证任何类型。

2.5 封装一个类型验证的方法

大型项目中往往会使用Object.prototype.toString.call()封装一个isType方法来验证类型,封装代码如下:

 function isType(data, type) {
   const typeObj = {
     "[object String]": "string",
     "[object Number]": "number",
     "[object Boolean]": "boolean",
     "[object Null]": "null",
     "[object Undefined]": "undefined",
     "[object Object]": "object",
     "[object Array]": "array",
     "[object Function]": "function",
     "[object Date]": "date", // Object.prototype.toString.call(new Date())
     "[object RegExp]": "regExp",
     "[object Map]": "map",
     "[object Set]": "set",
     "[object HTMLDivElement]": "dom", // document.querySelector('#app')
     "[object WeakMap]": "weakMap",
     "[object Window]": "window", // Object.prototype.toString.call(window)
     "[object Error]": "error", // new Error('1')
     "[object Arguments]": "arguments",
   };
   let name = Object.prototype.toString.call(data); // 借用Object.prototype.toString()获取数据类型
   let typeName = typeObj[name] || "未知类型"; // 匹配数据类型
   return typeName === type; // 判断该数据类型是否为传入的类型
 }

下面我们可以测试一下封装结果:

 console.log(
   isType({}, "object"), // true
   isType([], "array"), // true
   isType(new Date(), "object"), // false
   isType(new Date(), "date") // true
 );

2.6 总结

方法名效果
typeof数组、对象和null都会被视为object,其他类型都能判定正确
instanceof只能正确判断引用类型,基本数据类型无法判定
constructor除了不能判断nullundefined外,其它类型都能判断
Object.prototype.toString.call()可以判断所有类型,但是返回结果是字符串【最推荐,封装isType

3. 判断数组的方式有哪些?

 let arr = []

0. Object.prototype.toString.call(arr).slice(8,-1) === 'Array'或者Object.protoType.toString.call(arr) === '[object Array]'

  1. 通过原型链判断:arr.__proto__ === Array.prototype
  2. 通过Array.isArray(arr)
  3. 通过arr instanceof Array

4. null、undefined、Null有什么区别?

  1. 含义不同:undefined代表的含义是未定义,而null代表的含义是空对象NaN表示不是一个数字,用于指出数字类型中的错误情况,通过执行数学运算没有成功时返回NaN
  2. 初始化场景不同:通常变量声明了但还没有定义的时候使用undefinednull主要用在初始化一些可能会返回对象的变量,NaN不用于初始化。
  3. typeof判断结果不同:typeof undefined返回undefinedtypeof null返回objecttypeof NaN返回number

一般变量声明了但还没定义的时候会返回undefined

需要注意的是:

  1. 使用null == undefined 返回true,null === undefined返回false
  2. NaN与自身不相等,NaN == NaNNaN === NaN得到的结果都是false

5. 为什么0.1+0.2 !== 0.3,怎么才能让它们相等

大白话

首先:因此js将数据转为二进制后处理数据【要点一】0.1转化为二进制为:0.0001 1001 1001 1001无限循环..,0.2转化为二进制为:0.001 1001 1001 1001(无限循环)

又因为js的Number类型遵循IEEE754标准64位存储【要点二】 ,IEEE754标准64位内只有52位来表示小数,有很多小数转为二进制后存储无限位数,如果第53位为1的话,只保留52为就会进位【要点三】,从而导致精度丢失【第一次精度丢失】

而后进行二进制相加的时候,也可能会存在进位的问题,进而导致精度丢失【第二次精度丢失】 ,最后相加得到的二进制结果转化为数字就会与我们平常相加得到的结果有偏差。

解决方法:

  1. 将两数转换为整数,在相加后转回小

     let x = (0.1 * 10 + 0.2 * 10) / 10
     console.log(x === 0.3)  // true
    
  2. 使用toFixed方法配合parseFloat方法

     console.log(parseFloat((0.1 + 0.2).toFixed(1)) === 0.3)  // true
    
  3. 根据真实结果减去预测结果是否小于Number.EPSILON

    在ES6中,提供了Number.EPSILON属性,它的值为2^-52

     console.log((0.1 + 0.2) - 0.3 < Number.EPSILON)  // true
    

下面详细讲讲:

Number类型遵循的IEEE754 64位标准,也就是双精度浮点数(double)存储,它为每个数值分配64位存储空间,以科学计数法的方式存储。64位分配如下:1位符号位,11位指数位,剩余52位为小数位。

image.png

这里以0.1为例:

image.png

6. == 操作符的强制转换规则是怎么样的?

==在比对双方类型不一样时,会进行类型转换。

其中包括:

  • string转为number
  • boolean转为number
  • object转为字符串[object Object]

判断流程如下:

  1. 先判断两者类型是否相同,同则比较大小
  2. 不同进行类型转换
  3. 先判断是否在比对nullundefined,是的话返回true
  4. 接着按上述三个点的顺序类型转化

7. 显式类型转换

转为Number类型:

 console.log(Number(undefined));  // NaN
 console.log(Number(null));  // 0
 console.log(Number(true));  // 1
 console.log(Number('ad'));  // NaN
 console.log(Number('11'));  // 11
 console.log(Number('11a'));  // NaN
 console.log(Number(''));  // 0
 console.log(Number({}));  // NaN
 console.log(Number({a: 1}));  // NaN
 console.log(Number([]));  // 0
 console.log(Number([1, 2]));  // NaN

值得注意的是:undefined转为Number类型的结果是NaN

转为Boolean类型:

 console.log(Boolean(undefined));  // false
 console.log(Boolean(null));  // false
 console.log(Boolean(NaN));  // false
 console.log(Boolean(false));  // false
 console.log(Boolean(+0));  // false
 console.log(Boolean(-0));  // false
 console.log(Boolean(''));  // false

转为String类型:

 console.log(String(undefined));  // 'undefined'
 console.log(String(null));  // 'null'
 console.log(String(true));  // 'true'
 console.log(String(1));  // '1'
 console.log(String({}));  // '[object Object]'
 console.log(String([]));  // '

转为Object类型:

 console.log(Object(undefined));  // {}
 console.log(Object(null));  // {}
 console.log(Object(true));  // {Boolean: true}
 console.log(Object(1));  // {Number: 1}
 console.log(Object(''));  // {String: ''}
 console.log(Object([]));  // []
 console.log(Object({}));  // {}

8. 什么是JavaScript中的包装类型?

在JavaScript中,基本类型不像引用类型那样,它是没有属性和方法的,因此为了更便于操作基本类型的值,ECMAScript提供了3个特殊引用类型,也就是包装类型:BooleanNumberString

在调用基本类型的方法或者属性时,JavaScript会在后台隐式的将基本类型的值转换为对象,如:

 const a = 'abc'
 a.length  // 3

在访问'abc'.length时,JavaScript将'abc'在后台转换为String('abc'),然后再访问其length属性。

也可以使用Object函数显式转换基本类型为包装类型:

 let b = 'abc'
 let c = Object(b)
 // Object显式将基本类型转换为包装类型
 console.log(typeof c);  // object
 console.log(c);  // String {'abc'}
 // valueof将包装类型转回基本类型
 let d = c.valueOf()
 console.log(typeof d);  // string
 console.log(d);  // abc

需要注意的是:

 let a = new Boolean(false)
 console.log(typeof a);  // object
 console.log(typeof Boolean(false));  // boolean
 if (!a) {
     console.log('实例化');  // 不输出
 }
 if(!Boolean(false)){
     console.log('非实例化');  // '非实例化'
 }

new过的实例对象被包裹成包装类型后就成了对象,所以其非值为false,因此判断不成立所以不输出。

9. 隐式类型转换

+:两边至少有一个string类型时,两边变量都会被隐式转换为string,其他情况两边变量都转为number

 console.log(1 + '23');  // '123'
 console.log(2 + true);  // 3
 console.log('1' + false);  // '1false'
 console.log(false + true); // 1

- * :当变量为对象或者有长度大于1的子孙元素的数组时,变量会被视为NaN(其实NaN也算是数字),因此得到的结果为NaN,其余情况都转为number类型。

 console.log(2 * '23');  // 46
 console.log(2 * true);  // 2
 console.log(2 * {});  // NaN
 console.log(2 * []);  // 0
 console.log(2 * [1]);  // 2
 console.log(2 * [1, 3]);  // NaN
 console.log('------------');
 console.log(2 - '23');  // -21
 console.log(2 - true);  // 1
 console.log(2 - {});  // NaN
 console.log(2 - []);  // 2
 console.log(2 - [1]);  // 1
 console.log(2 - [1, 3]);  // NaN
 console.log(2 - [[1]]);  // 1
 console.log(2 - [[1, 2]]);  // NaN

==:两边都转为number,同理{}或者有长度大于1的子孙元素的数组会被判定为NaN,计算结果自然为false

> <:两边都是字符串,按字母表顺序比较,其他情况转为数字再比较。

10. 判断一个对象是空对象有哪些方法?

  • 静态方法Object.keys(obj).length == 0

     let obj5 = {};
     console.log(Object.keys(obj5).length == 0);  // true
    
  • 转换为JSON字符串后与'{}'比对

     let obj6 = {};
     console.log(JSON.stringify(obj6) == '{}');  // true
    

11. Object.assign、扩展运算符是深拷贝还是浅拷贝?

 let obj1 = {
   child1: { a: 1, b: 2 },
 };
 let obj2 = { ...obj1 };
 // 修改
 obj2.child1.a = 3;
 console.log(obj1); // { child1: { a: 3, b: 2 } }
 console.log(obj2); // { child1: { a: 3, b: 2 } }

由此可得:扩展运算符是浅拷贝

 let obj3 = {
   child1: { a: 1, b: 2 },
 };
 let obj4 = Object.assign({}, obj3);
 // 修改
 obj4.child1.a = 3;
 console.log(obj3); // { child1: { a: 3, b: 2 } }
 console.log(obj4); // { child1: { a: 3, b: 2 } }

由此可得:Object.assign()是浅拷贝

二、ES6

1. 详细说说let、const、var的区别

  1. 块级作用域:let和const都具有块级作用域(由{}包裹的区域),var不存在块级作用域。

    块级作用域解决了ES5中的两个问题:

    • 内层变量覆盖外层变量
    • 用于计数的循环变量泄漏为全局变量
  2. 变量提升:var存在变量提升,即变量和函数的声明会在物理层面移动到代码的最前面,因此可以先使用变量后声明,而let和const都不存在变量提升,即变量必须在声明之后才能使用,否则会报错。

  3. 重复声明:var定义的变量可以重新声明,新声明的会覆盖旧声明的,let和const定义的变量不允许在块级范围内重新声明。

  4. 给全局添加属性:var声明的变量会添加为全局对象上的属性,let和const并不会。

     let a_let = {}
     const a_const = {}
     var a_var = {}
     console.log(window);  // 浏览器环境下的全局对象 node环境下输出会报错
     console.log(globalThis);  // node环境下的全局对象 浏览器环境下输出得到的是window
    

    代码运行结果展示如下: image.png

  5. 初识值设置:var和let声明的变量可以不设置初始值,而const设置的变量必须设置初始值,否则会报错

    代码运行结果展示如下:

image.png

  1. 指针指向:let和const都是ES6新增的创建变量语法,let创建的变量可以改变指针指向(可以重新赋值),但const声明的变量不允许改变指针指向,会报错。

     // var改变指针指向重新赋值
     var a = [1, 2, [3, 4]];
     a = { 1: "---------" };
     console.log(a);
     ​
     // let改变指针指向重新赋值
     let b = [1, 2, [3, 4]];
     b = { 1: "---------" };
     console.log(b);
     ​
     // const改变指针指向重新赋值
     const c = [1, 2, [3, 4]];
     c = { 1: "---------" };
     console.log(c);
    

    代码运行结果展示如下:

image.png

2. 待续