前端面试核心精华版

303 阅读17分钟

前端面试押题精华版

HTML+CSS+JavaScript

flex布局

常用参数

flex布局常见的参数有以下6个:

参数取值
flex-directionrow、column、row-reverse、column-reverse
flex-wrapno-wrap、wrap、wrap-reverse
flex-flowflex-direction || flex-wrap
justify-contentflex-start、center、flex-end、space-between、space-around
align-itemsstretch、flex-start、center、flex-end、baseline
align-contentstretch、flex-start、center、flex-end、space-between、space-around

常用功能

  • 左右定宽,中间自适应(两侧元素宽度固定,中间子元素巧妙使用flex:1即可达成效果)
<div class="container">
    <div class="left"></div>
    <div class="mid"></div>
    <div class="right"></div>
</div>
.container {
	display: flex;
    height: 100%;
    .left {
        width: 100px;
        height: 100%;
        background: rosybrown;
    }
    .mid {
        width: 100%;
        height: 100%;
        flex: 1;
        background: royalblue;
    }
    .right {
        width: 100px;
        height: 100%;
        background: cadetblue;
    }
}

效果如下:

img

  • 等分布局
<div class="container">
    <div class="left"></div>
    <div class="mid"></div>
    <div class="right"></div>
</div>
.container {
	display: flex;
    height: 100%;
    .left {
        flex: 1;
        height: 100%;
        background: rosybrown;
    }
    .mid {
        height: 100%;
        flex: 1;
        background: royalblue;
    }
    .right {
        flex: 1;
        height: 100%;
        background: cadetblue;
    }
}

定位

常见的定位有如下几个取值:absolute、relative、static、fixed、sticky (粘性定位)、Inherit(继承父级)

static:一般元素默认都是静态定位

absolute:相对于最近有定位(除static以外)的父元素进行定位,

relative:相对自身进行定位

fixed:固定定位,一般导航侧边栏用的比较多

sticky:粘性定位是一个流盒,设置sticky的同时也要设置top | bottom | left | right,与fixed定位不同的是sticky会随着鼠标滑轮运动到指定位置停止。

<div class="sticky"></div>

body {
    height: 3000px;
    margin: 0;
}
.sticky {
    position: sticky;
    margin-top: 450px;
    transform: translateX(1100px);
    top: 10px;
    width: 100px;
    height: 100px;
    background-color: bisque;
}

实现效果如下:可以看到sticky元素开始随着滚动条滑动,最后固定在top:10px的位置。粘性定位常用于标题、表头、底部评论、操作栏。

sticky

css选择器

css选择器及优先级

选择器格式优先级权重
id选择器#id100
类选择器.classname10
属性选择器a[ref="eee"]10
伪类选择器li:first-child10
标签选择器div1
伪元素选择器li::after1
相邻兄弟选择器h1+p0
子选择器、后代选择器ul>li,li a0
通配符选择器*0
  • 内联样式的优先级为1000 > 内部样式 > 外部样式 > 浏览器用户自定义样式 > 浏览器默认样式
  • !important声明的样式优先级最高
  • 继承得到的样式优先级最低

清除浮动

  • 为什么需要清除浮动?

​ 在非IE浏览器下,父容器不设置高度且子元素浮动时,会造成父容器的高度坍塌,子元素会溢出到父容器外面从而影响页面布局。

  • 清除浮动的方法
  1. 给父级定义height属性
  2. 最后一个浮动元素添加一个空div标签,并且添加clear:both属性
  3. 包含浮动元素的父级标签添加overflow:hidden或者overflow:auto
  4. 使用伪元素清除浮动
/*给父元素添加clearfix类名,即可清除浮动*/
.clearfix::after {
	content: "";
	display: block;
	clear: both;
}

head标签里常见的标签有哪些

常见标签:title、meta、script、style、link、noscript、base

  • title:定义文档标题,显示在浏览器的标签页上,用于概括整个网页的内容
  • link:规定外部资源与当前文档的关系,常用于引入css样式
  • style:包含文档的样式信息
  • script:用于嵌入或引用可执行脚本,script标签常见的全局属性有asyncdefer,二者均用于异步加载js脚本;区别在于async在当脚本下载完成后会停止浏览器渲染并执行脚本,而defer是当js脚本下载完成会等浏览器渲染完成后再执行js脚本
  • meta: 用于描述网页文档的属性,比如网页的作者、网页的描述、关键词等

