Photo from unsplash: christopher-gower-m_HRfLhgABo-unsplash_dwlhij

字节跳动-广告系统

Written on September 23, 2020 by Choi Yang.

Last updated September 18, 2022.

See changes
25 min read
views


字节跳动广告系统 面经

一面

自我介绍

手撕防抖(如果滚动条判断一个 div 是否存在会用什么来做?节流)

<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" /> <title>实现防抖</title> <style> .container { width: 200px; height: 200px; background-color: aqua; font-size: 30px; line-height: 200px; text-align: center; } </style> </head> <body> <div class="container"></div> <script> var btn = document.getElementsByClassName('container')[0]; // let cnt = 0 // let timeId = null // btn.onmouseover = () => { // clearTimeout(timeId) // timeId = setTimeout(() => { // btn.innerHTML = ++cnt // }, 2000) // } let cnt = 0; let doSomething = () => { btn.innerHTML = ++cnt; }; let debounce = (fn, time, triggerNow) => { let timeId = null; let debounced = (...args) => { if (timeId) { clearTimeout(timeId); } if (triggerNow) { let exec = !timeId; timeId = setTimeout(() => { timeId = null; }, time); if (exec) { fn.apply(this, args); } } else { timeId = setTimeout(() => { fn.apply(this, args); }, time); } }; debounced.remove = () => { clearTimeout(timeId); timeId = null; }; return debounced; }; btn.onmouseover = debounce(doSomething, 2000, false); </script> </body> </html>
html

CSS 实现三角形

<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" /> <title>画三角形</title> <style> .triggle { width: 0px; height: 0px; border-top: 40px solid transparent; border-left: 40px solid transparent; border-bottom: 40px solid red; border-right: 40px solid transparent; margin: 40px; } </style> </head> <body> <div class="triggle"></div> </body> </html>
html

了解伪元素和伪类吗?

参考:CSS伪类和伪元素的区别 - Web前端工程师面试题讲解

盒模型

div: width=100px; border: 10px; padding: 15px; margin: 10px; height=100px; content-box => 100px border-box => 150px
css

Vue 双向绑定实现 Object.defineProperty() 它有哪些不足点?

  • 只能监听某个属性,不能对全对象进行监听
  • 需要 for in 遍历找对象中的属性
  • 不能监听数组,需要单独的对数组进行特异性操作
  • 会污染原对象
data : { name; 'abc'; age: 23, }, this.name = 'brown'; this.gender = '男';
javascript

如何让 gender 改变也会让视图变化(面试官意思是如何用 Vue 动态新增对象属性,触发 dom 渲染)

背景:项目中因为一些需求需要在 JSON 中新增一个属性,也能 console 出来,但是就是不能在页面渲染,即不能触发视图更新

其实在 vue 中新增属性应该用 $set 这个方法的

1. 添加单个属性

$set()方法,既可以新增属性,又可以触发视图更新。

this.$set(this.data,”key”,value)
javascript

2.添加多个属性

使用 Object.assign()用原对象与要混合进去的对象的属性一起创建一个新的对象。

this.obj = Object.assign({}, this.obj, { age: 18, name: 'Chocolate', });
javascript

询问输出结果

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();
javascript

参考:一道常被人轻视的前端JS面试题

第一问

先看此题的上半部分做了什么,首先定义了一个叫Foo 的函数,之后为 Foo 创建了一个叫getName静态属性存储了一个匿名函数,之后为 Foo 的原型对象新创建了一个叫getName的匿名函数。之后又通过函数变量表达式创建了一个 getName 的函数,最后再声明一个叫getName函数。

第一问的 Foo.getName 自然是访问 Foo 函数上存储的静态属性,自然是 2,没什么可说的。

第二问

第二问,直接调用 getName 函数。既然是直接调用那么就是访问当前上文作用域内的叫getName的函数,所以跟 1 2 3 都没什么关系。此题有无数面试者回答为 5。此处有两个坑,一是变量声明提升,二是函数表达式。

变量声明提升

所有声明变量或声明函数都会被提升到当前函数的顶部。 例如下代码:

console.log('x' in window); //true var x; x = 0;
javascript

代码执行时 js 引擎会将声明语句提升至代码最上方,变为:

var x; console.log('x' in window); //true x = 0;
javascript

函数表达式

var getName function getName 都是声明语句,区别在于 var getName函数表达式,而function getName函数声明。关于 JS 中的各种函数创建方式可以看 大部分人都会做错的经典 JS 闭包面试题 这篇文章有详细说明。

