前端面试总结

前端基础

HTML 基础

DOCTYPE 的作用是什么?

html5 标准网页声明,告知浏览器的解析器用什么文档标准解析文档,不同的渲染模式会影响到 css 甚至 js 的脚本解析。必须声明在第一行。
文档解析类型:

  • 标准模式:W3C 标准
  • 怪异模式: 浏览器自己标准

CSS 基础

CSS 选择器的优先级是怎么样的?

内联 > id 选择器 > 类选择器 > 标签选择器, 权重从左到右依次减小。

  • link 是 xhtml 元素,@import 是 css 提供的(需要 IE5 以上)
  • 页面加载时,link 会同时被加载;@import 引用的 css 等到页面加载完再加载
  • link 的样式权重高于@import

如何理解 z-index?

css 中的 z-index 属性控制重叠元素的垂直叠加顺序,默认元素的 z-index 为 0,修改 z-index 来控制元素的图层位置,而且 z-index 仅在定位元素(position 不等于 static)中生效。

如何理解层叠上下文?

是什么?

层叠上下文是 HTML 元素的三维概念,这些 HTML 元素在一条假想的相对于面向用户的 z 轴上叠放排列,HTML 元素依据其自身优先级顺序占用层叠上下文的空间。

作用是什么?

该元素的所有后代元素相对于该祖先元素都有其自己的叠放顺序。

如何产生?

触发一下条件则会产生层叠上下文:

  • 根元素(HTML)
  • z-index 值不为 “auto”的
  • opacity 属性值小于 1 的元素(参考 the specification for opacity),
  • transform 属性值不为 “none”的元素
  • mix-blend-mode 属性值不为 “normal”的元素,
  • filter 值不为“none”的元素,
  • perspective 值不为“none”的元素,
  • isolation 属性被设置为 “isolate”的元素,
  • position: fixed
  • 在 will-change 中指定了任意 CSS 属性,即便你没有直接指定这些属性的值
  • webkit-overflow-scrolling 属性被设置 “touch”的元素

你对盒模型的理解?

是什么?

当一个文档进行布局的时候,浏览器的渲染引擎会根据标准盒模型将所有元素渲染为一个矩形盒子。CSS 决定盒子的大小、位置、颜色、背景、边框····
盒模型由 content、padding、border、margin 组成。

box-sizeing: content-box border-box

谈谈对 BFC 的理解?

是什么?

BFC 是块级格式上下文,页面中一个独立的渲染区域,让处于内部的元素与外部的元素互相隔离。

如何形成?

  • 根元素
  • position 不为 static
  • float 不为 none
  • overflow 不为 visible
  • display: inline-block table-cell table-caption

作用是什么?

  • 防止 margin 重叠
  • 消除浮动的副作用
    • 防止文字环绕
    • 防止高度坍塌

伪类和伪元素的区别是什么?

是什么?

伪类(pseudo-class) 是一个以冒号(:)作为前缀,被添加到一个选择器末尾的关键字,当你希望样式在特定状态下才被呈现到指定的元素时,你可以往元素的选择器后面加上对应的伪类。

伪元素用于创建一些不在文档树中的元素,并为其添加样式。比如说,我们可以通过::before 来在一个元素前增加一些文本,并为这些文本添加样式。虽然用户可以看到这些文本,但是这些文本实际上不在文档树中。

区别

伪类是通过在元素选择器上加入伪类改变元素状态,而伪元素通过对元素的操作进行对元素的改变。

js 基础

js 的作用域链理解吗?

js 是词法作用域(静态作用域),声明的作用域在编译阶段就已经确定。
js 在执行时会创建执行上下文,执行上下文中的词法环境会包含外层词法环境的引用,通过引用可以获取外层词法环境的变量,这些引用层层串联最终指向全局环境,因此形成作用域连。s

ES6 模块与 CommonJS 模块的差异

两个重大差异:

  • CommonJS 模块输出的是一个值的拷贝,ES6 模块输出的是值的引用
  • CommonJS 模块是运行时加载,ES6 模块是编译时输出接口

