2020-02-19字节跳动面试总结

为了不让生活留下遗憾和后悔,我们应该尽可能抓住一切改变生活的机会。

一次长达 90 min 的面试,字节冲冲冲!


前言

有了 第一次面试 的经验,这次发挥的相比于前一次好一些。

第一个问题依然是自我介绍。

因为面了 90 分钟,只记住了一些问题

开发过程中遇到过跨域问题吗?怎么解决的?

Chrome 浏览器设置

这是我最常用的方法,直接设置 Chrome 去跨域:

  • 首先在电脑上新建一个目录,比如 C:\EuphoriaChrome
  • 在 Chrome 可执行程序的 属性 一栏中加上 --disable-web-security --user-data-dir=C:\EuphoriaChrome
  • 保存关闭后打开 Chrome,如果出现有 --disable-web-security 相关字样,则此时可以跨域工作了。

JSONP 跨域

JSONP 和普通的 xmlHTTPRequest 对象不同,其利用了 <script> 标签的 src 属性去跨域访问资源,后端按照给定的 callback 名称返回一个可被 JavaScript 执行的函数,然后 JS 根据之前约定好的函数名称去调用这个函数,即可拿到对应的值。

PHP 允许跨域

1
2
3
4
5
6
<?php
header('Content-Type: text/html;charset=utf-8');
header('Access-Control-Allow-Origin:*'); // *代表允许任何网址请求
header('Access-Control-Allow-Methods:POST,GET,OPTIONS,DELETE'); // 允许请求的类型
header('Access-Control-Allow-Credentials: true'); // 设置是否允许发送 cookies
header('Access-Control-Allow-Headers: Content-Type,Content-Length,Accept-Encoding,X-Requested-with, Origin'); // 设置允许自定义请求头的字段

Web 服务器直接设置允许跨域

1
2
3
4
5
6
7
#设置需要跨域的指定文件
location ^~/res/ {
add_header Access-Control-Allow-Origin *;
add_header Access-Control-Allow-Methods 'GET,POST';
add_header Access-Control-Allow-Headers 'DNT,X-Mx-ReqToken,Keep-Alive,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,Authorization';
alias /data/web/res/;
}

或者直接允许全局跨域:

1
2
3
4
5
6
server {
   ....
add_header Access-Control-Allow-Origin *;
add_header Access-Control-Allow-Methods 'GET,POST';
add_header Access-Control-Allow-Headers 'DNT,X-Mx-ReqToken,Keep-Alive,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,Authorization';
}

如果要配置 POST 可跨域,那么还需要配置什么?

我们会发现,在很多 POST、GET、DELETE 等请求之前,会有一次 OPTIONS 请求,根本原因就是 W3C 规范这样要求了

在跨域请求中,分为简单请求(get 和部分 post,post 时 content-type 属于 application/x-www-form-urlencodedmultipart/form-datatext/plain 中的一种)和 复杂请求。而复杂请求发出之前,就会出现一次 options 请求。

什么是 OPTIONS 请求?

OPTIONS 它是一种 探测性的请求,通过这个方法,客户端可以在采取具体资源请求之前,决定对该资源采取何种必要措施,或者了解服务器的性能。

在 Ajax 中出现 options 请求,也是一种提前探测的情况。Ajax 跨域请求时,如果请求的是 json,就属于复杂请求,因此需要提前发出一次 options 请求,用以检查请求是否是可靠安全的。如果 options 获得的回应是拒绝性质的,比如 404\403\500 等 http 状态,就会停止 post、put 等请求的发出。

当前台发起跨域 post 请求时,由于 CORS(cross origin resource share)规范的存在,浏览器会首先发送一次 options 嗅探,同时 header 带上 origin,判断是否有跨域请求权限。服务器响应 access control allow origin 的值,供浏览器与 origin 匹配,如果匹配则正式发送 post 请求。


HTTP 304 状态码是什么?

害,又死到了状态码,淘宝这篇 已经有对状态码进行了总结,在这里不再赘述。

