前端高频面试题-- Js

107 阅读19分钟

Js基本数据类型(Primitive Types)

基本数据类型是简单的数据段,它们直接存储在栈(stack)内存中的位置。这意味着当你将一个基本数据类型的值赋给另一个变量时,实际上是创建了一个新的值,然后将这个值存储在新的变量分配的位置上。基本数据类型包括以下几种:

  1. String:字符串,用于表示文本数据。
  2. Number:数字,用于表示数值。
  3. Boolean:布尔值,表示逻辑实体,只有两个值:true 和 false
  4. Undefined:当一个变量被声明了但没有被赋值时,它的值就是 undefined
  5. Null:表示一个空值,是一个字面量,不是数据类型,但常被视为基本数据类型的特殊值。
  6. Symbol(ES6 引入):表示独一无二的值。

Js引用数据类型(Reference Types)

引用数据类型(也称为对象类型)是存储在堆(heap)内存中的对象。当创建一个引用数据类型时,实际上是在堆内存中创建了一个对象,并返回这个对象的引用(或内存地址)。因此,当你将一个引用数据类型的值赋给另一个变量时,赋的是该值的引用,而不是值本身。这意味着两个变量实际上都指向同一个对象。引用数据类型包括:

  1. Object:普通对象,是所有其他引用类型的基础。
  2. Array:数组,是一种特殊的对象,用于存储有序的数据集合。
  3. Function:函数,表示可执行的代码块,用于实现某种功能。
  4. Date:日期对象,用于处理日期和时间。
  5. RegExp:正则表达式对象,用于匹配字符串中的字符组合。
  6. 特殊对象:如 Math(数学对象)、BigInt(任意精度的整数,ES2020 引入)、Global(全局对象)等。

关键点对比

  • 存储位置:基本数据类型存储在栈内存中,而引用数据类型存储在堆内存中,栈内存中只存储对堆内存中对象的引用(即地址)。
  • 赋值行为:基本数据类型的赋值是值拷贝,而引用数据类型的赋值是引用拷贝(即拷贝的是地址)。
  • 比较操作:对于基本数据类型,比较的是值;对于引用数据类型,比较的是引用(即地址),除非显式地比较两个对象的内容是否相同(这通常需要使用特殊的方法或循环遍历属性进行比较)。

判断数据类型

JavaScript中,判断一个变量的数据类型可以通过多种方式实现。这里列举几种常见的方法:

1. 使用 typeof 操作符

typeof 是JavaScript中的一个一元操作符,用于返回操作数的基本类型。但需要注意的是,对于数组(Array)和nulltypeof 的表现可能不如预期:

javascript复制代码
	console.log(typeof "Hello World"); // string  

	console.log(typeof 42); // number  

	console.log(typeof true); // boolean  

	console.log(typeof undefined); // undefined  

	console.log(typeof null); // object(注意:这里是一个历史遗留问题)  

	console.log(typeof {name: "John", age: 30}); // object  

	console.log(typeof [1, 2, 3]); // object(数组也是对象类型)  

	console.log(typeof function() {}); // function  

	console.log(typeof /abc123/); // object(在老版本JS中,正则表达式是对象类型,但在ES6及以后,大多数环境返回"function")  

	console.log(typeof Symbol()); // symbol(ES6引入)

2. 使用 instanceof 操作符

instanceof 用于测试一个对象在其原型链中是否存在一个构造函数的 prototype 属性。这对于判断数组、自定义对象等类型非常有用:

javascript复制代码
	console.log([] instanceof Array); // true  

	console.log({} instanceof Object); // true  

	console.log(function() {} instanceof Function); // true

但需要注意的是,instanceof 在处理跨iframe的数组时可能会失效,因为它检查的是对象在其原型链中是否含有特定构造函数的 prototype

3. 使用 Object.prototype.toString.call() 方法

这是判断数据类型的一种非常可靠的方式,因为Object.prototype.toString.call() 返回的是对象的内部[[Class]]属性,这是一个字符串,表示了对象的类型:

