JavaScript---八股

JavaScript

Promise

一.数据结构

1.JavaScript 有哪些数据类型

这些数据类型可以分为原始数据类型引用数据类型(复杂数据类型),他们在内存中的存储方式不同


其中 **Symbol** 和 **BigInt\*\* 是 ES6 中新增的数据类型:

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

堆: 存放引用数据类型,引用数据类型占据空间大、大小不固定。如果存储在栈中,将会影响程序运行的性能;引用数据类型在栈中存储了指针,该指针指向堆中该实体的起始地址,如ObjectArrayFunction栈: 存放原始数据类型,栈中的简单数据段,占据空间小,属于被频繁使用的数据,如StringNumberNullBoolean

  • 堆: 存放引用数据类型,引用数据类型占据空间大、大小不固定。如果存储在栈中,将会影响程序运行的性能;引用数据类型在栈中存储了指针,该指针指向堆中该实体的起始地址,如ObjectArrayFunction
  • 栈: 存放原始数据类型,栈中的简单数据段,占据空间小,属于被频繁使用的数据,如StringNumberNullBoolean

2.Undefined 与 Null 的区别


UndefinedNull 都是基本数据类型,这两个基本数据类型分别都只有一个值,就是 undefinednull

  • undefined 代表的含义是未定义,一般变量声明了但还没有定义的时候会返回 undefinedtypeofundefined
  • null 代表的含义是空对象,null 主要用于赋值给一些可能会返回对象的变量,作为初始化,typeofobject

3.typeof null 的结果是什么,为什么?

4.为什么 0.1 + 0.2 ≠ 0.3,如何让其相等

5.typeof NaN会返回什么?

会返回Number,他表示一个不能表示的数字

6.for…in…for…of…的区别

for...infor...of都是JavaScript中的循环语句,而for…of 是 ES6 新增的遍历方式,允许遍历一个含有iterator接口的数据结构(数组、对象等)并且返回各项的值,和ES3中的for…in的区别如下

  • for…of 遍历获取的是对象的键值for…in 获取的是对象的键名
  • for… in 会遍历对象的整个原型链,性能非常差不推荐使用,而 for … of 只遍历当前对象不会遍历原型链;
  • 对于数组的遍历,for…in 会返回数组中所有可枚举的属性(包括原型链上可枚举的属性),for…of 只返回数组的下标对应的属性值;

总结for...in 循环主要是为了遍历对象而生,不适用于遍历数组;for...of 循环可以用来遍历数组、类数组对象,字符串、SetMap 以及 Generator 对象。

7.对 AJAX 的理解,实现一个 AJAX 请求

AJAX是 Asynchronous JavaScript and XML 的缩写,指的是通过 JavaScript 的 异步通信,从服务器获取 XML 文档从中提取数据,再更新当前网页的对应部分,而不用刷新整个网页。 创建AJAX请求的步骤:

  • 创建一个 XMLHttpRequest 对象。
  • 在这个对象上使用 open 方法创建一个 HTTP 请求,open 方法所需要的参数是请求的方法、请求的地址、是否异步和用户的认证信息。
  • 在发起请求前,可以为这个对象添加一些信息和监听函数。比如说可以通过 setRequestHeader 方法来为请求添加头信息。还可以为这个对象添加一个状态监听函数。一个 XMLHttpRequest 对象一共有 5 个状态,当它的状态变化时会触发onreadystatechange 事件,可以通过设置监听函数,来处理请求成功后的结果。当对象的 readyState 变为 4 的时候,代表服务器返回的数据接收完成,这个时候可以通过判断请求的状态,如果状态是 2xx 或者 304 的话则代表返回正常。这个时候就可以通过 response 中的数据来对页面进行更新了。
  • 当对象的属性和监听函数设置完成后,最后调用 send 方法来向服务器发起请求,可以传入参数作为发送的数据体。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
const serve_url = '/serve_url'

let xhr = new XMLHttpRequest()

//创建 hhtp 请求
xhr.open('GET', serve_url, true)

// 设置状态的监听函数
xhr.onreadystatechange = function(){
if(xhr.readyState !==4) {
return
}
if(xhr.status ===200 ){
hander(this.response)
}else[
console.error(this.statusText)
]
}