这篇博客写完了,单独再起一篇有关 前端缓存 的博客吧。


手撕代码—归并排序、快速排序

当我听到 归并 这两个字时我快哭了,八大排序我恰恰只有归并排序的实现写的最磕磕绊绊,随便换一个另外的什么排序都能秒切出来。

在这里给面试官把归并的思路从头到尾讲了一遍,就没继续写下去。

然后被问还会啥排序,这个一下就非常开心,快速、基数、堆排、希尔、选择、插入、冒泡 都是可以秒切的,为了求稳我选择了快速排序(这里利用了弱类型的优势):

1
2
3
4
5
6
7
8
9
10
11
12
const quickSort = array => {
if (array.length <= 1) return array;
let lt = [];
let gt = [];
let eq = [array[0]];
for (let i = 1; i < array.length; i++) {
if (array[i] > eq[0]) gt.push(array[i]);
else if (array[i] < eq[0]) lt.push(array[i]);
else eq.push(array[i]);
}
return [...quickSort(lt), ...eq, ...quickSort(gt)];
};
第一次是看到用 Python 这样写快排的,被吓到了

非关系型数据库和关系型数据库的区别是什么?

关系型数据库

  • 关系型数据库是依据 关系模型 来创建的数据库(比如 ER 图);
  • 所谓关系模型就是“一对一、一对多、多对多”等关系模型,关系模型就是指 二维表格 模型,因而一个关系型数据库就是由二维表及其之间的联系组成的一个数据组织;
  • 关系型数据可以很好地存储一些关系模型的数据,比如一个老师对应多个学生的数据(“多对多”),一本书对应多个作者(“一对多”),一本书对应一个出版日期(“一对一”);
  • 关系模型是我们生活中能经常遇见的模型,存储这类数据一般用关系型数据库;
  • 关系模型包括数据结构(数据存储的问题,二维表)、操作指令集合(SQL语句)、完整性约束(表内数据约束、表与表之间的约束)。

常见的关系型数据库

个人用过的有 Oracle、MySQL、PostgreSQL、微软的 SQL Server、SQlite3,当然还有没用过的例如 Access 那样的数据库。

关系型数据库

优点

  1. 易于维护:都是使用表结构,格式一致;
  2. 使用方便:SQL语言通用,可用于复杂查询;
  3. 复杂操作:支持SQL,可用于一个表以及多个表之间非常复杂的查询。

缺点

  1. 读写性能比较差,尤其是海量数据的高效率读写;
  2. 固定的表结构,灵活度稍欠;
  3. 高并发读写需求,传统关系型数据库来说,硬盘I/O是一个很大的瓶颈。

非关系型数据库

非关系型数据库严格上不是一种数据库,应该是一种数据结构化存储方法的集合,可以是文档或者键值对等。
  • 非关系型数据库主要是基于“非关系模型”的数据库(由于关系型太大,所以一般用“非关系型”来表示其他类型的数据库)
  • 非关系型模型比如有:
    • 列模型:存储的数据是一列列的。关系型数据库以一行作为一个记录,列模型数据库以一列为一个记录。(这种模型,数据即索引,IO很快,主要是一些分布式数据库)
    • 键值对模型:存储的数据是一个个“键值对”,比如 name:liming,那么 name 这个键里面存的值就是 liming
    • 文档类模型:以一个个文档来存储数据,有点类似“键值对”。

常见的非关系型数据库

这个涉猎就很少了,用过 MongoDB(还见过 Redis…),当然还有 HBase、Neo4j 等等…

非关系型数据库

优点

  1. 格式灵活:存储数据的格式可以是 key,value 形式、文档形式、图片形式等等,文档形式、图片形式等等,使用灵活,应用场景广泛,而关系型数据库则只支持基础类型。
  2. 速度快:nosql 可以使用硬盘或者随机存储器作为载体,而关系型数据库只能使用硬盘;
  3. 高扩展性;
  4. 成本低:nosql 数据库部署简单,基本都是开源软件。

