校招前端面试题总结(一)(附答案)

113 阅读19分钟

解析 URL 参数为对象

function parseParam(url) {
    const paramsStr = /.+\?(.+)$/.exec(url)[1]; // 将 ? 后面的字符串取出来
    const paramsArr = paramsStr.split('&'); // 将字符串以 & 分割后存到数组中
    let paramsObj = {};
    // 将 params 存到对象中
    paramsArr.forEach(param => {
        if (/=/.test(param)) { // 处理有 value 的参数
            let [key, val] = param.split('='); // 分割 key 和 value
            val = decodeURIComponent(val); // 解码
            val = /^\d+$/.test(val) ? parseFloat(val) : val; // 判断是否转为数字

            if (paramsObj.hasOwnProperty(key)) { // 如果对象有 key,则添加一个值
                paramsObj[key] = [].concat(paramsObj[key], val);
            } else { // 如果对象没有这个 key,创建 key 并设置值
                paramsObj[key] = val;
            }
        } else { // 处理没有 value 的参数
            paramsObj[param] = true;
        }
    })

    return paramsObj;
}
复制代码

如何避免回流与重绘?

减少回流与重绘的措施:

  • 操作DOM时,尽量在低层级的DOM节点进行操作
  • 不要使用table布局, 一个小的改动可能会使整个table进行重新布局
  • 使用CSS的表达式
  • 不要频繁操作元素的样式,对于静态页面,可以修改类名,而不是样式。
  • 使用absolute或者fixed,使元素脱离文档流,这样他们发生变化就不会影响其他元素
  • 避免频繁操作DOM,可以创建一个文档片段documentFragment,在它上面应用所有DOM操作,最后再把它添加到文档中
  • 将元素先设置display: none,操作结束后再把它显示出来。因为在display属性为none的元素上进行的DOM操作不会引发回流和重绘。
  • 将DOM的多个读操作(或者写操作)放在一起,而不是读写操作穿插着写。这得益于浏览器的渲染队列机制

浏览器针对页面的回流与重绘,进行了自身的优化——渲染队列

浏览器会将所有的回流、重绘的操作放在一个队列中,当队列中的操作到了一定的数量或者到了一定的时间间隔,浏览器就会对队列进行批处理。这样就会让多次的回流、重绘变成一次回流重绘。

上面,将多个读操作(或者写操作)放在一起,就会等所有的读操作进入队列之后执行,这样,原本应该是触发多次回流,变成了只触发一次回流。

OSI七层模型

ISO为了更好的使网络应用更为普及,推出了OSI参考模型。

(1)应用层

OSI参考模型中最靠近用户的一层,是为计算机用户提供应用接口,也为用户直接提供各种网络服务。我们常见应用层的网络服务协议有:HTTPHTTPSFTPPOP3SMTP等。

  • 在客户端与服务器中经常会有数据的请求,这个时候就是会用到http(hyper text transfer protocol)(超文本传输协议)或者https.在后端设计数据接口时,我们常常使用到这个协议。
  • FTP是文件传输协议,在开发过程中,个人并没有涉及到,但是我想,在一些资源网站,比如百度网盘``迅雷应该是基于此协议的。
  • SMTPsimple mail transfer protocol(简单邮件传输协议)。在一个项目中,在用户邮箱验证码登录的功能时,使用到了这个协议。

(2)表示层

表示层提供各种用于应用层数据的编码和转换功能,确保一个系统的应用层发送的数据能被另一个系统的应用层识别。如果必要,该层可提供一种标准表示形式,用于将计算机内部的多种数据格式转换成通信中采用的标准表示形式。数据压缩和加密也是表示层可提供的转换功能之一。

在项目开发中,为了方便数据传输,可以使用base64对数据进行编解码。如果按功能来划分,base64应该是工作在表示层。

(3)会话层

会话层就是负责建立、管理和终止表示层实体之间的通信会话。该层的通信由不同设备中的应用程序之间的服务请求和响应组成。

(4)传输层