常用的meta标签有:

<!--charset用于描述HTML文档的编码类型 -->
<meta charset= "UTF-8">

<!--keywords 页面关键词 -->
<meta name="keywords" content="关键词" />

<!--viewport 适配移动端 可控制视口的大小和比例 -->
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1">

重排和重绘

  • 重排:当元素改变时,将会影响文档的结构、内容或元素位置;常见的会重排的属性:宽高、display、position、激活伪类、float等

  • 重绘:当元素的外观(visibility、outline、background、text-decoration)改变,但是布局没有发生改变

触发重排一定会产生重绘,但发生重绘不一定发生重排

水平垂直居中

flex布局实现居中

<div class="father">
	<div class="son"></div>
</div>
.father {
    display: flex;
    align-items: center;
    justify-content: center;
}
.son { 
    width: 100px;
    height: 100px;
    background-color: lightblue;
}

定位实现居中的方法有很多:

绝对定位配合transform:translate(-50%,-50%)实现

绝对定位配合margin:负值实现

绝对定位配合calc(50% - width/2)实现

<div class="father">
	<div class="son"></div>
</div>
/*绝对定位配合`transform:translate(-50%,-50%)`实现*/
.father {
    position: relative;
    width: 400px;
    height: 400px;
    margin: 0 auto;
    background-color: bisque;
}
.son { 
    width: 100px;
    height: 100px;
    position: absolute;
    top: 50%;
    left: 50%;
    transform: translate(-50%,-50%);
    background-color: lightblue;
}

另外还有grid布局和table布局也可以实现元素居中,grid布局实现和flex布局差不多,将display设置为grid即可。这两个布局的缺点就是兼容性不好

标准盒子模型和怪异盒模型

设置box-sizing即可设置盒模型:box-sizing:content-box | border-box

标准盒模型:width就是width,不包括其他的。

img

怪异盒模型 :width/height = content + padding + border

闭包

为什么会出现闭包?

JavaScript中内层函数可以访问外层函数的作用域,但是外层函数却不能够访问内层函数的作用域;为了让外层函数可以访问内层函数的作用域,因此诞生了闭包。

闭包的概念

一个函数与它周围的引用所捆绑在一起的组合。

看起来非常抽象,直接来看图解

image-20211209170212322

图中test函数内的若干变量和匿名函数绑定在一起成为一个组合,在test函数执行后,该组合被返回到全局环境下了并新开辟了一个内存块用于存放该组合。

test的执行结果赋予fn变量,之后令fn在全局环境下面执行;不难发现全局中有一个变量atest函数内也有一个变量a。那fn之后的打印a的结果是什么呢?

a最终打印为1。

因为fn函数内的若干变量已经和匿名函数绑定在一起返回到全局了,返回出去的这个绑定块在全局独立创建了一块独立的内存用于存放该绑定块。

所以即使fn函数是在全局环境下面执行的,它首先会在自己所拥有的内存区域内寻找a变量,如果找不到才会再去全局寻找。

闭包的特点

  • 局部变量常驻在内存中
  • 容易造成内存泄漏
  • 让外部访问函数内部变量成为可能
  • 可避免使用全局变量,防止全局变量污染

闭包的用途

  • 可以让函数外部读取函数内部局部变量的值
  • 让函数局部变量永久生存在内存当中,避免被垃圾回收机制回收
  • 可以进行模块化开发,防止全局变量污染

闭包导致的循环输出问题

for(var i = 1; i <= 5; i ++){
  setTimeout(function() {
    console.log(i)
  }, 0)
}

上面的代码毋庸置疑会输出五个6,那怎么才能输出1,2,3,4,5呢

  • setTimeout是宏任务,由于JavaScript是单线程机制,只有当执行栈中的任务执行完成后才会去执行宏任务;因此for循环在执行栈中执行完成后才会执行setTimeout
  • setTimeout函数也是一个闭包,它的父级作用域就是window,变量i成为了全局变量;在开始执行setTimeout之前变量i就已经变成6了
解决方法
立即执行函数

每次for循环时先把此时的变量传递给定时器然后执行

