前端面试练习24.3.2-3.3

目录

HTML+CSS部分

一.说一说HTML的语义化

二.说一说盒模型

三.说一下浮动

四.说一说样式优先级的规则是什么

五.说一说CSS尺寸设置的单位

六.说一说BFC

七.说几个未知宽高元素水平垂直居中方法

八.说一说三栏布局的实现方案

JS部分

九.说一说JS数据类型有哪些,区别是什么?

十.说一说null 和 undefined 的区别,如何让一个属性变为null

十一.说一说JavaScript有几种方法判断变量的类型?

1.typeof

2.instanceof

3.Array.isArray(obj)

4.constructor

5.Object.prototype.toString.call()

十二.说一说数组去重都有哪些方法?

具体实现代码参考:JS数组去重方法-CSDN博客

1.双层循环 + splice(当前索引,1)  时间复杂度O(n^2), 空间复杂度O(1)

2.循环 + indexOf 方法  时间复杂度O(n^2), 空间复杂度O(1)

3.循环 + arr.sort() 方法  时间复杂度O(n log n), 空间复杂度O(1)

4.循环 + includes()   时间复杂度O(n *m), 空间复杂度O(n)

5.reduce + includes  时间复杂度O(n^2), 空间复杂度O(n)

6. filter+indexOf  时间复杂度O(n^2), 空间复杂度O(n)

7.利用对象属性 key 排除重复项   时间复杂度O(n^2), 空间复杂度O(n)

 8.Set方法, new Set(oldArr)   时间复杂度O(n), 空间复杂度O(n)

 9.循环 + Map   时间复杂度O(n), 空间复杂度O(n)

十三.数组与伪数组的区别

十四.说一说map 和 forEach 的区别

十五.说一说es6中箭头函数

十六.说一说this指向(普通函数、箭头函数)

1.普通函数中的 this 指向:

2.箭头函数中的 this 指向:

3.立即执行函数的this指向

4.定时器的this指向

十七.事件扩展符用过吗(...),什么场景下

十八.说一说JS变量提升

十九.说一说js继承的方法和优缺点

1.原型链继承

2.借用构造函数继承

3.组合继承

4.原型式继承

5.寄生式继承

6.寄生组合式继承

二十.说一说defer和async区别

二十一.说一说JS实现异步的方法

二十二.说一说cookie sessionStorage localStorage 区别

二十三.说一说如何实现可过期的localstorage数据

二十四.说一下token 能放在cookie中吗


HTML+CSS部分

一.说一说HTML的语义化

在我看来,它的语义化其实是为了便于机器来看的,当然,程序员在使用语义化标签时也可以使得代码更加易读,对于用户来说,这样有利于构建良好的网页结构,可以在优化用户体验,对于机器而言,语义化的使用,方便了搜索引擎的一些优化,方便机器识别,便于爬取利用。

一般常用的标签有 

<header>,<footer><nav>,<main>,<article>等

二.说一说盒模型

我对于盒子模型的认知里,

盒模型的几个部分由外到内:外边距(margin),边框(border),内边距(padding),内容(content,包括长和宽)

所以根据盒子大小计算的不同方式,把盒子模型分为了两种,一种是标准盒,一种是怪异盒。

标准盒:在设置width和height时,只是修改内容(content)的大小,盒子的大小还要加上边框(border)和内边距(padding)

怪异盒:在设置width和height时,设置的是整个盒子的大小,它包含了边框(border),内边距(padding),内容(content)区域,所以显示的时候,内容区域看起来会被压缩,

一般我们使用的是W3C标准盒模型(content-box),

也可以通过设置box-sizing属性决定盒模型

box-sizing:border-box代表怪异盒模型

box-sizing:content-box代表标准盒模型

三.说一下浮动

浮动就是给块级元素添加一个属性:

float:left/right

 使用浮动可以实现文字的环绕图片,

浮动的特点:使得元素脱离文档流,容易造成塌陷,影响其他元素的排列

所以,在使用浮动时,我们还要解决可能出现的塌陷问题

塌陷问题就是指浮动的元素超出了父元素的宽高,使得父元素塌陷

所以解决方法如下:

1.给父元素设置 overflow:hidden,超出部分隐藏

2.给父元素添加高度。使其能包裹住浮动元素

3.在浮动元素的最后添加新的<div>标签,使用clear:left/right/both属性清除浮动

4.使用伪元素:::after { content: ""; display: block; clear: both; }

还可以说使用flex布局来解决浮动带来的问题,然后话题就跳转到flex 

四.说一说样式优先级的规则是什么

css的样式优先级