缺点

  1. 不提供sql支持,学习和使用成本较高;
  2. 无事务处理;
  3. 数据结构相对复杂,复杂查询方面稍欠。

MongoDB 中 Document 和 Collection 是什么

MongoDB 的基础概念:DatabasesCollctionsDocuments

MongoDB 以 BSON 格式的文档(Documents)形式存储。Databases 中包含集合(Collections),集合(Collections)中存储文档(Documents)。

  • Databases:在 MongoDB 中,databases 保存文档(Documents)的集合(Collections);
  • Collections:MongoDB 在 collections 中存储文档(documents)。Collections 类似于关系型数据库中的表(tables);
  • Documents:MongoDB 的文件是由 field 和 value 对的结构组成,value 值可以是任何 BSON 数据类型,包括:其他 document,数字,和 document 数组。

MySQL 的隔离级别有哪些?

(呜呜呜呜呜这个真的不会)

SQL 标准定义了 4 类隔离级别,包括了一些具体规则,用来限定事务内外的哪些改变是可见的,哪些是不可见的。

低级别的隔离级一般支持更高的并发处理,并拥有更低的系统开销。

read Uncommitted(读取未提交内容)

在该隔离级别,所有事务都可以看到其他未提交事务的执行结果。

本隔离级别很少用于实际应用,因为它的性能也不比其他级别好多少。读取未提交的数据,也被称之为脏读(Dirty Read)。

Read Committed(读取提交内容)

这是大多数数据库系统的默认隔离级别 (但不是 MySQL 默认的)

它满足了隔离的简单定义:一个事务只能看见已经提交事务所做的改变。

这种隔离级别也支持所谓的不可重复读(Nonrepeatable Read),因为同一事务的其他实例在该实例处理其间可能会有新的 commit,所以同一 select 可能返回不同结果。

Repeatable Read(可重读)

这是 MySQL 的默认事务隔离级别,它确保同一事务的多个实例在并发读取数据时,会看到同样的数据行。不过理论上,这会导致另一个棘手的问题:幻读 (Phantom Read)

简单的说,幻读指当用户读取某一范围的数据行时,另一个事务又在该范围内插入了新行,当用户再读取该范围的数据行时,会发现有新的“幻影” 行。InnoDB 和 Falcon 存储引擎通过多版本并发控制(MVCC,Multiversion Concurrency Control)机制解决了该问题。

Serializable(可串行化)

这是 最高的 隔离级别,它通过强制事务排序,使之不可能相互冲突,从而解决幻读问题。简言之,它是在每个读的数据行上加上共享锁。在这个级别,可能导致大量的超时现象和锁竞争。


怎么在 MySQL 中设置索引?组合索引有用过吗?

MySQL 索引的建立对于 MySQL 的高效运行是很重要的,索引可以大大提高 MySQL 的检索速度。

索引分单列索引和组合索引。单列索引,即一个索引只包含单个列,一个表可以有多个单列索引,但这不是组合索引。组合索引,即一个索引包含多个列。

创建索引时,需要确保该索引是应用在 SQL 查询语句的条件(一般作为 WHERE 子句的条件)。

普通索引

就是最基本的索引,没有任何限制,有以下几种创建方式:

1
2
3
4
5
6
7
8
9
10
11
12
# 直接创建一个索引
CREATE INDEX index_name ON table(table_col(length))

# 修改表结构,增加一个索引
ALTER TABLE table_name ADD INDEX index_name ON (table_col(length))

# 创建表的同时也创建一个索引
CREATE TABLE table_name (
id int(11) NOT NULL PRIMARY KEY AUTO_INCREMENT,
value varchar(32) NOT NULL,
INDEX index_name (value(length))
)

唯一索引

与前面的普通索引类似,不同的就是:索引列的值必须唯一,但允许有空值

如果是组合索引,则列值的组合必须唯一。

它有以下几种创建方式:

1
2
3
4
5
6
7
8
9
10
11
12
# 直接创建索引
CREATE UNIQUE INDEX index_name ON table_name(table_col(length))

