美文网首页前端面试题
前端面试基础题

前端面试基础题

作者: 高磊_IT | 来源:发表于2020-08-18 23:05 被阅读0次

    BFC、IFC、GFC、FFC

    CSS2.1中只有BFC和IFC, CSS3中才有GFC和FFC。

    到底什么是BFC、IFC、GFC和FFC

    What's FC?

    一定不是KFC,FC的全称是:Formatting Contexts,是W3C CSS2.1规范中的一个概念。它是页面中的一块渲染区域,并且有一套渲染规则,它决定了其子元素将如何定位,以及和其他元素的关系和相互作用。

    BFC

    BFC(Block Formatting Contexts)直译为"块级格式化上下文"。Block Formatting Contexts就是页面上的一个隔离的渲染区域,容器里面的子元素不会在布局上影响到外面的元素,反之也是如此。如何产生BFC?
    float的值不为none。
    overflow的值不为visible。
    position的值不为relative和static。
    display的值为table-cell, table-caption, inline-block中的任何一个。
    那BFC一般有什么用呢?比如常见的多栏布局,结合块级别元素浮动,里面的元素则是在一个相对隔离的环境里运行。

    IFC

    IFC(Inline Formatting Contexts)直译为"内联格式化上下文",IFC的line box(线框)高度由其包含行内元素中最高的实际高度计算而来(不受到竖直方向的padding/margin影响)
    IFC中的line box一般左右都贴紧整个IFC,但是会因为float元素而扰乱。float元素会位于IFC与与line box之间,使得line box宽度缩短。 同个ifc下的多个line box高度会不同。 IFC中时不可能有块级元素的,当插入块级元素时(如p中插入div)会产生两个匿名块与div分隔开,即产生两个IFC,每个IFC对外表现为块级元素,与div垂直排列。
    那么IFC一般有什么用呢?
    水平居中:当一个块要在环境中水平居中时,设置其为inline-block则会在外层产生IFC,通过text-align则可以使其水平居中。
    垂直居中:创建一个IFC,用其中一个元素撑开父元素的高度,然后设置其vertical-align:middle,其他行内元素则可以在此父元素下垂直居中。

    GFC

    GFC(GridLayout Formatting Contexts)直译为"网格布局格式化上下文",当为一个元素设置display值为grid的时候,此元素将会获得一个独立的渲染区域,我们可以通过在网格容器(grid container)上定义网格定义行(grid definition rows)和网格定义列(grid definition columns)属性各在网格项目(grid item)上定义网格行(grid row)和网格列(grid columns)为每一个网格项目(grid item)定义位置和空间。
    那么GFC有什么用呢,和table又有什么区别呢?首先同样是一个二维的表格,但GridLayout会有更加丰富的属性来控制行列,控制对齐以及更为精细的渲染语义和控制。

    FFC

    FFC(Flex Formatting Contexts)直译为"自适应格式化上下文",display值为flex或者inline-flex的元素将会生成自适应容器(flex container),可惜这个牛逼的属性只有谷歌和火狐支持,不过在移动端也足够了,至少safari和chrome还是OK的,毕竟这俩在移动端才是王道。
    Flex Box 由伸缩容器和伸缩项目组成。通过设置元素的 display 属性为 flex 或 inline-flex 可以得到一个伸缩容器。设置为 flex 的容器被渲染为一个块级元素,而设置为 inline-flex 的容器则渲染为一个行内元素。
    伸缩容器中的每一个子元素都是一个伸缩项目。伸缩项目可以是任意数量的。伸缩容器外和伸缩项目内的一切元素都不受影响。简单地说,Flexbox 定义了伸缩容器内伸缩项目该如何布局。

    div水平垂直居中的方法

    在平时,我们经常会碰到让一个div框针对某个模块上下左右都居中(水平垂直居中),其实针对这种情况,我们有多种方法实现。

    方法一:

    绝对定位方法:不确定当前div的宽度和高度,采用 transform: translate(-50%,-50%); 当前div的父级添加相对定位(position: relative;)

    图片展示:

    img

    代码如下:

    div{
        background:red;
        position: absolute;
        left:50%;
        top:50%;
        transform: translate(-50%, -50%);
    }
    

    方法二:

    绝对定位方法:确定了当前div的宽度,margin值为当前div宽度一半的负值

    图片展示: 如方法一的图片展示

    代码如下:

    div{
        width:600px;
        height: 600px;
        background:red;
        position: absolute;
        left:50%;
        top:50%;
        margin-left:-300px;
        margin-top:-300px;
    }
    

    方法三:

    绝对定位方法:绝对定位下top left right bottom 都设置0

    图片展示: 如方法一的图片展示

    代码如下:

    div.child{
        width: 600px;
        height: 600px;
        background: red;
        position:absolute;
        left:0;
        top: 0;
        bottom: 0;
        right: 0;
        margin: auto;
    }
    

    方法四:

    flex布局方法:当前div的父级添加flex css样式

    展示图如下:

    img

    代码如下:

    .box{
        height:800px;
        -webkit-display:flex;
        display:flex;
        -webkit-align-items:center;
        align-items:center;
        -webkit-justify-content:center;
        justify-content:center;
        border:1px solid #ccc;
    }
    div.child{
        width:600px;
        height:600px;
        background-color:red;
    }
    

    方法五:

    table-cell实现水平垂直居中: table-cell middle center组合使用

    展示图如下:

    img

    代码如下:

    .table-cell {
        display: table-cell;
        vertical-align: middle;
        text-align: center;
        width: 240px;
        height: 180px;
        border:1px solid #666;
    }
    

    方法六:

    绝对定位:calc() 函数动态计算实现水平垂直居中

    展示图如下:

    img

    代码如下:

    .calc{
      position: relative;
      border: 1px solid #ccc;
      width: 400px;
      height: 160px;
    }
    .calc .child{
      position: absolute;
      width: 200px;
      height: 50px;
      left:-webkit-calc((400px - 200px)/2);
      top:-webkit-calc((160px - 50px)/2);
      left:-moz-calc((400px - 200px)/2);
      top:-moz-calc((160px - 50px)/2);
      left:calc((400px - 200px)/2);
      top:calc((160px - 50px)/2);
    }  
    

    display: none;

    1. DOM 结构:浏览器不会渲染 display 属性为 none 的元素,不占据空间;
    2. 事件监听:无法进行 DOM 事件监听;
    3. 性能:动态改变此属性时会引起重排,性能较差;
    4. 继承:不会被子元素继承,毕竟子类也不会被渲染;
    5. transition:transition 不支持 display。

    visibility: hidden;

    1. DOM 结构:元素被隐藏,但是会被渲染不会消失,占据空间;
    2. 事件监听:无法进行 DOM 事件监听;
    3. 性 能:动态改变此属性时会引起重绘,性能较高;
    4. 继 承:会被子元素继承,子元素可以通过设置 visibility: visible; 来取消隐藏;
    5. transition:visibility 会立即显示,隐藏时会延时

    opacity: 0;

    1. DOM 结构:透明度为 100%,元素隐藏,占据空间;
    2. 事件监听:可以进行 DOM 事件监听;
    3. 性 能:提升为合成层,不会触发重绘,性能较高;
    4. 继 承:会被子元素继承,且,子元素并不能通过 opacity: 1 来取消隐藏;
    5. transition:opacity 可以延时显示和隐藏

    在不改变当前代码的情况下,使这张图片的宽度为300px?

    <img src="1.jpg" style="width:480px!important"/>
    

    1.css方法

    <img src="1.jpg" style="width:480px!important; max-width: 300px">
    <img src="1.jpg" style="width:480px!important; transform: scale(0.625, 1);" >
    <img src="1.jpg" style="width:480px!important; width:300px!important;">
    

    2.js方法

    document.getElementsByTagName("img")[0].setAttribute("style","width:300px!important;")
    

    7 种方法解决移动端 Retina 屏幕 1px 边框问题

    造成边框变粗的原因

    其实这个原因很简单,因为css中的1px并不等于移动设备的1px,这些由于不同的手机有不同的像素密度。在window对象中有一个devicePixelRatio属性,他可以反应css中的像素与设备的像素比。

    devicePixelRatio的官方的定义为:设备物理像素和设备独立像素的比例,也就是 devicePixelRatio = 物理像素 / 独立像素。

    解决边框变粗的6种办法

    1、0.5px边框

    在2014年的 WWDC,“设计响应的Web体验” 一讲中,Ted O’Connor 讲到关于“retinahairlines”(retina 极细的线):在retina屏上仅仅显示1物理像素的边框,开发者应该如何处理呢。

    他们曾介绍到 iOS 8 和 OS X Yosemite 即将支持 0.5px 的边框:

    [图片上传失败...(image-6431d2-1597762908407)]

    0.5px边框

    额的神呐!so easy! 果真如此吗?这样还不够(WWDC幻灯片通常是“唬人”的),但是相差不多。

    问题是 retina 屏的浏览器可能不认识0.5px的边框,将会把它解释成0px,没有边框。包括 iOS 7 和之前版本,OS X Mavericks 及以前版本,还有 Android 设备。

    解决方案:
    解决方案是通过 JavaScript 检测浏览器能否处理0.5px的边框,如果可以,给html标签元素添加个class。

    if (window.devicePixelRatio && devicePixelRatio >= 2) {
    var testElem = document.createElement('div');
    testElem.style.border = '.5px solid transparent';
    document.body.appendChild(testElem);
    if (testElem.offsetHeight == 1) {
    document.querySelector('html').classList.add('hairlines');
    }
    document.body.removeChild(testElem);
    }
    // 脚本应该放在内,如果在里面运行,需要包装 $(document).ready(function() {})
    

    然后,极细的边框样式就容易了:

    div {
    border: 1px solid #bbb;
    }
    .hairlines div {
    border-width: 0.5px;
    }
    

    2、使用border-image实现

    准备一张符合你要求的border-image:

    [图片上传失败...(image-b220f2-1597762908407)]

    底部边框

    样式设置:

    .border-bottom-1px {
    border-width: 0 0 1px 0;
    -webkit-border-image: url(linenew.png) 0 0 2 0 stretch;
    border-image: url(linenew.png) 0 0 2 0 stretch;
    }
    

    上文是把border设置在边框的底部,所以使用的图片是2px高,上部的1px颜色为透明,下部的1px使用视觉规定的border的颜色。如果边框底部和顶部同时需要border,可以使用下面的border-image:

    [图片上传失败...(image-d6c12a-1597762908407)]

    上下边框

    样式设置:

    .border-image-1px {
    border-width: 1px 0;
    -webkit-border-image: url(linenew.png) 2 0 stretch;
    border-image: url(linenew.png) 2 0 stretch;
    }
    

    到目前为止,我们已经能在iphone上展现1px border的效果了。但是我们发现这样的方法在非视网膜屏上会出现border显示不出来的现象,于是使用Media Query做了一些兼容,样式设置如下:

    .border-image-1px {
    border-bottom: 1px solid #666;
    }
    @media only screen and (-webkit-min-device-pixel-ratio: 2) {
    .border-image-1px {
    border-bottom: none;
    border-width: 0 0 1px 0;
    -webkit-border-image: url(../img/linenew.png) 0 0 2 0 stretch;
    border-image: url(../img/linenew.png) 0 0 2 0 stretch;
    }
    }
    

    缺点:

    • 修改颜色麻烦, 需要替换图片
    • 圆角需要特殊处理,并且边缘会模糊

    3、使用background-image实现

    background-image 跟border-image的方法一样,你要先准备一张符合你要求的图片。然后将边框模拟在背景上。
    样式设置:

    .background-image-1px {
    background: url(../img/line.png) repeat-x left bottom;
    -webkit-background-size: 100% 1px;
    background-size: 100% 1px;
    }
    

    优点:

    缺点:

    • 修改颜色麻烦, 需要替换图片
    • 圆角需要特殊处理,并且边缘会模糊

    4、多背景渐变实现

    与background-image方案类似,只是将图片替换为css3渐变。设置1px的渐变背景,50%有颜色,50%透明。
    样式设置:

    .background-gradient-1px {
    background:
    linear-gradient(180deg, black, black 50%, transparent 50%) top left / 100% 1px no-repeat,
    linear-gradient(90deg, black, black 50%, transparent 50%) top right / 1px 100% no-repeat,
    linear-gradient(0, black, black 50%, transparent 50%) bottom right / 100% 1px no-repeat,
    linear-gradient(-90deg, black, black 50%, transparent 50%) bottom left / 1px 100% no-repeat;
    }
    /* 或者 */
    .background-gradient-1px{
    background: -webkit-gradient(linear, left top, left bottom, color-stop(.5, transparent), color-stop(.5, #c8c7cc), to(#c8c7cc)) left bottom repeat-x;
    background-size: 100% 1px;
    }
    

    5、使用box-shadow模拟边框

    利用css 对阴影处理的方式实现0.5px的效果
    样式设置:

    .box-shadow-1px {
    box-shadow: inset 0px -1px 1px -1px #c8c7cc;
    }
    

    优点:

    缺点:

    6、viewport + rem 实现

    同时通过设置对应viewport的rem基准值,这种方式就可以像以前一样轻松愉快的写1px了。
    在devicePixelRatio = 2 时,输出viewport:

    在devicePixelRatio = 3 时,输出viewport:
    ​```html
    

    这种兼容方案相对比较完美,适合新的项目,老的项目修改成本过大。
    对于这种方案,可以看看《使用Flexible实现手淘H5页面的终端适配》
    优点:

    缺点:

    7、伪类 + transform 实现

    对于老项目,有没有什么办法能兼容1px的尴尬问题了,个人认为伪类+transform是比较完美的方法了。原理是把原先元素的 border 去掉,然后利用 :before 或者 :after 重做 border ,并 transform 的 scale 缩小一半,原先的元素相对定位,新做的 border 绝对定位。

    单条border样式设置:

    .scale-1px{
    position: relative;
    border:none;
    }
    .scale-1px:after{
    content: '';
    position: absolute;
    bottom: 0;
    background: #000;
    width: 100%;
    height: 1px;
    -webkit-transform: scaleY(0.5);
    transform: scaleY(0.5);
    -webkit-transform-origin: 0 0;
    transform-origin: 0 0;
    }
    

    四条boder样式设置:

    .scale-1px{
    position: relative;
    margin-bottom: 20px;
    border:none;
    }
    .scale-1px:after{
    content: '';
    position: absolute;
    top: 0;
    left: 0;
    border: 1px solid #000;
    -webkit-box-sizing: border-box;
    box-sizing: border-box;
    width: 200%;
    height: 200%;
    -webkit-transform: scale(0.5);
    transform: scale(0.5);
    -webkit-transform-origin: left top;
    transform-origin: left top;
    }
    

    最好在使用前也判断一下,结合 JS 代码,判断是否 Retina 屏:

    if(window.devicePixelRatio && devicePixelRatio >= 2){
    document.querySelector('ul').className = 'scale-1px';
    }
    

    优点:

    • 所有场景都能满足
    • 支持圆角(伪类和本体类都需要加border-radius)

    缺点:

    • 对于已经使用伪类的元素(例如clearfix),可能需要多层嵌套

    ['1', '2', '3'].map(parseInt)

    what & why ?

    早在 2013年, 有人在微博上发布了以下代码段:

    ['10','10','10','10','10'].map(parseInt);
    // [10, NaN, 2, 3, 4]
    

    parseInt

    parseInt() 函数解析一个字符串参数,并返回一个指定基数的整数 (数学系统的基础)。

    const intValue = parseInt(string[, radix]);
    

    string 要被解析的值。如果参数不是一个字符串,则将其转换为字符串(使用 ToString 抽象操作)。字符串开头的空白符将会被忽略。

    radix 一个介于2和36之间的整数(数学系统的基础),表示上述字符串的基数。默认为10。 返回值 返回一个整数或NaN

    parseInt(100); // 100
    parseInt(100, 10); // 100
    parseInt(100, 2); // 4 -> converts 100 in base 2 to base 10
    

    注意:radix为 undefined,或者radix为 0 或者没有指定的情况下,JavaScript 作如下处理:

    • 如果字符串 string 以"0x"或者"0X"开头, 则基数是16 (16进制).
    • 如果字符串 string 以"0"开头, 基数是8(八进制)或者10(十进制),那么具体是哪个基数由实现环境决定。ECMAScript 5 规定使用10,但是并不是所有的浏览器都遵循这个规定。因此,永远都要明确给出radix参数的值。
    • 如果字符串 string 以其它任何值开头,则基数是10 (十进制)。

    更多详见parseInt | MDN

    map

    map() 方法创建一个新数组,其结果是该数组中的每个元素都调用一个提供的函数后返回的结果。

    var new_array = arr.map(function callback(currentValue[,index[, array]]) {
     // Return element for new_array
     }[, thisArg])
    

    可以看到callback回调函数需要三个参数, 我们通常只使用第一个参数 (其他两个参数是可选的)。 currentValue 是callback 数组中正在处理的当前元素。 index可选, 是callback 数组中正在处理的当前元素的索引。 array可选, 是callback map 方法被调用的数组。 另外还有thisArg可选, 执行 callback 函数时使用的this 值。

    const arr = [1, 2, 3];
    arr.map((num) => num + 1); // [2, 3, 4]
    

    更多详见Array.prototype.map() | MDN

    回到真实的事例上

    回到我们真实的事例上

    ['1', '2', '3'].map(parseInt)
    

    对于每个迭代map, parseInt()传递两个参数: 字符串和基数。 所以实际执行的的代码是:

    ['1', '2', '3'].map((item, index) => {
        return parseInt(item, index)
    })
    

    即返回的值分别为:

    parseInt('1', 0) // 1
    parseInt('2', 1) // NaN
    parseInt('3', 2) // NaN, 3 不是二进制
    

    所以:

    ['1', '2', '3'].map(parseInt)
    // 1, NaN, NaN
    

    由此,加里·伯恩哈德例子也就很好解释了,这里不再赘述

    ['10','10','10','10','10'].map(parseInt);
    // [10, NaN, 2, 3, 4]
    

    如何在现实世界中做到这一点

    如果您实际上想要循环访问字符串数组, 该怎么办? map()然后把它换成数字?使用编号!

    ['10','10','10','10','10'].map(Number);
    // [10, 10, 10, 10, 10]
    

    作者解答:
    概念:以第二个参数为基数来解析第一个参数字符串,通常用来做十进制的向上取整。

    特点:接收两个参数parseInt(string,radix)

    radix:解析字符串的基数,基数规则如下:

    1) 区间范围介于2~36之间;

    2 ) 当参数为 0,parseInt() 会根据十进制来解析;

    3 ) 如果忽略该参数,默认的基数规则

    parseInt('10',0);radix 为 0,parseInt() 会根据十进制来解析,所以结果为 10;

    parseInt('10',1);radix 为 1,超出区间范围,所以结果为 NaN;

    parseInt('10',2);radix 为 2,用2进制来解析,1乘以2的1次方+0乘以2的0次方,所以结果是2

    parseInt('10',3);radix 为 3,用3进制来解析,1乘以3的1次方+0乘以3的0次方,所以结果是3

    parseInt('10',4);radix 为 4,用4进制来解析,1乘以4的1次方+0乘以4的0次方,所以结果是4

    函数节流与函数防抖

    什么是函数节流与函数防抖

    举个栗子,我们知道目前的一种说法是当 1 秒内连续播放 24 张以上的图片时,在人眼的视觉中就会形成一个连贯的动画,所以在电影的播放(以前是,现在不知道)中基本是以每秒 24 张的速度播放的,为什么不 100 张或更多是因为 24 张就可以满足人类视觉需求的时候,100 张就会显得很浪费资源。

    再举个栗子,假设电梯一次只能载一人的话,10 个人要上楼的话电梯就得走 10 次,是一种浪费资源的行为;而实际生活正显然不是这样的,当电梯里有人准备上楼的时候如果外面又有人按电梯的话,电梯会再次打开直到满载位置,从电梯的角度来说,这时一种节约资源的行为(相对于一次只能载一个人)。

    • 函数节流: 指定时间间隔内只会执行一次任务;
    • 函数防抖: 任务频繁触发的情况下,只有任务触发的间隔超过指定间隔的时候,任务才会执行。

    函数节流(throttle)

    这里以判断页面是否滚动到底部为例,普通的做法就是监听 window 对象的 scroll 事件,然后再函数体中写入判断是否滚动到底部的逻辑:

    $(window).on('scroll', function () {
        // 判断是否滚动到底部的逻辑
        let pageHeight = $('body').height(),
            scrollTop = $(window).scrollTop(),
            winHeight = $(window).height(),
            thresold = pageHeight - scrollTop - winHeight;
        if (thresold > -100 && thresold <= 20) {
            console.log('end');
        }
    });
    

    这样做的一个缺点就是比较消耗性能,因为当在滚动的时候,浏览器会无时不刻地在计算判断是否滚动到底部的逻辑,而在实际的场景中是不需要这么做的,在实际场景中可能是这样的:在滚动过程中,每隔一段时间在去计算这个判断逻辑。而函数节流所做的工作就是每隔一段时间去执行一次原本需要无时不刻地在执行的函数,所以在滚动事件中引入函数的节流是一个非常好的实践:

    $(window).on('scroll', throttle(function () {
        // 判断是否滚动到底部的逻辑
        let pageHeight = $('body').height(),
            scrollTop = $(window).scrollTop(),
            winHeight = $(window).height(),
            thresold = pageHeight - scrollTop - winHeight;
        if (thresold > -100 && thresold <= 20) {
            console.log('end');
        }
    }));
    

    加上函数节流之后,当页面再滚动的时候,每隔 300ms 才会去执行一次判断逻辑。

    简单来说,函数的节流就是通过闭包保存一个标记(canRun = true),在函数的开头判断这个标记是否为 true,如果为 true 的话就继续执行函数,否则则 return 掉,判断完标记后立即把这个标记设为 false,然后把外部传入的函数的执行包在一个 setTimeout 中,最后在 setTimeout 执行完毕后再把标记设置为 true(这里很关键),表示可以执行下一次的循环了。当 setTimeout 还未执行的时候,canRun 这个标记始终为 false,在开头的判断中被 return 掉。

    function throttle(fn, interval = 300) {
        let canRun = true;
        return function () {
            if (!canRun) return;
            canRun = false;
            setTimeout(() => {
                fn.apply(this, arguments);
                canRun = true;
            }, interval);
        };
    }
    

    函数防抖(debounce)

    这里以用户注册时验证用户名是否被占用为例,如今很多网站为了提高用户体验,不会再输入框失去焦点的时候再去判断用户名是否被占用,而是在输入的时候就在判断这个用户名是否已被注册:

    $('input.user-name').on('input', function () {
        $.ajax({
            url: `https://just.com/check`,
            method: 'post',
            data: {
                username: $(this).val(),
            },
            success(data) {
                if (data.isRegistered) {
                    $('.tips').text('该用户名已被注册!');
                } else {
                    $('.tips').text('恭喜!该用户名还未被注册!');
                }
            },
            error(error) {
                console.log(error);
            },
        });
    });
    

    很明显,这样的做法不好的是当用户输入第一个字符的时候,就开始请求判断了,不仅对服务器的压力增大了,对用户体验也未必比原来的好。而理想的做法应该是这样的,当用户输入第一个字符后的一段时间内如果还有字符输入的话,那就暂时不去请求判断用户名是否被占用。在这里引入函数防抖就能很好地解决这个问题:

    $('input.user-name').on('input', debounce(function () {
        $.ajax({
            url: `https://just.com/check`,
            method: 'post',
            data: {
                username: $(this).val(),
            },
            success(data) {
                if (data.isRegistered) {
                    $('.tips').text('该用户名已被注册!');
                } else {
                    $('.tips').text('恭喜!该用户名还未被注册!');
                }
            },
            error(error) {
                console.log(error);
            },
        });
    }));
    

    其实函数防抖的原理也非常地简单,通过闭包保存一个标记来保存 setTimeout 返回的值,每当用户输入的时候把前一个 setTimeout clear 掉,然后又创建一个新的 setTimeout,这样就能保证输入字符后的 interval 间隔内如果还有字符输入的话,就不会执行 fn 函数了。

    function debounce(fn, interval = 300) {
        let timeout = null;
        return function () {
            clearTimeout(timeout);
            timeout = setTimeout(() => {
                fn.apply(this, arguments);
            }, interval);
        };
    }
    

    总结

    其实函数节流与函数防抖的原理非常简单,巧妙地使用 setTimeout 来存放待执行的函数,这样可以很方便的利用 clearTimeout 在合适的时机来清除待执行的函数。

    使用函数节流与函数防抖的目的,在开头的栗子中应该也能看得出来,就是为了节约计算机资源。

    Set 和 Map 主要的应用场景在于 数据重组数据储存

    Set 是一种叫做集合的数据结构,Map 是一种叫做字典的数据结构

    1. 集合(Set)

    ES6 新增的一种新的数据结构,类似于数组,但成员是唯一且无序的,没有重复的值。

    Set 本身是一种构造函数,用来生成 Set 数据结构。

    new Set([iterable])
    

    举个例子:

    const s = new Set()
    [1, 2, 3, 4, 3, 2, 1].forEach(x => s.add(x))
    
    for (let i of s) {
        console.log(i)  // 1 2 3 4
    }
    
    // 去重数组的重复对象
    let arr = [1, 2, 3, 2, 1, 1]
    [... new Set(arr)]  // [1, 2, 3]
    

    Set 对象允许你储存任何类型的唯一值,无论是原始值或者是对象引用。

    向 Set 加入值的时候,不会发生类型转换,所以5"5"是两个不同的值。Set 内部判断两个值是否不同,使用的算法叫做“Same-value-zero equality”,它类似于精确相等运算符(===),主要的区别是NaN等于自身,而精确相等运算符认为NaN不等于自身。

    let set = new Set();
    let a = NaN;
    let b = NaN;
    set.add(a);
    set.add(b);
    set // Set {NaN}
    
    let set1 = new Set()
    set1.add(5)
    set1.add('5')
    console.log([...set1])  // [5, "5"]
    
    • Set 实例属性

      • constructor: 构造函数

      • size:元素数量

        let set = new Set([1, 2, 3, 2, 1])
        
        console.log(set.length) // undefined
        console.log(set.size)   // 3
        
    • Set 实例方法

      • 操作方法

        • add(value):新增,相当于 array里的push

        • delete(value):存在即删除集合中value

        • has(value):判断集合中是否存在 value

        • clear():清空集合


          let set = new Set()
          set.add(1).add(2).add(1)
          
          set.has(1)    // true
          set.has(3)    // false
          set.delete(1) 
          set.has(1)    // false
          

          Array.from 方法可以将 Set 结构转为数组

          const items = new Set([1, 2, 3, 2])
          const array = Array.from(items)
          console.log(array)    // [1, 2, 3]
          // 或
          const arr = [...items]
          console.log(arr)  // [1, 2, 3]
          
      • 遍历方法(遍历顺序为插入顺序)

        • keys():返回一个包含集合中所有键的迭代器

        • values():返回一个包含集合中所有值得迭代器

        • entries():返回一个包含Set对象中所有元素得键值对迭代器

        • forEach(callbackFn, thisArg):用于对集合成员执行callbackFn操作,如果提供了 thisArg 参数,回调中的this会是这个参数,没有返回值

          let set = new Set([1, 2, 3])
          console.log(set.keys())   // SetIterator {1, 2, 3}
          console.log(set.values()) // SetIterator {1, 2, 3}
          console.log(set.entries())    // SetIterator {1, 2, 3}
          
          for (let item of set.keys()) {
            console.log(item);
          } // 1    2    3
          for (let item of set.entries()) {
            console.log(item);
          } // [1, 1]   [2, 2]  [3, 3]
          
          set.forEach((value, key) => {
              console.log(key + ' : ' + value)
          })    // 1 : 1    2 : 2   3 : 3
          console.log([...set]) // [1, 2, 3]
          

          Set 可默认遍历,默认迭代器生成函数是 values() 方法

          Set.prototype[Symbol.iterator] === Set.prototype.values   // true
          

          所以, Set可以使用 map、filter 方法

          let set = new Set([1, 2, 3])
          set = new Set([...set].map(item => item * 2))
          console.log([...set]) // [2, 4, 6]
          
          set = new Set([...set].filter(item => (item >= 4)))
          console.log([...set]) //[4, 6]
          

          因此,Set 很容易实现交集(Intersect)、并集(Union)、差集(Difference)

          let set1 = new Set([1, 2, 3])
          let set2 = new Set([4, 3, 2])
          
          let intersect = new Set([...set1].filter(value => set2.has(value)))
          let union = new Set([...set1, ...set2])
          let difference = new Set([...set1].filter(value => !set2.has(value)))
          
          console.log(intersect)    // Set {2, 3}
          console.log(union)        // Set {1, 2, 3, 4}
          console.log(difference)   // Set {1}
          

    2. WeakSet

    WeakSet 对象允许你将弱引用对象储存在一个集合中

    WeakSet 与 Set 的区别:

    • WeakSet 只能储存对象引用,不能存放值,而 Set 对象都可以
    • WeakSet 对象中储存的对象值都是被弱引用的,即垃圾回收机制不考虑 WeakSet 对该对象的应用,如果没有其他的变量或属性引用这个对象值,则这个对象将会被垃圾回收掉(不考虑该对象还存在于 WeakSet 中),所以,WeakSet 对象里有多少个成员元素,取决于垃圾回收机制有没有运行,运行前后成员个数可能不一致,遍历结束之后,有的成员可能取不到了(被垃圾回收了),WeakSet 对象是无法被遍历的(ES6 规定 WeakSet 不可遍历),也没有办法拿到它包含的所有元素

    属性:

    • constructor:构造函数,任何一个具有 Iterable 接口的对象,都可以作参数

      const arr = [[1, 2], [3, 4]]
      const weakset = new WeakSet(arr)
      console.log(weakset)
      

    方法:

    • add(value):在WeakSet 对象中添加一个元素value
    • has(value):判断 WeakSet 对象中是否包含value
    • delete(value):删除元素 value
    • clear():清空所有元素,注意该方法已废弃
    var ws = new WeakSet()
    var obj = {}
    var foo = {}
    
    ws.add(window)
    ws.add(obj)
    
    ws.has(window)  // true
    ws.has(foo) // false
    
    ws.delete(window)   // true
    ws.has(window)  // false
    

    3. 字典(Map)

    集合 与 字典 的区别:

    • 共同点:集合、字典 可以储存不重复的值
    • 不同点:集合 是以 [value, value]的形式储存元素,字典 是以 [key, value] 的形式储存
    const m = new Map()
    const o = {p: 'haha'}
    m.set(o, 'content')
    m.get(o)    // content
    
    m.has(o)    // true
    m.delete(o) // true
    m.has(o)    // false
    

    任何具有 Iterator 接口、且每个成员都是一个双元素的数组的数据结构都可以当作Map构造函数的参数,例如:

    const set = new Set([
      ['foo', 1],
      ['bar', 2]
    ]);
    const m1 = new Map(set);
    m1.get('foo') // 1
    
    const m2 = new Map([['baz', 3]]);
    const m3 = new Map(m2);
    m3.get('baz') // 3
    

    如果读取一个未知的键,则返回undefined

    new Map().get('asfddfsasadf')
    // undefined
    

    注意,只有对同一个对象的引用,Map 结构才将其视为同一个键。这一点要非常小心。

    const map = new Map();
    
    map.set(['a'], 555);
    map.get(['a']) // undefined
    

    上面代码的setget方法,表面是针对同一个键,但实际上这是两个值,内存地址是不一样的,因此get方法无法读取该键,返回undefined

    由上可知,Map 的键实际上是跟内存地址绑定的,只要内存地址不一样,就视为两个键。这就解决了同名属性碰撞(clash)的问题,我们扩展别人的库的时候,如果使用对象作为键名,就不用担心自己的属性与原作者的属性同名。

    如果 Map 的键是一个简单类型的值(数字、字符串、布尔值),则只要两个值严格相等,Map 将其视为一个键,比如0-0就是一个键,布尔值true和字符串true则是两个不同的键。另外,undefinednull也是两个不同的键。虽然NaN不严格相等于自身,但 Map 将其视为同一个键。

    let map = new Map();
    
    map.set(-0, 123);
    map.get(+0) // 123
    
    map.set(true, 1);
    map.set('true', 2);
    map.get(true) // 1
    
    map.set(undefined, 3);
    map.set(null, 4);
    map.get(undefined) // 3
    
    map.set(NaN, 123);
    map.get(NaN) // 123
    

    Map 的属性及方法

    属性:

    • constructor:构造函数

    • size:返回字典中所包含的元素个数

      const map = new Map([
        ['name', 'An'],
        ['des', 'JS']
      ]);
      
      map.size // 2
      

    操作方法:

    • set(key, value):向字典中添加新元素
    • get(key):通过键查找特定的数值并返回
    • has(key):判断字典中是否存在键key
    • delete(key):通过键 key 从字典中移除对应的数据
    • clear():将这个字典中的所有元素删除

    遍历方法

    • Keys():将字典中包含的所有键名以迭代器形式返回
    • values():将字典中包含的所有数值以迭代器形式返回
    • entries():返回所有成员的迭代器
    • forEach():遍历字典的所有成员
    const map = new Map([
                ['name', 'An'],
                ['des', 'JS']
            ]);
    console.log(map.entries())  // MapIterator {"name" => "An", "des" => "JS"}
    console.log(map.keys()) // MapIterator {"name", "des"}
    

    Map 结构的默认遍历器接口(Symbol.iterator属性),就是entries方法。

    map[Symbol.iterator] === map.entries
    // true
    

    Map 结构转为数组结构,比较快速的方法是使用扩展运算符(...)。

    对于 forEach ,看一个例子

    const reporter = {
      report: function(key, value) {
        console.log("Key: %s, Value: %s", key, value);
      }
    };
    
    let map = new Map([
        ['name', 'An'],
        ['des', 'JS']
    ])
    map.forEach(function(value, key, map) {
      this.report(key, value);
    }, reporter);
    // Key: name, Value: An
    // Key: des, Value: JS
    

    在这个例子中, forEach 方法的回调函数的 this,就指向 reporter

    与其他数据结构的相互转换

    1. Map 转 Array

      const map = new Map([[1, 1], [2, 2], [3, 3]])
      console.log([...map])    // [[1, 1], [2, 2], [3, 3]]
      
    2. Array 转 Map

      const map = new Map([[1, 1], [2, 2], [3, 3]])
      console.log(map) // Map {1 => 1, 2 => 2, 3 => 3}
      
    3. Map 转 Object

      因为 Object 的键名都为字符串,而Map 的键名为对象,所以转换的时候会把非字符串键名转换为字符串键名。

      function mapToObj(map) {
          let obj = Object.create(null)
          for (let [key, value] of map) {
              obj[key] = value
          }
          return obj
      }
      const map = new Map().set('name', 'An').set('des', 'JS')
      mapToObj(map)  // {name: "An", des: "JS"}
      
    4. Object 转 Map

      function objToMap(obj) {
          let map = new Map()
          for (let key of Object.keys(obj)) {
              map.set(key, obj[key])
          }
          return map
      }
      
      objToMap({'name': 'An', 'des': 'JS'}) // Map {"name" => "An", "des" => "JS"}
      
    5. Map 转 JSON

      function mapToJson(map) {
          return JSON.stringify([...map])
      }
      
      let map = new Map().set('name', 'An').set('des', 'JS')
      mapToJson(map)   // [["name","An"],["des","JS"]]
      
    6. JSON 转 Map

      function jsonToStrMap(jsonStr) {
        return objToMap(JSON.parse(jsonStr));
      }
      
      jsonToStrMap('{"name": "An", "des": "JS"}') // Map {"name" => "An", "des" => "JS"}
      

    4. WeakMap

    WeakMap 对象是一组键值对的集合,其中的键是弱引用对象,而值可以是任意

    注意,WeakMap 弱引用的只是键名,而不是键值。键值依然是正常引用。

    WeakMap 中,每个键对自己所引用对象的引用都是弱引用,在没有其他引用和该键引用同一对象,这个对象将会被垃圾回收(相应的key则变成无效的),所以,WeakMap 的 key 是不可枚举的。

    属性:

    • constructor:构造函数

    方法:

    • has(key):判断是否有 key 关联对象
    • get(key):返回key关联对象(没有则则返回 undefined)
    • set(key):设置一组key关联对象
    • delete(key):移除 key 的关联对象
    let myElement = document.getElementById('logo');
    let myWeakmap = new WeakMap();
    
    myWeakmap.set(myElement, {timesClicked: 0});
    
    myElement.addEventListener('click', function() {
      let logoData = myWeakmap.get(myElement);
      logoData.timesClicked++;
    }, false);
    

    介绍下Set_Map_WeakSet和WeakMap的区别

    5. 总结

    • Set
      • 成员唯一、无序且不重复
      • [value, value],键值与键名是一致的(或者说只有键值,没有键名)
      • 可以遍历,方法有:add、delete、has
    • WeakSet
      • 成员都是对象
      • 成员都是弱引用,可以被垃圾回收机制回收,可以用来保存DOM节点,不容易造成内存泄漏
      • 不能遍历,方法有add、delete、has
    • Map
      • 本质上是键值对的集合,类似集合
      • 可以遍历,方法很多可以跟各种数据格式转换
    • WeakMap
      • 只接受对象作为键名(null除外),不接受其他类型的值作为键名
      • 键名是弱引用,键值可以是任意的,键名所指向的对象可以被垃圾回收,此时键名是无效的
      • 不能遍历,方法有get、set、has、delete

    6. 扩展:Object与Set、Map

    1. Object 与 Set

      // Object
      const properties1 = {
          'width': 1,
          'height': 1
      }
      console.log(properties1['width']? true: false) // true
      
      // Set
      const properties2 = new Set()
      properties2.add('width')
      properties2.add('height')
      console.log(properties2.has('width')) // true
      
    2. Object 与 Map

    ES6 WeakMap和WeakSet的使用场景

    JS 中的对象(Object),本质上是键值对的集合(hash 结构)

    const data = {};
    const element = document.getElementsByClassName('App');
    
    data[element] = 'metadata';
    console.log(data['[object HTMLCollection]']) // "metadata"
    

    但当以一个DOM节点作为对象 data 的键,对象会被自动转化为字符串[Object HTMLCollection],所以说,Object 结构提供了 字符串-值 对应,Map则提供了 值-值 的对应

    JavaScript垃圾回收是一种内存管理技术。在这种技术中,不再被引用的对象会被自动删除,而与其相关的资源也会被一同回收。

    Map和Set中对象的引用都是强类型化的,并不会允许垃圾回收。这样一来,如果Map和Set中引用了不再需要的大型对象,如已经从DOM树中删除的DOM元素,那么其回收代价是昂贵的。

    为了解决这个问题,ES6还引入了另外两种新的数据结构,即称为WeakMap和WeakSet的弱集合。这些集合之所以是“弱的”,是因为它们允许从内存中清除不再需要的被这些集合所引用的对象。

    使用场景:储存 DOM 节点,而不用担心这些节点从文档移除时,会引发内存泄漏

    有以下 3 个判断数组的方法,请分别介绍它们之间的区别和优劣

    1. Object.prototype.toString.call()

    每一个继承 Object 的对象都有 toString 方法,如果 toString 方法没有重写的话,会返回 [Object type],其中 type 为对象的类型。但当除了 Object 类型的对象外,其他类型直接使用 toString 方法时,会直接返回都是内容的字符串,所以我们需要使用call或者apply方法来改变toString方法的执行上下文。

    const an = ['Hello','An'];
    an.toString(); // "Hello,An"
    Object.prototype.toString.call(an); // "[object Array]"
    

    这种方法对于所有基本的数据类型都能进行判断,即使是 null 和 undefined 。

    Object.prototype.toString.call('An') // "[object String]"
    Object.prototype.toString.call(1) // "[object Number]"
    Object.prototype.toString.call(Symbol(1)) // "[object Symbol]"
    Object.prototype.toString.call(null) // "[object Null]"
    Object.prototype.toString.call(undefined) // "[object Undefined]"
    Object.prototype.toString.call(function(){}) // "[object Function]"
    Object.prototype.toString.call({name: 'An'}) // "[object Object]"
    

    Object.prototype.toString.call() 常用于判断浏览器内置对象时。

    更多实现可见 谈谈 Object.prototype.toString

    2. instanceof

    instanceof 的内部机制是通过判断对象的原型链中是不是能找到类型的 prototype

    使用 instanceof判断一个对象是否为数组,instanceof 会判断这个对象的原型链上是否会找到对应的 Array 的原型,找到返回 true,否则返回 false

    []  instanceof Array; // true
    

    instanceof 只能用来判断对象类型,原始类型不可以。并且所有对象类型 instanceof Object 都是 true。

    []  instanceof Object; // true
    

    3. Array.isArray()

    • 功能:用来判断对象是否为数组

    • Array.isArray()Object.prototype.toString.call()

      Array.isArray()是ES5新增的方法,当不存在 Array.isArray() ,可以用 Object.prototype.toString.call() 实现。

      if (!Array.isArray) {
        Array.isArray = function(arg) {
          return Object.prototype.toString.call(arg) === '[object Array]';
        };
      }
      

    ES5/ES6 的定义对象的区别

    1. class 声明会提升,但不会初始化赋值。Foo 进入暂时性死区,类似于 letconst 声明变量。
    
    const bar = new Bar(); // it's ok
    function Bar() {
      this.bar = 42;
    }
    
    const foo = new Foo(); // ReferenceError: Foo is not defined
    class Foo {
      constructor() {
        this.foo = 42;
      }
    }
    
    1. class 声明内部会启用严格模式。
    // 引用一个未声明的变量
    function Bar() {
      baz = 42; // it's ok
    }
    const bar = new Bar();
    
    class Foo {
      constructor() {
        fol = 42; // ReferenceError: fol is not defined
      }
    }
    const foo = new Foo();
    

    class` 的所有方法(包括静态方法和实例方法)都是不可枚举的。

    // 引用一个未声明的变量
    function Bar() {
      this.bar = 42;
    }
    Bar.answer = function() {
      return 42;
    };
    Bar.prototype.print = function() {
      console.log(this.bar);
    };
    const barKeys = Object.keys(Bar); // ['answer']
    const barProtoKeys = Object.keys(Bar.prototype); // ['print']
    
    class Foo {
      constructor() {
        this.foo = 42;
      }
      static answer() {
        return 42;
      }
      print() {
        console.log(this.foo);
      }
    }
    const fooKeys = Object.keys(Foo); // []
    const fooProtoKeys = Object.keys(Foo.prototype); // []
    
    1. class 的所有方法(包括静态方法和实例方法)都没有原型对象 prototype,所以也没有[[construct]],不能使用 new 来调用。
    function Bar() {
      this.bar = 42;
    }
    Bar.prototype.print = function() {
      console.log(this.bar);
    };
    
    const bar = new Bar();
    const barPrint = new bar.print(); // it's ok
    
    class Foo {
      constructor() {
        this.foo = 42;
      }
      print() {
        console.log(this.foo);
      }
    }
    const foo = new Foo();
    const fooPrint = new foo.print(); // TypeError: foo.print is not a constructor
    
    1. 必须使用 new 调用 class
    function Bar() {
      this.bar = 42;
    }
    const bar = Bar(); // it's ok
    
    class Foo {
      constructor() {
        this.foo = 42;
      }
    }
    const foo = Foo(); // TypeError: Class constructor Foo cannot be invoked without 'new'
    
    1. class 内部无法重写类名。
    function Bar() {
      Bar = 'Baz'; // it's ok
      this.bar = 42;
    }
    const bar = new Bar();
    // Bar: 'Baz'
    // bar: Bar {bar: 42}  
    
    class Foo {
      constructor() {
        this.foo = 42;
        Foo = 'Fol'; // TypeError: Assignment to constant variable
      }
    }
    const foo = new Foo();
    Foo = 'Fol'; // it's ok
    

    全局作用域中,用 const 和 let 声明的变量不在 window 上,那到底在哪里?如何去获取?

    在ES5中,顶层对象的属性和全局变量是等价的,var 命令和 function 命令声明的全局变量,自然也是顶层对象。

    var a = 12;
    function f(){};
    
    console.log(window.a); // 12
    console.log(window.f); // f(){}
    

    但ES6规定,var 命令和 function 命令声明的全局变量,依旧是顶层对象的属性,但 let命令、const命令、class命令声明的全局变量,不属于顶层对象的属性。

    let aa = 1;
    const bb = 2;
    
    console.log(window.aa); // undefined
    console.log(window.bb); // undefined
    

    在哪里?怎么获取?通过在设置断点,看看浏览器是怎么处理的:

    通过上图也可以看到,在全局作用域中,用 let 和 const 声明的全局变量并没有在全局对象中,只是一个块级作用域(Script)中

    怎么获取?在定义变量的块级作用域中就能获取啊,既然不属于顶层对象,那就不加 window(global)呗。

    let aa = 1;
    const bb = 2;
    
    console.log(aa); // 1
    console.log(bb); // 2
    

    const和let会生成块级作用域,可以理解为

    let a = 10;
    const b = 20;
    相当于:
    (function(){
             var  a = 10;
             var b = 20;
    })()
    

    ES5没有块级作用域的概念,只有函数作用域,可以近似理解成这样。 所以外层window必然无法访问。

    为何会出现浏览器兼容问题

    • 同一产品,版本越老 bug 越多
    • 同一产品,版本越新,功能越多
    • 不同产品,不同标准,不同实现方式

    处理兼容问题的思路

    1. 要不要做
    • 产品的角度(产品的受众、受众的浏览器比例、效果优先还是基本功能优先)
    • 成本的角度 (有无必要做某件事)

    2.做到什么程度

    • 让哪些浏览器支持哪些效果

    3..如何做

    • 根据兼容需求选择技术框架/库(jquery)
    • 根据兼容需求选择兼容工具(html5shiv.js、respond.js、css reset、normalize.css、Modernizr)
    • 条件注释、CSS Hack、js 能力检测做一些修补
    • 渐进增强(progressive enhancement): 针对低版本浏览器进行构建页面,保证最基本的功能,然后再针对高级浏览器进行效果、交互等改进和追加功能达到更好的用户体验
    • 优雅降级 (graceful degradation): 一开始就构建完整的功能,然后再针对低版本浏览器进行兼容。

    前端性能优化

    • 减少 HTTP 请求
    • 减少 DOM 操作
    • 避免不必要的重绘与重排
    • 优化 CSS 选择器(从右向左匹配)
    • CSS/JS minify,减少文件体积
    • 开启 Gzip 压缩
    • 将 CSS 放到顶部,JavaScript 放到尾部
    • 压缩图片以及使用 CSS Sprite
    • 使用 CDN 加速,适当进行文件缓存
    • 合理控制 cookie 大小(每次请求都会包含 cookie)

    XSS

    XSS是什么

    XSS是一种经常出现在web应用中的计算机安全漏洞,它允许恶意web用户将代码植入到提供给其它用户使用的页面中。

    比如这些代码包括HTML代码和客户端脚本。攻击者利用XSS漏洞旁路掉访问控制——例如同源策略(same origin policy)。

    这种类型的漏洞由于被黑客用来编写危害性更大的网络钓鱼(Phishing)攻击而变得广为人知。

    对于跨站脚本攻击,黑客界共识是:跨站脚本攻击是新型的“缓冲区溢出攻击“,而JavaScript是新型的“ShellCode”。

    示例:
    <script>alert(document.cookie)</script>
    

    特点

    能注入恶意的HTML/JavaScript代码到用户浏览的网页上,从而达到Cookie资料窃取、会话劫持、钓鱼欺骗等攻击。
    <攻击代码不一定(非要)在 <script></script> 中>

    原因

    • Web浏览器本身的设计不安全。浏览器能解析和执行JS等代码,但是不会判断该数据和程序代码是否恶意。

    • 输入和输出是Web应用程序最基本的交互,而且网站的交互功能越来越丰富。如果在这过程中没有做好安全防护,很容易会出现XSS漏洞。

    • 程序员水平参差不齐,而且大都没有过正规的安全培训,没有相关的安全意识。

    • XSS攻击手段灵活多变。

    危害

    • 盗取各类用户帐号,如机器登录帐号、用户网银帐号、各类管理员帐号
    • 控制企业数据,包括读取、篡改、添加、删除企业敏感数据的能力
    • 盗窃企业重要的具有商业价值的资料
    • 非法转账
    • 强制发送电子邮件
    • 网站挂马
    • 控制受害者机器向其它网站发起攻击

    如何防范

    • 将重要的cookie标记为http only, 这样的话Javascript 中的document.cookie语句就不能获取到cookie了.
    • 表单数据规定值的类型,例如:年龄应为只能为int、name只能为字母数字组合。。。。
    • 对数据进行Html Encode 处理
    • 过滤或移除特殊的Html标签, 例如: <script>, <iframe> , < for <, > for >, &quot for
    • 过滤JavaScript 事件的标签。例如 "onclick=", "onfocus" 等等。

    参考资料:

    https://www.cnblogs.com/phpstudy2015-6/p/6767032.html

    https://www.cnblogs.com/443855539-wind/p/6055816.html

    https://baike.baidu.com/item/XSS%E6%94%BB%E5%87%BB/954065?fr=aladdin

    CSRF

    CSRF(Cross-site request forgery)跨站请求伪造,也被称为“One Click Attack”或者Session Riding,通常缩写为CSRF或者XSRF,是一种对网站的恶意利用。尽管听起来像跨站脚本(XSS),但它与XSS非常不同,XSS利用站点内的信任用户,而CSRF则通过伪装来自受信任用户的请求来利用受信任的网站。与XSS攻击相比,CSRF攻击往往不大流行(因此对其进行防范的资源也相当稀少)和难以防范,所以被认为比XSS更具危险性。

    特点

    • 依靠用户标识危害网站
    • 利用网站对用户标识的信任
    • 欺骗用户的浏览器发送HTTP请求给目标站点
    • 另外可以通过IMG标签会触发一个GET请求,可以利用它来实现CSRF攻击。

    防御

    • 通过referer、token或者验证码来检测用户提交。
    • 尽量不要在页面的链接中暴露用户隐私信息。
    • 对于用户修改删除等操作最好都使用post操作 。
    • 避免全站通用的cookie,严格设置cookie的域。

    圣杯布局和双飞翼布局是前端工程师需要日常掌握的重要布局方式。两者的功能相同,都是为了实现一个两侧宽度固定,中间宽度自适应的三栏布局

    圣杯布局与双飞翼布局

    圣杯布局来源于文章In Search of the Holy Grail,而双飞翼布局来源于淘宝UED。虽然两者的实现方法略有差异,不过都遵循了以下要点:

    • 两侧宽度固定,中间宽度自适应
    • 中间部分在DOM结构上优先,以便先行渲染
    • 允许三列中的任意一列成为最高列
    • 只需要使用一个额外的``标签

    下面我将依次介绍圣杯布局和双飞翼布局的实现方法,并在最后根据个人思考对原有方法做出一些修改,给出其它一些可行的方案。

    圣杯布局

    1. DOM结构

    <div id="header"></div>
    <div id="container">
      <div id="center" class="column"></div>
      <div id="left" class="column"></div>
      <div id="right" class="column"></div>
    </div>
    <div id="footer"></div>
    

    首先定义出整个布局的DOM结构,主体部分是由container包裹的center,left,right三列,其中center定义在最前面。

    2. CSS代码

    假设左侧的固定宽度为200px,右侧的固定宽度为150px,则首先在container上设置:

    #container {
      padding-left: 200px; 
      padding-right: 150px;
    }
    

    为左右两列预留出相应的空间,得到如下示意图:

    创建布局框架

    随后分别为三列设置宽度与浮动,同时对footer设置清除浮动:

    #container .column {
      float: left;
    }
    
    #center {
      width: 100%;
    }
    
    #left {
      width: 200px; 
    }
    
    #right {
      width: 150px; 
    }
    
    #footer {
      clear: both;
    }
    

    得到如下效果:

    设置宽度和清除浮动

    根据浮动的特性,由于center的宽度为100%,即占据了第一行的所有空间,所以leftright被“挤”到了第二行。

    接下来的工作是将left放置到之前预留出的位置上,这里使用负外边距(nagetive margin)

    #left {
      width: 200px; 
      margin-left: -100%;
    }
    

    得到:

    将left移动到预留位置-1

    随后还需要使用定位(position)方法:

    #left {
      width: 200px; 
      margin-left: -100%;
      position: relative;
      right: 200px;
    }
    

    这里使用position: relativeright: 200pxleft的位置在原有位置基础上左移200px,以完成left的放置:

    将left移动到预留位置-2

    接下来放置right,只需添加一条声明即可:

    #right {
      width: 150px; 
      margin-right: -150px; 
    }
    

    得到最终的效果图:

    将right移动到预留位置

    至此,布局效果完成。不过还需要考虑最后一步,那就是页面的最小宽度:要想保证该布局效果正常显示,由于两侧都具有固定的宽度,所以需要给定页面一个最小的宽度,但这并不只是简单的200+150=350px。回想之前left使用了position: relative,所以就意味着在center开始的区域,还存在着一个left的宽度。所以页面的最小宽度应该设置为200+150+200=550px:

    body {
      min-width: 550px;
    }
    

    综上所述,圣杯布局的CSS代码为:

    body {
      min-width: 550px;
    }
    
    #container {
      padding-left: 200px; 
      padding-right: 150px;
    }
    
    #container .column {
      float: left;
    }
    
    #center {
      width: 100%;
    }
    
    #left {
      width: 200px; 
      margin-left: -100%;
      position: relative;
      right: 200px;
    }
    
    #right {
      width: 150px; 
      margin-right: -150px; 
    }
    
    #footer {
      clear: both;
    }
    

    关于圣杯布局的示例,可参考:圣杯布局

    最后提醒一下很多朋友可能会忽略的小细节:在#center中,包含了一条声明width: 100%,这是中间栏能够做到自适应的关键。可能会有朋友认为不需要设置这条声明,因为觉得center在不设置宽度的情况下会默认将宽度设置为父元素(container)的100%宽度。但需要注意到,center是浮动元素,由于浮动具有包裹性,在不显式设置宽度的情况下会自动“收缩”到内容的尺寸大小。如果去掉width: 100%,则当中间栏不包含或者包含较少内容时,整个布局会“崩掉”,而达不到这样的效果:

    中间栏仅包含较少内容

    双飞翼布局

    1. DOM结构

    <body>
      <div id="header"></div>
      <div id="container" class="column">
        <div id="center"></div>
      </div>
      <div id="left" class="column"></div>
      <div id="right" class="column"></div>
      <div id="footer"></div>
    <body>
    

    双飞翼布局的DOM结构与圣杯布局的区别是用container仅包裹住center,另外将.column类从center移至container上。

    2. CSS代码

    按照与圣杯布局相同的思路,首先设置各列的宽度与浮动,并且为左右两列预留出空间,以及为footer设置浮动清除:

    #container {
      width: 100%;
    }
    
    .column {
      float: left;
    }
    
    #center {
      margin-left: 200px;
      margin-right: 150px;
    }
    
    #left {
      width: 200px; 
    }
    
    #right {
      width: 150px; 
    }
    
    #footer {
      clear: both;
    }
    

    得到如下效果示意图:

    双飞翼布局初始设置

    以上代码将container,left,right设置为float: left,而在container内部,center由于没有设置浮动,所以其宽度默认为container的100%宽度,通过对其设置margin-leftmargin-right为左右两列预留出了空间。

    left放置到预留位置:

    #left {
      width: 200px; 
      margin-left: -100%;
    }
    

    得到:

    放置left到预留位置

    right放置到预留位置:

    #right {
      width: 150px; 
      margin-left: -150px;
    }
    

    得到最终效果:

    双飞翼布局最终效果

    最后计算最小页面宽度:由于双飞翼布局没有用到position:relative进行定位,所以最小页面宽度应该为200+150=350px。但是当页面宽度缩小到350px附近时,会挤占中间栏的宽度,使得其内容被右侧栏覆盖,如下所示:

    中间栏内容被覆盖

    因此在设置最小页面宽度时,应该适当增加一些宽度以供中间栏使用(假设为150px),则有:

    body {
      min-width: 500px;
    }
    

    至此双飞翼布局大功告成!其布局整体代码为:

    body {
      min-width: 500px;
    }
    
    #container {
      width: 100%;
    }
    
    .column {
      float: left;
    }
            
    #center {
      margin-left: 200px;
      margin-right: 150px;
    }
            
    #left {
      width: 200px; 
      margin-left: -100%;
    }
            
    #right {
      width: 150px; 
      margin-left: -150px;
    }
            
    #footer {
      clear: both;
    }
    

    关于双飞翼布局的示例,可参考:双飞翼布局

    总结与思考

    通过对圣杯布局和双飞翼布局的介绍可以看出,圣杯布局在DOM结构上显得更加直观和自然,且在日常开发过程中,更容易形成这样的DOM结构(通常/一起被嵌套在中);而双飞翼布局在实现上由于不需要使用定位,所以更加简洁,且允许的页面最小宽度通常比圣杯布局更小。

    其实通过思考不难发现,两者在代码实现上都额外引入了一个``标签,其目的都是为了既能保证中间栏产生浮动(浮动后还必须显式设置宽度),又能限制自身宽度为两侧栏留出空间。

    从这个角度出发,如果去掉额外添加的``标签,能否完成相同的布局呢?答案是肯定的,不过这需要在兼容性上做出牺牲

    DOM结构

    <div id="header"></div>
    <div id="center" class="column"></div>
    <div id="left" class="column"></div>
    <div id="right" class="column"></div>
    <div id="footer"></div>
    

    去掉额外的``标签后,得到的DOM结构如上所示,基于双飞翼布局的实现思路,只需要在center上做出修改:

    1. 使用calc()

    .column {
      float: left;
    }
        
    #center {
      margin-left: 200px;
      margin-right: 150px;
      width: calc(100% - 350px);
    }
    

    通过calc()可以十分方便地计算出center应该占据的自适应宽度,目前calc()支持到IE9

    2. 使用border-box

    .column {
      float: left;
    }
        
    #center {
      padding-left: 200px;
      padding-right: 150px;
      box-sizing: border-box;
      width: 100%;
    }
    

    使用border-box可以将center的整个盒模型宽度设置为父元素的100%宽度,此时再利用padding-leftpadding-right可以自动得到中间栏的自适应宽度。不过需要注意的是,由于padding是盒子的一部分,所以padding部分会具有中间栏的背景色,当中间栏高于侧栏时,会出现这样的情况:

    padding背景色影响左侧空间

    目前box-sizing支持到IE8

    3. 使用flex

    这里使用flex还是需要与圣杯布局相同的DOM结构,不过在实现上将更加简单:

    <!-- DOM结构 -->
    <div id="container">
      <div id="center"></div>
      <div id="left"></div>
      <div id="right"></div>
    </div>
    

    CSS代码如下:

    #container {
        display: flex;
    }
    
    #center {
        flex: 1;
    }
    
    #left {
        flex: 0 0 200px;
        order: -1;
    }
    
    #right {
        flex: 0 0 150px;
    }
    

    同源策略

    同源策略可防止 JavaScript 发起跨域请求。源被定义为 URI、主机名和端口号的组合。此策略可防止页面上的恶意脚本通过该页面的文档对象模型,访问另一个网页上的敏感数据。

    跨域

    • 原因

      浏览器的同源策略导致了跨域
    • 作用

      用于隔离潜在恶意文件的重要安全机制
    • 解决
    1. jsonp ,允许 script 加载第三方资源
    2. 反向代理(nginx 服务内部配置 Access-Control-Allow-Origin *)
    3. cors 前后端协作设置请求头部,Access-Control-Allow-Origin 等头部信息
    4. iframe 嵌套通讯,postmessage

    https://zhuanlan.zhihu.com/p/41479807

    跨域资源共享 CORS 阮一峰

    JSONP

    这是我认为写得比较通俗易懂的一篇文章 直接转载过来

    https://blog.csdn.net/hansexploration/article/details/80314948

    域名收敛

    PC 时代为了突破浏览器的域名并发限制。有了域名发散。

    浏览器有并发限制,是为了防止DDOS攻击。

    域名收敛:就是将静态资源放在一个域名下。减少DNS解析的开销。

    域名发散:是将静态资源放在多个子域名下,就可以多线程下载,提高并行度,使客户端加载静态资源更加迅速。

    域名发散是pc端为了利用浏览器的多线程并行下载能力。而域名收敛多用与移动端,提高性能,因为dns解析是是从后向前迭代解析,如果域名过多性能会下降,增加DNS的解析开销。

    事件绑定的方式

    • 嵌入dom
    <button onclick="func()">按钮</button>
    
    • 直接绑定
    btn.onclick = function(){}
    
    • 事件监听
    btn.addEventListener('click',function(){})
    

    事件委托

    事件委托利用了事件冒泡,只指定一个事件处理程序,就可以管理某一类型的所有事件。所有用到按钮的事件(多数鼠标事件和键盘事件)都适合采用事件委托技术,
    使用事件委托可以节省内存。

    <ul>
      <li>苹果</li>
      <li>香蕉</li>
      <li>凤梨</li>
    </ul>
    
    // good
    document.querySelector('ul').onclick = (event) => {
      let target = event.target
      if (target.nodeName === 'LI') {
        console.log(target.innerHTML)
      }
    }
    
    // bad
    document.querySelectorAll('li').forEach((e) => {
      e.onclick = function() {
        console.log(this.innerHTML)
      }
    }) 
    

    prototype和proto的关系是什么

    所有的对象都拥有proto属性,它指向对象构造函数的prototype属性

    let obj = {}
    obj.__proto__ === Object.prototype // true
    
    function Test(){}
    test.__proto__ == Test.prototype // true
    

    所有的函数都同时拥有proto和protytpe属性
    函数的proto指向自己的函数实现 函数的protytpe是一个对象 所以函数的prototype也有proto属性 指向Object.prototype

    function func() {}
    func.prototype.__proto__ === Object.prototype // true
    

    Object.prototype.proto指向null

    Object.prototype.__proto__ // null
    

    原型继承

    所有的JS对象都有一个prototype属性,指向它的原型对象。当试图访问一个对象的属性时,如果没有在该对象上找到,它还会搜寻该对象的原型,以及该对象的原型的原型,依次层层向上搜索,直到找到一个名字匹配的属性或到达原型链的末尾。

    继承

    JS高程第3版 第6章 继承
    寄生组合式继承

    function SuperType(name) {
        this.name = name
        this.colors = ['red']
    }
    
    SuperType.prototype.sayName = function() {
        console.log(this.name)
    }
    // 继承实例属性
    function SubType(name, age) {
        SuperType.call(this, name)
        this.age = age
    }
    
    function inheritPrototype(subType, superType) {
        let prototype = Object.create(superType.prototype)
        prototype.constructor = subType
        subType.prototype = prototype
    }
    // 继承原型方法
    inheritPrototype(SubType, SuperType)
    
    // 定义自己的原型方法
    SubType.prototype.sayAge = function() {
        console.log(this.age)
    }
    

    闭包

    闭包是指有权访问另一个函数作用域中的变量的函数。

    function sayHi(name) {
        return () => {
           console.log(`Hi! ${name}`)
        }
    }
    const test = sayHi('xiaoming')
    test() // Hi! xiaoming
    

    虽然sayHi函数已经执行完毕,但是其活动对象也不会被销毁,因为test函数仍然引用着sayHi函数中的变量name,这就是闭包。

    但也因为闭包引用着另一个函数的变量,导致另一个函数已经不使用了也无法销毁,所以闭包使用过多,会占用较多的内存,这也是一个副作用,内存泄漏。

    有一个函数,参数是一个函数,返回值也是一个函数,返回的函数功能和入参的函数相似,但这个函数只能执行3次,再次执行无效,如何实现

    这个题目是考察闭包的使用

    function sayHi() {
        console.log('hi')
    }
    
    function threeTimes(fn) {
        let times = 0
        return () => {
            if (times++ < 3) {
                fn()
            }
        }
    }
    
    const newFn = threeTimes(sayHi)
    newFn()
    newFn()
    newFn()
    newFn()
    newFn() // 后面两次执行都无任何反应
    

    通过闭包变量 times 来控制函数的执行

    实现add函数,让add(a)(b)和add(a,b)两种调用结果相同

    function add(a, b) {
        if (b === undefined) {
            return function(x) {
                return a + x
            }
        }
    
        return a + b
    }
    

    虽然sayHi函数已经执行完毕,但是其活动对象也不会被销毁,因为test函数仍然引用着sayHi函数中的变量name,这就是闭包。

    但也因为闭包引用着另一个函数的变量,导致另一个函数已经不使用了也无法销毁,所以闭包使用过多,会占用较多的内存,这也是一个副作用,内存泄漏。

    JS垃圾回收机制

    https://www.cnblogs.com/sunhuahuaa/p/7655587.html
    https://zhuanlan.zhihu.com/p/60484579

    cookie、session、localStorage、sessionStorage区别

    特性 cookie localStorage sessionStorage
    由谁初始化 客户端或服务器,服务器可以使用Set-Cookie请求头。 客户端 客户端
    数据的生命周期 一般由服务器生成,可设置失效时间,如果在浏览器生成,默认是关闭浏览器之后失效 永久保存,可清除 仅在当前会话有效,关闭页面后清除
    存放数据大小 4KB 5MB 5MB
    与服务器通信 每次都会携带在HTTP头中,如果使用cookie保存过多数据会带来性能问题 仅在客户端保存 仅在客户端保存
    用途 一般由服务器生成,用于标识用户身份 用于浏览器缓存数据 用于浏览器缓存数据
    访问权限 任意窗口 任意窗口 当前页面窗口

    创建对象有几种方法

    • 字面量
    const obj = {a: 1}
    
    • 构造函数
    function Obj(val) {
        this.a = val
    }
    
    const obj = new Obj(1)
    
    • Object.create
    const obj = Object.create({a: 1})
    

    ES6 class

    class abc{
        constructor(){
            this.a = 123
        }
    }
    

    bind、call、apply、箭头函数、new的区别

    call和apply其实是一样的,区别就在于传参时参数是一个一个传或者是以一个数组的方式来传。

    call和apply都是在调用时生效,改变调用者的this指向。

    let name = 'Jack'
    const obj = {name: 'Tom'}
    function sayHi() {console.log('Hi! ' + this.name)}
    
    sayHi() // Hi! Jack
    sayHi.call(obj) // Hi! Tom
    
    

    bind也是改变this指向,不过不是在调用时生效,而是返回一个新函数。

    const newFunc = sayHi.bind(obj)
    newFunc() // Hi! Tom
    

    请简述JavaScript中的this

    JS 中的this是一个相对复杂的概念,不是简单几句能解释清楚的。粗略地讲,函数的调用方式决定了this的值。我阅读了网上很多关于this的文章,Arnav Aggrawal 写的比较清楚。this取值符合以下规则:

    1. 在调用函数时使用new关键字,函数内的this是一个全新的对象。
    2. 如果applycallbind方法用于调用、创建一个函数,函数内的 this 就是作为参数传入这些方法的对象。
    3. 当函数作为对象里的方法被调用时,函数内的this是调用该函数的对象。比如当obj.method()被调用时,函数内的 this 将绑定到obj对象。
    4. 如果调用函数不符合上述规则,那么this的值指向全局对象(global object)。浏览器环境下this的值指向window对象,但是在严格模式下('use strict'),this的值为undefined
    5. 如果符合上述多个规则,则较高的规则(1 号最高,4 号最低)将决定this的值。
    6. 如果该函数是 ES2015 中的箭头函数,将忽略上面的所有规则,this被设置为它被创建时的上下文。

    想获得更深入的解释,请查看他在 Medium 上的文章

    https://github.com/yangshun/front-end-interview-handbook/blob/master/Translations/Chinese/questions/javascript-questions.md#%E8%AF%B7%E7%AE%80%E8%BF%B0javascript%E4%B8%AD%E7%9A%84this

    如何确定this指向

    如果要判断一个运行中函数的 this 绑定,就需要找到这个函数的直接调用位置。找到之后就可以顺序应用下面这四条规则来判断 this 的绑定对象。

    1. 由 new 调用?绑定到新创建的对象。
    2. 由 call 或者 apply (或者 bind )调用?绑定到指定的对象。
    3. 由上下文对象调用?绑定到那个上下文对象。
    4. 默认:在严格模式下绑定到 undefined ,否则绑定到全局对象。

    一定要注意,有些调用可能在无意中使用默认绑定规则。如果想“更安全”地忽略 this 绑定,你可以使用一个 DMZ 对象,比如 ø = Object.create(null) ,以保护全局对象。

    ES6 中的箭头函数并不会使用四条标准的绑定规则,而是根据当前的词法作用域来决定this ,具体来说,箭头函数会继承外层函数调用的 this 绑定(无论 this 绑定到什么)。这其实和 ES6 之前代码中的 self = this 机制一样

    相关文章

      网友评论

        本文标题:前端面试基础题

        本文链接:https://www.haomeiwen.com/subject/mejljktx.html