为什么会有 BigInt 的提案?

Number.MAX_SAFE_INTEGER,表示最大安全数字

0.1 + 0.2 为什么不等于 0.3?

JS 的 Number 类型是双精度浮点数,遵循 IEEE 754 标准。

以 0.1 转换 IEEE 754 标准为例,会有 3 个阶段:

  • 0.1 转换为二进制表示
  • 二进制用科学计数法表示
  • 科学计数法表示的二进制转换为 IEEE 754 标准表示
    问题出在第三步,以 IEEE 754 标准转换后的 0.1 换成十进制,变成 0.100000000000000005551115123126

谈谈你对原型链的理解?

先谈原型对象

绝大部分函数(少数内建函数除外)都会有一个 prototype 属性,这个属性指向函数的原型对象,当调用构造函数创建实例时,该实例的内部将包含一个内部__proto__属性,指向构造函数的原型对象。所有被创建的实例都会共享原型对象,这些实例可以访问原型对象的属性。

原型链

原因是每个对象都有__proto__属性,此属性指向该对象的构造函数的原型。
对象可以通过__proto__与上游的构造函数的原型对象连接起来,而上游的原型对象也有一个__proto__,这样形成了原型链。

谈谈你对原型链的理解?

this 的指向不是在编写时确定的,而是在执行时确定的,同时,this 的指向遵循一定的规则。

  • 默认规则,指向全局对象
  • 隐式调用,函数被调用的位置存在上下文对象时,指向这个上下文对象
  • 显示调用(apply, call, bind),指向指定的对象
  • new 调用,优先级最高,用 new 调用一个构造函数,会创建一个新对象,this 会自动绑定到这个新对象

补充
箭头函数与传统函数的差异:

  • 没有thissupernew.target绑定 它们的值由外围最近一层非箭头函数决定。
  • 不能通过new调用 箭头函数没有[[Construct]]方法,所以不能被用作构造函数
  • 没有原型
  • 不支持arguments对象
  • 不支持重复的命名参数

async/await 是什么?

async 函数,就是 Generator 函数的语法糖,它建立在 Promise 上,并且与所有现有的 Promise 的 API 兼容。

  1. Async——声明一个异步函数
  • 自动将常规函数转换为 Promise,返回值也是一个 Promise 对象
  • 只有 asyn 函数内部的异步操作执行完,才会执行 then 方法指定的回调函数
  • 异步函数内部可以使用 await
  1. Await——暂停异步的功能执行
  • 放置在 Promise 调用之前,await 强制代码等待,直到 Promise 完成并返回结果
  • 只能与 Promise 一起使用,不适用回调
  • 只能在 async 函数内调用

浏览器与新技术

浏览器是如何渲染 UI 的?

  1. 浏览器获取 HTML 文件,然后对文件进行解析,形成 DOMTree
  2. 与此同时,进行 CSS 解析,形成 Style Rules
  3. 接着将 DOMTree 和 Style Rules 合成为 Render Tree
  4. 然后进入布局(layout)阶段,也就是为每个节点分配一个应出现在屏幕上的确切坐标
  5. 随后调用 GPU 进行绘制(Paint),遍历 Render Tree 的节点,并将元素呈现出来

DOM Tree 是如何建立的?

  1. 转码:浏览器将接收到的二进制数据按照指定编码格式转化为 HTML 字符串
  2. 生成 TOKEN:浏览器会将 HTML 字符串解析成 Tokens
  3. 构建 Nodes:对 Node 添加特定的属性,通过指针确定 Node 的父、子、兄弟关系和所属 treeScope
  4. 生成 Dom Tree: 通过 node 包含的指针确定的关系构建出 DOM Tree

前端基础笔试

javascript 笔试部分

实现防抖函数(debounce)

防抖函数原理:在事件被触发 n 秒后再执行回调,如果在这 n 秒内又被触发,则重新计时

1
2
3
4
5
6
7
8
9
10
//计时器版本
const debounce = (fn, ms = 0) => {
let timer = null;
return (...args) => {
clearTimer(timer);
timer = setTimeout(() => {
fn.apply(this, args)
}, ms)
}
}