# 修改表结构,为其增加一个索引
ALTER TABEL table_name ADD UNIQUE index_name ON (table_col(length))

# 创建表的时候直接指定一个索引
CREATE TABLE table_name (
id int(11) NOT NULL PRIMARY KEY AUTO_INCREMENT,
value varchar(32) NOT NULL,
UNIQUE index_name (value(length))
)

主键索引

是一种特殊的唯一索引,一个表只能有一个主键,不允许有空值。

一般是在建表的时候同时创建主键索引:

1
2
3
4
5
CREATE TABLE table_name (
id int(11) NOT NULL AUTO_INCREMENT,
value varchar(32) NOT NULL,
PRIMARY KEY (id)
)

组合索引

指多个字段上创建的索引,只有在查询条件中使用了创建索引时的第一个字段,索引才会被使用。

使用组合索引时遵循 最左前缀集合

1
ALTER TABLE table_name ADD INDEX name_city_age (name, city, age);

全文索引

主要用来查找文本中的关键字,而不是直接与索引中的值相比较。fulltext 索引跟其它索引大不相同,它更像是一个搜索引擎,而不是简单的 where 语句的参数匹配。

fulltext 索引配合 match against 操作使用,而不是一般的 where 语句加 like。它可以在 create tablealter tablecreate index 使用,不过目前只有 charvarchartext 列上可以创建全文索引。

值得一提的是,在数据量较大时候,现将数据放入一个没有全局索引的表中,然后再用 CREATE INDEX 创建 fulltext 索引,要比先为一张表建立 fulltext 然后再将数据写入的速度快很多。


最左前缀匹配原则是什么?

(呜呜呜呜呜…)

事后上网查了一下其实不难理解,在 MySQL 创建联合(组合)索引时会遵循 最左前缀匹配 原则,说白了就是 最左优先,在检索数据的时候从联合索引的最左端开始匹配:

1
ALTER TABLE table_name ADD INDEX col1_col2_col3 (col1, col2, col3);

随着这个联合索引 col1_col2_col3 的建立,其实是建立了 (col1)(col1, col2)(col1, col2, col3) 三个索引。

1
SELECT * FROM table_name WHERE col1="1" AND col2="2" AND colx="x";

所以上面这个查询语句在执行的时候,会遵循最左前缀匹配原则,在检索时会索引 (col1, col2) 进行数据匹配,索引的字段可以是任意序列的

联合索引有啥好处呢?

  • 减少开销:每多一个索引,都会增加写操作的开销和磁盘空间的开销。对于大量数据的表,使用联合索引会大大的减少开销!
  • 效率高:索引列越多,通过索引筛选出的数据越少。

HTTP 2.0 和 1.0 的区别是什么?

HTTP 1.0 和 HTTP 1.1 的区别

长链接

HTTP 1.0 中。浏览器的每次请求都需要与服务器建立一个TCP连接,服务器处理完成后立即断开TCP连接(无连接),服务器不跟踪每个客户端也不记录过去的请求(无状态)。

HTTP 1.0 需要使用 keep-alive 参数来告知服务器端要建立一个长连接,而 HTTP 1.1 默认支持长连接。

HTTP 是基于 TCP/IP 协议的,创建一个 TCP 连接是需要经过三次握手的,有一定的开销,如果每次通讯都要重新建立连接的话,对性能有影响。因此最好能维持一个长连接,可以用个长连接来发多个请求。

节约带宽

HTTP 1.1 支持只发送 header 信息(不带任何 body 信息),如果服务器认为客户端有权限请求服务器,则返回 100,否则返回 401。客户端如果接受到 100,才开始把请求 body 发送到服务器。

这样当服务器返回 401 的时候,客户端就可以不用发送请求 body 了,节约了带宽。

另外 HTTP 还支持传送内容的一部分。这样当客户端已经有一部分的资源后,只需要跟服务器请求另外的部分资源即可。这是支持文件断点续传的基础。

HTTP 1.1 和 HTTP 2.0 的区别

多路复用

