前端系统课程 - 31. 阶段考试及相关知识

题目 01

请写出一个符合 W3C 规范的 HTML 文件,要求:

  1. 页面标题为“我的页面”;

  2. 页面中引入一个外部 CSS 文件,文件路径为 /style.css

  3. 页面中引入一个外部 CSS 文件,路径为 /print.css,该文件仅在打印时生效;

  4. 页面中引入一个外部 CSS 文件,路径为 /mobile.css,该文件仅在设备宽度小于 500 像素时生效;

  5. 页面中引入一个外部 JS 文件,路径为 /main.js

  6. 页面中引入一个外部 JS 文件,路径为 /gbk.js,文件编码为 GBK;

  7. 页面中添加一个 <svg> 标签,里面有一个直径为 100 像素的圆圈,颜色随意;

  8. 注意题目中的路径。


回答:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
<!DOCTYPE html>
<html lang="zh_Hans">
<head>
<meta charset="UTF-8">
<title>我的页面</title>
<link rel="stylesheet" href="./style.css">
<link rel="stylesheet" href="./print.css" media="print">
<link rel="stylesheet" href="./mobile.css" media="(max-width: 500px)">
</head>
<body>
<svg version="1.1"
baseProfile="full"
width="300" height="200"
xmlns="http://www.w3.org/2000/svg">
<circle cx="100" cy="100" r="50" stroke="red" fill="transparent"></circle>
</svg>
<script src="./main.js"></script>
<script src="./gbk.js" charset="GBK"></script>
</body>
</html>

补充:

大部分是扩展知识,例如媒体查询的设置,<svg> 标签的基本用法,都能很轻松的查阅到;另外给出的文件路径不能直接使用,不然就代表根目录了,一般不会这样存放资源;要么去除斜杆,要么直接在斜杠前加个点,为了保险起见,还是加了个点,表示同一层级的目录下。

题目 02

2016年腾讯前端面试题:移动端是怎么做适配的?

回答要点:

  1. <meta name="viewport">

  2. 媒体查询;

  3. 动态 rem 方案。


回答:

  • 一般要将页面宽度适配为设备宽度,除了使用 flex 布局和新的宽度单位 vw / vh 以外,在 HTML 文件的 <head> 标签中进行视口设置也是必要的,内容如下:

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

    content 属性中的内容含义依次是:页面宽度等于设备宽度,用户缩放禁用,初识缩放比例 1.0 倍,最大缩放比例 1.0 倍,最小缩放比例 1.0 倍。

  • 如果还想做 PC 端页面的响应式适配,也就是让页面在不同的分辨率拥有不同的样式,除了类似 Bootstrap 中的“十二栅格”,还有就是媒体查询了,可以使用 CSS 中的媒体查询功能,来匹配不同宽度页面下的样式,例如:

    1
    2
    3
    4
    5
    @media all and(max-width: 768px) { /* 以下样式在所有设备中最大支持到宽度为768px,超过便失效 */
    body {
    font: bold 12px/1.6 '微软雅黑';
    }
    }

    媒体查询是非常好用的,但是缺点是如果支持的分辨率太多,那就需要写很多的样式才行,比较麻烦。

  • 如果是以小屏设备(主要是手机)为主,但是手机分辨率太多,这时可以使用 em 单位或 rem 单位。这两个长度单位会根据设置好的与像素的比例,在不同的页面宽度中,保持相同比例的像素大小。em 单位比较繁琐,因为它的大小是基于父元素文字大小的,如果层级过多,将会涉及大量的计算,一般不推荐使用;而 rem 的大小,是基于根元素的文字大小,这就比较统一且好算,一般将首要的支持设备页面中(或设计图的大小),根元素文字大小设置为 100px 是最好用的了,不设置为 10px 可以避免很多麻烦哟!

    那么如何保证不同宽度设备下页面根元素的像素比例呢?最好还是用 JS 方法实现,在页面加载完和窗口宽度改变时更新根元素上的文字大小即可,以安卓手机的设计图宽度,示例如下:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    !function (n) {
    var e = n.document,
    t = e.documentElement,
    o = "orientationchange" in n ? "orientationchange" : "resize",
    a = function () {
    var n = t.clientWidth || 360;
    t.style.fontSize = n / 360 * 100 + "px";
    };

    if (e.addEventListener) {
    n.addEventListener(o, a, !1);
    e.addEventListener("DOMContentLoaded", a, !1);
    }
    }(window);

