一、JavaScript

一、[‘1’, ‘2’, ‘3’].map(parseInt) ?

[1,NaN,NaN]

parseInt(string, radix),字符串转十进制整数

string必需。要被解析的字符串。
radix : 可选。表示要解析的数字的基数。该值介于 2 ~ 36 之间。如果省略该参数或其值为 0,则数字将以 10 为基础来解析。如果它以 “0x” 或 “0X” 开头,将以 16 为基数。如果该参数小于 2 或者大于 36,则 parseInt() 将返回 NaN。

可以理解为 radix 是表明 string 的进制的,最终是要转为 10 进制数的
tips:注意第一个参数一定是字符串,如果是number类型可能会发生不可预期错误,例如:

1
2
3
> parseInt(01232, 10); //  666
> parseInt('01232', 10); // 1232
>


解析的时候,parseInt会认为 string 参数是 radix 进制的,比如:
parseInt(3,4) ,这里会认为“3”是 4进制的,所以当遇到这种情况:
parseInt(3,2), 会认为这里的“3”是二进制的,那么3的二进制是3吗?不是啊,是 “11”啊 ,所以会返回NaN

二、防抖和节流

下面我都用的 函数属性timer作为记录定时器的变量,其实也可以在函数内声明一个变量

1. 防抖实现


触发高频事件后n秒内函数只会执行一次,如果n秒内高频事件再次被触发,则重新计算时间,
比如窗口缩放事件 window.resize