javascript复制代码
	console.log(Object.prototype.toString.call("Hello World")); // [object String]  

	console.log(Object.prototype.toString.call(42)); // [object Number]  

	console.log(Object.prototype.toString.call(true)); // [object Boolean]  

	console.log(Object.prototype.toString.call(undefined)); // [object Undefined]  

	console.log(Object.prototype.toString.call(null)); // [object Null]  

	console.log(Object.prototype.toString.call({})); // [object Object]  

	console.log(Object.prototype.toString.call([])); // [object Array]  

	console.log(Object.prototype.toString.call(function() {})); // [object Function]  

	console.log(Object.prototype.toString.call(/abc123/)); // [object RegExp]  

	console.log(Object.prototype.toString.call(new Date())); // [object Date]  

	console.log(Object.prototype.toString.call(Symbol())); // [object Symbol]

4. 使用 Array.isArray() 方法(针对数组)

对于数组,Array.isArray() 方法提供了一种清晰且准确的方式来判断:

javascript复制代码
	console.log(Array.isArray([])); // true  

	console.log(Array.isArray({})); // false

5. ES6中的 Symbol.toStringTag 属性(需要Object.prototype.toString.call()支持)

虽然Symbol.toStringTag本身不直接用于判断类型,但它可以被对象自定义以影响Object.prototype.toString.call()的返回值,从而提供一种更灵活的方式来标识对象类型。然而,这更多是用于自定义对象类型标识,而不是直接用于类型判断。

总结

对于大多数情况,推荐使用typeof来区分基本数据类型,使用Array.isArray()来判断数组,使用Object.prototype.toString.call()来准确判断所有类型的对象(包括数组、函数等)。instanceof在需要判断对象是否属于某个构造函数实例时很有用,但需要注意跨iframe的问题。

ES6 新特性有哪些

1. 变量声明 let/const

let 特性:1. 拥有块级作用域,这意味着在 if 语句、for 循环、while 循环等代码块中声明的 let 变量,只在该代码块内有效。2. 不允许在同一作用域内重复声明同一个变量

const 特性:1. 也具有块级作用域。2. 声明时必须进行初始化赋值,且赋值后不能再重新赋值修改其值。3. 如果 const 声明的是一个对象或数组,虽然不能重新赋值整个对象或数组,但可以修改对象的属性值或数组的元素值。

2. 箭头函数

箭头函数为 JavaScript 编程带来了更简洁、更清晰的代码风格,并且在处理作用域和 this 问题时提供了更直观的方式。

箭头函数特性:1. 消除了函数的二义性。2. 简洁的语法。3. 箭头函数不会创建自己的 this 上下文,而是继承外层函数的 this 值。这在处理回调函数和对象方法时非常有用,可以避免 this 指向错误的问题。4. 箭头函数不能使用 new 操作符来创建实例,因为它们没有自己的 prototype 属性。5. 箭头函数没有自己的 arguments 对象,但可以通过剩余参数来获取参数。

let fn = () => {};

3. 模板字符串

4. 解构赋值

ES6 中的解构赋值是一种方便的数据提取和赋值方式,它允许从数组或对象中提取值,并将其赋给变量。

5. 扩展运算符

ES6 的扩展运算符(...)是一种方便的操作符

6. 剩余参数

ES6 中的剩余参数语法允许将不定数量的参数表示为一个数组。