补充:

虽然没有看过上面的系列课程,但是好歹做过一些移动端适配的小页面,基本上个人所知道的移动端适配的方法,上面都提到了。

题目 03

2017年腾讯前端实习面试题(二面):用过 CSS3 吗?实现圆角矩形和阴影怎么做?


回答:

当然用过了……

  • 圆角矩形比较简单,可以用 border-radius 这个属性,它可以同时设置四个圆角边框,半径单位可以是像素,也可以是百分比;每个角也可以设置两个不同的半径;设置顺序是:左上、右上、右下、左下,如果只写一个,那么表示设置统一值;也可以分别设置,但比较麻烦。例如:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    .test {
    width: 100px;
    height: 40px;
    border-radius: 4px 6px 5px 3% / 2%;
    /* 相当于 */
    border-top-left-radius: 4px 2%;
    border-top-right-radius: 6px 2%;
    border-bottom-right-radius: 5px 2%;
    border-bottom-left-radius: 3% 2%;
    }
  • 阴影问的应该是盒子阴影吧……

    如果是文字阴影,就一个属性 text-shadow,属性值依次为阴影的:水平位置、垂直位置、模糊距离(程度)、颜色;两个位置是必需的,其他不写范围就是 0 ,颜色默认是文字颜色;可以给同一文字设置多个阴影,多个设置用逗号隔开。

    如果是盒子阴影就又复杂一些,使用 box-shadow 属性,属性值依次为阴影的:水品位置、垂直位置、模糊距离(程度)、大小、颜色、内侧或外侧;位置都是必须的,但是可以为 0 或负值,其他的都好理解,最后一个内侧和外侧意思是将阴影添加到盒子边内部还是外部;多个阴影设置可以用逗号分隔,另外,阴影可以在某些时候模拟边框哟!示例代码:

    1
    2
    3
    .test {
    box-shadow: 3px 3px 5px 5px #ccc, 5px 5px 10px 10px #666; /* 立体效果 */
    }

补充:

基本上用过的话就没什么,主要是把属性顺序搞清楚,不清楚也很容易查到文档;另外分开写的属性,实际很少用。

题目 04

出处同上:什么是闭包,闭包的用途是什么?


回答:

  • 如果一个函数使用了它范围外的变量,那么(这个函数加这个变量)就叫做闭包;也就是说,闭包就是能够读取其他函数内部变量的函数,以及它读取的这个变量的合称!?

  • 用途太广泛了,其实我们经常不可避免的使用着闭包,主要就是用来读取函数范围外的变量,例如:

    1
    2
    3
    4
    5
    6
    7
    var a = 100;

    function sum() {
    return a + 20;
    }

    console.log(sum()); // 120

    另外就是作为钩子,使一些局部变量的值始终保持在内存中,以供外部使用并且不暴露,例如:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    function foo() {
    var num = 100;

    sum = function () {
    num += 10;
    };

    function bar() {
    console.log(num);
    }

    return bar;
    }

    var result = foo();

    result(); // 100
    sum();
    result(); // 110

    循环中也可以使用闭包来解决很多问题,例如常见的循环添加点击事件:

    1
    2
    3
    4
    5
    6
    7
    for (var j = 0; j < btns.length; j++) {
    !function (num) {
    btns[j].onclick = function () {
    console.log('按钮' + (num + 1));
    }
    }(j);
    }

    闭包还可以创建私有变量,外部可以获取和修改,但是却无法得知内部的结构,例如:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    function Person(name) {
    var _age;

    function setAge(age) {
    _age = age;
    }

    function getAge() {
    return _age;
    }

    return {
    name: name,
    setAge: setAge,
    getAge: getAge
    }
    }

    var p1 = new Person('lily');

    p1.setAge(18);
    console.log(p1.getAge()); // 18

补充:

虽然经常在不经意间使用最常见的“闭包”,也能理解闭包的原理,但是实际开发的时候自己用到的复杂一些的闭包不是很多,希望老师讲一些实战中哪些地方常用闭包来解决问题。

题目 05

出处同上:call()apply()bind() 的用法分别是什么?


回答:

这三个方法都能改变调用函数中 this 的指向(作用域),并且可以向调用函数传递参数;三者的用法分别是:

  • call() 方法的第一个参数就是用来设置 this 指向的,第二个及以后的参数是指定的参数列表,适合参数较少时使用;

  • apply() 方法的作用与 call() 方法基本相同,只不过它只接收两个参数,第一个也是 this 的值,而第二个是一个数组,这个数组中可以保存指定的参数列表,即使只传一个参数,也要以数组的形式,故适合参数较多时使用;

  • bind() 方法的参数形式与 call() 方法相同,但使用方式与前两种不同,前两种方法是没有返回值的,而此方法返回一个绑定了固定 this 值的新函数;个人理解就是,前两种是临时工,bind() 方法是合同工……下面是三种方法的示例:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    const cat = {
    name: '大猫',
    eatFish(...args) {
    console.log(args);
    console.log(this.name + '吃鱼');
    }
    };

    const dog = {
    name: '二哈',
    eatBone(...args) {
    console.log(args);
    console.log(this.name + '吃骨头');
    }
    };

    cat.eatFish.call(dog, '汪汪', 'call'); // 二哈临时可以吃鱼;
    dog.eatBone.call(cat, '喵喵', 'call'); // 大猫临时可以吃骨头;

    cat.eatFish.apply(dog, ['汪汪', 'apply']); // 二哈临时可以吃鱼;
    dog.eatBone.apply(cat, ['喵喵', 'apply']); // 大猫临时可以吃骨头;

    let test1 = cat.eatFish.bind(dog, '汪汪', 'bind');
    let test2 = dog.eatBone.bind(cat, '喵喵', 'bind');

    test1(); // 二哈学会了吃鱼;
    test2(); // 大猫学会了吃骨头;

补充:

call()apply() 常常在继承属性的时候使用,但是 bind() 方法是新出的,个人没怎么接触过,但是都是相关联的方法,用法还是比较好理解的。

题目 06

出处同上:请说出至少 8 个 HTTP 状态码,并描述各状态码的意义。 例如:状态码 200 表示响应成功。


回答:

状态码有 5 类 60 余种,常用的一般 14 种:

  • 1XX:称为“信息性状态码”,表示接收的请求正在处理;不常用。

  • 2XX:称为“成功状态码”,表示请求正常处理完毕。

    • 200 OK,表示从客户端发来的请求在服务器端被正常处理了。

    • 204 No Content,表示服务器接收的请求已成功处理,但在返回的响应报文中不含实体的主体部分。

    • 206 Partial Content,表示客户端进行了范围请求,而服务器成功执行了这部分的 GET 请求。

  • 3XX:称为“重定向状态码”,表示需要进行附加操作以完成请求。

    • 301 Moved Permanently,永久性重定向;表示请求的资源已被分配了新的 URI,以后应使用资源现在所指的 URI。

    • 302 Found,临时性重定向;表示请求的资源已被分配了新的 URI,希望用户(本次)能使用新的 URI 访问。

    • 303 See Other,表示由于请求对应的资源存在着另一个 URI,应使用 GET 方法定向获取请求的资源。

    • 304 Not Modified,表示客户端发送附带条件的请求时,服务器端允许请求访问资源,但因为发生了请求不能满足条件的情况后,便会直接返回此状态码。

    • 307 Temporary Redirect,临时重定向;该状态码与 302 Found 含义相同,但此状态码遵循浏览器标准,不会从 POST 变成 GET。

  • 4XX:称为“客户端错误状态码”,表示服务器无法处理请求。

    • 400 Bad Request,表示请求报文中存在语法错误;但是浏览器会像 200 OK 一样对待该状态码。

    • 401 Unauthorized,表示发送的请求需要有通过 HTTP 认证的认证信息。

    • 403 Forbidden,表示对请求资源的访问被服务器拒绝了,服务器没有必要给出拒绝理由,但一般是应为访问权限问题。

    • 404 Not Found,表示服务器上无法找到请求的资源,除此之外,也可以在服务端拒绝请求且不想说明理由时使用。

  • 5XX:称为“服务器错误状态码”,表示服务器处理请求出错。

    • 500 Internal Server Error,表示服务器端在执行请求时发生了错误,也有可能是 Web 应用存在 bug 或某些临时的故障。

    • 503 Service Unavailable,表示服务器暂时处于超负载或正在进行停机维护,现在无法处理请求。