传输层建立了主机端到端的链接,传输层的作用是为上层协议提供端到端的可靠和透明的数据传输服务,包括处理差错控制和流量控制等问题。该层向高层屏蔽了下层数据通信的细节,使高层用户看到的只是在两个传输实体间的一条主机到主机的、可由用户控制和设定的、可靠的数据通路。我们通常说的,TCP UDP就是在这一层。端口号既是这里的“端”。

(5)网络层

本层通过IP寻址来建立两个节点之间的连接,为源端的运输层送来的分组,选择合适的路由和交换节点,正确无误地按照地址传送给目的端的运输层。就是通常说的IP层。这一层就是我们经常说的IP协议层。IP协议是Internet的基础。我们可以这样理解,网络层规定了数据包的传输路线,而传输层则规定了数据包的传输方式。

(6)数据链路层

将比特组合成字节,再将字节组合成帧,使用链路层地址 (以太网使用MAC地址)来访问介质,并进行差错检测。 网络层与数据链路层的对比,通过上面的描述,我们或许可以这样理解,网络层是规划了数据包的传输路线,而数据链路层就是传输路线。不过,在数据链路层上还增加了差错控制的功能。

(7)物理层

实际最终信号的传输是通过物理层实现的。通过物理介质传输比特流。规定了电平、速度和电缆针脚。常用设备有(各种物理设备)集线器、中继器、调制解调器、网线、双绞线、同轴电缆。这些都是物理层的传输介质。

OSI七层模型通信特点:对等通信 对等通信,为了使数据分组从源传送到目的地,源端OSI模型的每一层都必须与目的端的对等层进行通信,这种通信方式称为对等层通信。在每一层通信过程中,使用本层自己协议进行通信。

虚拟DOM转换成真实DOM

描述:将如下 JSON格式的虚拟DOM结构转换成真实DOM结构。

// vnode 结构
{
    tag: 'DIV',
    attrs: {
        id: "app"
    },
    children: [
        {
            tag: 'SPAN',
            children: [
                {
                    tag: 'A',
                    children: []
                }
            ]
        }
    ]
}
// 真实DOM 结构
<div id="app">
    <span>
        <a></a>
    </span>
</div>
复制代码

实现

function _render(vnode) {
    // 如果是数字类型转化为字符串;
    if(typeof vnode === "number") {
        vnode = String(vnode);
    }
    // 字符串类型直接就是文本节点
    if(typeof vnode === "string") {
        return document.createTextNode(vnode);
    }
    // 普通 DOM
    const dom = document.createElement(vnode.tag);
    if(vnode.attrs) {
        // 遍历属性
        Object.keys(vnode.attrs).forEach((key) => {
            dom.setAttribute(key, vnode.attrs[key]);
        });
    }
    // 子数组进行递归操作
    vnode.children.forEach((child) => dom.appendChild(_render(child)));
    return dom;
}

// 测试
let vnode = {
    tag: "DIV",
    attrs: {
        id: "app",
    },
    children: [
        {
            tag: "SPAN",
            children: [
                {
                    tag: "A",
                    children: [],
                },
            ],
        },
    ],
};
console.log(_render(vnode)); // <div id="app"><span><a></a></span></div>
复制代码

compose

题目描述:实现一个 compose 函数

// 用法如下:
function fn1(x) {
  return x + 1;
}
function fn2(x) {
  return x + 2;
}
function fn3(x) {
  return x + 3;
}
function fn4(x) {
  return x + 4;
}
const a = compose(fn1, fn2, fn3, fn4);
console.log(a(1)); // 1+4+3+2+1=11
复制代码

实现代码如下:

function compose(...fn) {
  if (!fn.length) return (v) => v;
  if (fn.length === 1) return fn[0];
  return fn.reduce(
    (pre, cur) =>
      (...args) =>
        pre(cur(...args))
  );
}
复制代码

实现一个扇形

用CSS实现扇形的思路和三角形基本一致,就是多了一个圆角的样式,实现一个90°的扇形:

div{
    border: 100px solid transparent;
    width: 0;
    heigt: 0;
    border-radius: 100px;
    border-top-color: red;
}
复制代码

浅拷贝

