《面试》

194 阅读19分钟

一. HTML

1. 如何理解HTML语义化

我平时写代码用的就是HTML语义化的标签,比如标题用h1-h6,段落用p,表示一个章节用section等等。我认为HTML语义化就是使用语义恰当的标签写HTML,它可以让整个页面具有良好的结构,可以让开发人员快读理解网页的内容。

从前端的几个发展阶段,一开始没有专业的前端开发,都是由后台写HTML的,大部分用table标签,一个table套另一个table,让代码不好维护。后来开始用div,span和css写页面,这样虽然没什么问题,但是不够语义化。(这个div代表什么,这个div和那个div之间是什么联系,都不清楚)。现在我们更专业的做法就是使用语义化的标签,用正确的标签做正确的事。

2. meta viewport是做什么的,怎么写

meta viewport 是用于适配移动设备的,为了使不管是什么宽度的页面都能在移动设备端得到完美适配.让页面在移动端不要缩小显示。

 <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0">

让当前viewport的宽度等于设备的宽度,同时不允许用户手动缩放

name为viewport表示供移动设备使用. content定义了viewport的属性. width表示移动设设备下显示的宽度为设备宽度(device-width) height表示移动设设备下显示的宽度为设备宽度. user-scalable表示用户缩放能力, no表示不允许. initial-scale表示设备与视口的缩放比率 maximum和minimum分别表示缩放的最大最小值, 要注意的是, maximum必须必minimum大. 上面的meta标签就是告诉浏览器, 不要在移动端显示的时候缩放.

移动设备上的 viewport 是设备屏幕上用来显示网页的那部分区域,再具体一点就是浏览器上用来显示网页的那部分区域,但 viewport 又不局限于浏览器可视区域的大小,它可能比浏览器的可视区域大,也可能比浏览器的可视区域小。

如果不知道设备的理想宽度就用特殊值 device-width,同时 initial-scale=1 来的到一个理想的 viewport (ideal viewport)。

3. 用过哪些HTML5标签?

内容标签:header,footer,main,acticle

功能标签:(看MDN)

  1. canvas:

    canvas是如何进行绘制的?(看MDN的示例) 首先获取到canvas元素,获得它的二维上下文,就可以开始绘制了。设置笔刷的颜色,画布的范围。

  2. vedio: 会用到哪写属性?width,height设置视频展示区域的宽度高度。src视频的地址。aotuplay自动播放。poster海报帧的地址(封面)。还可以选择字幕。

  3. audio:

在文档中嵌入音频内容。常用属性:src,controls如果声明了该属性,浏览器将提供一个包含声音,播放进度,播放暂停的控制面板,让用户可以控制音频的播放。

4. 什么是H5?

HTML5是第五代HTML的标准。

H5并不是HTML5!

H5是指移动端页面。就像一个很大的容器,里面可以放文本、图片、音视频等基本的流媒体格式的文件。常见的应用比如微信H5,我们在微信朋友圈看到一些酷炫的小视频,节日贺卡,微信小游戏等。包含了HTML5新增的一些功能标签,canvas,audio,拖拽特性,本地存储,同时包括盒模型,绝对定位等所有前端的知识。

二. CSS

1. 说一下两种盒模型

(先说两种盒模型分别怎么写,具体到代码。)

标准盒模型:box-sizing:content-box 宽度只包含content

IE盒模型:box-sizing:border-box 宽度是content+padding+border

border-box更好用。写起来更方便。比如要在一个容器里并排显示两个一样的盒子,如果使用标准盒模型,设置width=50%,但是两个盒子会挤在一起没有空隙,于是要设置padding,但是这样又导致两个盒子的宽度超过了容器的总宽度,就又要去调整width。这样就很不方便。采用border box就可以直接设置width为50%,此时调整padding就是在内部调整,不会影响整体的布局,非常方便。

拔高点:为什么会有第二种?早期是IE盒模型,后来W3C制定的标准以后规定在标准解析模式下使用标准盒模型(content-box)。

2. 垂直居中

如果 .parent 的 height 不写,你只需要 padding: 10px 0; 就能将 .child 垂直居中;

如果 .parent 的 height 写死了,就很难把 .child 居中,以下是爸爸的高度写死时,儿子垂直居中的方法。 忠告:能不写 height 就千万别写 height。

flex

flex布局,column排列