其实状态码经常和状况不一致,但是可能用户察觉不到,这种是正常情况。

状态码查询

补充:

对于状态码,死记硬背也可以,但是会很容易忘记;如果结合实际状况记忆,就会比较深刻了。明明很方便就能查到,而且经验多了自然就记住一些常用的状态码了,不是很理解为什么要考这些。

题目 07

出处同上:请写出一个 HTTP POST 请求的内容,包括四部分。 其中第四部分的内容是username=ff&password=123,第二部分必须含有 Content-Type 字段,请求的路径为 /path。


回答:

1
2
3
4
5
6
POST /path HTTP/1.1
Host: localhost
Content-Type: application/x-www-form-urlencoded
Content-Length: 24

username=ff&password=123

补充:

  • 请求最多包含四部分,最少包含三部分(第四部分可以为空)。

  • 第三部分永远都是一个回车 \n,主要是用来区分第二部分和第四部分内容的。

  • 动词有 GET(获取)、POST(新增或上传)、PUT(整体更新)、PATCH(局部更新)、DELETE(删除)、HEAD、OPTIONS等。

  • 请求中的路径包括“查询参数”,但不包括“锚点”;因为“锚点”不是由服务器识别,而是由浏览器识别的。

  • 如果没有写路径,那么路径默认为 /,代表根目录。

  • 第二部分中的“Content-Type”标注了第四部分的格式。

题目 08

请说出至少三种排序的思路,这三种排序的时间复杂度分别为:

  1. O(n * n)

  2. O(n log2 n)

  3. O(n + max)


回答:

  1. 冒泡排序:从第一个数字开始,每次比较相邻的两个数字,将符合条件的数字(较大或较小)与另一个数字交换位置,如此执行一轮,第一个数字便是符合条件的数字;第二轮则从第二个数字开始,轮次与数字位置关联;最后一轮结束,数字便按顺序排列好了。

  2. 快速排序:在数组中随机选一个数字(或默认第一个数字),数组中小于等于此数字的放在此数字前面,大于此数字的放在此数字后面,完成后,再对这个数字两边的数组进行相同动作,重复此过程直到剩下一个数字便结束。

  3. 基数排序:只能排正整数,基于桶排序;0 ~ 9 共 10 个桶,先根据所有数字的个位数进行入桶,即个位为 0 的按顺序放入 0 组,为 1 的放入 1 组,以此类推;完成个位入桶,在按照 10 个桶的从大到小顺序出桶,出桶完毕后再按所有数字的十位数进行入桶,重复此操作,直到所有数字的最大位数入桶出桶完毕,数字便按顺序排好了。

补充:

常见的第一种还有选择排序,第二种还有堆排序,第三种貌似就有基数排序。

题目 09

著名面试题:一个页面从输入 URL 到页面加载显示完成,这个过程中都发生了什么?

这一题是在挖掘你的知识边界,所以你知道多少就要答多少。