其语法形式为:在函数的最后一个命名参数前加上三个点(...),后面跟着参数名,例如:function(a, b,...theArgs){ // 函数体 } ,在这个例子中,theArgs 将收集该函数的第三个参数及以后的所有剩余参数。

7. 类

ES6 引入了类(class)的概念,使面向对象编程在 JavaScript 中更加清晰和直观。

详情参考 前端 js 经典:class 类

8. 模块化

ES6 引入了模块化的概念,这使得 JavaScript 代码的组织和管理更加清晰和高效。

一个模块就是一个独立的 JavaScript 文件。模块内的变量、函数、类等默认是私有的,只有通过 export 关键字导出的部分才能被其他模块使用。

9. promise

10. map/set

在 ES6 中,Map 和 Set 是两种新的数据结构,它们为数据的存储和操作提供了更强大和灵活的方式。

Map:Map 是键值对的集合,其中键可以是任何类型的值(包括对象),而不仅仅是字符串。


 
let myMap = new Map();
 
myMap.set("key1", "value1");
console.log(myMap.get("key1"));
console.log(myMap.has("key1"));
myMap.delete("key1");
console.log(myMap.size);
 

11. symbol

在 ES6 中,Symbol 是一种新的基本数据类型,它的主要目的是创建独一无二的值,通常用于对象的属性名,以避免属性名冲突。

let sym1 = Symbol();
 
let sym2 = Symbol("description"); // 可以提供一个可选的描述字符串

深浅拷贝及其实现

image.png

Object.prototype.toString.call(target); 判断数据类型方法

  1. JS深浅拷贝及其实现

  2. .js继承的6种方式

  3. .vue和react的相同点和区别

  4. .前端三大主流框架的区别

原型

  1. 原型是什么
    首先给出定义:给其它对象提供共享属性的对象,简称为原型( prototype )。

    凡是对象都会有一个属性那就是__proto__, 这个__proto__指向的就是他的构造函数的prototype

    Object.prototype是所有对象的爸爸

image.png

2.什么是原型链

定义:如果要访问对象中并不存在的一个属性,就会查找对象内部 prototype 关联的对象。在查找属性时会对它进行层层寻找遍历,这个关联关系实际上定义了一条原型链。 原型链最终会指向Object.prototype,原型链的终点是Object.prototype.proto 也就是 null。

cookie 、sessionStorage与localStorage的区别

image.png

js继承的6种方式

js 常用排序

javaScript中defer和async

在JavaScript中,<script>标签的deferasync属性都是用于异步加载和执行JavaScript脚本的,但它们之间存在一些关键差异。

1. defer

  • defer属性被设置时,浏览器会在HTML解析完成后,但在DOMContentLoaded事件触发之前执行脚本。如果有多个带有defer属性的脚本,它们会按照在HTML文档中出现的顺序执行。
  • 使用defer时,脚本的加载和执行不会阻塞HTML解析。但是,所有defer脚本的执行都会等待HTML解析完成。
  • 需要注意的是,defer脚本对DOM元素是可用的,因为它们会在整个文档解析完成后执行。但是,它们不会等待样式表、图片和子框架的完成加载。

2. async

  • async属性被设置时,浏览器会在后台异步加载并执行脚本,而不会阻塞HTML解析。但是,一旦脚本加载完成,HTML解析会暂停,脚本会立即执行,然后HTML解析会恢复。
  • defer不同,async脚本不会按照在HTML文档中出现的顺序执行。如果页面上有多个async脚本,它们可能会乱序执行。
  • 使用async时,脚本的加载和执行不会阻塞HTML解析,但脚本的执行可能会阻塞渲染。这是因为一旦脚本加载完成,它就会立即执行,可能会修改DOM或执行其他可能阻塞渲染的操作。

所以async和defer的最主要的区别就是async是异步下载并立即执行,然后文档继续解析,defer是异步加载后解析文档,然后再执行脚本。

Promise: JavaScript 异步编程的优雅解决方案

Promise是异步编程的一种解决方案,主要用于处理回调地狱,异步编程的问题

Promise对象有三种状态:pending(进行中)、fulfilled(已成功)和rejected(已失败)。这三种状态分别对应着三种回调:

  1. then方法:用于处理Promise对象最终的成功状态(fulfilled)。
  2. catch方法:用于处理Promise对象最终的失败状态(rejected)。
  3. finally方法:不管Promise对象最终状态如何,都会执行的回调

promise对象的状态不受外界影响, 一旦状态改变就不会再变。任何时候都可以得到这个结果。


// 创建一个Promise对象

const myPromise = new Promise((resolve, reject) => {

// 异步操作

setTimeout(() => {

// 如果一切正常,调用resolve

resolve('操作成功');

// 如果出现错误,调用reject

// reject('操作失败');

}, 1000);

});

// 使用then方法处理成功的情况

myPromise.then((successMessage) => {

console.log(successMessage); // 输出: 操作成功

}).catch((errorMessage) => {

console.error(errorMessage); // 输出错误信息

}).finally(() => {

console.log('Promise状态已经决定,无论成功还是失败都会执行这里的代码');

});

浏览器的垃圾回收机制

浏览器的垃圾回收机制是指在JavaScript中自动管理内存的一种机制。它的主要目标是识别和回收不再使用的对象,以释放内存并防止内存泄漏。

  • 标记清除 是现代浏览器中最常用的垃圾回收方式。当变量进入环境时(例如,在函数中声明一个变量),该变量被标记为“进入环境”。当变量离开环境时,则将其标记为“离开环境”。垃圾回收机制运行时,会给存储在内存中的所有变量加上标记,然后去掉处在环境中的变量及被环境中的变量引用的变量标记。剩下的带有标记的变量被视为准备删除的变量,因为环境中的变量已经无法访问到这些变量了。垃圾回收机制到下一个周期运行时,将释放这些变量的内存,回收它们所占用的空间。
  • 引用计数 则是通过一张“引用表”来保存内存里面所有资源的引用次数。如果一个值的引用次数是0,就表示这个值不再用到,因此可以将这块内存释放。然而,这种方法存在一个问题,即循环引用。如果对象A有一个指针指向对象B,同时对象B也有一个指针指向对象A,那么这两个对象的引用数将永远不为0,导致它们无法被垃圾回收机制正确回收。为了避免这种情况,程序员需要确保在确认不使用某个对象的情况下,将其引用变量赋值为null,从而切断引用链,使对象能够在下次垃圾回收时被正确处理。

此外,V8引擎等现代JavaScript引擎采用了更复杂的垃圾回收策略,如分代垃圾回收,将内存分为新生代和老生代,采用不同的垃圾回收算法来优化性能。新生代采用Scavenge算法,存储存活时间短的对象,而老生代则存储经过多轮垃圾回收仍存活的对象或较大的对象。这种策略有助于提高垃圾回收的效率,减少对应用性能的影响。

综上所述,浏览器的垃圾回收机制通过自动管理内存,帮助开发者避免内存泄漏问题,从而提升Web应用的性能和用户体验。

js哪些操作是异步的

在JavaScript中,异步操作是那些不会立即执行完成,而是在将来的某个时间点(比如某个事件发生后或某个条件满足时)继续执行的操作。这种机制允许JavaScript在等待异步操作完成时,不会阻塞代码的执行,从而提高应用程序的响应性和性能。以下是一些常见的JavaScript异步操作示例:

  1. 定时器函数

    • setTimeout(function() { ... }, delay):在指定的延迟时间后执行函数。
    • setInterval(function() { ... }, interval):每隔指定的时间间隔重复执行函数。
  2. 网络请求

    • 使用XMLHttpRequest对象(虽然在现代开发中较少使用,但仍是异步的)。
    • 使用fetch API进行网络请求和资源获取,它基于Promise。
    • 使用axiossuperagent等第三方库发起的HTTP请求,这些库内部通常也是基于Promise或async/await。
  3. 事件监听

    • 监听DOM事件(如点击、鼠标移动、键盘事件等),当事件发生时执行回调函数。
    • 监听自定义事件,同样是在事件触发时异步执行回调函数。
  4. 文件读写(在浏览器端通常通过API实现,如File API和IndexedDB,而在Node.js中):

    • 浏览器端通常不直接支持文件读写(除了用户通过<input type="file">选择的文件),但可以通过File API等处理文件数据。
    • Node.js中,使用fs模块进行文件操作时,大部分操作(如fs.readFilefs.writeFile等)都是异步的,它们接受回调函数或返回Promise。
  5. Web Workers

    • Web Workers允许JavaScript代码在后台线程中运行,不会阻塞用户界面。这对于执行长时间运行的任务特别有用。
  6. Promises 和 async/await

    • 虽然Promises和async/await本身不是异步操作,但它们是处理异步操作结果的有效方式。通过使用它们,可以以更同步的方式编写异步代码。
  7. Streams(流):

    • 在Node.js中,Streams API允许你以流的形式处理数据(如读取或写入文件),这对于处理大量数据特别有用,因为数据可以分块处理,而不是一次性加载到内存中。
  8. WebSocket

    • WebSocket提供了一种在单个TCP连接上进行全双工通讯的协议。客户端和服务器之间的数据交换是异步的,允许实时通信。
  9. Service Workers

    • Service Workers是一种运行在浏览器后台的脚本,独立于网页。它们可以用来处理一些后台任务,如推送通知、缓存资源等,而不会阻塞页面的渲染。

这些异步操作在JavaScript开发中非常常见,理解它们的工作原理和用法对于编写高效、响应迅速的应用程序至关重要。

forEach和map的区别

这方法都是用来遍历数组的,两者区别如下:

  • forEach()方法会针对每一个元素执行提供的函数,对数据的操作会改变原数组,该方法没有返回值;
  • map()方法不会改变原数组的值,返回一个新数组,新数组中的值为原数组调用函数处理之后的值;

什么是事件循环?调用堆栈和任务队列之间有什么区别?

事件循环是一个单线程循环,用于监视调用堆栈并检查是否有工作即将在任务队列中完成。如果调用堆栈为空并且任务队列中有回调函数,则将回调函数出队并推送到调用堆栈中执行。

js的任务可以分为同步任务和异步任务,同步任务一般会直接进入到主线程中执行;而异步任务,就是异步执行的任务,比如ajax网络请求,setTimeout 定时函数等都属于异步任务,异步任务会通过任务队列( Event Queue )的机制来进行协调。

同步和异步任务分别进入不同的执行环境,同步的进入主线程,即主执行栈,异步的进入 Event Queue 。主线程内的任务执行完毕为空,会去 Event Queue 读取对应的任务,推入主线程执行。 上述过程的不断重复就是我们说的 Event Loop (事件循环)

html页面是怎样渲染的?

HTML页面的渲染过程涉及多个步骤,‌包括解析HTML、‌构建DOM树、‌生成CSSOM树、‌布局渲染、‌绘制和合成渲染等。‌

  1. 解析HTML:‌当用户输入网址并访问一个HTML页面时,‌浏览器首先向服务器发出请求,‌服务器返回HTML文件。‌浏览器开始载入HTML代码,‌这是渲染过程的起点。‌
  2. 构建DOM树:‌浏览器读取HTML代码并构建一个DOM树(‌文档对象模型树)‌,‌这个树代表了页面的结构。‌DOM树是页面内容的表示,‌包括文本、‌图像、‌链接等所有可见和不可见的元素。‌
  3. 生成CSSOM树:‌浏览器同时解析CSS文件(‌内部样式表、‌外部样式表或内联样式)‌来构建CSSOM(‌CSS对象模型)‌树。‌这个树包含了所有CSS规则和样式信息,‌用于确定元素的布局和外观。‌
  4. 布局渲染:‌接下来,‌浏览器将DOM树和CSSOM树合并,‌生成一个渲染树。‌这个树包含了需要显示在屏幕上的所有节点,‌即那些可见的元素。‌然后,‌浏览器进行布局计算,‌确定每个元素在页面上的确切位置和尺寸,‌这个过程称为reflow(‌重排)‌。‌
  5. 绘制:‌一旦布局完成,‌浏览器开始绘制每个节点,‌这包括将每个节点转换为屏幕上的实际像素。‌这个过程可能包括颜色、‌背景、‌字体等视觉属性的处理。‌
  6. 合成渲染:‌最后,‌GPU或CPU将所有绘制好的图层合成最终的视觉输出,‌这就是用户最终在屏幕上看到的页面。‌

在这个过程中,‌还有一些优化措施,‌如异步加载和渲染,‌以减少页面加载时间并提高性能。‌例如,‌当JavaScript代码修改了影响布局的属性时(‌如尺寸或位置)‌,‌会触发reflow过程;‌而当只修改不影响布局的属性时(‌如颜色或字体)‌,‌则只会引发repaint,‌这通常更快一些。‌此外,‌transform属性因其不会影响布局或绘制指令,‌因此效率较高,‌因为它只在渲染流程的最后阶段(‌draw阶段)‌影响渲染结果

闭包

闭包就是说,能够读取其他函数内部变量的函数。所以闭包可以理解成“定义在一个函数内部的函数“。在本质上,闭包是将函数内部和函数外部连接起来的桥梁。

闭包的三大特点为:

1、函数嵌套函数

2、内部函数可以访问外部函数的变量

3、参数和变量不会被回收。

闭包的缺点就是常驻内存,会增大内存使用量,使用不当很容易造成内存泄露,所以需要手动将外部变量和函数置为 null,以释放内存