HTTP 2.0 使用了多路复用的技术,做到同一个连接并发处理多个请求,而且并发请求的数量比 HTTP 1.1 大了好几个数量级。

当然 HTTP 1.1 也可以多建立几个 TCP 连接,来支持处理更多并发的请求,但是创建 TCP 连接本身也是有开销的(三次握手四次挥手)。

TCP 连接有一个预热和保护的过程,先检查数据是否传送成功,一旦成功过,则慢慢加大传输速度。因此对应瞬时并发的连接,服务器的响应就会变慢。所以最好能使用一个建立好的连接,并且这个连接可以支持瞬时并发的请求。

数据压缩

HTTP 1.1 不支持 header 数据的压缩,HTTP 2.0 使用 HPACK 算法对 header 的数据进行压缩,这样数据体积小了,在网络上传输就会更快。

服务器推送

当我们对支持 HTTP 2.0 的 web server 请求数据的时候,服务器会 顺便把一些客户端需要的资源一起推送到客户端,免得客户端再次创建连接发送请求到服务器端获取。这种方式非常合适加载静态资源。

服务器端推送的这些资源其实存在客户端的某处地方,客户端直接从本地加载这些资源就可以了,不用走网络,速度自然是快很多的。

服务器推送

服务端推送过来的资源,会统一放在一个网络与 http 缓存之间的一个地方,在这里可以理解为“本地”。

当客户端把 index.html 解析完以后,会向本地请求这个资源。由于资源已经本地化,所以这个请求的速度非常快,这也是服务端推送性能优势的体现之一。


Vue 中 watch 和 computed 的区别是什么?

  • 计算属性和 watch 虽然本质上都是函数,但计算属性的函数内部必须要 return 一个值;
  • watch 中,键是需要观察的数据,值就是对应的回调函数,其主要用于监听某些特定数据的变化,从而进行某些具体的业务逻辑操作;
重点是,计算属性的结果会被缓存,除非依赖的响应式属性发生变化才会重新计算。

Vue 中父子组件怎么交互的

传递数值

父组件在引用子组件的时候,可以通过属性绑定的形式,将需要传递给子组件的数据以属性绑定的形式传递:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
<div id="app">
<child :parentmsg="msg"></child>
</div>
<template id="child">
<h1 v-text="parentmsg"></h1>
</template>
<script>
new Vue({
el: '#app',
data: { msg: 'xxx' },
components: {
child: {
template: '#child',
data() { return {}; },
props: ['parentmsg']
}
}
});
</script>

在子组件中直接使用 this.parentmsg 就可以访问父组件的值了。

通过 props 访问到的值是只读的,即子组件不能直接地去修改父组件中的值。

如果子组件也恰好有一个名为 parentmsg 的值,那么在调用 this.parentmsg 时,打印的是父组件的值。这个时候会报一个 Warning:

[Vue warn]: The data property "parentmsg" is already declared as a prop. Use prop default value instead.

这个名为 “parentmsg” 的数据已经在 prop 中被定义了,将会使用 prop 中的数据去替换。

调用方法

如果要传递父组件中的方法,因为事件不能用 v-bind,所以需要用 v-on

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
div id="app">
<child v-on:father-method="fatherMethod"></child>
</div>
<template id="child">
<h1 @click="childEmitMethod">Click Me</h1>
</template>
<script>
const child = {
template: '#child',
data() { return {}; },
methods: {
childEmitMethod() {
this.$emit('father-method');
}
}
};
new Vue({
el: '#app',
data: {
msg: 'father'
},
components: {
child
},
methods: {
fatherMethod() {
console.log('father');
}
}
});
</script>

首先父组件在子组件元素身上用 v-on 绑定一个自己的方法,这个 v-on 绑定的方法名称将来会进入到子组件的 this.$emit() 中,上述代码中子组件绑定父组件事件的名称是 father-method

然后在子组件中定义一个方法,这个方法就专门去激活父组件的方法,在方法内部使用 this.$emit(father-method) 去调用。

this.$emit() 可以接受多个参数,第一个参数恒定是在子组件身上绑定的事件名称,之后的参数会传递父组件的函数中作为参数出现。