实现节流函数(throttle)

节流函数原理:规定在一个单位时间内,只能触发一次函数。如果这个单位时间内触发多次函数,只有一次生效。

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
31
32
33
34
35
36
37
//简单版本
const throttle = (fn, delay) => {
let timer = null;

return (...args) => {
if(!timer) {
timer = setTimeout(() => {
clearTimeout(timer)
timer = null
}, delay)
fn.apply(this, args);
}
}
}


//升级版
const throttle = (fn, wait) => {
let inThrottle, lastFn, lastTime;
return function() {
const context = this,
args = arguments;
if (!inThrottle) {
fn.apply(context, args);
lastTime = Date.now();
inThrottle = true;
} else {
clearTimeout(lastFn);
lastFn = setTimeout(function() {
if (Date.now() - lastTime >= wait) {
fn.apply(context, args);
lastTime = Date.now();
}
}, Math.max(wait - (Date.now() - lastTime), 0));
}
};
};

实现深拷贝

一行代码的深拷贝

1
const copyJSON = obj => JSON.parse(JSON.stringify(obj));

存在的问题:

  • 不能拷贝正则、函数等特殊对象
  • 循环引用的问题
  • 会抛弃对象的 constructor,所有的构造函数会指向 Object

面试版

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
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
// 对象类型判断函数
const isType = (obj, type) => {
if (typeof obj === "object") return false;
const typeString = Object.prototype.toString.call(obj);
switch (type) {
case "Array":
flag = typeString === "[object Array]";
break;
case "Date":
flag = typeString === "[object Date]";
break;
case "RegExp":
flag = typeString === "[object RegExp]";
break;
default:
flag = false;
}
return flag;
};

// 提取正则flags的函数
const getRegExp = re => {
let flags = "";
if (re.global) flags += "g";
if (re.ignoreCase) flags += "i";
if (re.multiline) flags += "m";
return flags;
};

/**
* deep clone
* @param {[type]} parent object 需要进行克隆的对象
* @return {[type]} 深克隆后的对象
*/

const clone = parent => {
// 维护两个循环引用的数组
const parents = [];
const children = [];

const _clone = parent => {
if (parent === null) return null;
if (typeof parent !== "object") return parent;

let child, proto;

if (isType(parent, "Array")) {
// 对数组做特殊处理
child = [];
} else if (isType(parent, "RegExp")) {
// 对正则对象做特殊处理
child = new RegExp(parent.source, getRegExp(parent));
if (parent.lastIndex) child.lastIndex = parent.lastIndex;
} else if (isType(parent, "Date")) {
// 对Date对象做特殊处理
child = new Date(parent.getTime());
} else {
// 处理对象原型
proto = Object.getPrototypeOf(parent);
child = Object.create(proto);
}

// 处理循环引用
const index = parents.indexOf(parent);

if (index != -1) {
// 如果父数组存在本对象,说明之前已经被引用过,直接返回此对象
return children[index];
}

parents.push(parent);
children.push(child);

for (let i in parent) {
// 递归
child[i] = _clone(parent[i]);
}

return child;
};

return _clone(parent);
};

实现一个 Event Bus

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
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
class EventEmeitter {
constructor() {
this._events = this._events || new Map(); // 存储事件/回调键值对
this._maxListeners = this._maxListeners || 10; // 设立监听上限
}
}

EventEmeitter.prototype.emit = function(type, ...args) {
let handler;
// 从存储事件键值对对this._events中获取对应事件回调函数
handler = this._events.get(type);
if (Array.isArray(handler)) {
// 如果是一个数组说明有多个监听者,需要依次触发里面的函数
for (let i = 0; i < handler.length; i++) {
if (args.length > 0) {
handler[i].apply(this, args);
} else {
handler[i].call(this);
}
}
} else {
// 单个函数的情况我们直接触发即可
if (args.length > 0) {
handler.apply(this, args);
} else {
handler.call(this);
}
}
return true;
};