1
2
3
4
5
6
7
8
9
function debounce(fn,time){
return function(){
clearTimeout(fn.timer)
fn.timer=setTimeout(()=>{
fn.apply(this, arguments)
},time)
}
}
`


问:为啥用apply?
答:改变函数this指向
问:为啥要改变fn的this指向?如果不apply这个this又指向谁?要是不改有啥影响?
卒~

解析:首先要明白一件事情,this的指向问题,
this 的指向只是与如何调用这个方法有关,看几个例子

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
a={
m:function(){
console.log(this)
}
}
----------------------------------------------
1.
a.m() // a{} 对象

----------------------------------------------
2.
var b=a.m;
m() // 指向全局对象,这里是window
----------------------------------------------
3.
a.s=function(){
setTimeout(function(){
console.log(this)
})
}
// 注意这里setTimeout我用的是 普通函数,this指向全局对象,这里指向window

4.
a.s=function(){
setTimeout(()=>{
console.log(this)
})
}
// 因为用了箭头函数,这里的this指向上一层this,即a对象

34 注意对比
----------------------------------------------
5.
在函数里面执行a.m
function c(){
a.m()
}
// a对象

6.
function m(){
console.log(this)
}
a.c=function(){
m()
}
a.c() // window

56
注意对比,这俩都是函数里面的函数执行。
因为里面函数里面的函数并不是作为一个方法所调用,
所以指向的是window,注意与 5 做对比
----------------------------------------------


看完上面例子,你就知道了setTimeout里如果用箭头函数,那么this是当前作用于,但是 fn() 这样执行的时候,里面的this指向全局,所以要给这个fn绑定当前执行环境的this,以避免一些因为作用域问题引起bug

2. 节流实现


高频事件触发,但在n秒内只会执行一次

1
2
3
4
5
6
7
8
9
function debounce(fn,time){
return function(){
if(fn.timer)return
fn.timer=setTimeout(()=>{
fn.apply(this, arguments)
clearTimeout(fn.timer)
},time)
}
}

三、介绍下 Set、Map、WeakSet 和 WeakMap 的区别?

阮一峰 es6

四、深度优先遍历和广度优先遍历

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<div class="parent">
<div class="child-1">
<div class="child-1-1">
<div class="child-1-1-1"></div>
</div>
<div class="child-1-2"></div>
</div>
<div class="child-2">
<div class="child-2-1"></div>
<div class="child-2-2"></div>
</div>
<div class="child-3">
<div class="child-3-1"></div>
<div class="child-3-2"></div>
</div>
</div>

1. 深度优先遍历

1
2
3
4
5
6
7
8
9
10
function deepTraversal(node) {
let result = [];
result.push(node);
if (node.children) {
for (let i = 0, length = node.children.length; i < length; i++) {
result = result.concat(deepTraversal(node.children[i]));
}
}
return result;
}
深度优先遍历
深度优先遍历

2. 广度优先遍历

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
function widthTraversal(node) {
let result = [];
let stack = [];
let cd = undefined;
stack.push(node);
while ((cd = stack.shift())) {
result.push(cd);
if (cd.children) {
for (let i = 0, length = cd.children.length; i < length; i++) {
stack.push(cd.children[i]);
}
}
}
return result;
}
广度优先遍历
广度优先遍历

五、深拷贝函数

1
2
3
4
5
6
7
8
function deepCopy(obj) {
if (typeof obj != 'object') return obj;
let newValue = obj.constructor.name == 'Array' ? [] : {};
for (let i in obj) {
newValue[i] = typeof obj[i] == 'object' ? deepCopy(obj[i]) : obj[i];
}
return newValue;
}

六、事件循环

js 中的分别存储值类型和引用类型
执行栈和上面的栈意义不同,

当一个脚本第一次执行的时候,js 引擎会解析这段代码,并将其中的同步代码按照执行顺序加入执行栈中,然后从头开始执行。如果当前执行的是一个方法,那么 js 会向执行栈中添加这个方法的执行环境,然后进入这个执行环境继续执行其中的代码。当这个执行环境中的代码 执行完毕并返回结果后,js 会退出这个执行环境并把这个执行环境销毁,回到上一个方法的执行环境。。这个过程反复进行,直到执行栈中的代码全部执行完毕。

事件循环

js 引擎遇到一个异步事件后并不会一直等待其返回结果,而是会将这个事件挂起(浏览器去执行),继续执行执行栈中的其他任务。当一个异步事件返回结果后(注意是异步有了返回结果后,将回调函数添加到事件队列),js 会将这个事件加入与当前执行栈不同的另一个队列,我们称之为事件队列。被放入事件队列不会立刻执行其回调,而是等待当前执行栈中的所有任务都执行完毕(这也是为什么setTimeout 时间不会非常精确的原因), 主线程处于闲置状态时,主线程会去查找事件队列是否有任务。如果有,那么主线程会从中取出排在第一位的事件,并把这个事件对应的回调放入执行栈中,然后执行其中的同步代码…,如此反复,这样就形成了一个无限的循环。这就是这个过程被称为“事件循环(Event Loop)”的原因。

事件循环
事件循环

callback queue:事件队列
webAPIs :异步操作(浏览器执行)
stack:执行栈

七、宏任务和微任务

异步事件也分宏任务和微任务,他们执行顺序的优先级不同
宏任务:setTimeout、setInterval、I/O、UI 渲染
微任务:Promise、MutaionObserver
同一次事件循环中微任务比宏任务优先级高

执行顺序
执行队列空闲=>检查微任务队列=>检查宏任务队列

1
2
3
4
5
6
7
8
9
10
11
setTimeout(function () {
console.log(1);
});

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

执行过程

script 进入主线程开始执行同步代码,将异步操作交给浏览器执行,所以先将 setTimeout 挂起,然后打印 2 ,遇到了 promise 则挂起,此时任务队列空闲(这时异步操作都已经执行完加入了事件队列),那么去检查事件队列,先检查微任务队列,好,发现了 promise 的 then 回调,执行 ,打印 3,微任务队列空了,检查宏任务队列 打印 1 ,此时执行栈为空 ,退出

比较特殊的例子

1
2
3
4
5
6
7
8
9
10
11
12
13
14
async function async1() {
console.log('1');
await async2();
console.log('2');
}
async function async2() {
console.log('3');
}

console.log('4');
async1();
console.log('5');

// 4,1,3,5,2


async方法执行时,遇到await会立即执行表达式,然后把表达式后面的代码放到微任务队列里,让出执行栈让同步代码先执行
也就是 await async2()后面(下面)的部分是放在了promise(微任务),async1 转换一下就是这样:

1
2
3
4
5
6
async function async1(){
console.log('1');
return new Promise(resolve=>resolve(async2())).then(e=>{
console.log('2')
})
}

可以看到 async2() 实在promise的executor(执行器)里同步执行的,所以是
主线程 4 , async 1 ,async2 , 将console.log(2) 加入微任务, 主线程的 console.log(5)
执行栈已经空闲,查看事件队列(优先微任务,然后宏任务) 输出2

练习题,写出执行结果
考点:就是事件循环、宏任务、微任务的执行顺序,上面说的弄明白了,就很容易了

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
async function async1() {
console.log('async1 start');
await async2();
console.log('async1 end');
}
async function async2() {
console.log('async2');
}
console.log('script start');
setTimeout(function() {
console.log('setTimeout');
}, 0)
async1();
new Promise(function(resolve) {
console.log('promise1');
resolve();
}).then(function() {
console.log('promise2');
});
console.log('script end');

八、for 循环每隔 1 秒输出对应数值

因为以前面试遇到过,所以就记录下来

  1. async、await
1
2
3
4
5
6
7
8
9
10
async function a() {
for (var i = 0; i < 10; i++) {
await new Promise((resolve, reject) => {
console.log(i);
setTimeout(() => {
resolve(i);
}, 1000);
});
}
}
  1. 闭包,这种方式的缺陷就是第一次其实是有间隔时间的,而上面是马上输出的
1
2
3
4
5
6
7
8
9
function b() {
for (var i = 0; i < 10; i++) {
(function (i) {
setTimeout(() => {
console.log(i);
}, 1000 * i);
})(i);
}
}
  1. let/const,原理是每次循环 let 或 const 形成了自己的局部作用域
1
2
3
4
5
6
7
function b() {
for (let i = 0; i < 10; i++) {
setTimeout(() => {
console.log(i);
}, 1000 * i);
}
}

九、编写一个程序将数组扁平化去并除其中重复部分数据,最终得到一个升序且不重复的数组

arr = [ [1, 2, 2], [3, 4, 5, 5], [6, 7, 8, 9, [11, 12, [12, 13, [14] ] ] ], 10];

  1. 方法一
1
[...new Set(arr.flat(Infinity))].sort((a, b) => a - b);
  1. 方法二
1
2
3
4
5
6
7
8
9
10
11
function flat(arr) {
let result = [];
for (let i in arr) {
if (typeof arr[i] == 'object') {
result = result.concat(flat(arr[i]));
} else {
result.push(arr[i]);
}
}
return result.sort((a, b) => a - b);
}

十、new 的时候发生了啥 || 实现一个 new

1
2
3
4
5
6
7
8
9
10
11
/*
@param constructor被new的类(构造函数)
*/
function new2(constructor) {
//创建一个实例对象o,并将该对象原型(__proto__)指向func(构造函数)的原型对象
var o = Object.create(constructor.prototype);
//将o作为构造函数的this执行构造函数
var k = constructor.call(o);
// 如果构造函数返回值是引用类型则将实例对象o代替
return typeof k === 'object' ? k : o;
}

十一、检测 js 类型

这个问题都快要问烂了,一般都是说 Object.prototype.toString.call(参数) ,可是为什么要用这种方式?
先列出来我知道的几种方式

  1. typeof
    缺点:不能判断出来数组,对象 , undefined,null 因为它们都返回 object
  2. 参数.constructor.name
    缺点:不能检测 undefined,null
  3. Object.prototype.toString.call(参数)
    这种方式很全面,但是为什么要用 Object 的原型?
    Array ,function 继承了 Object,但重写了 toSting,可以验证一下
1
2
3
delete Array.prototype.toString > true;
a = [];
a.toString() > '[object Array]';

十二、模板编译

解析模板

1
2
3
4
5
6
7
let template = `
<ul>
<% for(let i=0; i < data.supplies.length; i++) { %>
<li><%= data.supplies[i] %></li>
<% } %>
</ul>
`;

解析函数

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
function compile(template) {
let m = template
.replace(/<%=(.+?)%>/g, ' `) \n echo(`$1`) \n echo(`')
.replace(/<%(.+?)%>/g, '`) \n $1 \n echo(` ');
m = 'echo(` ' + m + '`)';

// 要解析成这样
// echo('<ul>')
// for(let i=0; i < data.supplies.length; i++) {
// echo('<li>')
// echo(data.supplies[i])
// echo('</li>')
// }
// echo('</ul>')

// 这个需要加小括号,否则eval解析不出来
let script = `(function parse(data){
let outHtml=''
function echo(html){
outHtml+=html
}
${m}
return outHtml
})`;
return eval(script);
}

let x = compile(template);
x({ supplies: ['broom', 'mop', 'cleaner'] });

十三、补全字符串

1
2
3
4
5
// 如果某个字符串不够指定长度,会在头部或尾部补全。padStart()用于头部补全,padEnd()用于尾部补全
'12'.padStart(10, 'YYYY-MM-DD'); // "YYYY-MM-12"
'09-12'.padStart(10, 'YYYY-MM-DD'); // "YYYY-09-12"

'xxx'.padEnd(4, 'ab'); //"xxxa"

十四、链表

单链表

链表组成

  • 节点
  • 链表
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
// 节点类
class Node {
constructor(val) {
this.val = val;
this.next = null;
}
}

// 链表
class NodeList {
constructor() {
this.head = null;
}
/**
* 创建节点
*/
static createNode(value) {
return new Node(value);
}

push(node) {
if (!this.head) {
this.head = node;
return;
}
let cur = this.head;
while (cur && cur.next) {
cur = cur.next;
}
cur.next = node;
}
/**
* 插入到头
*/
inset(node) {
let firstNode = this.head;
this.head = node;
node.next = firstNode;
}
/**
* 查
*/
find(val) {
let cur = this.head;
while (cur && cur.val != val) {
cur = cur.next;
}
return cur;
}
/**
* 删除
*/
delete(node) {
let cur = this.head;
if (this.head == node) {
const next = this.head.next;
this.head = next;
return;
}
let prev = cur;
while (cur && cur != node) {
prev = cur;
cur = cur.next;
}
if (prev) {
prev.next = cur.next;
}
}
}

附加

知识点

附加

知识点

二、CSS

一、什么是包含块?怎么确定包含块?

一个元素的大小和位置要依据一个矩形框来设置,这个矩形框就是包含块

1. 确定包含块

一个元素的包含块完全依赖于这个元素的position属性

  1. 如果 postion 属性为 static/relative,包含块就是他最近的祖先块元素的内容区的边缘组成的(注意是块元素哦)
    下面几条注意了,是内边距的外边缘(边框的内边缘),而不是内容区域边缘了,上面的是内容区边缘
  2. 如果 position 属性为 absolute,包含块就是由它的最近的 position 的值不是 static (也就是值为 fixed, absolute, relative 或 sticky)的祖先元素的内边距区的外边缘组成。
  3. 如果 position 属性是 fixed,包含块是 viewport(浏览器中就是 window,可视区域)
  4. 如果 position 属性是 absolute/fixed,包含块也可能是由满足以下条件的最近父级元素的内边距区的外边缘组成的
    1. transform 或 perspective 属性(不为 none)
    2. 剩下几条都是些浏览器不怎么兼容的属性,略

参考链接

2. 根据包含块定位

margin/position:absolute 这类的元素,如果设置百分比,则会根据它的包含块的大小来计算定位
ps: margin 不管是 margin-top 还是 margin-left 等都是根据 父元素宽度来计算的

css 的offsetParent 指的是祖先元素中已经定位(position/transfrom/perspective)的元素,如果没有就是 body
css 的parentNode 指的是父级元素

二、块级格式化上下文 BFC

BFC 隔离了的独立容器,容器里面的元素不会在布局上影响到外面的元素
创建 BFC:

  1. 元素
  2. 浮动元素(float 不是 none)
  3. 绝对定位元素(position 取值为 absolute 或 fixed)
  4. display 取值为 inline-block,table-cell, table-caption,flex, inline-flex 之一的元素
  5. overflow 不是 visible 的元素

比如:

1
2
3
<div id="box" style="margin-top:10px">
<div id="son" style="margin-top:5px"></div>
</div>

这种情况实际上会发生外边距重叠(浏览器试一下就明白),实际我想要不让它们重叠,也就是 box 里的元素不要影响到外面或者外面的元素属性不要影响到里面,那么给 box 创建一个 BFC 即可

三、块级元素的垂直居中

1
2
3
<div class="f">
<div class="s">sadf</div>
</div>
  1. 子元素必须固定大小
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
.f {
position: relative;
width: 200px;
height: 400px;
}
.s {
width: 100px;
height: 100px;
margin: auto;
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
}
  1. 子元素宽高没有限制
1
2
3
4
5
6
7
8
9
10
.f {
position: relative;
width: 200px;
height: 400px;
}
.s {
position: absolute;
top: 50%;
transform: translate(-50%, -50%);
}
  1. 子元素宽高没有限制
1
2
3
4
5
6
7
8
9
.f {
display: table-cell;
vertical-align: middle;
width: 200px;
height: 400px;
}
.s {
margin: auto;
}
  1. 子元素要固定大小
1
2
3
4
5
6
7
8
9
10
11
.f {
width: 200px;
height: 400px;
}
.s {
position: absolute;
top: 50%;
left: 50%;
height: 100px;
margin: -50px 0 0 -50px;
}
  1. 子元素宽高没有限制
1
2
3
4
5
6
7
.f {
width: 200px;
height: 400px;
display: flex;
justify-content: center;
align-items: center;
}
  1. 子元素宽高没有限制
1
2
3
4
5
6
7
8
.f {
display: grid;
align-content: center;
justify-content: center;

/* align-content: center;
justify-content: center; */
}