for(var i = 1;i<=5;i++){
(function(j){
	setTimeout(function(){
        console.log(j)
    },0)
})(i)
}
let

让每个不同值的i放入块级作用域中

for(let i = 1; i <= 5; i ++){
  setTimeout(function() {
    console.log(i)
  }, 0)
}
setTimeout传入第三个参数

setTimeout的第三个参数可以提升该函数执行的优先级

for(var i = 1; i <= 5; i ++){
  setTimeout(function() {
    console.log(i)
  }, 0,i)
}

继承

四种继承方式

  1. 构造函数
  2. 原型链
  3. 构造函数+原型链
  4. ES6类实现继承
构造函数实现继承
function Parent(){
    this.name = "Parent"
}

function Child(){
    Parent.call(this)
    this.type = "child的属性"
}
console.log(new Child)

在子类构造函数中写了Parent.call(this)指让Parent的构造函数在Child的构造函数中执行。让this指向由parent的实例改变为child的实例。

image-20211202165414970

缺点****

即使这样改变了this指向,但是Child的实例无法继承Parent的原型。如果我给Parent的原型上增加一个属性或者方法,Child的实例是不可以继承到的。

Parent.prototype.say = function(){}
通过原型链实现继承
function Parent() {
    this.name = 'Parent 的属性';
}

function Child() {
    this.type = 'Child 的属性';
}

Child.prototype = new Parent();
console.log(new Child)
image-20211202170713881

每个函数都有一个prototype属性,这个属性是一个对象。我们把Parent的实例赋值给了Child的Prototype,从而实现了继承。

image-20211202171128810

缺点****

如果修改child1实例的name属性,child2实例中的name属性也会随之改变。

function Parent(){
	this.name = 'Parent'
    this.arr = [1,2,3]
}
function Child(){
	this.type = 'Child的属性'
}

Child.prototype = new Parent();

var child1 = new Child()
var child2 = new Child()
child1.name = "child1改变后的名字"

console.log('child1的name' + child1.name)
console.log('child2的name' + child2.name)

child1.arr.push(4)//child1和child2中都会添加"4"进去
console.log('child1的arr' + child1.arr)
console.log('child2的arr' + child2.arr)

image-20211202172748140

这里可以发现因为二者公用一个原型,child1和child2中都会添加"4"进去,改变一个对象的内容,另一个对象的内容也随之改变,这样显然不太好。

构造函数+原型链实现继承
function Parent(){
	this.name = "Parent"
    this.arr = [1,2,3]
}
function Child(){
    Parent.call(this)
    this.type = "Child的属性"
}
Child.prototype = new Parent()

var child = new Child()

这样既可以继承父类原型的内容,又不会造成原型属性的修改。

缺点

让Parent的构造方法执行了两次。

ES6的类实现继承

话不多说,上代码。

class Person {
    constructor(name, age) {
        this.name = name;
        this.age = age;
    }
    drink() {
        console.log("喝水");
    }
}

class Teacher extends Person {
    constructor(name, age, subject) {
        super(name, age);
        this.subject = subject;
    }
    teach() {
        console.log(`${this.name}${this.subject}`);
    }
}

const teacher = new Teacher("张三", 18, "前端");

​ 首先定义了Person父类,然后定义了Teacher类继承Person类。Person类中使用constructor定义的属性或者方法可以被子类直接继承,但是在除constructor构造器之外定义的内容就不能被子类直接继承。其中将name和age两个属性提取到了父类中,在子类中使用到了super关键字可以直接拿到父类构造器中的name属性和age属性并进行赋值。

image-20211202185814638

可以看出teacher这个实例对象包含name、age和subject这三个属性,但是该实例并不直接拥有teach方法和drink方法,即便如此teacher实例对象仍然可以正常使用这两个方法。

image-20211202190304282
使用instanceof判断teacher实例是否属于该原型链上的若干构造函数。

image-20211202190707976

可以发现,instanceof可以判断实例对象是否属于原型链上的某一构造函数直至原型链顶端。

hasOwnProperty判断自身是否拥有某一属性或方法

image-20211202191721771

teacher自身拥有name、age、subject,但teach、drink在它的原型链上面,它自身没有这两个方法。因此输出以上结果。

注意:hasOwnProperty()的括号中一定要传入字符串!