绝对定位,margin:auto

爸爸relative,儿子absolute,上下左右都是0,margin auto 这种实现方式的两个核心是:把要垂直居中的元素相对于父元素绝对定位,top和bottom设为相等的值,我这里设成了0,当然也可以设为 99999px 或者 -99999px 无论什么,只要两者相等就行,这一步做完之后再将要居中元素的 margin 属性值设为 auto,这样便可以实现垂直居中了。

绝对定位,translate -50%

绝对定位,margin-left和margin-top -自己的一半

table标签自带垂直居中功能

display table

vertical-align 属性只对拥有 valign 特性的 html元素起作用,例如表格元素中的 <td> <th> 等等,而像<div><span>这样的元素是不行的。valign 属性规定单元格中内容的垂直排列方式

line-height 单行文字垂直居中

把要垂直居中的元素,高度和行高设为相同,必须是单行文字,才能把文字垂直居中

line-height 和 vertical-align 对图片进行垂直居中

HTML

<div id="box">
    <img src="smallpotato.jpg">
</div>

CSS

#box{
    width: 300px;
    height: 300px;
    background: #ddd;
    line-height: 300px;
}
#box img {
    width: 200px;
    height: 200px;
    vertical-align: middle;
}

父元素的高度和行高一样。图片的vertical-align:middle

3. flex怎么用,常用属性有哪些

比如这样一个需求: 容器,display:flex; justify-content:space-between;

第二个还要;margin-left:auto;

4. BFC是什么

(举例,不要尝试回答什么是BFC)

BFC是块级格式化上下文,如果给一个div写一个overflow:hidden,它里边的浮动元素就会被他包裹起来。

举例什么情况会创建一个BFC?

根元素 html

浮动元素(元素的 float 不是 none)

绝对定位元素(元素的 position 为 absolute 或 fixed)

行内块元素 display的值为inline-block

overflow 值不为 visible 的块元素

弹性元素(display为 flex 或 inline-flex元素的直接子元素)

下边的不用回答:BFC的应用:

  1. 外边距合并:两个div的上下外边距会合并。如何避免?分别给两个div父元素,父元素分别创建BFC,如overflow:hidden。就把他们隔离了。
  2. 清除浮动:一个float:left的左浮元素,会脱离文档流,脱离他的父元素。怎么让它的父元素包裹住它呢?给父元素创建BFC
  3. 普通文档流元素会被浮动元素覆盖:一个元素浮动了,就脱离了文档流,就会覆盖在它下方元素的上面。怎么让下方元素不被覆盖?让下边元素成为一个BFC

参考文章

5. CSS 选择器优先级

说法一:按权重:

权重计算规则

第一优先级:!important会覆盖页面内任何位置的元素样式

1.内联样式,如style="color: green",权值为1000

2.ID选择器,如#app,权值为0100

3.类、伪类、属性选择器,如.foo, :first-child, div[class="foo"],权值为0010

4.标签、伪元素选择器,如div::first-line,权值为0001

5.通配符、子类选择器、兄弟选择器,如*, >, +,权值为0000 6.继承的样式没有权值

CSS 伪类 是添加到选择器的关键字,指定要选择的元素的特殊状态。

:first-child 表示在一组兄弟元素中的第一个元素。

:not() 用来匹配不符合一组选择器的元素。:not(p) { color: blue; }

伪元素:如:before,:after,:first-line

说法2:正确的:越具体优先级越高 同样优先级写在后面的覆盖写在前面的 !important 优先级最高,但是要少用

6. 清除浮动说一下

背出代码:

.clearfix:after{
     content: '';
     display: block; /*或者 table*/
     clear: both;
 }
 .clearfix{
     zoom: 1; /* IE 兼容*/
 }

把clearfix类加到父容器上,里边的子元素就清除浮动了。

三. JS

1. 知道哪些ES6语法,怎么用