// 监听名为type的事件
EventEmeitter.prototype.addListener = function(type, fn) {
const handler = this._events.get(type);
if (!handler) {
this._events.set(type, fn);
} else if (handler && typeof handler === "function") {
// 如果handler是函数说明只有一个监听者
this._events.set(type, [handler, fn]);
} else {
// 这里判断监听者数量是否已经超过最大数量
// ...

// 已经有多个监听者,那么直接忘数组里push函数即可
handler.push(fn);
}
};

EventEmeitter.prototype.removeListener = function(type, fn) {
const handler = this._events.get(type); // 获取对应事件名称的函数清单

// 如果是函数,说明被监听了一次
if (handler && typeof handler === "function") {
this._events.delete(type, fn);
} else {
let position;
// 如果handler是数组,说明被监听多次要找到对应的函数
for (let i = 0; i < handler.length; i++) {
if (handler[i] === fn) {
position = i;
} else {
position = -1;
}
}

// 如果找到匹配的函数,从数组中清除
if (position !== -1) {
// 找到数组对应的位置,直接清除此回调
handler.splice(position, 1);
// 如果清除后只有一个函数,那么取消数组,以函数形式保存
if (handler.length === 1) {
this._events.set(type, handler[0]);
}
} else {
return this;
}
}
};

实现instanceOf

1
2
3
4
5
6
7
8
9
10
11
// 模拟 instanceof
function instance_of(L, R) {
//L 表示左表达式,R 表示右表达式
var O = R.prototype; // 取 R 的显示原型
L = L.__proto__; // 取 L 的隐式原型
while (true) {
if (L === null) return false;
if (L === O) return true;
L = L.__proto__;
}
}

模拟 new

1
2
3
4
5
6
7
8
9
10
function objectFactory() {
var obj = new Object();
var Constructor = [].shift.call(arguments);

obj.__proto__ = Constructor.prototype;

var ret = Constructor.apply(obj, arguments);

return typeof ret === "object" ? ret : obj;
}

实现一个 call

call 做了什么:

  • 将函数设为对象的属性
  • 执行并删除这个函数
  • 指定 this 到函数并传入给定参数执行函数
  • 如果不传入参数,默认指向 window
1
2
3
4
5
6
7
8
9
10
11
Function.prototype.myCalll = function(context) {
var context = Object(context) || window;
context.fn = this;
let args = [];
for (let i = 1; i < arguments.length; i++) {
args.push(arguments[i]);
}
let result = context.fn(...args);
delete context.fn;
return result;
};

实现一个 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
28
29
30
Function.prototype.bind = function(context) {
if (typeof this !== 'function') {
// closest thing possible to the ECMAScript 5
// internal IsCallable function
throw new TypeError('Function.prototype.bind - what is trying to be bound is not callable');
}

var args = Array.prototype.slice.call(arguments, 1);
var fToBind = this;
var fNOP = function(){};
var fBound = function() {
// this instanceof fBound === true时,说明返回的fBound被当做new的构造函数调用
return fToBind.apply(this instanceOf fBound
? this
: context,
args.concat(Array.prototype.slice.call(arguments)))
}

if(this.prototype) {
// 当执行Function.prototype.bind()时, this为Function.prototype
// this.prototype(即Function.prototype.prototype)为undefined
fNOP.prototype = this.prototype
}

// 下行的代码使fBound.prototype是fNOP的实例,因此
// 返回的fBound若作为new的构造函数,new生成的新对象作为this传入fBound,新对象的__proto__就是fNOP的实例
fBound.prototype = new fNOP;

return fBound;
}

实现 Promise

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
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
function Promise(executor) {
var self = this;
self.status = 'pending'; // Promise当前的状态
self.data = undefined // Promise的值
self.onResolvedCallback = [] // Promise resolve时的回调函数集,因为在Promise结束之前有可能有多个回调添加到它上面
self.onRejectedCallback = []

function resolve(value) {
if (value instanceof Promise) {
return value.then(resolve, reject)
}

setTimeout(function(){
if(self.status === 'pending') {
self.status = 'resolved';
self.data = value;
for(var i = 0; i < self.onResolvedCallback.length; i++) {
self.onResolvedCallback[i](value)
}
}
})
}

function reject(reason) {
setTimeout(function(){
if(self.status === 'pending') {
self.status = 'rejected';
self.data = reason;
for(var i = 0; i < seld.onRejectedCallback; i++) {
self.onRejectedCallback[i](reason)
}
}
})
}

// 考虑到执行executor的过程中有可能出错,所以我们用try/catch块给包起来,并且在出错后以catch到的值reject掉这个Promise
try {
executor(resolve, reject)
} catch(e) {
reject(e)
}
}