作用域

作用域

作用域是定义变量的区域(可以理解为变量的可访问性),它有一套访问变量的规则,这套规则管理浏览器引擎如何在当前作用域或者作用域链根据变量进行变量查找。

作用域链

作用域链的作用保证执行环境可以有权并有序地访问所有变量以及函数。正因为有了作用域链,我们可以访问到外层环境的变量及函数。

当前执行期上下文的变量对象(AO)始终处于作用域链的最前端,全局对象(GO)始终是作用域链的最后一个对象。

查找对象

当我们查找某一变量时,如果在当前作用域上没有找到,那么就会沿着作用域链依次向下查找。

三种作用域

全局作用域

在非严格模式下,全局变量时挂载在window对象下的变量。因此在网页的任何位置都可以访问到该变量。

拥有全局作用域的三种情况:

  • 在window定义的变量
var age = 10
  • 在window定义的函数
var age = 10
function getAge(){
	console.log(age)
}

前面两种比较常见,而下面这一种比较特别。

  • 未经声明而直接赋值的变量
function getAge(){
	sex = '男'
    function inner(){
        age = 10
    }
    inner()
}
sex = "女"
getAge()//必须要执行这一步,不然根本不知道sex是什么
console.log(sex)//男
console.log(age)//10

未经声明而直接赋值的变量在函数执行后会挂载到全局上面。

缺点

  • 容易污染命名空间

在全局作用域定义变量可能会引起命名的冲突,所以在定义变量的时候需要注意作用域的问题。

函数作用域

函数中定义的变量叫做函数变量,此时只能在函数内部访问到它,由此产生了函数作用域(只能在函数内部访问到变量)。

function get(){
 var age = 1
 function inner(){
     console.log(age)//1
 }
 inner()
}
get()
console.log(age)//error: age is not defined

get函数的作用域是inner函数的作用域的上一级作用域,因此作为儿子函数的inner就可以找父函数拿到父亲的变量;同理window作为get函数的父作用域就拿不到儿子函数get中的变量。

总结:儿子可以访问父亲,但是父亲不可以访问儿子。

image-20211208113314996

块级作用域

ES6新增了块级作用域最直接的表现就是let关键字,使用let关键字定义的变量只能在块级作用域中被访问,它有占时性死区的特点;换言之,此变量定义之前是不可以被使用的,也没有变量提升。

console.log(a) //a is not defined
if(true){
  let a = '123'console.log(a); // 123
}
console.log(a) //a is not defined

JS 编码过程中 if 语句for 语句后面 {...} 这里面所包括的,就是块级作用域

  • {}就会产生块级作用域
  • 使用letconst声明的变量不可重复声明

执行期上下文和作用域最大的区别

执行上下文在运行时确定,随时可能改变(动态)

作用域在定义时就确定,并且不会改变(静态)

基本数据类型判断

基本数据类型:Number、String、Boolean、undefined、null、Symbol、bigInt

引用数据类型:Array、Function、Object

手写判断基本类型的方法

数组相关的方法

改变数组与不改变数组的方法

  • 改变数组的方法

push、pop、unshift、shift、reverse、sort、splice

sort:根据字符编码对数组元素进行重新排序,可以传入一个函数参数作为比较依据

splice:arr.splice(1,2,9)表示从数组索引1开始截取2个片段后,再添加一个数字9

  • 不改变数组的方法

concat、toString、join、slice

toString:将数组转换为字符串

slice:截取数组并返回截取的内容,不传参数表示截取整个数组

var arr = [1,2,3,4]
var sliceArr = arr.slice(1,3)
console.log(sliceArr)	//[2,3]

数组去重的方法

  • Set集合配合解构赋值
var arr = [1, 2, 3, 5, 1, 3, 5, 6, 4, 2, 2, 1, 3, 6];
var UniqueArr = [...new Set(arr)]
console.log(UniqueArr);
  • Set集合配合Array.from
function _UniqueArr(arr) {
    //首先判断是否是数组
	if(!Array.isArray(arr)) {
        return
    }
    //Array.from()将集合转换为数组
    return Array.from(new Set(arr))
}
  • indexOf方法去重
