【面试】社招中级前端笔试面试题总结-答案及拓展

18,367 阅读13分钟

最近看到有一篇文章总结了一些前端的面试题,面向的对象应该是社招中初、中级的前端,感觉有一定的参考价值,因此开一个帖子尝试解答这些问题,顺便当做自己的面试题积累。

原文链接戳这里

JavaScript基础

1、声明提前类问题

在网上找到一篇文章,里面有一道面试题,考察了包括变量定义提升、this指针指向、运算符优先级、原型、继承、全局变量污染、对象属性及原型属性优先级等许多知识点,而就其中声明提前相关的知识,我觉得也十分有参考价值:

function Foo() {
    getName = function () { alert (1); };
    return this;
}
Foo.getName = function () { alert (2);};
Foo.prototype.getName = function () { alert (3);};
var getName = function () { alert (4);};
function getName() { alert (5);}

// 请写出以下输出结果:
Foo.getName();
getName(); // 声明提前
Foo().getName();
getName();
new Foo.getName();
new Foo().getName();
new new Foo().getName();

这道题的答案是:2、4、1、1、2、3、3。

这里考察声明提前的题目在代码中已经标出,这里声明getName方法的两个语句:

var getName = function () { alert (4) };
function getName() { alert (5) }

实际上在解析的时候是这样的顺序:

function getName() { alert (5) }
var getName;
getName = function () { alert (4) };

如果我们在代码中间再加两个断点:

getName(); // 5
var getName = function () { alert (4) };
getName(); // 4
function getName() { alert (5) }

在第一次getName时,function的声明和var的声明都被提前到了第一次getName的前面,而getName的赋值操作并不会提前,单纯使用var的声明也不会覆盖function所定义的变量,因此第一次getName输出的是function声明的5; 而第二次getName则是发生在赋值语句的后面,因此输出的结果是4,所以实际代码的执行顺序是这样:

function getName() { alert (5) }
var getName;
getName(); // 5
getName = function () { alert (4) };
getName(); // 4

2、浏览器存储

localStorage,sessionStorage和cookie的区别

共同点:都是保存在浏览器端、仅同源可用的存储方式

  1. 数据存储方面
  • cookie数据始终在同源的http请求中携带(即使不需要),即cookie在浏览器和服务器间来回传递。cookie数据还有路径(path)的概念,可以限制cookie只属于某个路径下
  • sessionStorage和localStorage不会自动把数据发送给服务器,仅在本地保存。
  1. 存储数据大小
  • 存储大小限制也不同,cookie数据不能超过4K,同时因为每次http请求都会携带cookie、所以cookie只适合保存很小的数据,如会话标识。
  • sessionStorage和localStorage虽然也有存储大小的限制,但比cookie大得多,可以达到5M或更大
  1. 数据存储有效期
  • sessionStorage:仅在当前浏览器窗口关闭之前有效;
  • localStorage:始终有效,窗口或浏览器关闭也一直保存,本地存储,因此用作持久数据;
  • cookie:只在设置的cookie过期时间之前有效,即使窗口关闭或浏览器关闭
  1. 作用域不同
  • sessionStorage不在不同的浏览器窗口中共享,即使是同一个页面;
  • localstorage在所有同源窗口中都是共享的;也就是说只要浏览器不关闭,数据仍然存在
  • cookie: 也是在所有同源窗口中都是共享的.也就是说只要浏览器不关闭,数据仍然存在

3、跨域

不久我写了一个帖子,对同源策略及各种跨域的方式进行了总结:什么是跨域,为什么浏览器会禁止跨域,及其引起的发散性学习

4、Promise的使用及原理

Promise是ES6加入的新特性,用于更合理的解决异步编程问题,关于用法阮一峰老师在ECMAScript 6 入门中作出了详细的说明,在此就不重复了。

30分钟,让你彻底明白Promise原理

上面这篇文章则是对Promise的原理进行的详细的说明,在这里,我提取最简单的Promise实现方式来对Promise的原理进行说明:

function Promise(fn) {
    var value = null,
        callbacks = [];  // callbacks为数组,因为可能同时有很多个回调

    this.then = function (onFulfilled) {
        callbacks.push(onFulfilled);
    };

    function resolve(value) {
        callbacks.forEach(function (callback) {
            callback(value);
        });
    }

    fn(resolve);
}

首先,then里面声明的单个或多个函数,将被推入callbacks列表,在Promise实例调用resolve方法时遍历调用,并传入resolve方法中传入的参数值。

以下,使用一个简单的例子来对Promise的执行流程进行分析:

functionm func () {
	return new Promise(function (resolve) {
		setTimeout(function () {
			resolve('complete')
		}, 3000);
	})
}

func().then(function (res) {
	console.log(res); // complete
})

func函数的定义是返回了一个Promise实例,声明实例时传入的回调函数加入了一个resolve参数(这个resolve参数在Promise中的fn(resolve)定义中获取resolve的函数实体),回调中执行了一个异步操作,在异步操作完成的回调中执行了resolve函数。