// 这里只考虑对象类型
function shallowClone(obj) {
    if(!isObject(obj)) return obj;
    let newObj = Array.isArray(obj) ? [] : {};
    // for...in 只会遍历对象自身的和继承的可枚举的属性(不含 Symbol 属性)
    for(let key in obj) {
        // obj.hasOwnProperty() 方法只考虑对象自身的属性
        if(obj.hasOwnProperty(key)) {
            newObj[key] = obj[key];
        }
    }
    return newObj;
}
复制代码

柯里化

题目描述:柯里化(Currying),又称部分求值(Partial Evaluation),是把接受多个参数的函数变换成接受一个单一参数(最初函数的第一个参数)的函数,并且返回接受余下的参数而且返回结果的新函数的技术。核心思想是把多参数传入的函数拆成单参数(或部分)函数,内部再返回调用下一个单参数(或部分)函数,依次处理剩余的参数。

实现代码如下:

function currying(fn, ...args) {
  const length = fn.length;
  let allArgs = [...args];
  const res = (...newArgs) => {
    allArgs = [...allArgs, ...newArgs];
    if (allArgs.length === length) {
      return fn(...allArgs);
    } else {
      return res;
    }
  };
  return res;
}

// 用法如下:
// const add = (a, b, c) => a + b + c;
// const a = currying(add, 1);
// console.log(a(2,3))
复制代码

对this对象的理解

this 是执行上下文中的一个属性,它指向最后一次调用这个方法的对象。在实际开发中,this 的指向可以通过四种调用模式来判断。

  • 第一种是函数调用模式,当一个函数不是一个对象的属性时,直接作为函数来调用时,this 指向全局对象。
  • 第二种是方法调用模式,如果一个函数作为一个对象的方法来调用时,this 指向这个对象。
  • 第三种是构造器调用模式,如果一个函数用 new 调用时,函数执行前会新创建一个对象,this 指向这个新创建的对象。
  • 第四种是 apply 、 call 和 bind 调用模式,这三个方法都可以显示的指定调用函数的 this 指向。其中 apply 方法接收两个参数:一个是 this 绑定的对象,一个是参数数组。call 方法接收的参数,第一个是 this 绑定的对象,后面的其余参数是传入函数执行的参数。也就是说,在使用 call() 方法时,传递给函数的参数必须逐个列举出来。bind 方法通过传入一个对象,返回一个 this 绑定了传入对象的新函数。这个函数的 this 指向除了使用 new 时会被改变,其他情况下都不会改变。

这四种方式,使用构造器调用模式的优先级最高,然后是 apply、call 和 bind 调用模式,然后是方法调用模式,然后是函数调用模式。

单行、多行文本溢出隐藏

  • 单行文本溢出
overflow: hidden;            // 溢出隐藏
text-overflow: ellipsis;      // 溢出用省略号显示
white-space: nowrap;         // 规定段落中的文本不进行换行
复制代码
  • 多行文本溢出
overflow: hidden;            // 溢出隐藏
text-overflow: ellipsis;     // 溢出用省略号显示
display:-webkit-box;         // 作为弹性伸缩盒子模型显示。
-webkit-box-orient:vertical; // 设置伸缩盒子的子元素排列方式:从上到下垂直排列
-webkit-line-clamp:3;        // 显示的行数
复制代码

注意:由于上面的三个属性都是 CSS3 的属性,没有浏览器可以兼容,所以要在前面加一个-webkit- 来兼容一部分浏览器。

冒泡排序--时间复杂度 n^2

题目描述:实现一个冒泡排序

实现代码如下:

function bubbleSort(arr) {
  // 缓存数组长度
  const len = arr.length;
  // 外层循环用于控制从头到尾的比较+交换到底有多少轮
  for (let i = 0; i < len; i++) {
    // 内层循环用于完成每一轮遍历过程中的重复比较+交换
    for (let j = 0; j < len - 1; j++) {
      // 若相邻元素前面的数比后面的大
      if (arr[j] > arr[j + 1]) {
        // 交换两者
        [arr[j], arr[j + 1]] = [arr[j + 1], arr[j]];
      }
    }
  }
  // 返回数组
  return arr;
}
// console.log(bubbleSort([3, 6, 2, 4, 1]));
复制代码