function uniArr(arr) {
    //首先判断是否是数组
	if(!Array.isArray(arr)) {
        return
    }
    let res = []
    for (let i = 0; i < arr.length; i++) {
        if(res.indexOf(arr[i]) === -1) {
            res.push(arr[i])
        }
    }
    return res
}
  • 利用对象属性去重
function uniquerArray(arr) {
    //如果不是数组就return掉
    if(!Array.isArray(arr)) {
        return
    }
    let obj = {}
    let newArr = []
    for (let i = 0; i < arr.length; i++) {
        if(!obj[arr[i]]) {
            newArr.push(arr[i])
            obj[arr[i]] = 1
        }
    }
    return newArr
}
  • filter方法配合indexOf方法去重
//利用fiter方法进行数组去重
const arr = [1,3,5,6,4,1,3,4,5]
const res = arr.filter(function (item, index, arr) {
  return arr.indexOf(item) == index;
});
console.log(res);

判断数组的方法有哪些

  1. 数组的api:Array.isArray(arr)
  2. 原型链判断:Array.prototype === arr.__proto__
  3. es6新增isPrototypeOf判断:Array.prototype.isPrototypeOf(arr)
  4. instanceof关键字判断:arr instanceof Array
  5. 跨原型链调用toString方法:Object.prototype.toString.call(arr),如果是数组则返回[object Array]
  6. 利用constructor和typeof:typeof arr == "object" && arr.constructor == Array

数组循环的方法有哪些

forEach、map、filter、reduce、every、some

  • forEach:无返回值,仅仅是遍历数组,相当于for循环,该方法需要传入一个函数作为参数
var arr = [1, 2, 3, 4, 5, 9, 7];
const res = arr.forEach(function (item, index) {
  console.log(item, index);
});
  • map:映射数组,不会改变原数组,需要return返回出新数组
var arr = [1, 2, 3, 4, 5, 9, 7];
const res2 = arr.map(function(item) {
    return item**2	//映射出数组每一项的平方
})
console.log(res2);//[1, 4, 9, 16, 25, 81, 49]
  • filter:过滤掉数组中不满足条件的值,不会改变原数组,可以用来做数组去重
var arr = [1, 2, 3, 4, 5, 9, 7];
const res3 = arr.filter(function(item,index,arr) {
    //返回数组中的偶数
    return item%2 == 0
})
console.log(res3);//[2,4]
  • reduce:方法很灵活,可以对数组进行计算

array.reduce(function(prev, current, currentIndex, arr), initialValue)

  1. prev:函数传进来的初始值或上一次回调的返回值
  2. current:数组中当前处理的元素值
  3. currentIndex:当前元素索引
  4. arr:当前元素所属的数组本身
  5. initialValue:传给函数的初始值
var arr = [1, 2, 3, 4, 5, 9, 7];
const res6 = arr.reduce(function (prev,current) {
    //数组求和
    return prev + current
},0)
console.log(res6);//31
  • every:返回值是一个布尔值,数组里面所有元素满足条件才会返回true
var arr = [1, 2, 3, 4, 5, 9, 7];
const res4 = arr.every(function (item, index, arr) {
  return item > 2;
});
console.log(res4);//false
  • some:返回值是一个布尔值,数组里面只要有一个满足条件就返回true
var arr = [1, 2, 3, 4, 5, 9, 7];
const res5 = arr.some(function (item, index, arr) {
  return item > 2;
});
console.log(res5);//true

paseInt方法

paseInt的作用和第二个参数

paseInt可以解析一个字符串,并且返回一个整数

没有第二个参数

如果string以"0x"开头,paseInt()会把0x以后的部分转换为16进制;

如果string以"0"开头,会将0后面的字符解析为八进制或十六进制;

如果string以1~9开头,则转换为十进制的整数

parseInt("0x15") //21
parseInt("045") //45
parseInt("16") //16

有第二个参数

paseInt("string",radix),radix表示数字的基底,值在[2,36]区间

//将65当做八进制的数解析成十进制的数
parseInt("65",8) //53
//将110010当做二进制的数解析成十进制的数
parseInt("110010",2) //50

ES6模块化的方式,怎么引入怎么导出

导出数据

  • export {xxx}:在导入数据的时接收的变量名称必须与导出时的变量名称相同(本质是解构赋值)
  • export default xxx;导出的数据名可以和导入的数据名不同,在模块中只能使用一次export default