手撕代码—观察者模式

上面所说的 this.$emit() 就是一个观察者模式的体现,在子组件身上使用 v-on 可以看作是子组件订阅了父组件的一个方法。

要求:实现一个具有 on、off、emit 三个方法的观察者模式。

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
class Observer {
constructor(name) {
this.name = name;
this.eventList = {};
}

on(eventName, subscriberName, callback) {
console.log(`${subscriberName} 关注了 ${this.name}${eventName} 事件!`);
if (typeof this.eventList[eventName] === 'undefined') {
this.eventList[eventName] = [];
}
this.eventList[eventName].push({
name: subscriberName,
callback: callback
});
}

off(eventName, subscriberName) {
if (typeof this.eventList[eventName] === 'undefined') {
console.error(`${this.name} 不存在 ${eventName} 事件!`);
return;
}
let index = this.eventList[eventName].findIndex(value => {
return value.name === subscriberName;
});
if (index === -1) {
console.error(`${subscriberName} 不曾在 ${this.name} 这里订阅过 ${eventName}!`);
} else {
this.eventList[eventName].splice(index, 1);
console.log(`成功从 ${this.name}${eventName} 中删除了 ${subscriberName} 的订阅`);
}
}

emit(eventName) {
if (typeof this.eventList[eventName] === 'undefined') {
console.error(`${this.name} 不存在 ${eventName} 事件!`);
return;
}
this.eventList[eventName].forEach(value => {
value.callback();
});
}
}

const observer = new Observer('o');
observer.on('a', 'A', () => {
console.log('A callback');
});
observer.on('a', 'B', () => {
console.log('B callback');
});
observer.off('a', 'B');
observer.on('a', 'C', () => {
console.log('C callback');
});
observer.emit('a');

输出如下:

1
2
3
4
5
6
A 关注了 o 的 a 事件!
B 关注了 o 的 a 事件!
成功从 o 的 a 中删除了 B 的订阅
C 关注了 o 的 a 事件!
A callback
C callback

callback 在当时没写进去,这个是后面加的需求,当时就简单地说了一下实现步骤。

因为之前有专门做过有关 观察者模式和发布订阅 的博客,在这里没有被难住。只不过和博客中的写法稍稍有点小小的区别,也没有太过在意,稍微变通一下就好。


手撕代码—实现 Vue 中虚拟 DOM 的 render 函数

面试官给出了以下代码,要求将其渲染至页面上:

1
2
3
4
5
6
7
8
const virtualDOM = Element('div', {'class': 'a'}, [
Element('div', {'class': 'b'}, ['div1']),
Element('p', {'class': 'c'}, ['p1']),
Element('ul', {}, [
Element('li', {'class': 'd'}, ['li1']),
Element('li', {'class': 'd'}, ['li2']),
]),
]);

很明显这就是虚拟 DOM,只需要实现 Element 类,并且这个类中会有一个 render 方法:

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
function Element(tagName, attr, children) {
if (!(this instanceof Element)) {
return new Element(tagName, attr, children);
}
this.tagName = tagName;
this.attr = attr || {};
this.children = children || [];
}

Element.prototype.render = function () {
const node = document.createElement(this.tagName);

Object.keys(this.attr).forEach(value => {
node.setAttribute(value, this.attr[value]);
});

this.children.forEach(value => {
let child;
if (value instanceof Element) {
child = value.render();
} else {
child = document.createTextNode(value);
}
node.appendChild(child);
});

return node;
};

更多有关虚拟 DOM 的在 这篇博客 中有提到。


开发过小程序吗?小程序的优势是什么?

emmm确实有开发过(两个),但是只能是一遍啃文档一遍去开发。

小程序的特点

  • 便捷性:小程序是不需要下载安装的,即用即走,非常方便用户的使用。并且不占用手机的内存,很便捷。
  • 唯一性:小程序的名称是具有唯一性的,谁先注册就是谁的,当别人已经注册成功了,你是没有办法在注册的。