AJAX

实现:利用 XMLHttpRequest

// get
const getJSON = (url) => {
    return new Promise((resolve, reject) => {
        let xhr = new XMLHttpRequest();
        // open 方法用于指定 HTTP 请求的参数: method, url, async(是否异步,默认true)
        xhr.open("GET", url, false);
        xhr.setRequestHeader('Content-Type', 'application/json');
        // onreadystatechange 属性指向一个监听函数。
        // readystatechange 事件发生时(实例的readyState属性变化),就会执行这个属性。
        xhr.onreadystatechange = function(){
            // 4 表示服务器返回的数据已经完全接收,或者本次接收已经失败
            if(xhr.readyState !== 4) return;
            // 请求成功,基本上只有2xx和304的状态码,表示服务器返回是正常状态
            if(xhr.status === 200 || xhr.status === 304) {
                // responseText 属性返回从服务器接收到的字符串
                resolve(xhr.responseText);
            }
            // 请求失败
            else {
                reject(new Error(xhr.responseText));
            }
        }
        xhr.send();
    });
}

// post
const postJSON = (url, data) => {
    return new Promise((resolve, reject) => {
        let xhr = new XMLHttpRequest();
        xhr.open("POST", url);
        xhr.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded');
        xhr.onreadystatechange = function(){
            if(xhr.readyState !== 4) return;
            if(xhr.status === 200 || xhr.status === 304) {
                resolve(xhr.responseText);
            }
            else {
                reject(new Error(xhr.responseText));
            }
        }
        xhr.send(data);
    });
}
复制代码

对媒体查询的理解?

媒体查询由⼀个可选的媒体类型和零个或多个使⽤媒体功能的限制了样式表范围的表达式组成,例如宽度、⾼度和颜⾊。媒体查询,添加⾃CSS3,允许内容的呈现针对⼀个特定范围的输出设备⽽进⾏裁剪,⽽不必改变内容本身,适合web⽹⻚应对不同型号的设备⽽做出对应的响应适配。

媒体查询包含⼀个可选的媒体类型和满⾜CSS3规范的条件下,包含零个或多个表达式,这些表达式描述了媒体特征,最终会被解析为true或false。如果媒体查询中指定的媒体类型匹配展示⽂档所使⽤的设备类型,并且所有的表达式的值都是true,那么该媒体查询的结果为true。那么媒体查询内的样式将会⽣效。

<!-- link元素中的CSS媒体查询 --> 
<link rel="stylesheet" media="(max-width: 800px)" href="example.css" /> 
<!-- 样式表中的CSS媒体查询 --> 
<style> 
@media (max-width: 600px) {   .facet_sidebar {     display: none;   } }
</style>
复制代码

简单来说,使用 @media 查询,可以针对不同的媒体类型定义不同的样式。@media 可以针对不同的屏幕尺寸设置不同的样式,特别是需要设置设计响应式的页面,@media 是非常有用的。当重置浏览器大小的过程中,页面也会根据浏览器的宽度和高度重新渲染页面。

setInterval 模拟 setTimeout

描述:使用setInterval模拟实现setTimeout的功能。

思路setTimeout的特性是在指定的时间内只执行一次,我们只要在setInterval内部执行 callback 之后,把定时器关掉即可。

实现

const mySetTimeout = (fn, time) => {
    let timer = null;
    timer = setInterval(() => {
        // 关闭定时器,保证只执行一次fn,也就达到了setTimeout的效果了
        clearInterval(timer);
        fn();
    }, time);
    // 返回用于关闭定时器的方法
    return () => clearInterval(timer);
}

// 测试
const cancel = mySetTimeout(() => {
    console.log(1);
}, 1000);  
// 一秒后打印 1
复制代码

首屏和白屏时间如何计算

首屏时间的计算,可以由 Native WebView 提供的类似 onload 的方法实现,在 ios 下对应的是 webViewDidFinishLoad,在 android 下对应的是onPageFinished事件。