函数表达式最大的问题,在于 js 会将此代码拆分为两行代码分别执行。 例如下代码:

console.log(x); //输出:function x(){} var x = 1; function x() {}
javascript

实际执行的代码为,先将 var x=1 拆分为 var x;x = 1; 两行,再将 var x;function x(){}两行提升至最上方变成:

var x; function x() {} console.log(x); x = 1;
javascript

所以最终函数声明的 x 覆盖了变量声明的 x,log 输出为 x 函数。 同理,原题中代码最终执行时的是:

function Foo() { getName = function () { alert(1); }; return this; } var getName; //只提升变量声明 function getName() { alert(5); } //提升函数声明,覆盖var的声明 Foo.getName = function () { alert(2); }; Foo.prototype.getName = function () { alert(3); }; getName = function () { alert(4); }; //最终的赋值再次覆盖function getName声明 getName(); //最终输出4
javascript

第三问

第三问的 Foo().getName(); 先执行了 Foo 函数,然后调用 Foo 函数的返回值对象的 getName 属性函数。

Foo 函数的第一句 getName = function () { alert (1); }; 是一句函数赋值语句,注意它没有var声明,所以先向当前 Foo 函数作用域内寻找getName变量,没有。再向当前函数作用域上层,即外层作用域内寻找是否含有getName变量,找到了,也就是第二问中的alert(4)函数,将此变量的值赋值为 function(){alert(1)}

此处实际上是将外层作用域内的 getName 函数修改了。

注意:此处若依然没有找到会一直向上查找到 window 对象,若 window 对象中也没有 getName 属性,就在 window 对象中创建一个 getName 变量。

简单的讲,this 的指向是由所在函数的调用方式决定的。而此处的直接调用方式,this 指向window对象

遂 Foo 函数返回的是 window 对象,相当于执行 window.getName() ,而 window 中的 getName 已经被修改为alert(1),所以最终会输出1

此处考察了两个知识点,一个是变量作用域问题,一个是this 指向问题

第四问

直接调用 getName 函数,相当于 window.getName() ,因为这个变量已经被Foo函数执行时修改了,遂结果与第三问相同,为1

第五问

new Foo.getName(); ,此处考察的是 js 的运算符优先级问题。

js 运算符优先级:

通过查上表可以得知点(.)的优先级高于new操作,遂相当于是:

new Foo.getName();
javascript

所以实际上将getName函数作为了构造函数来执行,遂弹出2

第六问

第六问 new Foo().getName() ,首先看运算符优先级括号高于 new,并且带参数的 new 操作符是优先级最高的,实际执行为

new Foo().getName();
javascript

遂先执行 Foo 函数,而 Foo 此时作为构造函数却有返回值,所以这里需要说明下 js 中的构造函数返回值问题

原题中,返回的是this,而 this 在构造函数中本来就代表当前实例化对象,遂最终 Foo 函数返回实例化对象。之后调用实例化对象的 getName 函数,因为在Foo 构造函数中没有为实例化对象添加任何属性,遂到当前对象的原型对象(prototype)中寻找 getName,找到了。

遂最终输出 3。

第七问

最终实际执行为:

new new Foo().getName();
javascript

先初始化 Foo 的实例化对象,然后将其原型上的 getName 函数作为构造函数再次 new。

遂最终结果为 3

答案

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(); //2 getName(); //4 Foo().getName(); //1 getName(); //1 new Foo.getName(); //2 new Foo().getName(); //3 new new Foo().getName(); //3
javascript

promise 相关,下面代码输出结果

Promise.reject(2) .catch((e) => e) .then((d) => { console.log(d); }); // 输出2
javascript

一般总是建议,Promise 对象后面要跟 catch 方法,这样可以处理 Promise 内部发生的错误。catch 方法返回的还是一个 Promise 对象,因此后面还可以接着调用 then 方法。

参考:你真的完全掌握了promise么?

Promise 必知必会(十道题)

关于 Promise 的 9 个面试题

http 是一个无状态协议

什么是无状态呢?就是说这一次请求和上一次请求是没有任何关系的,互不认识的,没有关联的。这种无状态的的好处是快速。坏处是假如我们想要把www.zhihu.com/login.htmlwww.zhihu.com/index.html关联起来,必须使用某些手段和工具