!important > 内联样式 > ID 选择器(#id{}) > 类选择器(.class{}) = 属性选择器(a[href="segmentfault.com"]{}) = 伪类选择器( :hover{}) > 标签选择器(span{}) = 伪元素选择器( ::before{})= 后代选择器(.father .child{})> 子选择器(.father > .child{}) = 相邻选择器( .bro1 + .bro2{}) > 通配符选择器(*{})
 

五.说一说CSS尺寸设置的单位

分为以下几类:

px:绝对大小,取决于屏幕的分辨率

%:相对父元素的大小所占据的百分比

rem:相对于根元素的大小(即 <html> 元素)的字体大小。

em:相对长度单位,在 `font-size` 中使用是相对于父元素的字体大小,在其他属性中使用是相对于自身的字体大小,如 width。如当前元素的字体尺寸未设置,由于字体大小可继承的原因,可逐级向上查找,最终找不到则相对于浏览器默认字体大小

vh,vw:相对于屏幕视口大小

六.说一说BFC

大致内容:

所谓 BFC,指的是一个独立的布局环境,BFC 内部的元素布局与外部互不影响。

触发 BFC 的方式有很多,常见的有:

  • 设置浮动
  • overflow 设置为 auto、scroll、hidden
  • positon 设置为 absolute、fixed

常见的 BFC 应用有:

  • 解决浮动元素令父元素高度坍塌的问题
  • 解决非浮动元素被浮动元素覆盖问题
  • 解决外边距垂直方向重合的问题

定义:

块级的格式化上下文,独立的渲染区域,不会影响边界以外的元素布局

产生BFC:

1.使用 float属性不为none

2.position为absolute或fixed

3、display为inline-block、table-cell、table-caption、flex、inline-flex

4、overflow不为visible

一些情况下使用border也可以产生BFC
例如:

把父元素的border和overflow都去除后,产生了外边距塌陷,即:浮动元素与另一个元素的上外边距产生了合并,都使用了大的那个上外边距,此时父元素添加任意一个属性都可以产生一个BFC解决外边距的塌陷。

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>BFC Example</title>
<style>
    .container {
        border: 1px solid black;
        overflow: auto; /* 触发 BFC */
    }

    .float-left {
        float: left;
        width: 100px;
        height: 100px;
        background-color: red;
        margin-right: 20px;
    }

    .child {
        background-color: blue;
        width: 100px;
        height: 100px;
        margin-top: 20px;
    }
</style>
</head>
<body>
<div class="container">
    <div class="float-left"></div>
    <div class="child"></div>
</div>
</body>
</html>

BFC的一些特性:

1、在BFC中,盒子从顶部开始垂直地一个接一个排列

2、盒子垂直方向的距离由margin决定。同一个BFC的两个相邻盒子margin会重叠

3、BFC中,margin-left会触碰到border-left(对于从左至右的方式,反之)

4、BFC区域不会与浮动的盒子产生交集,而是紧贴边缘浮动

5、计算BFC高度时,自然会检测浮动的盒子高度


目前来说,第五点算是比较直观的可以体现的,其他几点的话简单场景是否使用BFC样式体现是几乎没有区别的

使用 BFC 的好处体现在更复杂的布局和样式设计中,其中一些情况下会更明显:

  1. 阻止外边距折叠:BFC 可以防止相邻块级元素之间外边距的折叠,确保布局更加可控和可预测。

  2. 清除浮动:BFC 可以包含浮动元素,使得父元素可以自适应浮动元素的高度,防止父元素坍塌。

  3. 自适应布局:BFC 可以使得元素在布局过程中自适应父元素的大小,并防止元素溢出父元素的边界。

  4. 避免文字环绕:使用 BFC 可以防止文本环绕浮动元素,使得文本不会被浮动元素覆盖。

BFC解决问题:

1、清除内部浮动,父元素设置为BFC可以清除子元素的浮动(最常用overflow:hidden,IE6需加上*zoom:1):计算BFC高度时会检测浮动子盒子高度

2、解决外边距合并问题

3、右侧盒子自适应:BFC区域不会与浮动盒子产生交集,而是紧贴浮动边缘

七.说几个未知宽高元素水平垂直居中方法

1.使用display:flex布局

justify-content: center;
align-items: center;

2.设置元素相对父级定位

position:absolute;
left:50%;
right:50%

3.让自身平移自身高度50% ,这种方式兼容性好,被广泛使用的一种方式

transform: translate(-50%,-50%);

4.使用display:grid布局

justify-content:center;
align-items:center

5.使用display: table-cell,

设置元素的父级为表格元素
display: table-cell;
text-align: center;
vertical-align: middle;

设置子元素为行内块
display: inline-block;

八.说一说三栏布局的实现方案

粗略方案

  1. 使用浮动(Float)

    • 左右栏使用float: left;float: right;,中间内容区域使用margin来调整位置。
    • 优点:兼容性好,适用于旧版浏览器。
    • 缺点:需要清除浮动以避免父容器高度塌陷,可能需要额外的清除浮动的样式。
  2. 使用定位(Positioning)

    • 左右栏使用position: absolute;,中间内容区域使用margin来调整位置。
    • 优点:灵活性高,可以轻松实现各种复杂布局。
    • 缺点:对父容器定位可能造成影响,需要谨慎使用。
  3. 使用Flexbox布局

    • 将父容器设置为display: flex;,并且使用flex-grow来调整左右栏和中间内容的比例。
    • 优点:简单易用,支持响应式布局,适应性强。
    • 缺点:对于一些旧版浏览器的兼容性不好。
  4. 使用Grid布局

    • 使用CSS Grid布局,将父容器设置为网格布局,然后通过设置网格列来实现三栏布局。
    • 优点:灵活性强,对于复杂的布局可以更容易实现。
    • 缺点:对于一些旧版浏览器的兼容性不好。
  5. 使用表格布局

    • 使用HTML表格标签<table>来实现三栏布局,左右栏放在表格的两侧,中间内容放在表格的中间。
    • 优点:兼容性好,简单易懂。
    • 缺点:不推荐使用表格来布局,不利于语义化,不够灵活。

在选择布局方案时,可以根据项目需求、兼容性要求和开发者的技术栈选择合适的方案。Flexbox和Grid布局是现代Web开发中推荐的布局方式,它们提供了更多的布局控制和灵活性。

1.使用浮动,

一般情况的等比三栏布局:都设置 float:left,注意最后清除浮动

双飞翼:

  • 它的主要思想是将左右两个侧边栏用负外边距进行定位,使它们能够脱离文档流,而主要内容区域则通过左右内边距来避开侧边栏的位置。这样做的好处是,使得主要内容区域可以在文档流中优先渲染,而侧边栏则在视觉上紧跟在主要内容后面。
  • 双飞翼布局的关键在于使用额外的空元素作为浮动容器,通过负外边距来实现定位。

圣杯:

  • 与双飞翼布局类似,圣杯布局也使用了负外边距和浮动来实现。
  • 不同之处在于,圣杯布局采用了更多的 CSS 技巧来实现侧边栏的自适应高度,避免了双飞翼布局中使用空元素的方式。这种布局模型通常会使用相对定位和负边距来为侧边栏留出空间,并使用相对定位将主要内容区域拉回来。

总结:

圣杯流程:中间元素放最前,宽度100%,左右元素固定宽度,三个元素都用float:left

中间元素使用padding空出左右的位置,左右通过margin和相对定位进行移动

双飞翼:中间元素放最前,需要单独在把内容部分包裹,然后设置padding,之后只使用margin进行左右位置的移动

双飞翼布局比圣杯布局多了一层DOM节点

双飞翼布局源码:
<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>双飞翼布局</title>
</head>

<!-- 双飞翼布局实现效果 -->
<!-- 1、目的:两侧内容宽度固定,中间内容宽度自适应 -->
<!-- 2、三栏布局,中间一栏最先加载、渲染出来 -->
<!-- 实现方法:float+margin -->

<!-- 靠在中间这层外面套一层div加padding将内容挤出来中间 -->

<body>
    <div class="header">header</div>
    <div class="main middle">
        <div id="main-wrapper">middle</div>
    </div>
    <div class="left">left</div>
    <div class="right">right</div>
    <div class="footer">footer</div>
</body>

</html>
<style>
    * {
        margin: 0;
        padding: 0;
    }
    
    body {
        /* 设置最小宽度,防止挤压使中间内容消失 */
        min-width: 600px;
    }
    
    .header {
        text-align: center;
        height: 70px;
        background-color: coral;
    }
    
    .main #main-wrapper {
        margin-left: 100px;
        margin-right: 100px;
    }
    
    .left,
    .middle,
    .right {
        float: left;
    }
    
    .left {
        height: 100px;
        width: 100px;
        background-color: darkmagenta;
        margin-left: -100%;
    }
    
    .right {
        height: 100px;
        width: 100px;
        background-color: darkslategray;
        margin-left: -100px;
    }
    
    .middle {
        height: 100px;
        width: 100%;
        min-width: 200px;
        background-color: forestgreen;
        /* 不能在外层容器里面加padding,否则会使布局乱套 */
        /* padding-left: 100px;
        padding-right: 100px; */
    }
    
    .footer {
        text-align: center;
        height: 50px;
        clear: both;
        background-color: darkgrey;
    }
