前端面试练习24.3.2-3.3

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:

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解决外边距的塌陷。

html 复制代码
<!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布局

css 复制代码
justify-content: center;
align-items: center;

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

css 复制代码
position:absolute;
left:50%;
right:50%

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

css 复制代码
transform: translate(-50%,-50%);

4.使用display:grid布局

css 复制代码
justify-content:center;
align-items:center

5.使用display: table-cell,

css 复制代码
设置元素的父级为表格元素
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节点

双飞翼布局源码:
html 复制代码
<!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>
圣杯布局源码:
html 复制代码
<!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值。

javascript 复制代码
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)
javascript 复制代码
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)
javascript 复制代码
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)
javascript 复制代码
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)
javascript 复制代码
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)
javascript 复制代码
arr.reduce((result, current) => {
    if (!result.includes(current)) {
        result.push(current);
    }
    return result;
}, []);
6. filter+indexOf 时间复杂度O(n^2), 空间复杂度O(n)
javascript 复制代码
arr.filter((item, index) => arr.indexOf(item) === index);
7.利用对象属性 key 排除重复项 时间复杂度O(n^2), 空间复杂度O(n)
javascript 复制代码
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)
javascript 复制代码
const newArr= [...new Set(arr)];
9.循环 + Map 时间复杂度O(n), 空间复杂度O(n)
javascript 复制代码
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

转化方法
javascript 复制代码
// 方法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 函数。不能返回直接对象字面量。

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

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

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

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

箭头函数函数适用场景:

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

javascript 复制代码
var arr = [1,2,3];
var newArr = arr.map((num)=>num*num)

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

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

group.showList();

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

普通函数中的 this 指向:
  • 当一个函数被作为普通函数调用时,this 默认指向全局对象(在浏览器中通常是 window 对象)。
  • 在严格模式下,如果函数被作为普通函数调用,this 将会是 undefined
  • 如果函数被绑定在某个对象上并且通过该对象进行调用,this 将会指向该对象。
  • 使用 call()apply()bind() 可以显式地指定函数内部的 this
箭头函数中的 this 指向:
  • 箭头函数没有自己的 this,它继承自包含它的最近一层非箭头函数的 this 值。换句话说,箭头函数的 this 指向了它外部的作用域中的 this
  • 因为箭头函数没有自己的 this,所以在箭头函数内部无法通过 call()apply()bind() 来改变 this 的指向。
  • 箭头函数在定义时绑定了 this 的值,而不是在运行时。

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

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

1. 在函数调用中使用:

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

javascript 复制代码
function myFunction(x, y, z) {
    console.log(x, y, z);
}

const args = [1, 2, 3];
myFunction(...args); // 等同于 myFunction(1, 2, 3)
2. 在数组构造中使用:

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

javascript 复制代码
const arr1 = [1, 2, 3];
const arr2 = [4, 5, 6];
const mergedArray = [...arr1, ...arr2];
console.log(mergedArray); // 输出:[1, 2, 3, 4, 5, 6]
3. 在对象字面量中使用:

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

javascript 复制代码
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` 中的元素展开,并将它们插入到新数组中。

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

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

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

展开语法(Spread syntax):

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

剩余语法(Rest syntax):

  • 剩余语法用于将多个参数收集起来并"凝聚"为单个参数。它通常用于函数参数中,以允许函数接受任意数量的参数并将它们放入一个数组中。
  • 在函数参数中使用剩余语法时,它将未被显式命名的参数收集到一个数组中。
javascript 复制代码
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.原型链继承

优点:

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

缺点:

  • 引用类型值的实例属性会在子类型原型上变成原型属性,被所有子类型实例所共享。
  • 创建子类型实例时不能向父类型构造函数中传递参数。
javascript 复制代码
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.借用构造函数继承

优点:

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

缺点:

  • 方法都在构造函数中定义,无法实现函数复用。
  • 父类型原型中定义的方法对子类型不可见。
javascript 复制代码
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.组合继承

优点:

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

缺点:

  • 无论在什么情况下,都会调用两次超类型构造函数。
javascript 复制代码
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.原型式继承

优点:

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

缺点:

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

优点:

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

缺点:

  • 函数难以重用。
javascript 复制代码
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.寄生组合式继承

优点:

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

缺点:

  • 代码复杂。
javascript 复制代码
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 标签异步加载

区别有以下几点:

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

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

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

1.定时器 + 回调函数

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

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

优点:

简单、容易理解和实现,

缺点:

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

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

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

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

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

缺点:

它不能取消。

javascript 复制代码
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 语句时再次暂停。

javascript 复制代码
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 的方式。

javascript 复制代码
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 区别

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

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

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

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

拓展:token认证流程
  1. 客户端使用用户名跟密码请求登录

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

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

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

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

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

二十五.

很基础的重点

二十六.什么是原型链,说说相关的理解

二十七.说一说你对闭包的理解

二十八.说一说call apply bind的作用和区别,能手写一个吗

二十九.说一说new会发生什么,能手写一个吗

三十.说一说promise是什么与使用方法

三十一.说一说axios的拦截器原理及应用

三十二.说一说创建ajax过程

三十三.为什么使用虚拟DOM,而不操作真实DOM

三十四.vue的diff理解

三十五.什么是diff算法

相关推荐
mCell1 天前
GSAP ScrollTrigger 详解
前端·javascript·动效
gnip1 天前
Node.js 子进程:child_process
前端·javascript
excel1 天前
为什么在 Three.js 中平面能产生“起伏效果”?
前端
excel1 天前
Node.js 断言与测试框架示例对比
前端
天蓝色的鱼鱼1 天前
前端开发者的组件设计之痛:为什么我的组件总是难以维护?
前端·react.js
codingandsleeping1 天前
使用orval自动拉取swagger文档并生成ts接口
前端·javascript
石金龙1 天前
[译] Composition in CSS
前端·css
白水清风1 天前
微前端学习记录(qiankun、wujie、micro-app)
前端·javascript·前端工程化
Ticnix1 天前
函数封装实现Echarts多表渲染/叠加渲染
前端·echarts
用户22152044278001 天前
new、原型和原型链浅析
前端·javascript