白屏的定义有多种。可以认为“没有任何内容”是白屏,可以认为“网络或服务异常”是白屏,可以认为“数据加载中”是白屏,可以认为“图片加载不出来”是白屏。场景不同,白屏的计算方式就不相同。

方法1:当页面的元素数小于x时,则认为页面白屏。比如“没有任何内容”,可以获取页面的DOM节点数,判断DOM节点数少于某个阈值X,则认为白屏。 方法2:当页面出现业务定义的错误码时,则认为是白屏。比如“网络或服务异常”。 方法3:当页面出现业务定义的特征值时,则认为是白屏。比如“数据加载中”。

CSS 优化和提高性能的方法有哪些?

加载性能:

(1)css压缩:将写好的css进行打包压缩,可以减小文件体积。

(2)css单一样式:当需要下边距和左边距的时候,很多时候会选择使用 margin:top 0 bottom 0;但margin-bottom:bottom;margin-left:left;执行效率会更高。

(3)减少使用@import,建议使用link,因为后者在页面加载时一起加载,前者是等待页面加载完成之后再进行加载。

选择器性能:

(1)关键选择器(key selector)。选择器的最后面的部分为关键选择器(即用来匹配目标元素的部分)。CSS选择符是从右到左进行匹配的。当使用后代选择器的时候,浏览器会遍历所有子元素来确定是否是指定的元素等等;

(2)如果规则拥有ID选择器作为其关键选择器,则不要为规则增加标签。过滤掉无关的规则(这样样式系统就不会浪费时间去匹配它们了)。

(3)避免使用通配规则,如*{}计算次数惊人,只对需要用到的元素进行选择。

(4)尽量少的去对标签进行选择,而是用class。

(5)尽量少的去使用后代选择器,降低选择器的权重值。后代选择器的开销是最高的,尽量将选择器的深度降到最低,最高不要超过三层,更多的使用类来关联每一个标签元素。

(6)了解哪些属性是可以通过继承而来的,然后避免对这些属性重复指定规则。

渲染性能:

(1)慎重使用高性能属性:浮动、定位。

(2)尽量减少页面重排、重绘。

(3)去除空规则:{}。空规则的产生原因一般来说是为了预留样式。去除这些空规则无疑能减少css文档体积。

(4)属性值为0时,不加单位。

(5)属性值为浮动小数0.**,可以省略小数点之前的0。

(6)标准化各种浏览器前缀:带浏览器前缀的在前。标准属性在后。

(7)不使用@import前缀,它会影响css的加载速度。

(8)选择器优化嵌套,尽量避免层级过深。

(9)css雪碧图,同一页面相近部分的小图标,方便使用,减少页面的请求次数,但是同时图片本身会变大,使用时,优劣考虑清楚,再使用。

(10)正确使用display的属性,由于display的作用,某些样式组合会无效,徒增样式体积的同时也影响解析性能。

(11)不滥用web字体。对于中文网站来说WebFonts可能很陌生,国外却很流行。web fonts通常体积庞大,而且一些浏览器在下载web fonts时会阻塞页面渲染损伤性能。

可维护性、健壮性:

(1)将具有相同属性的样式抽离出来,整合并通过class在页面中进行使用,提高css的可维护性。

(2)样式与内容分离:将css代码定义到外部css中。

代码输出问题

function Parent() {
    this.a = 1;
    this.b = [1, 2, this.a];
    this.c = { demo: 5 };
    this.show = function () {
        console.log(this.a , this.b , this.c.demo );
    }
}

function Child() {
    this.a = 2;
    this.change = function () {
        this.b.push(this.a);
        this.a = this.b.length;
        this.c.demo = this.a++;
    }
}

Child.prototype = new Parent();
var parent = new Parent();
var child1 = new Child();
var child2 = new Child();
child1.a = 11;
child2.a = 12;
parent.show();
child1.show();
child2.show();
child1.change();
child2.change();
parent.show();
child1.show();
child2.show();
复制代码

输出结果:

parent.show(); // 1  [1,2,1] 5

child1.show(); // 11 [1,2,1] 5
child2.show(); // 12 [1,2,1] 5

parent.show(); // 1 [1,2,1] 5