由于 http 的无状态性,为了使某个域名下的所有网页能够共享某些数据,session 和 cookie 出现了。客户端访问服务器的流程如下:

  • 首先,客户端会发送一个 http 请求到服务器端。
  • 服务器端接受客户端请求后,建立一个 session,并发送一个 http 响应到客户端,这个响应头,其中就包含 Set-Cookie 头部。该头部包含了 sessionId。Set-Cookie 格式如下: Set-Cookie: value[; expires=date][; domain=domain][; path=path][; secure]
  • 在客户端发起的第二次请求,假如服务器给了 set-Cookie,浏览器会自动在请求头中添加 cookie
  • 服务器接收请求,分解 cookie,验证信息,核对成功后返回 response 给客户端

注意

  • cookie 只是实现 session 的其中一种方案。虽然是最常用的,但并不是唯一的方法。禁用 cookie 后还有其他方法存储,比如放在 url 中
  • 现在大多都是 Session + Cookie,但是只用 session 不用 cookie,或是只用 cookie,不用 session 在理论上都可以保持会话状态。可是实际中因为多种原因,一般不会单独使用
  • 用 session 只需要在客户端保存一个 id,实际上大量数据都是保存在服务端。如果全部用 cookie,数据量大的时候客户端是没有那么多空间的。
  • 如果只用 cookie 不用 session,那么账户信息全部保存在客户端,一旦被劫持,全部信息都会泄露。并且客户端数据量变大,网络传输的数据量也会变大

拓展:token

token 也称作令牌,由 uid+time+sign[+固定参数] token 的认证方式类似于临时的证书签名, 并且是一种服务端无状态的认证方式, 非常适合于 REST API(表现层状态转换) 的场景. 所谓无状态就是服务端并不会保存身份认证相关的数据。

组成

  • uid: 用户唯一身份标识
  • time: 当前时间的时间戳
  • sign: 签名, 使用 hash/encrypt 压缩成定长的十六进制字符串,以防止第三方恶意拼接 固定参数(可选): 将一些常用的固定参数加入到 token 中是为了避免重复查库

存放

token 在客户端一般存放于localStorage,cookie,或sessionStorage中。在服务器一般存于数据库

token 认证流程

token 的认证流程与 cookie 很相似

  • 用户登录,成功后服务器返回 Token 给客户端。
  • 客户端收到数据后保存在客户端
  • 客户端再次访问服务器,将 token 放入headers
  • 服务器端采用 filter 过滤器校验。校验成功则返回请求数据,校验失败则返回错误码

token 可以抵抗 csrf,cookie+session 不行

假如用户正在登陆银行网页,同时登陆了攻击者的网页,并且银行网页未对 csrf 攻击进行防护。攻击者就可以在网页放一个表单,该表单提交 src 为http://www.bank.com/api/transfer,body 为count=1000&to=Tom。倘若是 session+cookie,用户打开网页的时候就已经转给 Tom1000 元了.因为form 发起的 POST 请求并不受到浏览器同源策略的限制,因此可以任意地使用其他域的 Cookie 向其他域发送 POST 请求,形成 CSRF 攻击。在 post 请求的瞬间,cookie 会被浏览器自动添加到请求头中。但 token 不同,token 是开发者为了防范 csrf 而特别设计的令牌,浏览器不会自动添加到headers里,攻击者也无法访问用户的 token,所以提交的表单无法通过服务器过滤,也就无法形成攻击。

分布式情况下的 session 和 token

负载均衡多服务器的情况,不好确认当前用户是否登录,因为多服务器不共享 session。该解决方案是 session 数据持久化写入数据库或别的持久层。各种服务收到请求后,都向持久层请求数据。这种方案的优点是架构清晰,缺点是工程量比较大。另外,持久层万一挂了,就会单点失败。

而 token 是无状态的,token字符串里就保存了所有的用户信息

客户端登陆传递信息给服务端,服务端收到后把用户信息加密(token)传给客户端,客户端将 token 存放于 localStroage 等容器中。客户端每次访问都传递 token,服务端解密 token,就知道这个用户是谁了。通过 cpu 加解密,服务端就不需要存储 session 占用存储空间,就很好的解决负载均衡多服务器的问题了。这个方法叫做JWT(Json Web Token)