</style>
圣杯布局源码:
<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>圣杯布局</title>
</head>


<!-- 圣杯布局实现效果 -->
<!-- 1、目的:两侧内容宽度固定,中间内容宽度自适应 -->
<!-- 2、三栏布局,中间一栏最先加载、渲染出来 -->
<!-- 实现方法:float搭建布局+margin使三列布局到一行上+relative相对定位调整位置 -->

<!-- 不同之处:怎么处理两列的位置 -->
<!-- 给外部容器加padding,通过相对定位把两边定位出来 -->

<!-- 相同之处: -->
<!-- 让左中右三列浮动,通过父外边距形成三列布局 -->

<body>
    <div class="header">header</div>
    <div class="content wrapper">
        <div class="middle">middle</div>
        <div class="left">left</div>
        <div class="right">right</div>
    </div>
    <div class="footer">footer</div>
</body>

</html>
<style>
    * {
        margin: 0;
        padding: 0;
    }
    
    body {
        /* 设置最小宽度,防止挤压使中间内容消失 */
        min-width: 600px;
    }
    
    .header,
    .footer {
        background-color: #bbe0e3;
        height: 100px;
        border: dimgray 1px solid;
    }
    /* 通过BFC解决高度塌陷 */
    /* .content {
      overflow: hidden;  
  } */
    
    .footer {
        /* 通过清除底部浮动解决高度塌陷 */
        clear: both;
    }
    
    .wrapper {
        padding-left: 100px;
        padding-right: 100px;
    }
    
    .content .middle,
    .left,
    .right {
        background-color: #d9d9d9;
    }
    
    .middle {
        height: 100px;
        background-color: dimgray;
        /* 中间列自适应,所以宽度100%继承父元素宽度 */
        width: 100%;
        float: left;
    }
    
    .left {
        height: 100px;
        background-color: #d5d50f;
        width: 100px;
        float: left;
        position: relative;
        margin-left: -100%;
        right: 100px;
    }
    
    .right {
        height: 100px;
        background-color: #8cca4d;
        width: 100px;
        float: left;
        margin-right: -100px;
    }
</style>

2.使用 display:flex

通过flex-grow分配比例

拓展

flex: 1;flex-grow, flex-shrink, 和 flex-basis 属性的缩写形式。这三个属性通常一起使用来定义 Flexbox 容器中每个项目的伸缩性、收缩性和初始大小。

具体地说:

  • flex-grow:定义了项目的增长系数,决定了项目在可用空间中的分配比例。
  • flex-shrink:定义了项目的收缩系数,决定了项目在空间不足时的缩小比例。
  • flex-basis:定义了项目的初始大小。它可以是一个长度值(如像素、百分比等),也可以是 auto,表示由项目的内容决定初始大小。

当使用 flex: 1; 缩写时,这三个属性的值被设置为默认值:

  • flex-grow: 1;,即项目可以根据可用空间扩张。
  • flex-shrink: 1;,即项目可以缩小。
  • flex-basis: 0%;,即项目的初始大小为0%,允许项目根据内容和空间自动调整大小。

因此,flex: 1; 的效果是使得所有具有该属性的项目平均地占据剩余空间,而不考虑它们的初始大小。这在创建灵活的布局时非常有用,例如使所有项目在父容器中均匀分布并填充剩余空间。

JS部分

九.说一说JS数据类型有哪些,区别是什么?

JS数据类型分为两类:

基本数据类型,

也叫简单数据类型,包含7种类型,分别是Number 、String、Boolean、BigInt、Symbol、Null、Undefined。

引用数据类型(复杂数据类型)