// then方法接收两个参数,onResolved,onRejected,分别为Promise成功或失败后的回调
Promise.prototype.then = function(onResolved, onRejected) {
var self = this;
var promise2

// 根据标准,如果then的参数不是function,则我们需要忽略它,此处以如下方式处理
onResolved = typeof onResolved === 'function' ? onResolved : function(value) {return value} // 值的穿透
onRejected = typeof onRejected === 'function' ? onRejected : function(reason) {throw resaon} // 值的穿透

if(self.status === 'resolved') {
// 如果promise1(此处即为this/self)的状态已经确定并且是resolved,我们调用onResolved
// 因为考虑到有可能throw,所以我们将其包在try/catch块里
return promise2 = new Promise(function(resolve, reject){
setTimeout(function() { // 异步执行onResolved
try {
var x = onResolved(self.data)
resolvePromise(promise2, x, resolve, reject)
} catch (reason) {
reject(reason)
}
})
})
}

// 此处与前一个if块的逻辑几乎相同,区别在于所调用的是onRejected函数,就不再做过多解释
if (self.status === 'rejected') {
return promise2 = new Promise(function(resolve, reject) {
setTimeout(function() { // 异步执行onRejected
try {
var x = onRejected(self.data)
resolvePromise(promise2, x, resolve, reject)
} catch (reason) {
reject(reason)
}
})
})
}

if (self.status === 'pending') {
// 如果当前的Promise还处于pending状态,我们并不能确定调用onResolved还是onRejected,
// 只能等到Promise的状态确定后,才能确实如何处理.
// 所以我们需要把**两种情况**的处理逻辑做为callback放入promise1(此处即this/self)的回调数组里
// 逻辑本身跟第一个if块内的几乎一致,此处不做过多解释
return promise2 = new Promise(function(resolve, reject) {
self.onResolvedCallback.push(function(value){
try {
var x = onResolved(value)
resolvePromise(promise2, x, resolve, reject)
} catch (r) {
reject(r)
}
})

self.onRejectedCallback.push(function(reason) {
try {
var x = onRejected(reason)
resolvePromise(promise2, x, resolve, reject)
} catch (r) {
reject(r)
}
})
})
}
}

Promise.prototype.catch = function(onRejected) {
retutn this.then(null, onRejected)
}


function resolvePromise(promise2, x, resolve, reject) {
var then
var thenCalledOrThrow = false

if (promise2 === x) {
return reject(new TypeError('Chaining cycle detected for promise!'))
}

if (x instanceof Promise) {
if (x.status === 'pending') { //because x could resolved by a Promise Object
x.then(function(v) {
resolvePromise(promise2, v, resolve, reject)
}, reject)
} else { //but if it is resolved, it will never resolved by a Promise Object but a static value;
x.then(resolve, reject)
}
return
}

if ((x !== null) && ((typeof x === 'object') || (typeof x === 'function'))) {
try {
then = x.then //because x.then could be a getter
if (typeof then === 'function') {
then.call(x, function rs(y) {
if (thenCalledOrThrow) return
thenCalledOrThrow = true
return resolvePromise(promise2, y, resolve, reject)
}, function rj(r) {
if (thenCalledOrThrow) return
thenCalledOrThrow = true
return reject(r)
})
} else {
resolve(x)
}
} catch (e) {
if (thenCalledOrThrow) return
thenCalledOrThrow = true
return reject(e)
}
} else {
resolve(x)
}
}
liborn wechat
欢迎您扫一扫上面的微信二维码,订阅我的公众号!