回答:

  1. 浏览器需要知道这个 URL 中包含的域名对应的服务器 IP,那么首先要对域名进行 DNS 解析:浏览器对自己的 DNS 缓存、操作系统的 DNS 缓存以及操作系统的 hosts 文件逐个进行搜索,看看有没有这域名对应的 IP 地址,有便停止搜索,没有便通知操作系统发送一个 DNS 请求到本地的 DNS 服务器,DNS 服务器将解析的 IP 地址返回给操作系统,操作系统缓存一份,然后给浏览器,浏览器也缓存一份,现在浏览器得到了域名对应的 IP 地址。

  2. 得到 IP 后,浏览器便通知主机(你的电脑)开始与服务器通过 TCP 协议建立连接,这个过程分三步,又称为“三次握手”:

    • 主机向服务器发送一个请求,表示建立连接的意愿;

    • 服务器收到请求后,向主机发一个响应信息,表示同意连接;

    • 主机收到了响应信息后,给服务器也发送一个响应信息,然后它们之间的连接建立完成。

  3. 主机与服务器连接成功后,浏览器便开始请求网页数据:

    • 浏览器根据 URL 的内容生成 HTTP 请求,请求中包含文件位置、文件类型等信息,并将请求通过主机发送给服务器;

    • 服务器收到请求,根据 HTTP 请求内容中的要求准备相应的 HTML 文件;

    • 服务器将准备好的 HTML 文件通过主机发送给浏览器;

    • 浏览器边接收边根据 DOM 树解析页面,如果页面上有其他需要的外部文件(如 CSS、JS文件),浏览器会继续向服务器发送请求并接收文件;

  4. 浏览器自上而下加载并展示页面,遇到外部文件请求得到后,加载并解析这些必要的文件,然后才会继续向下加载、渲染页面,这是一个同步(一条道走到黑)的过程,直到页面上所有文件加载、解析、渲染完毕。

  5. 页面展示完成后,主机与服务器断开连接,需要四步,称为“四次挥手”:

    • 主机得知浏览器页面加载完成后,向服务器发送断开连接的请求;

    • 服务器收到断开连接请求后,向主机发送“收到断开请求”的信息,但不会现在断开,因为可能还有数据未发送完毕;

    • 服务器向主机发送“连接可以断开”的信息;

    • 主机收到“连接可以断开”的信息后,向服务器发送“确认连接断开”信息,服务器收到确认信息后便断开连接;

  6. 至此,主机与服务器通信完成,页面也展示完毕了。

补充:

这是在个人理解范围内的粗糙回答,一些技术上的细节因为没有深度了解过,所以也不好照抄,例如 DNS 解析过程中涉及的 LDNS、 TCP 协议连接的过程、页面渲染中涉及的浏览器背后的工作原理等。

题目 10

著名面试题:如何实现数组去重?假设有数组 array = [1, 5, 2, 3, 4, 2, 3, 1, 3, 4],你要写一个函数 unique,使得 unique(array) 的值为 [1, 5, 2, 3, 4],也就是把重复的值都去掉,只保留不重复的值。

要求:不要做多重循环,只能遍历一次请给出两种方案,一种能在 ES 5 环境中运行,一种能在 ES 6 环境中运行(提示 ES6 环境多了一个 Set 对象)。


回答:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
var arr = [3, 33, 6, 774, 1235, 23, 3, 33, 321, 6745, 3, 774];

function unique(arr) {
var json = {};
var temp = [];

for (var i = 0; i < arr.length; i++) {
if (!json[arr[i]]) {
temp.push(arr[i]);
json[arr[i]] = 1;
}
}

return temp;
}

function uniqueRe(arr) {
return arr.reduce(function (pre, cur, idx) {
if(arr.indexOf(cur) === idx){
pre.push(cur)
}

return pre;
}, []);
}

console.log(unique(arr)); // [3, 33, 6, 774, 1235, 23, 321, 6745]
console.log(uniqueRe(arr)); // [3, 33, 6, 774, 1235, 23, 321, 6745]

console.log([...new Set(arr)]); // [3, 33, 6, 774, 1235, 23, 321, 6745]

补充:

第一种方法利用对象属性名不能重复的规则,循环数组,以数组项内的数据为属性名,向对象内添加属性,同时将此数据填入新数组;遇到重复数据时,由于有对象属性筛选,所以不会添加到新数组;这种方式只适合简单数组数据,复杂数据还是多重循环靠谱。

第二种是利用 indexOf() 方法只找第一个符合条件的数据下标的特性来完成筛选的。

第三种 ES6 新方法,传入 Set() 对象中的元素只会出现一次,也就是说元素是唯一的。三个点是数组展开语法,也可以将拥有 length 属性的类数组展开为数组;也可以用 Array.form() 方法。