通常用Object代表,普通对象,数组,正则,日期,Math数学函数都属于Object。

 数据分成两大类的本质区别:

基本数据类型和引用数据类型它们在内存中的存储方式不同。

基本数据类型

是直接存储在栈中的简单数据段,占据空间小,属于被频繁使用的数据。


引用数据类型

是存储在堆内存中,占据空间大。引用数据类型在栈中存储了指针,该指针指向堆中该实体的起始地址,当解释器寻找引用值时,会检索其在栈中的地址,取得地址后从堆中获得实体。

(拓展到数据类型判断方法)

拓展:

 Symbol

是ES6新出的一种数据类型,这种数据类型的特点就是没有重复的数据,可以作为object的key。
数据的创建方法Symbol(),因为它的构造函数不够完整,所以不能使用new Symbol()创建数据。由于Symbol()创建数据具有唯一性,所以 Symbol() !== Symbol(), 同时使用Symbol数据作为key不能使用for获取到这个key,需要使用Object.getOwnPropertySymbols(obj)获得这个obj对象中key类型是Symbol的key值。

 

let key = Symbol('key');
let obj = { [key]: 'symbol'};
let keyArray = Object.getOwnPropertySymbols(obj); // 返回一个数组[Symbol('key')]
obj[keyArray[0]] // 'symbol'

BigInt

也是ES6新出的一种数据类型,这种数据类型的特点就是数据涵盖的范围大,能够解决超出普通数据类型范围报错的问题。

使用方法:
-整数末尾直接+n:647326483767797n
-调用BigInt()构造函数:BigInt("647326483767797")
 

注意:BigInt和Number之间不能进行混合操作

十.说一说null 和 undefined 的区别,如何让一个属性变为null

1.类型不同

null和undefined其实是两种数据类型

当使用 typeof 进行判断时

typeof null === 'object'

而 typeof undefined  === 'undefined'

但是实际上的null 的类型是NULL,

typeof 判断为 object,是因为在JS的底层的二进制判断中,二进制的前三位为0都会被判断为对象类型,而null的值都是0,所以使用 typeof 判断是object

2.含义不同

null表示的是 一个数据被定义,并且赋值是 null空,

undefined 表示的是,一个数据被定义了,但是没有被赋值,或是函数的返回为空

 让一个属性变为null

其实很简单,只需要使这个变量等于 null即可

注意当基本类型被设置为null时,由于基本类型是储存在栈上按值传递的,所以设置为空(null)时,并不会影响内存的变化。

当引用类型被设置为null时,会引起内存的变化,因为引用类型的属性方法是储存在堆内存上,在创建引用类型时,会在栈中存储一个地址,来指向对应的堆内存,当为null时,即把栈内存的地址指向为null,所以此时的堆内存没有了对应的引用,当JS的垃圾回收机制就会清除掉这种没有被引用的内存空间。

(这么回答可以引导面试官 到 JS的垃圾回收机制,再细思一下还可以引导到 深拷贝和浅拷贝)

十一.说一说JavaScript有几种方法判断变量的类型?

1.typeof

用于基本数据类型判断,对于引用类型一致返回 object,对于function返回 function

2.instanceof

用于具体判断区分引用类型

[] insranceof Array -> true

{} insranceof Object -> true
注意,如果使用  引用类型/函数 insranceof Object -> 最后结果都是true,涉及到原型链知识

对于 undefined, null, symbol 基本类型无法判断 

instanceof的实现原理:验证当前类的原型prototype是否会出现在实例的原型链__proto__上,只要在它的原型链上,则结果都为true。因此,`instanceof` 在查找的过程中会遍历左边变量的原型链,直到找到右边变量的 `prototype`,找到返回true,未找到返回false。

(期间可能会拓展到 原型链 上)

3.Array.isArray(obj)

这个方法就是来判断一个数据是否为数组

4.constructor

(用于引用数据类型)

检测方法是获取实例的构造函数判断和某个类是否相同,如果相同就说明该数据是符合那个数据类型的,这种方法不会把原型链上的其他类也加入进来,避免了原型链的干扰。
[].constructor === Array -> true

{}..constructor === Object ->  报错

在 JavaScript 中,由于对象字面量 {} 是一个独立的语法结构,不是一个对象实例,因此无法直接通过 .constructor 来访问其构造函数

5.Object.prototype.toString.call()

(对象原型链判断方法)

 Object.prototype.toString.call()原理:Object.prototype.toString 表示一个返回对象类型的字符串,call()方法可以改变this的指向,那么把Object.prototype.toString()方法指向不同的数据类型上面,返回不同的结果

(讲到这里可能会牵引到原型链,对象的 call bind 等方法,或者手写一个 call方法)

十二.说一说数组去重都有哪些方法?

具体实现代码参考:JS数组去重方法-CSDN博客
1.双层循环 + splice(当前索引,1)  时间复杂度O(n^2), 空间复杂度O(1)
for(let i=0;i<arr.length;i++){
    for(let j=i+1;j<arr.length;j++){
        if(arr[i] === arr[j]){
            arr.splice(j,1)
        }
    }
}

这种方法虽然可以去除数组中的重复元素,但它的时间复杂度相对较高,为 O(n^2),因为每次 splice 操作都会导致数组的重新排列。在大型数组中使用时可能性能较差,尤其是当数组长度较大时。

2.循环 + indexOf 方法  时间复杂度O(n^2), 空间复杂度O(1)
for(let i=0;i<arr.length;i++){
    let index = arr.indexOf(arr[i],i+1)
    if(index !== -1){
        arr.splice(index,1)
        i--
    }
}

这种方法的缺点也是相对较高的时间复杂度,因为每次调用 indexOf() 都需要遍历数组来查找元素的位置。对于大型数组,性能可能会受到影响。