导入数据

  • import {b as a} from "./a.js";在当前文件中a被b替代了,也就是a被改名了
  • import debounce from “./debounce.js”;

垃圾回收机制

为什么会产生垃圾?

程序在运行的过程中会产生很多垃圾,这些垃圾是程序不用的内存或者是之前用过但是以后就不会在用的内存空间。

垃圾回收策略

标记清除法
  1. 垃圾收齐器运行时会给每一个变量都加上一个标记,假设此标记为falg,其值为true或者false,如果该变量是垃圾则标记为true,反之为false,首先假设所有变量均为垃圾标记为true
  2. 然后开始遍历各个变量,把不是垃圾的变量标记为false
  3. 清理所有flagtrue的垃圾,销毁并回收它们所占用的内存空间
  4. 最后把所有内存中的变量标记修改为true,等待下一轮垃圾回收

优点:实现比较简单

缺点:容易造成内存碎片化

引用计数法
  1. 当声明一个变量(a)后,并且将一个引用类型(obj)赋值给该变量(a)时,obj的引用次数就加1;
  2. 如果obj又被赋予给另一变量(b),obj的引用次数继续加1;
  3. 如果变量b的值被其他值覆盖了,obj的引用次数就减1;
  4. 当obj的引用次数变为0时,说明没有变量引用它了,obj就可以当做垃圾被回收了;
  5. 垃圾回收机制会清除引用次数为0的数据。

应用举例:

let a = new Object() 	// 此对象的引用计数为 1(a引用)
let b = a 		// 此对象的引用计数是 2(a,b引用)
a = null  		// 此对象的引用计数为 1(b引用)
b = null 	 	// 此对象的引用计数为 0(无引用)
...			// 垃圾回收

缺点:每隔一段时间清理一次垃圾,那么就可能也会阻塞其他js脚本运行;并且每个引用类型数据都需要一个计数器,但这个计数器的个数也没有上限,那么计数器所占的内存空间可能也是巨大的;另外也无法解决循环引用的问题;。

事件循环机制(EventLoop)

浏览器端的事件循环机制

浏览器引擎除了依靠函数调用栈执行顺序外,还需要依靠任务队列来执行另外的代码;在一个线程中,事件循环是唯一的,但是任务队列并不是唯一的;任务队列分为宏任务队列和微任务队列,微任务的优先级比宏任务高,当执行栈中无任务执行时会首先执行微任务队列的任务直至清空微任务队列为止,之后才会去执行宏任务队列的任务。如果执行宏任务时产生了新的微任务,浏览器引擎在事件轮训时会优先执行该微任务。

常见宏任务和微任务

宏任务:setInterval、setTimeout、requestAnimationFrame

微任务:Promise、MutationObserver、Object.observe()、async/await(本质是Promise)

例题1:
setTimeout(() => {
  console.log('1')

  new Promise(function (resolve, reject) {
    console.log('2')
    setTimeout(() => {
      console.log('3')
    }, 0)
    resolve()
  }).then(function () {
    console.log('4')
  })
}, 0)

console.log('5')

setTimeout(() => {
  console.log('6')
}, 0)

new Promise(function (resolve, reject) {
  console.log('7')
  resolve()
})
  .then(function () {
    console.log('8')
  })
  .catch(function () {
    console.log('9')
  })

console.log('10')

首先运行script脚本代码,输出 5 、7、10,new Promise会立即执行,所以7在10前面输出

宏任务队列目前有两个setTimeout的宏任务,微任务队列有一个then微任务(没有catch任务是因为Promise被resolve了)

执行then微任务,输出 8

微任务清空,执行第一个宏任务输出1,遇到new Promise立即执行输出2,然后遇到setTimeout宏任务加入到宏任务队列(此时宏任务队列有2个宏任务),Promise被resolve了将then加入微任务队列,此时执行微任务输出4

清空宏任务队列的任务,依次输出6、3

最终输出结果:5、7、10、8、1、2、4、6、3

箭头函数和普通函数的区别

  • 箭头函数没有arguements和this
  • 箭头函数不能当做构造函数使用
  • 箭头函数没有原型链
  • 当只有一条语句时,箭头函数可以省略return和花括号