举例回答:

  1. let : 声明变量。只在它的块级作用域内有效;没有变量提升,必须先声明后使用;不能重复声明。

  2. const : 声明一个只读的常量。一旦声明,常量的值就不能改变。只在声明所在的块级作用域内有效;一旦声明变量,就必须立即初始化,不能留到以后赋值;不提升;不可重复声明。

  3. 解构赋值:数组的解构:let [a,b,c]=[1,2,3] 从数组中取出值,根据对应的位置赋给变量。 对象的解构:let a=obj.a 等价于let {a}=obj

  4. ...扩展运算符:接一个数组,把数组变成一个参数序列

  5. class:类关键字

  6. 模块功能export import:export命令定义了模块的对外接口以后,其他 JS 文件就可以通过import命令加载这个模块。

    分为两种:具名导出:export {fn1,fn2} 导入:import {fn1,fn2} from 'xxx.js' 导入的必须和导出的同名 默认导出:export default fn1 导入:import f from 'xxx.js' 可以任意名称

  7. 箭头函数:写起来简短,没有this

  8. 默认参数:函数默认参数允许在没有值或undefined被传入时使用默认形参。

2. Promise,Promise.all,Promise.race 怎么用

Promise是异步编程的一种解决方案,比传统的回调函数要更方便,合理,避免了回调地狱。

Promise对象只有三种状态:进行中,已成功,已失败。一个Promise对象代表了一个异步操作。这个状态只能改变一次。进行中->成功,进行中->失败。

用法:new Promise创建一个promise实例,Promsie构造函数接受一个函数作为参数。这个函数的两个参数分别是resolve和reject,它们也是函数。resolve函数会在异步操作成功后被调用,把异步操作的结果作为参数传递出去;reject函数会在异步操作失败后被调用,把错误的信息作为参数传递出去。

创建了promise实例后,通过then方法给状态变化时添加回调函数。then接受两个回调函数作为参数,第一个是状态变为resolve时被调用,第二个状态变为reject被调用。

Promise.all: 接收多个promise实例作为参数,包装成一个新的promise实例const p = Promise.all([p1, p2, p3]);p的状态由p1、p2、p3决定,分成两种情况。

(1)只有p1、p2、p3的状态都变成fulfilled,p的状态才会变成fulfilled,此时p1、p2、p3的返回值组成一个数组,传递给p的回调函数。

(2)只要p1、p2、p3之中有一个被rejected,p的状态就变成rejected,此时第一个被reject的实例的返回值,会传递给p的回调函数。

Promise.race: 同样是将多个 Promise 实例,包装成一个新的 Promise 实例.const p = Promise.race([p1, p2, p3]);只要p1、p2、p3之中有一个实例率先改变状态,p的状态就跟着改变。那个率先改变的 Promise 实例的返回值,就传递给p的回调函数。

将异步操作以同步操作的流程表达出来,避免了层层嵌套的回调函数。此外,Promise对象提供统一的接口,使得控制异步操作更加容易。

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

3. 手写函数防抖和函数节流

在时间轴上控制函数的执行次数。

  1. 函数防抖:在事件被触发n秒后,再执行它的回调函数。如果在这n秒的期间又被触发了,就再次重新计时。总的来说,适合多次事件一次响应的情况

    任务频繁触发的情况下,只有任务触发的间隔超过指定间隔的时候,任务才会执行。

    就像进电梯,第一个人进电梯(触发事件),电梯不会马上运行,而是等10秒钟,如果这10秒内又有人上来了(又被触发了),就重新计时10秒钟。直到10秒钟内没人再上来了,电梯就运行了。

    function debounce(fn,delay){
        let timer=null
        return function(){
            let context=this
            if(timer){
                window.clearTimeout(timer)
            }
            timer=setTimeout(()=>{
                fn.apply(context,arguments)
                timer=null
            },delay)
        }
    }
    const debounced=debounce(()=>{console.log('hi')})
    debounced()
    debounced()
    

    关于为什么fn要用apply调用:因为可能回调函数fn需要参数,或者需要指定this。这些信息只能通过debounced 函数传进去,也就是debounce里return的那个匿名函数。所以需要保持fn被调用的时候,它里边的this和参数与debounced传进去的一致,才能将这些信息给到fn。如果不用apply,而是直接fn() 去调用,那么fn里的this就是window了。所以需要用apply,去指定fn里边的this和参数。

    如果setTimeout里用的是箭头函数,在return的函数里,不将this赋给context也行,也就是这句let context=this 可以删掉。直接fn.apply(this,arguments)就可以。因为setTimeout里用的是箭头函数,所以里边的this和外边的一样,也就是和传给debounced的this一样。

    但是如果setTimeout里用的是匿名函数,就需要先保存this了。原因:关于setTimeout的this指向

  2. 函数节流:理解为冷却时间,一次技能发出后,必须间隔一段时间才能再次发出。怎么实现?利用闭包,为true就可以执行,执行后置为false,然后规定,几秒钟后再把该标记置为true

    function throttle(fn,delay){
        let canuse=true
        return function(){
            if(canuse){
                fn.apply(this,arguments)
                canuse=false
                setTimeout(()=>{
                    canuse=true
                },delay)
            }
        }
    }
    const throttled=throttle(()=>{console.log('hi')})
    throttled()
    throttled()
    

    一段时间执行一次之后,就不执行第二次