3.循环 + arr.sort() 方法  时间复杂度O(n log n), 空间复杂度O(1)
arr.sort()
for (let i = 1; i < arr.length; i++) {
    if (arr[i] === arr[i - 1]) {
      arr.splice(i, 1);
      i--; // 减少 i 的值,以便下一次继续比较同位置的元素
    }
  }

这种方法的时间复杂度取决于 arr.sort() 的实现,通常为 O(n log n),再加上一次循环,总体效率较高。但请注意,这种方法改变了原数组的顺序,如果要保持原数组的顺序,可以在排序前创建数组的副本。

4.循环 + includes()   时间复杂度O(n *m), 空间复杂度O(n)
let newArr = []
for(let i=0;i<arr.length;i++){
    if(!newArr.includes(arr[i])){
        newArr.push(arr[i])
    }
}

这种方法通过 includes() 来判断当前元素是否已经存在于结果数组中,如果不存在则添加到结果数组中。这样可以保证结果数组中的元素是唯一的。

然而,需要注意的是 includes() 方法的时间复杂度为 O(n),因此总体的时间复杂度为 O(n*m),在大型数组上性能可能较差。

5.reduce + includes  时间复杂度O(n^2), 空间复杂度O(n)
arr.reduce((result, current) => {
    if (!result.includes(current)) {
        result.push(current);
    }
    return result;
}, []);
6. filter+indexOf  时间复杂度O(n^2), 空间复杂度O(n)
arr.filter((item, index) => arr.indexOf(item) === index);
7.利用对象属性 key 排除重复项   时间复杂度O(n^2), 空间复杂度O(n)
const obj = {};
const result = [];

for (let i = 0; i < arr.length; i++) {
    const item = arr[i];
    if (!obj.hasOwnProperty(item)) {
        obj[item] = true;
        result.push(item);
    }
}
 8.Set方法, new Set(oldArr)   时间复杂度O(n), 空间复杂度O(n)
const newArr= [...new Set(arr)];
 9.循环 + Map   时间复杂度O(n), 空间复杂度O(n)
let map = new Map()
let newArr = []