再看执行步骤,func函数返回了一个Promise实例,实例则可以执行Promise构造函数中定义的then方法,then方法中传入的回调则会在resolve(即异步操作完成后)执行,由此实现了通过then方法执行异步操作完成后回调的功能。

5、JavaScript事件循环机制

原文中贴出的文章具有很大参考价值,先贴个链接:详解JavaScript中的Event Loop(事件循环)机制

JavaScript是一种单线程、非阻塞的语言,这是由于它当初的设计就是用于和浏览器交互的:

  • 单线程:JavaScript设计为单线程的原因是,最开始它最大的作用就是和DOM进行交互,试想一下,如果JavaScript是多线程的,那么当两个线程同时对DOM进行一项操作,例如一个向其添加事件,而另一个删除了这个DOM,此时该如何处理呢?因此,为了保证不会 发生类似于这个例子中的情景,JavaScript选择只用一个主线程来执行代码,这样就保证了程序执行的一致性。
  • 非阻塞:当代码需要进行一项异步任务(无法立刻返回结果,需要花一定时间才能返回的任务,如I/O事件)的时候,主线程会挂起(pending)这个任务,然后在异步任务返回结果的时候再根据一定规则去执行相应的回调。而JavaScript实现异步操作的方法就是使用Event Loop。
setTimeout(function () {
    console.log(1);
});

new Promise(function(resolve,reject){
    console.log(2)
    resolve(3)
}).then(function(val){
    console.log(val);
})

下面通过一段代码来分析这个问题,首先setTimeoutPromise中的then回调都是异步方法,而new Promise则是一个同步操作,所以这段代码应该首先会立即输出2JavaScript将异步方法分为了marco task(宏任务:包括setTimeoutsetInterval等)和micro task(微任务:包括new Promise等),在JavaScript的执行栈中,如果同时存在到期的宏任务和微任务,则会将微任务先全部执行,再执行第一个宏任务,因此,两个异步操作中then的回调会率先执行,然后才执行setTimeout的回调,因此会依次输出3、1,所以最终输出的结果就是2、3、1。

6、ES6作用域及let和var的区别

这个问题阮一峰老师在ECMAScript 6 入门中的let 和 const 命令章节对这个问题作出了详细的说明,下面提取一些我认为关键的点进行讲解。

ES6引入了使用{}包裹的代码区域作为块级作用域的声明方式,其效果与ES5中function声明的函数所生成的函数作用域具有相同的效果,作用域外部不能访问作用域内部声明的函数或变量,这样的声明在ES6中对于包括for () {}if () {}等大括号包裹的代码块中都会生效,生成一个单独的作用域。

ES6新增的let声明变量的方式相比var具有以下几个重要特点:

  • let声明的变量只在作用域内有效,如下方代码,if声明生成了一个块级作用域,在这个作用域内声明的变量在作用域外部无法访问,假如访问会产生错误:
if (true) {
	let me = 'handsome boy';
}
console.log(me); // ReferenceError
  • let声明的变量与var不同,不会产生变量提升,如下方代码,在声明之前输出代码,会产生错误:
// var 的情况
console.log(foo); // 输出undefined
var foo = 2;
// let 的情况
console.log(bar); // ReferenceError
let bar = 2;
  • let的声明方式不允许重复声明,如重复声明会报错,而var声明变量时,后声明的语句会对先声明的语句进行覆盖:
// 报错
function func() {
  let a = 10;
  var a = 1;
}
// 报错
function func() {
  let a = 10;
  let a = 1;
}
  • 只要块级作用域内存在let命令,它所声明的变量就“绑定”(binding)这个区域,不再受外部的影响,这个特性称为暂时性死区
var tmp = 123;
if (true) {
  tmp = 'abc'; // ReferenceError
  let tmp;
}

7、闭包

待补充

8、原型及原型链

待补充

9、浏览器的回流与重绘 (Reflow & Repaint)

参考:juejin.cn/post/684490…

浏览器在接收到 htmlcss 后,渲染的步骤是:html 经过渲染生成 DOM 树, css 经过渲染生成 css 渲染树,两者再经过结合,生成 render tree,浏览器就可以根据 render tree 进行画面绘制。

如果浏览器从服务器接收到了新的 css ,需要更新页面时,需要经过什么操作呢?这就是回流 reflow 与重绘 repaint。由于浏览器在重新渲染页面时会先进行 reflow 再进行 repaint,因此,回流必将引起重绘,而重绘不一定会引起回流。

重绘:当前元素的样式(背景颜色、字体颜色等)发生改变的时候,我们只需要把改变的元素重新的渲染一下即可,重绘对浏览器的性能影响较小。发生重绘的情形:改变容器的外观风格等,比如 background:black 等。改变外观,不改变布局,不影响其他的 DOM。    回流:是指浏览器为了重新渲染部分或者全部的文档而重新计算文档中元素的位置和几何构造的过程。

因为回流可能导致整个 DOM 树的重新构造,所以是性能的一大杀手,一个元素的回流导致了其所有子元素以及 DOM 中紧随其后的祖先元素的随后的回流。下面贴出会触发浏览器 reflow 的变化:

  • 页面首次渲染
  • 浏览器窗口大小发生改变
  • 元素尺寸或位置发生改变
  • 元素内容变化(文字数量或图片大小等等)
  • 元素字体大小变化
  • 添加或者删除可见的DOM元素
  • 激活CSS伪类(例如::hover)
  • 查询某些属性或调用某些方法