4. 手写AJAX

const request=new XMLHttpRequest()
request.open('GET','/xxx')
request.onreadystatechange=()=>{
    if(request.readyState===4){
        console.log('下载完成')
        if(request.status>=200 && request.status<300){
            console.log('请求成功')
        }else{
            console.log('请求失败')
        }
    }
}
request.send()

5. 这段代码里的this是什么

  1. 背代码 fn() this => window/global

obj.fn() this => obj

fn.call(xx) this => xx

fn.apply(xx) this => xx

fn.bind(xx) this => xx

new Fn() this => 新的对象

fn = ()=> {} this => 外面的 this

看调用 总结

6.1 什么是闭包

阐述什么是闭包,闭包的作用:

一个函数内部使用了外部的变量,那么这个函数和变量就构成了一个闭包。

闭包的作用:隐藏一个变量,间接访问一个变量。

function foo(){
  var local = 1
  function bar(){
    local++
    return local
  }
  return bar
}

var func = foo()
func()

比如这样,一个函数嵌套一个函数,return这个函数。调用外边的函数就会得到一个接口,用这个接口访问局部变量。local和bar就构成了一个闭包。

通过使用闭包,我们可以实现在一个函数外边访问到它的内部变量。通常这是一个父函数,我们想在父函数外部访问它的局部变量,可以在它的内部声明一个子函数,这个子函数是可以使用父函数的内部变量的,所以我们return 这个子函数。在外边通过子函数这个接口访问父函数的内部变量。

闭包还会让这些局部变量的值始终保存在内存里。比如foo调用完以后,local并不会被回收。因为子函数bar被引用给了一个全局变量,所以bar要一直存在,而bar的存在又依赖它的父函数,所以foo函数也会一直保存在内存里。

闭包的缺点:1. 闭包会使函数里的变量一直在内存里,导致内存消耗很大。

6.2 什么是立即执行函数,有什么作用

声明一个匿名函数,在后面接一对圆括号(),调用它。

但是这样写浏览器会报错,为了通过检查,我们一般有以下几种写法:

(function(){alert('我是匿名函数')} ()) // 用括号把整个表达式包起来
(function(){alert('我是匿名函数')}) () //用括号把函数包起来
!function(){alert('我是匿名函数')}() // 求反,我们不在意值是多少,只想通过语法检查。

作用:创建一个独立的作用域,使外边访问不到这个作用域里的内部变量,避免变量污染。

7. 什么是跨域,CORS,JSONP

浏览器出于安全方面的考虑,有一个叫做同源策略的规定,它规定,不同源的页面之间不允许互相访问数据。但是现实情况我们需要访问不同源的资源,跨域就是我们通过一些手段实现访问不同源的资源。

CORS和JSONP是实现跨域的两种方法。

CORS是跨域资源共享,比如某一个源想把自己的数据公开给谁,就在后台服务器添加一个响应头:

response.setHeader("Access-Control-Allow-Origin", "http://anqi.com:9999");后面可以指定允许谁访问。

我们在跨域的时候,由于当前浏览器不支持CORS,所以就需要另外一种方法来跨域。我们通过创建一个script标签,请求另外一个网站的一个JS文件,这个JS文件里通过执行一个回调,来获取我们想得到的数据。这个回调函数的名字是我们随机生成的,是一个随机数,我们常常以callback这个参数把函数名传到后台,后台得到这个名字再传给我们,然后执行这个函数。

JSONP的优点,兼容 IE,可以跨域。 缺点:由于JSONP是用script标签引用JS的,所以没有AJAX那么精确,它读不到状态码,也不知道响应头,只知道执行成功还是失败。并且,由于是script标签,只能发GET请求,不支持POST。

8. async和await怎么用,如何捕获异常?

async是什么?与promise有关;让异步函数更像是同步函数。