for(let i=0;i<arr.length;i++){
    if(map.has(arr[i]){
        map.set(arr[i],true)
    } else {
        newArr.push(arr[i])
        map.set(arr[i],false)
    }
}

(最后可能会问一下哪一个比较好,就是比较时间复杂度,空间复杂度)

十三.数组与伪数组的区别

区别

数组类型是 Array 可以使用数组的方法

伪数组 是 Object ,不能使用数组方法获取,但是可以使用 .[item]获取值,可以使用.length

转化方法
// 方法1: 使用 Array.prototype.slice.call()
const pseudoArray = document.querySelectorAll('.pseudo'); // 伪数组
const trueArray1 = Array.prototype.slice.call(pseudoArray);

// 方法2: 使用 [].slice.call()
const trueArray2 = [].slice.call(pseudoArray);

// 方法3: 使用 Array.from()
const trueArray3 = Array.from(pseudoArray);

(可能转到对象,原型链,方法上去)

十四.说一说map 和 forEach 的区别

map方法是创建一个新数组,返回处理之后的值,

forEach方法返回值是undefined

二者都不会主动修改原数组

map的处理速度比forEach快,而且返回一个新的数组,方便链式调用其他数组新方法

十五.说一说es6中箭头函数

简答:

没有this、this是从外部获取、不能使用new、没有arguments、没有原型和super

展开 

箭头函数相当于匿名函数,简化了函数定义。箭头函数有两种写法,当函数体是单条语句的时候可以省略{}和return。另一种是包含多条语句,不可以省略{}和return。

箭头函数最大的特点就是没有this,所以this是从外部获取,就是继承外部的执行上下文中的this,由于没有this关键字所以箭头函数也不能作为构造函数, 同时通过 `call()` 或 `apply()` 方法调用一个函数时,只能传递参数(不能绑定this),第一个参数会被忽略。

箭头函数也没有原型和super。不能使用yield关键字,因此箭头函数不能用作 Generator 函数。不能返回直接对象字面量。

箭头函数的不适用场景:
var dog = { lives: 20, jumps: () => { this.lives--; } } 

-定义对象上的方法 当调用` dog.jumps` 时,`lives` 并没有递减。因为 `this` 没有绑定值,而继承父级作用域。

var button = document.querySelector('button'); 
button.addEventListener('click',
 () => { this.classList.toggle('on'); }); 

-不适合做事件处理程序 此时触发点击事件,this不是button,无法进行class切换

箭头函数函数适用场景:

-简单的函数表达式,内部没有this引用,没有递归、事件绑定、解绑定,适用于map、filter等方法中,写法简洁

var arr = [1,2,3];
var newArr = arr.map((num)=>num*num)

-内层函数表达式,需要调用this,且this应与外层函数一致时

let group = {
  title: "Our Group",
  students: ["John", "Pete", "Alice"],
  showList() {
    this.students.forEach(
      student => alert(this.title + ': ' + student)
    );
  }
};

group.showList();

十六.说一说this指向(普通函数、箭头函数)

1.普通函数中的 this 指向:
  • 当一个函数被作为普通函数调用时,this 默认指向全局对象(在浏览器中通常是 window 对象)。
  • 在严格模式下,如果函数被作为普通函数调用,this 将会是 undefined
  • 如果函数被绑定在某个对象上并且通过该对象进行调用,this 将会指向该对象。
  • 使用 call()apply()bind() 可以显式地指定函数内部的 this

2.箭头函数中的 this 指向:
  • 箭头函数没有自己的 this,它继承自包含它的最近一层非箭头函数的 this 值。换句话说,箭头函数的 this 指向了它外部的作用域中的 this
  • 因为箭头函数没有自己的 this,所以在箭头函数内部无法通过 call()apply()bind() 来改变 this 的指向。
  • 箭头函数在定义时绑定了 this 的值,而不是在运行时。

3.立即执行函数的this指向

立即执行函数(Immediately Invoked Function Expression,IIFE)的 this 指向取决于它被调用的上下文。

//在浏览器环境下,如果立即执行函数直接作为全局代码执行,那么 this 指向全局对象 window
(function() {
  console.log(this === window); // 输出 true
})();


//立即执行函数作为对象的方法被调用,那么 this 将指向该对象
var obj = {
  method: function() {
    (function() {
      console.log(this === window); // 输出 false
      console.log(this === obj);    // 输出 true
    })();
  }
};

obj.method();
4.定时器的this指向

定时器的 this 指向取决于函数的调用方式。当使用 setTimeoutsetInterval 创建定时器时,回调函数内部的 this 默认指向全局对象(在浏览器中是 window 对象)。

如果你想在定时器的回调函数中使用外部函数的 this,可以通过一些方法来解决,如使用箭头函数、在外部保存 this 到一个变量等。

//全局环境下的定时器:
setTimeout(function() {
    console.log(this); // 输出 window 对象
}, 1000);

//对象方法中的定时器:
var obj = {
    name: 'John',
    greet: function() {
        setTimeout(function() {
            console.log(this.name); 
            // 输出 undefined,因为定时器回调函数的 this 指向全局对象
        }, 1000);
    }
};

obj.greet();


//使用箭头函数:
var obj = {
    name: 'John',
    greet: function() {
        setTimeout(() => {
            console.log(this.name); 
            // 输出 "John",箭头函数不会改变 this 的指向,仍然指向外部函数的 this
        }, 1000);
    }
};

obj.greet();

十七.事件扩展符用过吗(...),什么场景下

展开语法(Spread syntax)在 JavaScript 中的应用非常广泛,它可以在函数调用、数组构造、对象字面量等场景中展开数组、字符串和对象。

1. 在函数调用中使用:

  这里的 `...args` 将数组 `args` 中的元素展开,作为函数的参数传递给 `myFunction`。

function myFunction(x, y, z) {
    console.log(x, y, z);
}

const args = [1, 2, 3];
myFunction(...args); // 等同于 myFunction(1, 2, 3)

2. 在数组构造中使用:

   展开语法可以将一个数组中的元素展开,并将它们合并到另一个数组中。

const arr1 = [1, 2, 3];
const arr2 = [4, 5, 6];
const mergedArray = [...arr1, ...arr2];
console.log(mergedArray); // 输出:[1, 2, 3, 4, 5, 6]

3. 在对象字面量中使用:

对象的展开语法可以将一个对象中的属性展开,并将它们合并到另一个对象中。

const obj1 = { x: 1, y: 2 };
const obj2 = { z: 3 };
const mergedObject = { ...obj1, ...obj2 };
console.log(mergedObject); // 输出:{ x: 1, y: 2, z: 3 }
4. 在字面量数组或字符串连接中使用:

 这里 `...parts` 将数组 `parts` 中的元素展开,并将它们插入到新数组中。

const parts = ['apple', 'banana'];
const fruits = ['orange', ...parts, 'kiwi'];
console.log(fruits); // 输出:['orange', 'apple', 'banana', 'kiwi']
5. 构造字面量对象时进行浅克隆或属性拷贝:

 使用展开语法可以方便地进行对象的浅克隆或属性拷贝,将原对象的属性展开到新对象中。

const obj = { x: 1, y: 2 };
const clone = { ...obj };
console.log(clone); // 输出:{ x: 1, y: 2 }
拓展

展开语法(Spread syntax):

  • 展开语法用于展开数组或对象中的元素,并将它们作为独立的参数传递给函数、数组字面量或对象字面量。
  • 在数组或对象中使用展开语法时,被展开的对象必须是可迭代的。如果对象不是可迭代的,则会抛出 TypeError 错误。
const obj = { 'key1': 'value1' };
const array = [...obj]; // TypeError: obj is not iterable

剩余语法(Rest syntax):

  • 剩余语法用于将多个参数收集起来并“凝聚”为单个参数。它通常用于函数参数中,以允许函数接受任意数量的参数并将它们放入一个数组中。
  • 在函数参数中使用剩余语法时,它将未被显式命名的参数收集到一个数组中。
function f(...rest) {
    return rest;
}
console.log(f(1));          // [1] (b 和 c 未定义)
console.log(f(1, 2, 3));    // [1, 2, 3]
console.log(f(1, 2, 3, 4)); // [1, 2, 3, 4] (第四个参数未被解构)

十八.说一说JS变量提升

简答:

Var声明的变量声明提升、函数声明提升、let和const变量不提升

展开:

变量提升是指JS的变量和函数声明会在代码编译期,提升到代码的最前面。

变量提升成立的前提是使用Var关键字进行声明的变量,并且变量提升的时候只有声明被提升,赋值并不会被提升,同时函数的声明提升会比变量的提升优先。

变量提升的结果,可以在变量初始化之前访问该变量,返回的是undefined。在函数声明前可以调用该函数。

使用let和const声明的变量是创建提升,形成暂时性死区,在初始化之前访问let和const创建的变量会报错。

十九.说一说js继承的方法和优缺点

1.原型链继承

优点:

  • 写法方便简洁,容易理解。

缺点:

  • 引用类型值的实例属性会在子类型原型上变成原型属性,被所有子类型实例所共享。
  • 创建子类型实例时不能向父类型构造函数中传递参数。

function Parent() {
    this.name = 'Parent';
}
Parent.prototype.sayHello = function() {
    console.log('Hello, I am ' + this.name);
};

function Child() {
    // 继承了 Parent
}
Child.prototype = new Parent();

var child1 = new Child();
var child2 = new Child();
child1.sayHello(); // 输出 "Hello, I am Parent"
child2.sayHello(); // 输出 "Hello, I am Parent"
2.借用构造函数继承

优点:

  • 解决了原型链继承的共享属性和无法传参的问题。

缺点:

  • 方法都在构造函数中定义,无法实现函数复用。
  • 父类型原型中定义的方法对子类型不可见。
function Parent(name) {
    this.name = name;
}
Parent.prototype.sayHello = function() {
    console.log('Hello, I am ' + this.name);
};

function Child(name) {
    Parent.call(this, name); // 借用父类构造函数
}
var child1 = new Child('Child1');
var child2 = new Child('Child2');
child1.sayHello(); // 输出 "Hello, I am Child1"
child2.sayHello(); // 输出 "Hello, I am Child2"
3.组合继承

优点:

  • 解决了原型链继承和借用构造函数继承的影响。
  • 函数复用。
  • 每个实例都有自己的属性。

缺点:

  • 无论在什么情况下,都会调用两次超类型构造函数。
function Parent(name) {
    this.name = name;
}
Parent.prototype.sayHello = function() {
    console.log('Hello, I am ' + this.name);
};

function Child(name, age) {
    Parent.call(this, name); // 借用父类构造函数
    this.age = age;
}
Child.prototype = new Parent(); // 继承父类原型
Child.prototype.constructor = Child; // 修复构造函数指向

var child1 = new Child('Child1', 20);
var child2 = new Child('Child2', 25);
child1.sayHello(); // 输出 "Hello, I am Child1"
child2.sayHello(); // 输出 "Hello, I am Child2"
4.原型式继承

优点:

  • 不需要单独创建构造函数。

缺点:

  • 属性中包含的引用值始终会在相关对象间共享。
var person = {
    name: 'Alice',
    age: 25
};
var anotherPerson = Object.create(person);
console.log(anotherPerson.name); // 输出 "Alice"
5.寄生式继承

优点:

  • 写法简单,不需要单独创建构造函数。

缺点:

  • 函数难以重用。
function createAnother(original) {
    var clone = Object.create(original);
    clone.sayHello = function() {
        console.log('Hello, I am ' + this.name);
    };
    return clone;
}
var person = {
    name: 'Alice',
    age: 25
};
var anotherPerson = createAnother(person);
anotherPerson.sayHello(); // 输出 "Hello, I am Alice"
6.寄生组合式继承

优点:

  • 高效率只调用一次父构造函数。
  • 避免了在子原型上创建不必要,多余的属性。
  • 原型链保持不变。

缺点:

  • 代码复杂。
function Parent(name) {
    this.name = name;
}
Parent.prototype.sayHello = function() {
    console.log('Hello, I am ' + this.name);
};

function Child(name, age) {
    Parent.call(this, name); // 借用父类构造函数
    this.age = age;
}
Child.prototype = Object.create(Parent.prototype); // 继承父类原型
Child.prototype.constructor = Child; // 修复构造函数指向

var child1 = new Child('Child1', 20

二十.说一说defer和async区别

它们都可以让 script 标签异步加载

区别有以下几点:

  1. 执行时机:
    • async:脚本的加载和执行是异步的,当脚本加载完成后,会立即执行,不会阻塞 HTML 解析和其他资源的加载。脚本加载完成和执行的顺序与它们在 HTML 中的顺序不一定相同。
    • defer:脚本的加载是异步的,但是脚本会在 HTML 解析完毕之后,DOMContentLoaded 事件触发之前执行。多个 defer 脚本按照它们在 HTML 中出现的顺序依次执行。
  2. 执行时机受限制:
    • async:脚本一旦加载完成,会立即执行,无论 HTML 文档的解析是否完成。因此,如果有多个 async 脚本,它们的执行顺序是不确定的,取决于加载完成的顺序。
    • defer:脚本会在 HTML 解析完毕之后执行,因此它们的执行顺序与它们在 HTML 中出现的顺序一致。
  3. 对文档的影响:
    • async:由于脚本加载完成后会立即执行,可能会影响页面的内容和交互,特别是对于需要立即执行的脚本。适合不需要依赖页面内容的脚本。
    • defer:脚本会在 HTML 解析完毕后执行,因此不会影响页面内容和交互。适合需要等待页面解析完毕后执行的脚本,例如操作 DOM 元素的脚本。

拓展:

渲染阻塞的原因,由于js的执行与页面的渲染,它是由两个不同的线程操纵的,并且js是可以操作DOM的,如果二者同时进行,假如在渲染过程,js操作了某个DOM,那么就会造成渲染的元素可能出现前后不一致,从而产生一些列的潜在问题,所以为了避免出现这种情况,浏览器就将JS引擎与GUI渲染引擎设置为互斥的关系,这样,当JS引擎执行的时候,GUI渲染引擎就会进入一个队列,等待JS引擎运行完成它才运行。所以,假如JS运行时间过长,就会造成页面的阻塞。

二十一.说一说JS实现异步的方法

1.定时器 + 回调函数

早期的异步编程方法,原理就是将一个函数作为参数传递,当异步操作完成后调用该函数,

例如,在 AJAX 请求完成时调用一个回调函数来处理响应数据。

优点:

简单、容易理解和实现,

缺点:

不利于代码的阅读和维护,各个部分之间高度耦合,使得程序结构混乱、流程难以追踪(尤其是多个回调函数嵌套的情况),而且每个任务只能指定一个回调函数。此外它不能使用 try catch 捕获错误,不能直接 return

function fetchData(callback) {
  // 模拟异步操作
  setTimeout(function() {
    var data = '异步数据';
    callback(data); // 异步操作完成后调用回调函数
  }, 1000);
}

// 调用 fetchData 函数,并传递一个回调函数处理数据
fetchData(function(data) {
  console.log('收到数据:', data);
});
2.promise的使用
优点:

它能够解决回调地狱的问题

原理就是使用时,它会创建一个Promise实例,然后根据异步调用的结果,分别调用实例中的resolve 和 reject方法,然后通过 .then/.catch分别接收成功/失败的对应信息数据,做出相应的处理。

缺点:

它不能取消。

function fetchData() {
  return new Promise(function(resolve, reject) {
    // 模拟异步操作
    setTimeout(function() {
      // 模拟错误
      var error = true;
      if (error) {
        reject('发生错误');
      } else {
        var data = '异步数据';
        resolve(data); // 异步操作成功,调用 resolve 方法
      }
    }, 1000);
  });
}

// 使用 Promise 的 catch() 方法捕获错误
fetchData()
  .then(function(data) {
    console.log('收到数据:', data);
  })
  .catch(function(error) {
    console.error('发生错误:', error);
  });
3.生成器Generators/yield

Generator 函数是 ES6 提供的一种异步编程解决方案,Generator 函数是一个状态机,封装了多个内部状态,可暂停函数, yield可暂停,next方法可启动,每次返回的是yield后的表达式结果。优点是异步语义清晰,缺点是手动迭代`Generator` 函数很麻烦,实现逻辑有点绕

生成器(Generators)是 ES6 中引入的一种特殊类型的函数,它与普通函数不同,可以在需要时暂停和恢复执行。生成器函数通过 function* 关键字定义,而不是普通函数的 function 关键字。

在生成器函数内部,使用 yield 关键字来暂停函数的执行,并返回一个值给调用方。每次调用生成器函数时,它都会返回一个迭代器对象(Iterator),可以通过该对象的 next() 方法来继续执行生成器函数,并在遇到下一个 yield 语句时再次暂停。

function* counter() {
  let count = 0;
  while (true) {
    yield count;
    count++;
  }
}

const iterator = counter();

console.log(iterator.next().value); // 输出:0
console.log(iterator.next().value); // 输出:1
console.log(iterator.next().value); // 输出:2
// 依次类推...

4.async/await

 async/awt是基于Promise实现的,async/awt使得异步代码看起来像同步代码,所以优点是,使用方法清晰明了,缺点是awt 将异步代码改造成了同步代码,如果多个异步代码没有依赖性却使用了 awt 会导致性能上的降低,代码没有依赖性的话,完全可以使用 Promise.all 的方式。

async function fetchData() {
  return new Promise(function(resolve, reject) {
    // 模拟异步操作
    setTimeout(function() {
      var data = '异步数据';
      resolve(data); // 异步操作成功,调用 resolve 方法
    }, 1000);
  });
}

// 使用 async 函数调用 fetchData,并使用 await 等待异步操作结果
async function getData() {
  var data = await fetchData();
  console.log('收到数据:', data);
}

getData();

拓展:

JS 异步编程进化史:callback -> promise -> generator/yield -> async/awt。 async/awt函数对 Generator 函数的改进,体现在以下三点: - 内置执行器。 Generator 函数的执行必须靠执行器,而 async 函数自带执行器。也就是说,async 函数的执行,与普通函数一模一样,只要一行。 - 更广的适用性。 yield 命令后面只能是 Thunk 函数或 Promise 对象,而 async 函数的 awt 命令后面,可以跟 Promise 对象和原始类型的值(数值、字符串和布尔值,但这时等同于同步操作)。 - 更好的语义。 async 和 awt,比起星号和 yield,语义更清楚了。async 表示函数里有异步操作,awt 表示紧跟在后面的表达式需要等待结果。 目前使用很广泛的就是promise和async/awt

二十二.说一说cookie sessionStorage localStorage 区别

这些都是浏览器的本地存储,都是储存在本地浏览器的

存储大小一般是4K,经常用来存储一些信息凭证之类的,例如登录后的token信息,他一般是由服务器端设置过期时间,前端也可以通过setCookies时添加一个时间戳参数,用来设置cookie的过期时间。

sessionStorage

大小在5M左右,它的存在时间是浏览器窗口的存在时间。即关闭当前窗口时,它就会自动清除。它的限制就是必须是在同一页面的使用。

localStorage

大小也是5M左右,但是它的存在时间是永久在浏览器本地,要想清除必须手动设置,一般用来存储一些不易变动的数据,减少服务器的性能压力

二十三.说一说如何实现可过期的localstorage数据

1.惰性删除

惰性删除是指某个键值过期后,该键值不会被马上删除,而是等到下次被使用的时候,才会被检查到过期,此时才能得到删除。

在存储信息时,保存一对键值,value用来存储数据,key用来存储一个当前时间,下次访问请求数据时,用存储的时间和获取此次数据时的时间进行对比,超过设置的时间限制后,进行删除

2.定时删除

每隔一段时间执行一次删除操作,并通过限制删除操作执行的次数和频率,来减少删除操作对CPU的长期占用。另一方面定时删除也有效的减少了因惰性删除带来的对localStorage空间的浪费。

获取所有设置过期时间的key判断是否过期,过期就存储到数组中,遍历数组,每隔1S(固定时间)删除5个(固定个数),直到把数组中的key从localstorage中全部删除。

LocalStorage清空应用场景:token存储在LocalStorage中,要清空

二十四.说一下token 能放在cookie中吗

单说这个问题,它是可以的,一般自己做的一些小项目,当用户输入用户名与密码后,服务器端会返回一个token,此时,我们就可以把它设置在cookie中,用来当作登陆的一个凭证。

但是这么做会有一个弊端,无法防止 CSRF的攻击

拓展:token认证流程

1. 客户端使用用户名跟密码请求登录

2. 服务端收到请求,去验证用户名与密码

3. 验证成功后,服务端签发一个 token ,并把它发送给客户端

4. 客户端接收 token 以后会把它存储起来,比如放在 cookie 里或者 localStorage 里

5. 客户端每次发送请求时都需要带着服务端签发的 token(把 token 放到 HTTP 的 Header 里)

6. 服务端收到请求后,需要验证请求里带有的 token ,如验证成功则返回对应的数据

  • 28
    点赞
  • 28
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

程序猿online

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值