除了上述特点,小程序还具有例如:入口多、良好的新零售落地工具、成本低、体验好效率高、用户精准等等的优点。


阅读理解

这道题聪明反被聪明误了,但是最后面试官给了一个小提醒,还是做出来了。

两个同目录下的 JS 文件,a.jsb.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// a.js
let val = 1;

const setVal = () => {
val++;
};

setTimeout(() => {
console.log('a ' + val);
}, 1000);

module.exports = {
val,
setVal
};
1
2
3
4
5
6
7
8
9
10
// b.js
const a = require('./a.js');

console.log('b ' + a.val);

a.setVal();

setTimeout(() => {
console.log('bb ' + a.val);
}, 2000);

问输出结果是什么?

自以为及其了解事件循环机制,还知道 require 是同步加载的(对,就这里,我以为 a.js 是一个独立的执行栈),所以一开始给的答案是这样的:

  • 首先 b 去同步加载 a 模块(执行 a.js);
  • a 中同步定义了一个值和一个方法;
  • a 中有一个定时器,放到 Task Queue 中;
  • a.js 执行结束,然后拿出存在于队列中的定时器去执行(对,就这里 GG 了);
  • 回到 b,因为在导出模块的时候,对 a.js 中的 val 做的是拷贝,所以 b 中输出的 a.val 的值仍然是 1;
  • 然后执行 a.setVal() 将那个独立于 a 中的 val 值加 1;
  • 最后执行 b 中的定时器,输出拷贝出来的 val,值还是 1。

所以一开始我的答案是:

1
2
3
a 1
b 1
bb 1
然后面试官看不下去了,告诉我这样不对(既然这样不对,那么就可以确认是 a.js 中的定时器错了,我觉得这就算是一个很好的提醒了)

随后我便认为,a 和 b 是在一起的,即:

  • 首先 b 去同步加载 a 模块(执行 a.js);
  • a 中同步定义了一个值和一个方法;
  • a 中有一个定时器,放到 Task Queue 中;
  • a.js 执行结束
  • 回到 b,因为在导出模块的时候,对 a.js 中的 val 做的是拷贝,所以 b 中输出的 a.val 的值仍然是 1;
  • 然后执行 a.setVal() 将那个独立于 a 中的 val 值加 1;
  • 遇到了 b 中的定时器,将其加入到任务队列中,即 a 定时器的后面
  • 至此,主线程空闲,开始事件轮询。当轮询至定时器时,a 的定时器一定先完成
  • 所以输出 a 中的,已经被加过 1 的 val
  • 等到 b 的 2000ms 到时间后,再输出 b 中的那个拷贝值

最后我确定的答案是:

1
2
3
b 1
a 2
bb 1

害,在 这篇博客 中有讲过 Event Loop,但是还真没提到牵扯到导入模块的坑。


总结

每次面试都可以学到很多很多新的东西,这次相比于前一次已经没有那种 过度紧张 的心情。讲真问到数据库的时候很慌,那方面压根没有准备过一切只能靠过往经验。

面试官很耐心…还给我讲了那些我不会的东西。声音也好好听..

发现了自己在缓存那方面还是有缺陷,最恨的也不过 304 没答上来,还有 1/8 概率的刚好不能秒切出来的归并排序(叹),下篇专门开一次八大排序的博客吧。

-------------本文结束 Euphoria 在此感谢您的阅读-------------

本文标题:2020-02-19字节跳动面试总结

文章作者:王钦弘

发布时间:2020年02月20日 - 16:59

最后更新:2020年02月21日 - 15:36

原始链接:https://www.wqh4u.cn/2020/02/20/2020-02-19%E5%AD%97%E8%8A%82%E8%B7%B3%E5%8A%A8%E9%9D%A2%E8%AF%95%E6%80%BB%E7%BB%93/

许可协议: 署名-非商业性使用-禁止演绎 4.0 国际 转载请保留原文链接及作者。

您的支持将鼓励 Euphoria 继续创作!
(如果你还是学生请千万不要打赏!留点钱在学习上啊!)