Json web token (JWT), 是为了在网络应用环境间传递声明而执行的一种基于 JSON 的开放标准((RFC 7519).该 token 被设计为紧凑且安全的,特别适用于分布式站点的单点登录(SSO)场景。JWT 的声明一般被用来在身份提供者和服务提供者间传递被认证的用户身份信息,以便于从资源服务器获取资源,也可以增加一些额外的其它业务逻辑所必须的声明信息,该 token 也可直接被用于认证,也可被加密。

参考:阮一峰 JSON Web Token 入门教程

总结

  • session 存储于服务器,可以理解为一个状态列表,拥有一个唯一识别符号 sessionId,通常存放于 cookie 中。服务器收到 cookie 后解析出 sessionId,再去 session 列表中查找,才能找到相应 session 所依赖 cookie
  • cookie 类似一个令牌,装有 sessionId,存储在客户端,浏览器通常会自动添加。
  • token 也类似一个令牌,无状态,用户信息都被加密到 token 中,服务器收到 token 后解密就可知道是哪个用户。需要开发者手动添加。
  • jwt 只是一个跨域认证的方案

Secure 和 HttpOnly

Secure属性意味着把 cookie 通信限制在加密传输中,指示浏览器只能通过安全/加密连接使用 cookie。然而如果一个 web 服务器在非安全连接中给 cookie 设置了一个 secure 属性,这个 cookie 在发送给用户时仍然可以通过中间人攻击拦截到。因此,为了安全必须通过安全连接设置 cookie 的 Secure 属性。

HttpOnly属性指示浏览器除了 HTTP/HTTPS 请求之外不要显示 cookie。这意味着这种 cookie 不能在客户端通过脚本获取,因此也不会轻易的被跨站脚本窃取。

浏览器设置

大部分浏览器都支持 cookie,并且允许用户禁止掉他们。下面是一些常用的选项:

  • 完全允许或者禁止 cookie,以便浏览器总是接受或者总是阻止 cookie
  • 通过 cookie 管理器查看或者删除 cookie
  • 彻底清除所有的隐私数据,包括 cookie

HTTPS 中 TLS 握手过程简述

参考:三元博客 (传统RSA版本)HTTPS为什么让数据传输更安全?

参考:TLS1.2 握手的过程是怎样的?

Koa 中间件 passport

仿美团项目登录怎么实现的

浏览器缓存 强缓存 和 协商缓存 状态码

(见后文)

结果

一面通过

二面

面经

开篇没有自我介绍,面试官直接说一面问的比较简单,我来考察一下。

1、考你一点操作系统知识,你知道进程和线程吗?它们有什么区别联系?

2、进程间通信有了解过吗?linux 文件系统说一说?

3、说说网络吧,你知道子网掩码这个概念吗?这个出错了会怎么办?是访问不了内网还是外网还是怎么?

4、刚刚说了 ABC 类地址,你知道这个是怎么区分的吗?

5、你怎么学习前端的?

6、那你有了解过 BOM 和 DOM 吗?

7、有了解过 map 吗?那你知道 Map 和 WeakMap 的区别吗?

8、你刚刚讲到了垃圾回收,那你知道 v8 垃圾回收机制吗?说说

9、你了解 class 吗?你能模拟实现它的私有属性吗?让他具有 private 功能

10、ES5 中的继承实现方式

11、提供类似框架,实现时间过滤器

class DateHelper { /** * 将 UNIX timestamp 时间标签转换成 formatter 格式 * @param {Number} 时间标签 e.g. 1463368789 * @param {String} 格式 e.g. 'yyyy-mm-dd hh:MM' * @returns {String} e.g. '2016-05-16 18:17' */ timestampConverter(timestamp: number, formatter: string): string { // write code here } }
javascript

12、算法题

给定一个整数数组nums ,找到一个具有最大和的连续子数组(子数组最少包含一个元素),返回其最大和。 示例: 输入: [-2,1,-3,4,-1,2,1,-5,4], 输出: 6
javascript

这道题卡了一小会,但最后还是以 O(n)时间做出来了

13、设计题

微信扫描二维码登录网页是什么原理,前后两个事件是如何联系的?

场景是一台手机(已经登录了微信),PC 端服务器,微信服务器,网页二维码。你怎么处理这四者的关系,不牵扯到网络,中间人攻击层面来讲。

这题想了挺久,不断尝试去套面试官的话,不过后面面试官说我还是猜到了一点点。

14、你还有什么要问我的吗?

请教了一下最后那个设计题的简单思路,我是最后结束了才明白场景居然是那样,不过面试官那边确实有点吵,一些点也没抓住。

参考知乎:微信扫描二维码登录网页是什么原理,前后两个事件是如何联系的?

Tweet this article

Enjoying this post?

Don't miss out 😉. Get an email whenever I post, no spam.

Subscribe Now