child1.show(); // 5 [1,2,1,11,12] 5

child2.show(); // 6 [1,2,1,11,12] 5
复制代码

这道题目值得神帝,他涉及到的知识点很多,例如this的指向、原型、原型链、类的继承、数据类型等。

解析:

  1. parent.show(),可以直接获得所需的值,没啥好说的;
  2. child1.show(),Child的构造函数原本是指向Child的,题目显式将Child类的原型对象指向了Parent类的一个实例,需要注意Child.prototype指向的是Parent的实例parent,而不是指向Parent这个类。
  3. child2.show(),这个也没啥好说的;
  4. parent.show(),parent是一个Parent类的实例,Child.prorotype指向的是Parent类的另一个实例,两者在堆内存中互不影响,所以上述操作不影响parent实例,所以输出结果不变;
  5. child1.show(),child1执行了change()方法后,发生了怎样的变化呢?
  • this.b.push(this.a),由于this的动态指向特性,this.b会指向Child.prototype上的b数组,this.a会指向child1a属性,所以Child.prototype.b变成了**[1,2,1,11]**;
  • this.a = this.b.length,这条语句中this.athis.b的指向与上一句一致,故结果为child1.a变为4;
  • this.c.demo = this.a++,由于child1自身属性并没有c这个属性,所以此处的this.c会指向Child.prototype.cthis.a值为4,为原始类型,故赋值操作时会直接赋值,Child.prototype.c.demo的结果为4,而this.a随后自增为5(4 + 1 = 5)。
  1. child2执行了change()方法, 而child2child1均是Child类的实例,所以他们的原型链指向同一个原型对象Child.prototype,也就是同一个parent实例,所以child2.change()中所有影响到原型对象的语句都会影响child1的最终输出结果。
  • this.b.push(this.a),由于this的动态指向特性,this.b会指向Child.prototype上的b数组,this.a会指向child2a属性,所以Child.prototype.b变成了**[1,2,1,11,12]**;
  • this.a = this.b.length,这条语句中this.athis.b的指向与上一句一致,故结果为child2.a变为5;
  • this.c.demo = this.a++,由于child2自身属性并没有c这个属性,所以此处的this.c会指向Child.prototype.c,故执行结果为Child.prototype.c.demo的值变为child2.a的值5,而child2.a最终自增为6(5 + 1 = 6)。

深/浅拷贝

首先判断数据类型是否为对象,如果是对象(数组|对象),则递归(深/浅拷贝),否则直接拷贝。

function isObject(obj) {
    return typeof obj === "object" && obj !== null;
}
复制代码

这个函数只能判断 obj 是否是对象,无法判断其具体是数组还是对象。

实现一个三角形

CSS绘制三角形主要用到的是border属性,也就是边框。

平时在给盒子设置边框时,往往都设置很窄,就可能误以为边框是由矩形组成的。实际上,border属性是右三角形组成的,下面看一个例子:

div {
    width: 0;
    height: 0;
    border: 100px solid;
    border-color: orange blue red green;
}
复制代码

将元素的长宽都设置为0

(1)三角1

div {    width: 0;    height: 0;    border-top: 50px solid red;    border-right: 50px solid transparent;    border-left: 50px solid transparent;}
复制代码

(2)三角2

div {
    width: 0;
    height: 0;
    border-bottom: 50px solid red;
    border-right: 50px solid transparent;
    border-left: 50px solid transparent;
}
复制代码

(3)三角3

div {
    width: 0;
    height: 0;
    border-left: 50px solid red;
    border-top: 50px solid transparent;
    border-bottom: 50px solid transparent;
}
复制代码

(4)三角4

div {
    width: 0;
    height: 0;
    border-right: 50px solid red;
    border-top: 50px solid transparent;
    border-bottom: 50px solid transparent;
}
复制代码

(5)三角5

div {
    width: 0;
    height: 0;
    border-top: 100px solid red;
    border-right: 100px solid transparent;
}
复制代码

还有很多,就不一一实现了,总体的原则就是通过上下左右边框来控制三角形的方向,用边框的宽度比来控制三角形的角度。