优化方案:

CSS

  • 避免使用table布局。
  • 尽可能在DOM树的最末端改变class
  • 避免设置多层内联样式。
  • 将动画效果应用到position属性为absolutefixed的元素上。
  • 避免使用CSS表达式(例如:calc())。

JavaScript

  • 避免频繁操作样式,最好一次性重写style属性,或者将样式列表定义为class并一次性更改class属性。
  • 避免频繁操作DOM,创建一个documentFragment,在它上面应用所有DOM操作,最后再把它添加到文档中。
  • 也可以先为元素设置display: none,操作结束后再把它显示出来。因为在display属性为none的元素上进行的DOM操作不会引发回流和重绘。
  • 避免频繁读取会引发回流/重绘的属性,如果确实需要多次使用,就用一个变量缓存起来。
  • 对具有复杂动画的元素使用绝对定位,使它脱离文档流,否则会引起父元素及后续元素频繁回流。

10、JS对象的深复制

一般的思路就是递归解决,对不同的数据类型做不同的处理:

function deepCopy (obj) {
  let result = {}
  for (let key in obj) {
    if (obj[key] instanceof Object || obj[key] instanceof Array) {
      result[key] = deepCopy(obj[key])
    } else {
      result[key] = obj[key]
    }
  }
  return result
}

这个只能复制内部有数组、对象或其他基础数据类型的对象,假如有一些像RegExpDate这样的复杂对象复制的结果就是一个{},无法正确进行复制,因为没有对这些特殊对象进行单独的处理。若要参考对复杂对象进行复制,可以参考lodash中数组深复制方法_.cloneDeep()的实现方案,下面这篇文章对数组深复制的方法进行了详细的解析,有一定参考价值: jerryzou.com/posts/dive-…

另外如果要复制的对象数据结构较为简单,没有复杂对象的数据,那么可以用最简便的方法:

let cloneResult = JSON.parse(JSON.stringify(targetObj))

11、JS运算精度丢失

此前转载了一篇文章,对JavaScript运算精度丢失的原因及解决方案都有比较详细的说明: blog.csdn.net/qq_35271556…

浏览器相关

1、浏览器从加载到渲染的过程,比如输入一个网址到显示页面的过程

加载过程:

  • 浏览器根据 DNS 服务器解析得到域名的 IP 地址
  • 向这个 IP 的机器发送 HTTP 请求
  • 服务器收到、处理并返回 HTTP 请求
  • 浏览器得到返回内容

渲染过程:

  • 根据 HTML 结构生成 DOM 树
  • 根据 CSS 生成 CSSOM
  • 将 DOM 和 CSSOM 整合形成 RenderTree
  • 根据 RenderTree 开始渲染和展示
  • 遇到<script>时,会执行并阻塞渲染

2、浏览器缓存机制

参考文章:segmentfault.com/a/119000001…

3、性能优化

参考文章:blog.csdn.net/na_sama/art…

Vue

1、组件间通信方式

Vue的官方文档对组件间的通信方式做了详细的说明:cn.vuejs.org

父组件向子组件传输

  • 最常用的方式是在子组件标签上传入数据,在子组件内部用props接收:
// 父组件
<template>
  <children name="boy"></children>
</template>
<script>
// 子组件:children
export default {
  props: {
    name: String
  }
}
</script>
  • 还可以在子组件中用 this.$parent 访问父组件的实例,不过官方文档有这样一段文字,很好的说明了 $parent 的意义:节制地使用 $parent$children —— 它们的主要目的是作为访问组件的应急方法。更推荐用 propsevents 实现父子组件通信。

子组件向父组件传输

  • 一般在子组件中使用 this.$emit('eventName', 'data'),然后在父组件中的子组件标签上监听 eventName 事件,并在参数中获取传过来的值。
// 子组件
export default {
  mounted () {
    this.$emit('mounted', 'Children is mounted.')
  }
}
<template>
  <children @mounted="mountedHandle"></children>
</template>
<script>
// 父组件
export default {
  methods: {
    mountedHandle (data) {
      console.log(data) // Children is mounted.
    }
  }
}
</script>
  • $parent 一样,在父组件中可以通过访问 this.$children 来访问组件的所有子组件实例。

非父子组件之间的数据传递

  • 对于非父子组件间,且具有复杂组件层级关系的情况,可以通过 Vuex 进行组件间数据传递: vuex.vuejs.org/zh/

  • Vue 1.0 中常用的 event bus 方式进行的全局数据传递,在 Vue 2.0 中已经被移除,官方文档中有说明:$dispatch$broadcast 已经被弃用。请使用更多简明清晰的组件间通信和更好的状态管理方案,如:Vuex

2、双向绑定原理

blog.seosiwei.com/detail/35 blog.seosiwei.com/detail/36 blog.seosiwei.com/detail/37

3、路由导航钩子

router.vuejs.org/zh-cn/advan…

作者信息

作者其他文章