// 设置请求失败的监听函数
xhr.onerror = function() {
console.error(this.statusText)
}

// 设置请求头
xhr.responseType = 'json'
xhr.setRequestHeader('Content-Type', 'application/json')

// 发送http请求
xhr.send(

8.ajax,axios,fetch 的区别

  • 基于原生XHR开发,XHR本身架构不清晰。
  • 针对MVC编程,不符合现在前端 MVVM 的浪潮。
  • 多个请求之间如果有先后关系的话,就会出现回调地狱
  • 配置和调用方式非常混乱,而且基于事件的异步模型不友好。

  • 支持PromiseAPI
  • 从浏览器中创建XMLHttpRequest
  • node.js 创建 http 请求
  • 支持请求拦截和响应拦截
  • 自动转换JSON数据
  • 客服端支持防止CSRF/XSRF

浏览器原生实现的请求方式,ajax 的替代品基于标准 Promise 实现,支持async/awaitfetchtch只对网络请求报错,对 400,500 都当做成功的请求,需要封装去处理默认不会带cookie,需要添加配置项fetch没有办法原生监测请求的进度,而XHR可以。

  • 浏览器原生实现的请求方式,ajax 的替代品
  • 基于标准 Promise 实现,支持async/await
  • fetchtch只对网络请求报错,对 400,500 都当做成功的请求,需要封装去处理
  • 默认不会带cookie,需要添加配置项
  • fetch没有办法原生监测请求的进度,而XHR可以。

9.forEach 和 map 的区别

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

  • forEach() 对数据的操作会改变原来的数据,这个方法没有返回值
  • map() 方法不会改变原来数组的值,会返回一个新的数组,新的数组中的值是原来的数组进行处理后的值

10.什么是尾调用,使用尾调用有什么好处

这样做的好处就是:

在一个函数里调用另外一个函数会保留当前执行的上下文,如果在函数尾部调用,因为已经是函数最后一步,所以这时可以不用保留当前的执行上下文,从而节省内存。但是 ES6 的尾调用只能在严格模式下开启,正常模式是无效的。

11.如何实现深浅拷贝

使用JSON.stringify() 将 js 对象序列化,再通过 JSON.parse 反序列

  • 如果对象中有函数、undefinedsymbol时,都会丢失
  • 如果有正则表达式、Error对象等,会得到空对象

  • Objec.assign() 拷贝对象
  • 扩展运算符()

12.什么是深拷贝,浅拷贝,他们的区别是什么?

它们的区别总结:

特征浅拷贝 (Shallow Copy)深拷贝 (Deep Copy)
拷贝内容仅拷贝顶层属性拷贝所有属性,包括嵌套的引用类型
引用类型属性拷贝的是引用地址创建全新的对象或数组,拷贝的是值
独立性新对象和原对象共享嵌套引用类型的内存空间,互相影响新对象和原对象完全独立,互不影响
实现方式Object.assign(),扩展运算符 (...) (针对对象和数组)需要特殊方法实现,如递归拷贝、JSON.parse(JSON.stringify(obj))

咋么获取一个元素的位置

element.getBoundingClientRect() 返回一个DOMRect 对象,里面有这些属性:

属性含义
top元素上边缘到视口顶部的距离(单位:px)
left元素左边缘到视口左边的距离
bottom元素下边缘到视口顶部的距离(= top + height)
right元素右边缘到视口左边的距离(= left + width)
width元素宽度
height元素高度
x等同于 left(某些浏览器里还有这个字段)
y等同于 top

这些值都是相对于视口(viewport)左上角(0,0)点来计算的。

二.ES6

1.let,const,var 的区别

可以从六个方向来说明这个问题:

  • 块级作用域:

    块作用域由

    1
    { }

    包裹,

    1
    let

    1
    const

    具有块级作用域,

    1
    var

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

    • 内层变量可能覆盖外层变量
    • 用来计数的循环变量泄露为全局变量
  • 变量提升: var存在变量提升,letconst不存在变量提升,即变量只能在声明之后使用,否则会报错。

  • 给全局添加属性: 浏览器的全局对象是windowNode的全局对象是globalvar声明的变量为全局变量,并且会将该变量添加为全局对象的属性,但是letconst不会。

  • 重复声明: var声明变量时,可以重复声明变量,后声明的同名变量会覆盖之前声明的遍历。constlet不允许重复声明变量。

  • 初始值设置: 在变量声明时,varlet可以不用设置初始值。而const声明变量必须设置初始值。

  • 暂时性死区:在使用letconst命令声明变量之前,该变量都是不可用的。这在语法上,称为暂时性死区。使用var声明的变量不存在暂时性死区。

2.箭头函数与普通函数的区别

箭头函数是匿名函数,不能作为构造函数,使用new关键字。箭头函数没有arguments箭头函数没有自己的this,会获取所在的上下文作为自己的thiscall()applay()bind()方法不能改变箭头函数中的this指向箭头函数没有prototype箭头函数不能用作Generator函数,不能使用yeild关键字

  • 箭头函数是匿名函数,不能作为构造函数,使用new关键字。
  • 箭头函数没有arguments
  • 箭头函数没有自己的this,会获取所在的上下文作为自己的this
  • call()applay()bind()方法不能改变箭头函数中的this指向
  • 箭头函数没有prototype
  • 箭头函数不能用作Generator函数,不能使用yeild关键字

总结:

特性普通函数 (Regular Function)箭头函数 (Arrow Function)
this动态绑定,取决于调用方式词法作用域,继承自定义时所在上下文
arguments拥有 arguments 对象没有 arguments 对象,使用剩余参数 (...)
构造函数可以用 new 调用不能用 new 调用 (不是构造函数)
prototype拥有 prototype 属性没有 prototype 属性
yield可以作为生成器函数使用 (function*)不能作为生成器函数使用
重复命名参数非严格模式下允许 (已废弃),严格模式下不允许不允许
语法相对较长更简洁

3.Set、Map 的区别

Set

  • 创建: new Set([1, 1, 2, 3, 3, 4, 2])
  • add(value):添加某个值,返回 Set 结构本身。
  • delete(value):删除某个值,返回一个布尔值,表示删除是否成功。
  • has(value):返回一个布尔值,表示该值是否为 Set 的成员。
  • clear():清除所有成员,没有返回值。

Map

  • set(key, val):Map中添加新元素
  • get(key): 通过键值查找特定的数值并返回
  • has(key): 判断Map对象中是否有Key所对应的值,有返回true,否则返回false
  • delete(key): 通过键值从Map中移除对应的数据
  • clear(): 将这个Map中的所有元素删除

区别

  • Map是一种键值对的集合,和对象不同的是,键可以是任意值
  • Map可以遍历,可以和各种数据格式转换
  • Set是类似数组的一种的数据结构,类似数组的一种集合,但在 Set 中没有重复的值

4.map 和 Object 的区别

mapObject都是用键值对来存储数据,区别如下:

  • 键的类型Map 的键可以是任意数据类型(包括对象、函数、NaN 等),而 Object 的键只能是字符串或者 Symbol 类型。
  • 键值对的顺序Map中的键值对是按照插入的顺序存储的,而对象中的键值对则没有顺序。
  • 键值对的遍例Map 的键值对可以使用 for...of 进行遍历,而 Object 的键值对需要手动遍历键值对。
  • 继承关系Map 没有继承关系,而 Object 是所有对象的基类。

5.说说你对 Promise 的理解

Promise是异步编程的一种解决方案,将异步操作以同步操作的流程表达出来,避免了地狱回调。

Promise的实例有三个状态:

  • Pending(初始状态)
  • Fulfilled(成功状态)
  • Rejected(失败状态)

Promise的实例有两个过程:

  • pending -> fulfilled : Resolved(已完成)

  • pending -> rejectedRejected(已拒绝)

    注意:一旦从进行状态变成为其他状态就永远不能更改状态了,其过程是不可逆的。

Promise构造函数接收一个带有resolvereject参数的回调函数。

  • resolve的作用是将Promise状态从pending变为fulfilled,在异步操作成功时调用,并将异步结果返回,作为参数传递出去
  • reject的作用是将Promise状态从pending变为rejected,在异步操作失败后,将异步操作错误的结果,作为参数传递出去

Promise的缺点:

  • 无法取消 Promise,一旦新建它就会立即执行,无法中途取消。
  • 如果不设置回调函数,Promise内部抛出的错误,不会反应到外部。
  • 当处于pending状态时,无法得知目前进展到哪一个阶段(刚刚开始还是即将完成)。

6.Promise 方法

promise.then() 对应resolve成功的处理promise.catch()对应reject失败的处理promise.all()可以完成并行任务,将多个Promise实例数组,包装成一个新的Promise实例,返回的实例就是普通的Promise。有一个失败,代表该Primise失败。当所有的子Promise完成,返回值时全部值的数组promise.race()类似promise.all(),区别在于有任意一个完成就算完成promise.allSettled() 返回一个在所有给定的 promise 都已经 fulfilledrejected 后的 promise ,并带有一个对象数组,每个对象表示对应的promise 结果。

  • promise.then() 对应resolve成功的处理
  • promise.catch()对应reject失败的处理
  • promise.all()可以完成并行任务,将多个Promise实例数组,包装成一个新的Promise实例,返回的实例就是普通的Promise。有一个失败,代表该Primise失败。当所有的子Promise完成,返回值时全部值的数组
  • promise.race()类似promise.all(),区别在于有任意一个完成就算完成
  • promise.allSettled() 返回一个在所有给定的 promise 都已经 fulfilledrejected 后的 promise ,并带有一个对象数组,每个对象表示对应的promise 结果。

7.promise.all 和 promise.allsettled 区别

Promise.all()Promise.allSettled() 都是用来处理多个 Promise 实例的方法,它们的区别在于以下几点:

  • all: 只有当所有Promise实例都resolve后,才会resolve返回一个由所有Promise返回值组成的数组。如果有一个Promise实例reject,就会立即被拒绝,并返回拒绝原因。all是团队的成功才算,如果有一个人失败就算失败。
  • allSettled: 等所有Promise执行完毕后,不管成功或失败, 都会吧每个Promise状态信息放到一个数组里面返回。

8.async/await 对比 Promise 的优势

  • 代码可读性高,Promise虽然摆脱了回掉地狱,但自身的链式调用会影响可读性。
  • 相对Promise更优雅,传值更方便。
  • 对错误处理友好,可以通过try/catch捕获,Promise的错误捕获⾮常冗余

9.谈谈你对 ES6 的理解

  • 解构赋值
  • 扩展运算符
  • 箭头函数
  • 模版字符串
  • SetMap集合
  • 新增class
  • Proxy
  • Promise

10.ES6 模块和 CommonJS 模块有什么区别

  • 语法不同:ES6 模块使用 importexport 关键字来导入和导出模块,而 CommonJS 模块使用 requiremodule.exportsexports 来导入和导出模块。
  • 异步加载: ES6 模块支持动态导入(dynamic import),可以异步加载模块。这使得在需要时按需加载模块成为可能,从而提高了性能。CommonJS 模块在设计时没有考虑异步加载的需求,通常在模块的顶部进行同步加载。

三.性能优化

1.图片懒加载

1.概念:

仅在图片进入用户视口(viewport)时才加载该图片。

在页面初始加载时,未在视口中的图片不会被加载,只有当用户滚动页面,使图片进入视口时,才会触发加载,, 从而提升页面的加载速度和用户体验。

2.实现方式:

1.采用原生loading="lazy” 属性

在 h5 中引入了loading 属性,浏览器会自动延迟加载该图片

1
<img src="" loading="lazy" alt="">

这种方法简单易用,但需要注意浏览器的兼容性。

2.使用 JavaScript 和IntersectionObserver

IntersectionObserver 是一个浏览器 API,用于异步观察目标元素与其祖先元素或视口的交叉状态。

通过该 API,可以检测图片是否进入视口,从而动态加载图片。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// 获取全部的属性位data-src的img标签
const images = document.querySelectorAll("img[data-src]");

const observer = new IntersectionObserver((entries, observer) => {
entries.forEach((entry) => {
/**
* entry.isIntersecting 是一个 布尔值
* true:代表这个元素正在进入视口(看得见了)
* false:代表这个元素离开了视口(看不见了)
*/
if (entry.isIntersecting) {
// 图片进入视口
const img = entry.target;
img.src = img.dataset.src;
// 取消监听
observer.unobserve(img);
}
});
});

// 遍历所有图片,监听每一张图片
images.forEach((image) => {
observer.observe(image);
});

在 HTML 中,图片的 src 属性可以先留空,实际的图片路径存放在 data-src 属性中。

这种方法性能较好,适用于现代浏览器。

需要图片懒加载的就可以使用data-src 属性

3.使用滚动事件和 getBoundingClientRect

对于不支持 IntersectionObserver 的浏览器,可以使用 getBoundingClientRect 方法结合滚动事件来判断图片是否进入视口。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
function LazyLoad() {
const images = document.querySelectorAll("img[data-src]");
images.forEach((image) => {
// 获取图片的位置
const img = image.getBoundingClientRect();
if (img.top < window.innerHeight) {
image.src = image.dataset.src;
}
});
}
// 监听滚动事件
window.addEventListener("scroll", LazyLoad);
// 监听加载完成事件
window.addEventListener("load", LazyLoad);

节流与防抖

1.节流:

节流(throttle)是指在一定时间间隔内,无论事件被触发多少次,只执行一次事件处理数。 常用于高频触发但不需要每次都响应的场景,比如页面滚动、按钮点击防止连击、窗口缩放等。

核心实现原理

  • 记录上一次执行的时间lastTime
  • 每次触发事件时,比较当前时间 nowlastTime
  • 如果时间间隔大于或等于设定的 delay,就执行一次处理函数 fn
  • 更新 lastTime 为当前时间
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// 节流代码
function throttle(fn, delay = 500) {
let flag = true;
return function (...args) {
if (!flag) return;
flag = false;
setTimeout(() => {
// 改变this指向
fn.apply(this, args);
flag = true;
}, delay);
};
}
// 节流代码
function throttle(fn, delay = 500) {
let last_time = 0;
return function (...args) {
let now_time = Date.now();
if (now_time - last_time > delay) {
fn.apply(this, args);
last_time = now_time;
}
};
}

2.防抖

防抖是指短时间内多次触发同一事件时,只有最后一次生效。常用于搜索框输入、窗口大小调整等场景。

核心实现原理

  • 每次触发事件时,取消之前的定时器
  • 重新启动一个新的定时器。
  • 只有最后一次触发后的设定时间内没有再次触发,才会真正执行函数。
1
2
3
4
5
6
7
8
9
10
11
// 防抖代码
function debounce(fn, delay = 500) {
let timer = null;
return function (...args) {
if (timer) clearTimeout(timer);
// 延迟执行
timer = setTimeout(() => {
fn.apply(this, args);
}, delay);
};
}

网络优化

四.跨域、同源策略和 CORS

同源策略

同源策略(英文全称 Same origin policy)是浏览器提供的一个安全功能。

同源策略限制了从同一个源加载的文档或脚本如何与来自另一个源的资源进行交互。这是一个用于隔离潜在恶意文件的重要安全机制。

同源指的是两个 URL 的协议域名端口一致,反之,则是跨域

出现跨域的根本原因:浏览器的同源策略不允许非同源的 URL 之间进行资源的交互。

image.png

最主要的三种解决方案,分别是 JSONPCORSNginx 反向代理。

  • JSONP:出现的早,兼容性好(兼容低版本 IE)。是前端程序员为了解决跨域问题,被迫想出来的一种临时解决方案。缺点是只支持 GET 请求,不支持 POST 请求。
  • CORS:出现的较晚,它是 W3C 标准,属于跨域 AJAX 请求的根本解决方案。支持 GETPOST 请求。缺点是不兼容某些低版本的浏览器。
  • Nginx 反向代理:同源策略对服务器不加限制,是最简单的跨域方式。只需要修改 nginx 的配置即可解决跨域问题,支持所有浏览器,支持 session,不需要修改任何代码,并且不会影响服务器性能。

方法一:CORS(跨域资源共享,推荐)

服务器(后端)在 HTTP 响应头 添加:

1
Access-Control-Allow-Origin: *

*指允许所有的来源请求,或者可以指定相应的域名

方法二:采用ISONP (只可以支持 GET 的请求)