为什么要用await?和promise.then.then相比,用await可以让异步代码看起来更像是标准的同步代码。从上到下执行,但是promise代码的执行顺序不能直接看懂。

function resolveAfter2Seconds() {
  return new Promise(resolve => {
    setTimeout(() => {
      resolve('resolved');
    }, 2000);
  });
}

async function asyncCall() {
  console.log('calling');
  const result = await resolveAfter2Seconds();
  console.log(result);
  // expected output: "resolved"
}

asyncCall();

使用async function 声明一个异步函数,这个异步函数返回一个Promise对象。await关键字只能用在async函数里,await后边接一个返回Promise的函数,并且调用这个函数。await会暂停当前的异步函数,等待Promise执行完毕。await 关键字只在异步函数内有效

用try catch捕获异常,把await放在try里

参考文章:async/await 和 promise 的执行顺序

理解 JavaScript 的 async/await

10分钟理解JS引擎的执行机制

9. 如何实现深拷贝?

前置知识:浅拷贝如果是复杂数据类型,只会拷贝地址,还是共用同一块内存。所以改变原对象也会影响拷贝的对象。深拷贝是从内存完整的拷贝一份,没有共用内存地址,两个对象互不影响。

实现深拷贝最简陋的做法就是JSON.parse(JSON.stringify(source)) 但是无法涵盖数组,循环引用的情况。

基本的做法:先创建一个空对象作为结果。对于要拷贝的参数,首先判断它的类型,如果是对象,就要递归调用,把深层次的属性依次拷贝。如果是基本数据类型,就直接返回。

但是如果是数组,就不能拷贝成对象的形式。需要在初始化时判断如果要拷贝的是一个数组类型,就初始化一个空数组,其他和对象一样。

循环引用:如果这个对象的一个属性对应的value是它自己,obj.a=obj 递归就会进入死循环。解决办法:需要一块存储空间保存新对象和旧对象的关系。在拷贝对象的每一个属性的时候,先去这个空间里找,这个对象有没有被拷贝过,如果已经被拷贝过了(引用自己的情况),那就结束本地递归,直接返回上次被拷贝过了的结果值;如果这个对象还没被拷贝过,是一个新的属性,就正常往下走,还要把当前这个对象和它的拷贝对象存进空间里。这个存储空间用map对象,因为map也是以key-value存储,而且key可以是任意类型。

function deepClone(source,map=new Map()){
    if(typeof source==='object'){
        let result=Array.isArray(source) ? []:{}
        if(map.get(source)){
            return map.get(source)
        }
        map.set(source,result)
        for(let key in source){
            result[key]=deepClone(source[key],map)
        }
        return result
    }else{
        return source
    }
}

参考文章:如何写出一个惊艳面试官的深拷贝?

10. 如何用正则实现 trim()

function trim(string){
    return string.replace(/^\s+|\s+$/g,'')
}

去掉字符串开头和结尾的空白字符。

^匹配一个字符串的开头位置,\s空白字符,+一个或多个。字符串开头多个空白字符,或者字符串结尾多个空白字符,替换成空字符串。g表示全局匹配

如果不加g,只会去掉开头的空格,结尾的没去掉。

11.1 用class实现继承

class Animal{
    constructor(type){
        this.type=type
    }
    move(){
        console.log('跑起来')
    }
}
class Dog extends Animal{
    constructor(type,name){
        super(type)
        this.name=name
    }
    eat(){}
}

11.2 不用class实现继承

function Animal(type){
    this.type=type
}
Animal.prototype.move=function(){}

function Dog(type,name){
    Animal.call(this,type)
    this.name=name
}

// 实现Dog.prototype.__proto__=Animal.prototype
function temp(){}
temp.prototype=Animal.prototype
Dog.prototype=new temp()

Dog.prototype.constructor=Dog
Dog.prototype.eat=function(){}

const dog=new Dog('dog','hi')

12. 数组去重

1. hash

function unique(array){
    let hash={}
    let result=[]
    for(let i=0;i<array.length;i++){
        if(!hash[array[i]]){
            result.push(array[i])
            hash[array[i]]=true
        }
    }
    return result
}

2. set

function unique(array){
    return Array.from(new Set(array))
}
function unique(arr) {
    const map = new Map();
    return arr.filter((a) => !res.has(a) && res.set(a, 1))
}