序言

  哎!薪水不给力,工作内容又重复性高,队友不给力。就这样,我产生跳槽的想法,所以就有了这个计划。无奈的是,恰逢遇上了新冠疫情,跳槽的时机需要延后,而且现在找工作也面临着许多人的竞争,那么我需要做好各类准备以获得一份自己满意的工作岗位(中级前端工程师)。

技术类问题总结

关于 Javascript 基础部分

1,数组扁平化

解决思路:可以使用 es10 的 Array.flat()方法,或者自行遍历数组最后返回一个新的数组对象

2,关于深浅拷贝的使用与简易实现思路

lodash.js 和 clone.js 中都有克隆方法(深拷贝),

原生的 Object.asign()可以实现浅拷贝。

而 JSON.parse(JSON.stringify())也可以实现深拷贝但有一定的局限性(如:忽略拷贝函数、循环引用会报错、自动忽视 undefined 对象),一般常规使用没有问题。

但是如果是自己实现的话需要自行遍历对象(可能需要注意一些边界问题,null、undefined 是否还收录,symbol、function 等对象是否还保留)

3,JS 存在几种原始类型?null 是对象吗?

如下:null、undefined、number、string、boolean、symbol(es6 新增)

虽然 typeof null 得到的是 object,但其实这是历史 bug,null 不是对象。

补充:
关于装包与拆包:比如使用 new Boolean() (这是装包) 的方式创建的是一个对象类型且其中带有的是 Boolean 类型的值,需要手动调用 valueOf()返回我们需要的值,字符串的话调用 toString() (这是拆包)不建议手动装包,会让开发者产生迷惑(不知道其是否为引用类型)。

4,原始类型 和 对象类型 的区别?函数参数是对象存在什么问题?

对象类型和原始类型不同的是,原始类型存储的是值,对象类型存储的是地址(指针)。
函数传参是传递对象指针的副本(针对参数类型为对象时)

5,typeof 与 instanceof 的比较

1),typeof
相对于原始(简单)类型来说除了 null 都可以显示正确的类型

typeof 1 // 'number'
typeof 'string' // 'string'
typeof undefined // 'undefined'
typeof null // 'object' 这个是 bug
typeof true // 'boolean'
typeof Symbol() // 'symbol'

相对于对象(复杂)类型,除了 function 类型,其它类型都显示为 object 类型

typeof [] // 'object'
typeof {} // 'object'
typeof console.log // 'function'

综上所述,typeof 并不能正确的判断变量的类型,若要继续使用,则需要了解使用 typeof 的劣势。

2),instanceof

[] instanceof Array  // true
{} instanceof Object // true
console.log instanceof Function  // true

可以完整的判断对象(复杂)类型,但是又没法直接判断原始(简单)类型。
PS:// 其实可以通过 Symbol.hasInstance 修改 instanceof 内部行为以达到判断原始(简单)类型,与其动手修改,不如自己手写判断方法会更好,代码封装后的可读性更好。

也可以使用万能的 Object.prototype.toString().call();

可以参考以下方式

引用自掘金:https://juejin.im/post/5aba32d9f265da239e4e1b6c

PS:instanceof 的原理:其实就是通过原型链逐个查找,如果没能查找到就返回 false。

6,防抖、节流的应用场景(一句话总结,操作频率低用防抖,高就用节流。)

引用自掘金:https://juejin.im/entry/5b1d2d54f265da6e2545bfa4

1),防抖(debounce)

解释:当持续触发事件时,一定时间段内没有再触发事件,事件处理函数才会执行一次,如果设定的时间到来之前,又一次触发了事件,就重新开始延时。

例子:LOL 英雄回城

应用情景:1,搜索栏输入文字后延迟发起异步请求。2,提交按钮的点击事件(无论按了多少次,只操作最后一次的请求)。

示例代码:

function debounce(fn, wait) {
    var timeout = null;
    return function() {
        if(timeout !== null)
                clearTimeout(timeout);
        timeout = setTimeout(fn, wait);
    }
}
// 处理函数
function handle() {
    console.log(Math.random());
}
// 滚动事件(这里使用滚动事件作演示)
window.addEventListener('scroll', debounce(handle, 1000));

2),节流(throttle)

解释:当持续触发事件时,保证一定时间段内只调用一次事件处理函数

例子:LOL 英雄技能

应用情景:1,用在比 input, keyup 更频繁触发的事件中,resize, touchmove, mousemove, scroll。

示例代码:

var throttle = function(func, delay) {
   var timer = null;
   return function() {
       var context = this;
       var args = arguments;
       if (!timer) {
           timer = setTimeout(function() {
               func.apply(context, args);
               clearTimeout(timer);
           }, delay);
       }
   }
}
function handle() {
    console.log(Math.random());
}
// 滚动事件(这里使用滚动事件作演示)
window.addEventListener('scroll', throttle(handle, 1000));

7,this 关键字的绑定规则。与 call(),bind(),apply() 的联系。

绑定规则(按优先级顺序排序):
1,new
2,使用 call,bind,apply 方法。
3,通过对象引用方法 即:obj.fun()的方式。
4,直接调用函数的方式 即:fun()

bind(obj, …args)这个方法只能绑定一次 this,无论后续调用多少次 bind()只按第一个 bind()绑定 this
call(obj, …args) 与 apply(obj, [args]) 功能一样,但传递参数不同。

8,关于类型转换

本节点参考自 掘金 <<前端面试之道>> https://juejin.im/book/5bdc715fe51d454e755f75ef 需要额外付费

MDN 比较操作符 https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Operators/Comparison_Operators

JS 中类型转换只有三种结果类型,即:boolean、number、string。

需要转换的类型 目标类型 结果
number boolean 除了 0、 -0、NaN 都为 true
string boolean 除了空串都为 true
undefined, null boolean false
引用类型 (array, object, function) boolean true
number string 值都转为字符串
boolean, function string 值都转为字符串
array string 数组内容分别用“,”拼接 如:[1,2] 输出 “1,2”
object string “[object Object]”
string number 如果能转换成 number 类型就直接转换,否则为 NaN
array number 空数组为 0,若存在一个元素,则尝试转换为 number ,其它情况一律为 NaN
null number 0
object, function number NaN

== 在类型转换是怎样的?那么 == 与 === 的区别是什么?

1),首先会判断两者类型是否相同。相同的话就是比大小了
2),类型不相同的话,那么就会进行类型转换
3),会先判断是否在对比 null 和 undefined,是的话就会返回 true
4),判断两者类型是否为 string 和 number,是的话就会将字符串转换为 number

1 == '1'
      ↓
1 ==  1

5),判断其中一方是否为 boolean,是的话就会把 boolean 转为 number 再进行判断

'1' == true
        ↓
'1' ==  1
        ↓
 1  ==  1

6),判断其中一方是否为 object 且另一方为 string、number,是的话就会把 object 转为原始类型再进行判断

'1' == { name: 'yck' }
        ↓
'1' == '[object Object]'

一张图总结
一张图总结

=== 不会进行类型转换,仅判断两者类型和值是否相同。

9,var、let、const 的区别,var 为什么会导致变量的提升。

1),let 是有自己的一个作用域的变量

2),const 是常量

3),var 也是变量,但会存在变量提升。

什么是变量提升?

如以下代码,虽然变量还没有被声明,但是我们却可以使用这个未被声明的变量,这种情况就叫做提升,并且提升的是声明。

console.log(a) // undefined
var a = 1

// 可以看作
var a
console.log(a) // undefined
a = 1

// 但是函数提升的优先级比变量高
console.log(a) // ƒ a() {}
function a() {}
var a = 1

提升的意义是(需要死记硬背):当有多个同名变量声明的时候,函数声明会覆盖其他的声明。如果有多个函数声明,则是由最后的一个函数声明覆盖之前所有的声明。

ps:以上这段话是我从各种答案里找到我所能理解的那个,所以这道题没有固定答案。

10,js 中字符串常用的方法

从首字母开始排序

MDN:https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/String

charAt(index)

concat(string2, string3[, …, stringN])(拼接多个字符串)

includes(searchString[, position])(匹配某个字符串是否包含于指定字符串)

indexOf(searchValue)(获取匹配成功字符串的下标)

match(regexp)(使用正则进行匹配,并返回结果集)

replace(regexp|substr, newSubStr|function)(使用正则或字符串 匹配某个字符串,并替换)

split(separator)(按某个“字符串”分割字符串,默认父字符串,返回结果集)

slice(beginIndex[, endIndex])(按起始或指定末尾位置切割字符串 ,value >= begin < end )

splice(start[, deleteCount[, item1[, item2[, …]]]])

trim()(去除字符串中的空白字符)

11,substr(start[, length]) 与 substring(indexStart[, indexEnd]) 的一个区别

1),substr(start, length)

参考自 MDN https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/String/substr

从某处开始,截取一串指定长度的字符串。(由于用法比较骚,官方不推荐此方法,建议使用 substring())

start 可以是正负数,特别说明负数是从右边开始倒数进行截取,若 length(可选值)<= 0 的话,直接返回空串。

var str = "abcdefghij";

console.log("(1,2): "    + str.substr(1,2));   // (1,2): bc
console.log("(-3,2): "   + str.substr(-3,2));  // (-3,2): hi
console.log("(-3): "     + str.substr(-3));    // (-3): hij
console.log("(1): "      + str.substr(1));     // (1): bcdefghij
console.log("(-20, 2): " + str.substr(-20,2)); // (-20, 2): ab
console.log("(20, 2): "  + str.substr(20,2));  // (20, 2):

2),substring(start, end)

从某处开始,到某处结束,最后截取从开始到结束所经过的一串字符串。

start,end(可选参数)如果任一参数小于 0 或为 NaN,则被当作 0。

若 start 与 end 相等则返回空串

如果任一参数大于 length,则被当作 length。

如果 indexStart 大于 indexEnd,则 substring 的执行效果就像两个参数调换了一样。

var anyString = "Mozilla";

// 输出 "Moz"
console.log(anyString.substring(0,3));
console.log(anyString.substring(3,0));
console.log(anyString.substring(3,-3));
console.log(anyString.substring(3,NaN));
console.log(anyString.substring(-2,3));
console.log(anyString.substring(NaN,3));

// 输出 "lla"
console.log(anyString.substring(4,7));
console.log(anyString.substring(7,4));

// 输出 ""
console.log(anyString.substring(4,4));

// 输出 "Mozill"
console.log(anyString.substring(0,6));

// 输出 "Mozilla"
console.log(anyString.substring(0,7));
console.log(anyString.substring(0,10));

类似的如 Array.slice() 和 Array.splice() 的区别

slice([begin[, end]]) :返回一个新的数组对象,这一对象是一个由 begin 和 end 决定的原数组的浅拷贝(包括 begin,不包括 end)。原始数组不会被改变

splice(start[, deleteCount]) :通过删除或替换现有元素或者原地添加新的元素来修改数组,并以数组形式返回被修改的内容。此方法会改变原数组

12,关于前端常见的安全问题与解决方法

1),XSS(跨站脚本) 攻击
就是攻击者想尽一切办法将可以执行的代码注入到网页中。

一般的防范方法:(主要还是要阻止 script 标签的渲染)
转移字符:转义输入输出的内容,对于引号、尖括号、斜杠进行转义

function escape(str) {
  str = str.replace(/&/g, '&amp;')
  str = str.replace(/</g, '&lt;')
  str = str.replace(/>/g, '&gt;')
  str = str.replace(/"/g, '&quto;')
  str = str.replace(/'/g, '&#39;')
  str = str.replace(/`/g, '&#96;')
  str = str.replace(/\//g, '&#x2F;')
  return str
}

// -> &lt;script&gt;alert(1)&lt;&#x2F;script&gt;
escape('<script>alert(1)</script>')

此方法不适合处理富文本的格式,这里需要特别针对 script 标签的处理即可。

CSP:就是为网页设置一系列的安全白名单,开发者明确告诉浏览器哪些外部资源可以加载和执行。
对于这种方式来说,只要开发者配置了正确的规则,那么即使网站存在漏洞,攻击者也不能执行它的攻击代码,并且 CSP 的兼容性也不错。

代码参考如下:

// 使用 http header
Content-Security-Policy: default-src https:

// 使用 meta 标签
<meta http-equiv="Content-Security-Policy" content="default-src https:">

2),CSRF(跨站请求伪造) 攻击
原理就是攻击者构造出一个后端请求地址,诱导用户点击或者通过某些途径自动发起请求。如果用户是在登录状态下的话,正版的后端就以为是用户在操作,黑客从而进行相应的逻辑。

一个典型的 CSRF 攻击有着如下的流程:

受害者登录 a.com,并保留了登录凭证(Cookie)。
攻击者引诱受害者访问了 b.com。(或者植入一张 img 图片)
b.com 向 a.com 发送了一个请求:a.com/act=xx。
a.com 接收到请求后,对请求进行验证,并确认是受害者的凭证,误以为是受害者自己发送的请求。
a.com 以受害者的名义执行了 act=xx。
攻击完成,攻击者在受害者不知情的情况下,冒充受害者,让 a.com 执行了自己定义的操作。

防范方法:
1,Get 请求不对数据进行修改 (尽量满足 restful api 规则就行了)
2,不让第三方网站访问到用户 Cookie (或者不用 cookie,参照第 4 条)
3,阻止第三方网站请求接口 (相当于是跨域限制,或者设置白名单)
4,请求时附带验证信息,比如验证码或者 Token

3),中间人攻击
中间人攻击是攻击方同时与服务端和客户端建立起了连接,并让对方认为连接是安全的,但是实际上整个通信过程都被攻击者控制了。攻击者不仅能获得双方的通信信息,还能修改通信信息。(其实还是自己主动建立的不可靠连接)

防范方法:
1,通常来说不建议使用公共的 Wi-Fi 就 ok 了

4),点击劫持
是一种视觉欺骗的攻击手段。攻击者将需要攻击的网站通过 iframe 嵌套的方式嵌入自己的网页中,并将 iframe 设置为透明,在页面中透出一个按钮诱导用户点击。

防范方法:
1,服务端添加 X-FRAME-OPTIONS header
DENY,表示页面不允许通过 iframe 的方式展示
SAMEORIGIN,表示页面可以在相同域名下通过 iframe 的方式展示
ALLOW-FROM,表示页面可以在指定来源的 iframe 中展示

13,数组的常用方法

按字母开头进行排序

concat(value1[, value2[, …[, valueN]]]) 多个数组拼接

filter() 过滤元素

flat() 数组扁平化

forEach(callback) 遍历

map(callback) 遍历

pop()(删除最后一个元素)

push() (末尾插入一个元素)

shift()(删除第一个元素)

sort()(冒泡排序)

splice(start, deleteCount)(从某个位置删除指定个数元素)

14,对 es6 中 Proxy 的了解

是从 Vue3.0 中认识的,也是可以用于创建响应式对象,相对于 es5 中的 Object.defineProperty() 可以监听到对象属性的添加和删除。

缺点是:兼容性较差,不支持 IE ,也没有相应的 polyfill 方法。

15,闭包

(菜鸡专用版本)
函数 A 内部有一个函数 B,函数 B 可以访问到函数 A 中的变量,那么函数 B 就是闭包。

16,JS 中的原型继承和 Class 继承

注意:首先 JS 的继承比较特别,不像是面向对象语言一样能直接理解,可以从以下这篇文章了解下 JS 的继承的设计和思想。

阮一峰的网络日志: http://www.ruanyifeng.com/blog/2011/06/designing_ideas_of_inheritance_mechanism_in_javascript.html

先总结 JS 的继承的设计和思想:首先 JS 没有 “类” 这一个概念,更没有 “构造函数” 的概念。而是用 “函数(function)” 对象去描述一个“类”,而“函数(function)”内容部分则作为构造函数。在这里就可以实现一个简单的类与对象系统了,但是这还是不够的,比如:构造函数内容不能共享而导致的性能浪费。所以就引入 “prototype” 对象,以供数据共享。一句话:“需要共享数据就写在 prototype,不需要共享的就写在构造方法里”

关于原生继承:

1,组合继承
function Parent(value) {
  this.val = value
}
Parent.prototype.getValue = function() {
  console.log(this.val)
}
function Child(value) {
  Parent.call(this, value)
}
Child.prototype = new Parent()

const child = new Child(1)

child.getValue() // 1
child instanceof Parent // true

以上继承的方式核心是在子类的构造函数中通过 Parent.call(this) 继承父类的属性,然后改变子类的原型为 new Parent() 来继承父类的函数。

这种继承方式优点在于构造函数可以传参,不会与父类引用属性共享,可以复用父类的函数,但是也存在一个缺点就是在继承父类函数的时候调用了父类构造函数,导致子类的原型上多了不需要的父类属性,存在内存上的浪费。

2,寄生组合继承
function Parent(value) {
  this.val = value
}
Parent.prototype.getValue = function() {
  console.log(this.val)
}

function Child(value) {
  Parent.call(this, value)
}
Child.prototype = Object.create(Parent.prototype, {
  constructor: {
    value: Child,
    enumerable: false,
    writable: true,
    configurable: true
  }
})
// 以上代码可以转换成以下代码去理解
Child.prototype = Parent.prototype
Child.prototype.constructor = Child

const child = new Child(1)

child.getValue() // 1
child instanceof Parent // true

以上继承实现的核心就是将父类的原型赋值给了子类,并且将构造函数设置为子类,这样既解决了无用的父类属性问题,还能正确的找到子类的构造函数。

3,class 继承(使用 OOP 的方式编写,更好理解)
class Parent {
  constructor(value) {
    this.val = value
  }
  getValue() {
    console.log(this.val)
  }
}
class Child extends Parent {
  constructor(value) {
    super(value)
  }
}
let child = new Child(1)
child.getValue() // 1
child instanceof Parent // true

class 实现继承的核心在于使用 extends 表明继承自哪个父类,并且在子类构造函数中必须调用 super,因为这段代码可以看成 Parent.call(this, value)。

17,JS 异步编程

1,回调函数 (callback)

存在问题:

1,嵌套函数存在耦合性,一旦有所改动,就会牵一发而动全身(产生回调地狱,找需要改的地方,需要从头开始找)

2,嵌套函数一多,就很难处理错误(由于没法捕获错误,不知道哪里出错)

3,不能使用 try catch 捕获错误,不能直接 return (特指异步回调函数中)

2,Promise

有三种状态:
等待中(pending)
完成了 (resolved)
拒绝了(rejected)

特点:

1,这个承诺一旦从等待状态变成为其他状态就永远不能更改状态了,也就是说一旦状态变为 resolved 后,就不能再次改变

2,Promise 实现了链式调用,也就是说每次调用 then 之后返回的都是一个 Promise,并且是一个全新的 Promise,原因也是因为状态不可变。如果你在 then 中 使用了 return,那么 return 的值会被 Promise.resolve() 包装

Promise.resolve(1)
  .then(res => {
    console.log(res) // => 1
    return 2 // 包装成 Promise.resolve(2)
  })
  .then(res => {
    console.log(res) // => 2
  })

3,当我们在构造 Promise 的时候,构造函数内部的代码是立即执行的(同步)

new Promise((resolve, reject) => {
  console.log('new Promise')
  resolve('success')
})
console.log('finish')
// new Promise -> finifsh

延申两个 api :

1,Promise.all([promise]) (全部异步方法一起跑,即并发)
如果都成功即调用 then,否则条用 catch,且只捕获第一个异常

2,Promise.race([promise]) (按顺序组个调用异步方法)
如果都成功即调用 then,否则条用 catch,且只捕获第一个异常

缺点:
1,无法取消。(即无法在某个 then() 链中取消,会一直执行所有的 then() 方法)

2,缺少 finally 方法,后期虽然补上,但兼容性差,需要自行 polyfill。

3,async、await(异步编程终极解决方案)

特点:

1,一个函数如果加上 async ,那么该函数就会返回一个 Promise

async function test() {
  return "1"
}
console.log(test()) // -> Promise {<resolved>: "1"}

// 相当于
const task = new Promise((resolve, reject) => {
  return "1"
})

2,async 就是将函数返回值使用 Promise.resolve() 包裹了下,和 then 中处理返回值一样,并且 await 只能配套 async 使用

async function test() {
  let value = await sleep()
}

// 相当于
new Promise((resolve, reject) => {
  new Promise((resolve, reject) => {
    // 调用 sleep() 构造方法
    return value;
  })
  .then(value => {
    // value
  })
})

3,减少使用 then 调用链,让代码更符合逻辑直觉。

4,generator (目前对我来说还很难理解,先跳过)

TBD

5,定时函数(好像没啥好讲的)

1,setTimeout()

2,setInterval()

3,requestAnimationFrame() // 每 16.6 毫秒执行一次的自带节流方法的计时器

18,使用 Vuex 时,如果遇上刷新,该如何保存这些对象?

可以监听全局刷新方法,再进行 Vuex 对象进行持久化本地保存。

window.onbeforeunload = function (event) {
  // Vuex 处理
  // 以下两行代码必须有其中一行
  event.returnValue = "";
  return "";
}

也可以为 Vuex 对象添加一些插件如:vuex-persistedstate

18,如何手动实现 JS 中的一些关键字、方法?

1,手写一个 new 关键字

参考自 https://github.com/KieSun/Dream/issues/14

new 关键字运行过程如下:

1,new 操作符会返回一个对象,所以我们需要在内部创建一个对象

2,这个对象,也就是构造函数中的 this,可以访问到挂载在 this 上的任意属性

3,这个对象可以访问到构造函数原型上的属性,所以需要将对象与构造函数链接起来

4,返回原始值需要忽略,返回对象需要正常处理

翻译成代码如下:

function create(Constructor, ...args) {
  // 创建一个空对象,并将对象的__proto__指向构造函数的prototype 这里我两步一起做了
    const obj = Object.create(Constructor.prototype);
    let result = Constructor.apply(obj, args);
    return result instanceof Object ? result : obj;
}

19,如何理解原型链?

需要先从原型讲起。

一个对象:{ a: 1 }

一个数组:[ 1 ]

一个方法对象:Fun 代指 function Fun() {}

好像都有一些默认方法,比如:value()、toString() 等。。。

在浏览器上使用 log 打印可以看到都有 proto 这个属性(其实就是 prototype ),其实就是一个 object 对象。里面有一个 constructor 对象,里面有个 prototype 对象还是指回之前的 proto 这个属性。

像之前的 value() 或 toString() 就是通过原型链的方式找到的对应的方法。

下图所示就是原型怎么串成一条链。

原型链示例

总结:

所有对象都有一个属性 proto 指向一个对象,也就是原型

每个对象的原型都可以通过 constructor 找到构造函数,构造函数也可以通过 prototype 找到原型

所有函数都可以通过 proto 找到 Function 对象

所有对象都可以通过 proto 找到 Object 对象

对象之间通过 proto 连接起来,这样称之为原型链。当前对象上不存在的属性可以通过原型链一层层往上查找,直到顶层 Object 对象,再往上就是 null 了

20,JS 中 “静态方法”、“公有方法”、“私有方法” 的区别

为什么在 Vuejs 的源码中,有些方法可以在实例中调用,有些不可以?

21,Object.create(null) 有什么作用?(需要配合遍历对象的 for in 一起答,Vuejs 源码也有用到)

可以不用 hasOwnProperty()

22,各种遍历方式的区别,性能差异。

forEach

无法 break、return 不改变原数组

map

无法 break、return 会改变原数组

for in ES5(遍历 key)

可以 break、return 需要使用 obj.hasOwnProperty() 判断该 key 值是否为属于自己。

for of ES6 新增(遍历 value)

可以 break、return 。无法遍历 Object 对象,只是强化 for in 的作用。

23,Object 中的常用方法。

key、defineProperty、asign、

24,0.1 + 0.2 !== 0.3 怎么解决?

核心就是计算出两个浮点数最大的小数长度,比如说 0.1 + 0.22 的小数最大长度为 2,然后两数乘上 10 的 2 次幂再相加得出数字 32,然后除以 10 的 2 次幂即可得出正确答案 0.32。

25,对 js 中表达式和语句的理解?

26,常见的 HTTP Code。

参考 MDN https://developer.mozilla.org/zh-CN/docs/Web/HTTP/Status

27,GET 和 POST 的区别?

特指 Ajax 中的:

get 没有 body (不代表不能上传,HTTP 协议没有这个规定,是浏览器的没能这么做,在 CURL 中是可以带上 body)

安全性:放屁!!

从语义上(REST)说:

get 是用作获取任意资源的方法。不应该有副作用(比如:get 后是新增一个订单。这种是不应该存在的行为)

post 是用作创建数据的方法。比如:新增一个订单。

事件循环

流程总结

  1. 执行一个宏任务(一般一开始是整体代码(script)),如果没有可选的宏任务,则直接处理微任务
  2. 执行过程中如果遇到微任务,就将它添加到微任务的任务队列中
  3. 执行过程中如果遇到宏任务,就将它添加到宏任务的任务队列中
  4. 执行一个宏任务完成之后,就需要检测微任务队列有没有需要执行的任务,有的话,全部执行,没有的话,进入下一步
  5. 检查渲染,然后 GUI 线程接管渲染,进行浏览器渲染
  6. 渲染完毕后,JS 线程继续接管,开始下一个宏任务…(循环上面的步骤)

常见的几种任务:

宏任务:script 标签的代码块执行时、setTimeout、setInterval、window.postMessage(浏览器窗口的消息交互)、事件监听回调(addEventListener)、setImmediate(Node.js 环境)

微任务:Promise.resolve()、Promise.reject()、MutationObserver

ps: await 回阻塞后面的代码(即加入微任务队列)

async function fn1 (){
 console.log(1)
 await fn2()
 console.log(2)
}
async function fn2 (){
 console.log('fn2')
}
fn1()
console.log(3)

// 输出结果:1 fn2 3 2

参考自

https://juejin.cn/post/6880419772127772679

浏览器输入 url 后回车到页面渲染的过程中都经历了些什么?

  1. DNS 解析

将域名转换为 IP 地址。

先检查浏览器缓存、系统缓存、路由器缓存,然后是 ISP 的 DNS 缓存。

如果都没有,则向根域名服务器发起递归查询。

  1. TCP 连接

与目标 IP 地址建立 TCP 三次握手。

如果是 HTTPS,还需要进行 TLS 握手。

  1. 发送 HTTP 请求

浏览器构造 HTTP 请求报文。

添加请求头(如 User-Agent、Cookie 等)。

  1. 服务器处理请求并响应

服务器接收请求,进行处理。

生成 HTTP 响应,可能包含状态码、响应头和响应体。

  1. 浏览器接收响应

根据状态码判断请求是否成功。

处理响应头(如设置 Cookie、重定向等)。

  1. 渲染页面

解析 HTML,构建 DOM 树。

解析 CSS,构建 CSSOM 树。

将 DOM 和 CSSOM 合并成渲染树。

布局(Layout):计算每个节点的精确位置和大小。

绘制(Paint):将渲染树转换成屏幕上的实际像素。

  1. 执行 JavaScript

解析并执行 JavaScript 代码。

可能会修改 DOM、CSSOM,触发重排(reflow)和重绘(repaint)。

  1. 加载其他资源

下载并处理图片、视频、字体等资源。

可能会触发新的 HTTP 请求。

  1. 页面交互

绑定事件处理器。

响应用户操作。

Websocket 的优缺点

优点:

  1. 实时双向通信。
  2. 减少开销,减少开销。(不需要像 HTTP 那样在每次请求时都进行额外的 HTTP 头部开销。这样可以显著减少数据传输的开销,尤其是在频繁传输小数据包的情况下)
  3. 低延迟。
  4. 保持连接。

缺点:

  1. 服务器负载(连接长时间保持打开状态,服务器需要管理大量的并发连接,这可能会增加服务器的负载。)

Webpack 相关

  1. webpack 的加载方式

Vite 相关

  1. vite 的加载方式

  2. 开发环境和生产环境不一致怎么解决

Typescript 相关

  1. type 和 interface 的区别

语法

type 后面要通过 = 进行赋值,而 interface 不需要。

继承

interface 可以通过 extends 关键字继承其他接口或类。

type 可以使用交叉类型(&)来组合类型。

扩展性

interface 可以被多次声明并自动合并。

type 不能重复声明。

关于 Vue.js

1,关于 v-if 与 v-show 的区别

v-show 仅仅是切换 dom 中 css 的 display 属性以控制组件是否可视,但是 v-show 的值是 true or false 组件都会被挂载(初次挂载有一定的性能成本),即会调用 mounted()方法。

v-if 在组件的意义上才是条件渲染,因为它会确保在切换过程中条件块内的事件监听器和子组件适当地被销毁和重建。
v-if 也是惰性的:如果在初始渲染时条件为假,则什么也不做——直到条件第一次变为真时,才会开始渲染条件块。

摘抄自官方文档:
相比之下,v-show 就简单得多——不管初始条件是什么,元素总是会被渲染,并且只是简单地基于 CSS 进行切换。

一般来说,v-if 有更高的切换开销,而 v-show 有更高的初始渲染开销。因此,如果需要非常频繁地切换,则使用 v-show 较好;如果在运行时条件很少改变,则使用 v-if 较好。

2,关于自定义指令的应用场景和其自身的使用方式

本人我主要做的项目是后台类型的项目,比较常用到的是全局加载 loading 符号(v-loading)。
以及角色权限判断的 v-permission。
自定义指令中的:bind(),inserted(),update() 三个方法的区别
bind():只调用一次,指令第一次绑定到元素时调用。在这里可以进行一次性的初始化设置。

inserted():顾名思义就是 dom 插入到父节点时才会被调用。

update():vnode 更新后调用。

3,指出 Vue.js 中得所有生命周期,以及各个时期都在处理些什么事?(todo:以后补上各个生命周期相应的处理细节)

1),beforeCreate()
钩子函数调用的时候,是获取不到 props 或者 data 中的数据的,因为这些数据的初始化都在 initState 中。

2),created()
在这一步的时候已经可以访问到之前不能访问到的数据(props,data),但是这时候组件还没被挂载,所以是不可见的。

3),beforeMount()
开始创建 VDOM,最后执行 mounted 钩子,并将 VDOM 渲染为真实 DOM 并且渲染数据。组件中如果有子组件的话,会递归挂载子组件,只有当所有子组件全部挂载完毕,才会执行根组件的挂载钩子。

4),mounted()
上述动作完成后就会调用该回调

5),beforeUpdate()
数据更新前调用

6),updated()
数据更新后调用

// 如果使用了 keep-alive 组件会触发 activated 和 deactivated 方法
activated()
包裹的组件在切换时不会进行销毁,而是缓存到内存中并执行 deactivated 钩子函数,命中缓存渲染后会执行 actived 钩子函数。

deactivated()
如上

beforeDestory()
销毁组件前调用。适合移除事件、定时器等等,否则可能会引起内存泄露的问题。若有子组件也会递归销毁子组件。

destroyed()
所有组件销毁完毕调用

参考自掘金小册 <<前端面试之道>> https://juejin.im/book/5bdc715fe51d454e755f75ef

路由钩子:

全局:

beforeEach、afterEach

路由独享:

const router = new VueRouter({
  routes: [
    {
      path: '/foo',
      component: Foo,
      beforeEnter: (to, from, next) => {
        // ...
      }
    }
  ]
})

组件内钩子:

beforeRouteEnter(to, from, next) {
// 在渲染该组件的对应路由被 confirm 前调用
// 不!能!获取组件实例 this
// 因为当守卫执行前,组件实例还没被创建
},
beforeRouteUpdate(to, from, next) {
// 在当前路由改变,但是该组件被复用时调用
// 举例来说,对于一个带有动态参数的路径 /foo/:id,在 /foo/1 和 /foo/2 之间跳转的时候,
// 由于会渲染同样的 Foo 组件,因此组件实例会被复用。而这个钩子就会在这个情况下被调用。
// 可以访问组件实例 this
},
beforeRouteLeave(to, from, next) {
// 导航离开该组件的对应路由时调用
// 可以访问组件实例 this
}

4,各式各样的组件通信方式

1,父与子
1),通过 props 属性传递
2),在父组件中使用$refs[]获取子组件的实例并赋值以达到通信效果
3),在父组件中使用$children[],根据$options.name去查找相应的组件(相对$refs[]而言,$children 操作更麻烦一些)

2,子与父
1),使用$emit/v-on 向上冒泡事件传递数据(父组件可以使用语法糖.sync以进一步优化代码量)
2),使用$parent,用法和上面的$children 一样。

3,兄弟(平行)
1),推荐使用通用方式

4,跨级(👴 与孙)
1),provide/inject(数据为不可响应,如果不是封装通用组件,不建议使用此方法)
2),推荐使用通用方式

5,通用方式(适合于任何类型关系的组件)
1),外部 JS 文件(自行实现一个轻量级的 flux 架构,适合一些小项目)
2),使用 Vuex(方便调试,算是万金油解决方案)
3),local/sessionStorage 持久化数据到本地
4),cookie 个人不建议使用
5),路由传参 query 和 params

5,watch/computed/methods 的区别

1),watch 仅仅只是监听指定的属性(props,data)的值,一旦发生改变就会触发回调。
2),computed 用作计算一系列的变量并返回结果值,若这一系列的变量未发生修改,则缓存该结果值。直到这一系列的变量中发生改变。
3),methods 和常规的 JS 方法一样,和 computed 相比,没有缓存结果值的功能。

6,$route 和 $router 的区别

1),$route 是路由信息对象,其中包含了常用的path、query、params、name等等路由信息。
2),$router 是路由对象,其中包含了路由的操作方法 push()、go()、replace(),以及路由守卫方法以及钩子函数 beforeEach(),afterEach()等等。

7,相比 JQuery.js ,Vue.js 有什么不同?

1),JQuery 是专注于 View(视图)层,通过手动操作 DOM 以达到逻辑渲染的目的。而 Vue 更关注于 Model(数据)层,通过绑定数据的绑定来操作 DOM。最终简化 DOM 的操作,也就是数据驱动。

2),Vue 自身带有组件化的自然分层思想,能使得项目中不同的模块的功能清晰化,这提高了开发效率,也方便重复利用,更降低维护成本。

8,Vue 中的 key 属性有什么作用?以及使用 index 作为 key 时的弊端?(todo: 不是特别了解但感觉挺重要的,以后补上。)

9,使用 Vue-cli 所构建的单页应用(SPA)优劣势

优势:
1),用户体验好、内容的改变不需要重新加载整个页面(提升页面流畅感)。
2),相对 SSR 对服务器压力小。(不需要服务端对页面进行渲染)
3),前后端分离,能更有效的分工,提高开发效率。

劣势:
1),SEO 不友好
2),首次加载慢
3),学习成本提高(需要学习前端工程化,以及各式新框架如 vue、vue-router、vuex 等等)

10,组件封装需要注意哪些点

组件设计:
1),确定业务的范围,适当的对业务拆分。
2),定好 props,尽量使用 object 类型配置,能较好的控制数据的类型和默认值。
3),需要确定父子组件间的通信方式。(是否使用自定义事件以及$parent、$children 或者走全局数据)
4),在适当的空位留有 slot 有意想不到的奇效。
关于特殊的递归组件:
1),需要确定好一个递归结束条件,防止递归溢出。
2),如果是带有选项的组件,建议通过外部文件或 vuex 来保存选中的数据(这样可以减少很多不必要的递归操作 element-ui 中的 el-tree 也是这么做)

11,项目优化方案

1),打包优化 按需加载(跟着官方文档走即可)+异步组件或懒路由(如果组件体积不大,建议还是别用,这只会徒增 http 请求,不划算!)+静态资源和依赖挂载到 CDN+gzip(需要后端或运维配合)+关闭 productionSourceMap
实操部分:
gzip:在 vue-cli4 的项目下,需要安装 compression-webpack-plugin 这个开发依赖
在 vue.config.js 文件下

module.exports = {
    // gzip压缩
    const CompressionWebpackPlugin = require("compression-webpack-plugin");
    configureWebpack: config => {
    // 生产环境相关配置
    if (process.env.NODE_ENV !== "development") {
      //gzip压缩
      const productionGzipExtensions = ["html", "js", "css"];
      config.plugins.push(
        new CompressionWebpackPlugin({
          filename: "[path].gz[query]",
          algorithm: "gzip",
          test: new RegExp("\\.(" + productionGzipExtensions.join("|") + ")$"),
          threshold: 10240, // 只有大小大于该值的资源会被处理 10240
          minRatio: 0.8, // 只有压缩率小于这个值的资源才会被处理
          deleteOriginalAssets: false // 删除原文件
        })
      );
    }
  }
}

路由懒加载:需要安装 @babel/plugin-syntax-dynamic-import 这个开发依赖
在 babel.config.js 文件中进行配置

module.exports = {
  plugins: ["@babel/plugin-syntax-dynamic-import"]
}

在路由文件中这么使用

正确用法:
// import()中的注释是控制打包后文件名称,是可选项
const login = () => import(/* webpackChunkName: "Login" */ "...")
const routes = [
    {
        path: "/Login",
        name: "Login",
        component: Login
    }
]
错误用法(不会生效):
const routes = [
    {
        path: "/Login",
        name: "Login",
        component: () => import(/* webpackChunkName: "Login" */ "...")
    }
]

2),事件委托(长列表管用)为的是减少重复的监听器数量从而优化内存的使用

参考自 github https://github.com/Advanced-Frontend/Daily-Interview-Question/issues/145#issuecomment-504857099

原理:DOM 事件流中的 事件捕获(事件从父到子) 冒泡事件(事件从子到父,默认行为)

演示:(本实例代码使用 vue 框架)

// 使用事件代理(将事件处理程序代理到父节点,减少内存占用率)
<div  @click="handleClick">
  <span
    v-for="(item,index) of 100000"
    :key="index">
    {{item}}
  </span>
</div>

3),及时注销一些不用的局部或全局事件、计时器等会长期占用内存的方法。

可以在 beforeDestory() 中销毁掉全局事件。

适当的时候可以使用 节流、防抖 节省一些不必要的,高重复性的动作。

4),适当分包。

由于 webpack 一般会把 node_modules 下的库统一一起打包,会导致后期一旦添加新包重新打包会修改 hash 值,会让缓存失效,所以需要分包。

将 Element-ui 这种体积大的包且几乎不需要更新的包单独拆分。

12,Vue.extend()的作用与应用场景

Vue.extend(options) 返回一个 VueComponent(构造)方法,通常与 $mount() 一起使用挂载组件到DOM上。
示例(类似element-ui的$message):
// options 可以是一个引入的 .vue 的组件

let Component = Vue.extend({
  template: '<div>test</div>'
})
// 挂载到 #app 上
new Component().$mount('#app')

13,slot 与 slot-scope 的区别。

1),slot
是一个标签,用作父组件的内容分发。即:将父组件的特定内容分发到子组件的某个地方。

至于是插到哪里?是通过具名的形式进行区分,比如:

如果没有 name 属性(或 name="default"),则为默认插槽,则插在 以下两个节点。反之,你懂的
<slot></slot>
<slot name="default"></slot>

ps:如果子组件没有slot标签,则部分发任何内容。

2),slot-scope
从字义上看是“作用域插槽”。

我的理解是:让插槽内容能够访问子组件中才有的数据(即:父组件将数据传给子组件,子组件再将数据传到给父组件去使用)

示例:

<current-user>
  <template v-slot:default="slotProps">
    {{ slotProps.user.firstName }}
  </template>
</current-user>

常用场景(优化三个层级以上的组件层级的方案):
评论列表里会有一些按钮(发送图片,选择 Emoji ,点赞等等),如果统一封装到一个组件里就会如下:

土方法:将 props 和 自定义事件, 全都耦合在组件上,后续也不利于该组件的复用。

<post
  :data="[...]"
  @likeClick="onLikeClick"
  @postImgClick="onPostImgClick"
></post>

解决方案(利用作用域插槽):

<post-list :data="[...]">
  <template v-slot:default="scope">
    <like-btn @click="onLikeClick(scope.row)"></like-btn>
    <post-img-btn @click="onPostImgClick(scope.row)"></post-img-btn>
  </template>
</post-list>

好处:既能解耦业务,也提高了基础组件的利用率

14,v-if 与 v-for 的优先级问题

首先在同一个标签里 v-for 的优先级比 v-if 高,这导致这两个指令在同一个标签中使用会使 v-if 判断表达式重复执行。

如果是有条件地跳过循环的执行,那么可以将 v-if 置于外层元素(这样就可以避免多次执行判断表达式)。如下:

<template v-if="...">
  <div v-for="">
  </div>
</template>

15,你对 Vue 3.0 的了解

1,使用 es6 的 Proxy 代替 es5 的 Object.defineProperties() 以创建响应式对象。(提高性能)
2,新增了 Composition(组合) API

灵活的逻辑组合与复用

混入(mixin) 将不再作为推荐使用, Composition API 可以实现更灵活且无副作用的复用代码。

3,Tree shaking

可以将无用模块“剪辑”,仅打包需要的(比如 v-model,,用不到就不会打包)。

4,更好的 TypeScript 支持

配合 VSCode 可以达到自动的类型定义提示

16,Vue 组件中的属性如何继承(如果不会,那就直接答手动传值吧。应该比 I don’t know 要好)

在子组件使用 $attrs 属性获取父组件的传递过来的非 Props 的 Attributes。再通过 v-bind=”$attrs” 绑定到下一个子组件上从而实现 props 继承。

ps:题外话,还可以给组件的 inheritAttrs 设置为 false,防止一些父组件的无用 props 属性继承到子组件上。

17,v-for 中 in 和 of 关键字有什么差别?

for in 遍历 object 对象的 key 对应参数是 value, key, index。

for of 遍历 array 数组的 value 对应 item, index

18,vue-router 如果提供了 path,params 会被忽略,为什么?

我 TM 嗨 知道那个是我要替换的参数?如:”/a/b/c” 那个是要替换的参数?

所以建议不要直接用 path 切换路由,建议用具名 name 去切换路由。

关于微信小程序的问题

1,阐述微信小程序的生命周期

1),onLoad():页面创建时执行
2),onReady():页面渲染完毕
3),onHide():小程序切换到后台
4),onShow():小程序从后台切换到前台(即打开小程序显示在手机屏幕上)
5),onUnload():页面销毁时执行
以及一些不常见的生命周期方法……
PS:如果是使用 uni-app 的话要注意以上的是页面生命周期方法,而组件生命周期则为 Vue 组件的生命周期。

2,小程序项目优化方案

1),将静态资源(图片,iconfont)通过 CDN 的方式加载。// 七牛,阿里的 iconfont 带有 CDN 功能
2),图片资源尽量压缩,提高加载速度。
3),如果页面需要做比较多的手势操作(如:电影院选座 UI)建议使用 web-view 的方式去加载(能使用成熟的框架,且性能更好)。
4),长列表渲染需要惰性加载(监听滑到底部的事件,再一步步渲染)。
5),尽量使用 CSS 动画,JS 定时器动画有一定机会掉帧(在 JS 动画过程中调用异步方法,event loop 会导致时间间隔拉长导致损失特定帧的情况)。
6),控制包大小(勾选开发者工具中“上传代码时自动压缩混淆代码”选项,定时管理项目,及时清理无用的代码和资源文件)。
7),提前请求,减少白屏时间(在 onLoad()里进行异步请求)。

关于 CSS 部分:flex、grid 常见布局

1,css 的盒子模型

主要是考到 box-sizing 的这个 css 属性,以下将分两种情况去作一个简短总结

1,content-box(标准盒子模型)

属性 width、height 只包含内容 content,不包含 padding 和 border。

2,border-box(IE 的盒子模型)

属性 width、height 包含内容 content + padding + border。

2,如何做到垂直居中?

注意:这里的兼容性只兼容到 IE11,chrome49。(尽可能容易理解和使用,不去摘抄网上晦涩难懂的 css 代码)
1),已知盒子的尺寸

#box {
    width: 300px;
    height: 300px;
    background: #ddd;
}
#child {
    width: 150px;
    height: 100px;
    background: orange;
    position: relative;
    top: calc(50% - 50px);
}

2),不知道盒子自身的尺寸

#box {
    width: 100%;
    height: 300px;
    background: #ddd;
}
#child {
    background: orange;
    position: relative;
    /* 父元素的高度一半 */
    top: 50%;
    /* 自身高度的一半 */
    transform: translate(0, -50%);
}

3),利用绝对布局将盒子垂直居中以及水平居中(使用场景不多)

#box {
    width: 300px;
    height: 300px;
    background: #ddd;
}
#child {
    width: 200px;
    height: 100px;
    background: orange;
    position: absolute;
    /*  */
    /* top 与 bottom 必须相同,值随意填即可 */
    top: 0;
    bottom: 0;
    /* 居中是靠 margin 自动分配实现的 */
    margin: auto;
}

4),flex 布局实现垂直居中(常用)

#box {
    width: 300px;
    height: 300px;
    background: #ddd;
    display: flex;
    align-items: center;
}
#child {
    /* 内容尺寸可以不固定 */
    /* width: 200px;
    height: 100px; */
    background: orange;
}

3,如何图片在固定空间里按照片的比例尽可能展示?

1),兼容性强的方法:(ie7 及以上)

.parent {
  width: 100px;
  height: 100px;
}

.img {
  widht: auto;
  height: auto;
  max-width: 100%;
  max-height: 100%;
}

2),使用 background-size 属性中 contain 值:(将图片作为背景,直接使用一个标签即可)

width: 100px;
height: 100px;
background: url('image') no-repeat;
background-size: contain;

3),标签

object-fit: contain;

4,回流与重绘 (Reflow & Repaint)

一句话:回流必将引起重绘,重绘不一定会引起回流。

HR 或 BOSS 面试的应对技巧

如何自我介绍

综合各种情况应该这么回答:

你好!我是 XXX,想要应聘贵公司的前端工程师的岗位。我目前已有 3 年的工作经验。我在上家公司主要是做和政府相关的自研项目。我擅长的技术栈有 vue.js uni-app apicloud 而且也对 node.js 的服务端开发如:koa2.js egg.js nest.js 也有一些开发经验。目前多平台如:pc web,mobile web,小程序,app,桌面端,都有实际的开发成果。在开发工作上能根据 UI 设计师的要求尽可能还原设计稿,如果现成的组件库不能达到设计稿的要求,我还能自行动手造轮子重新设计组件。比如(递归组件,地图板块,选座,月历)之前有带过两人前端团队一起完成项目,积累了一些多人协作经验,平时也会常和项目组内的成员沟通(如 pm,be)尽可能负责好每一个项目。这就是我目前的一个大概情况,谢谢!

项目的介绍

为什么来异地发展(因为我要去深圳)

喜欢深圳的 IT 氛围,可以很容易就接触到许多新鲜事物,而且深圳年轻人居多,可以认识到一些志同道合的朋友。以及较好行业待遇和职业发展。可以看清自身的水平并找到属于自己的发展方向。

期望待遇与目前待遇(认真你就是索嗨啦!)

这里还是要看你面试表现再做决定,如果感觉面试表现不错,就可以适当提高以防 HR 压价。(但是遇到疫情,我只好降低自身期望,一般是最小值 + 1k 左右)

关于目前待遇,尽量和期望薪酬尽可能相近(大胆点,你懂的)

职业规划(很 NT 的一个问题)

只能按照自己喜欢的方向回答:(比如我自己是喜欢大前端,即:多端。当然还是要自行去润色润色。)

目前主要是往大前端技术的方向发展,对多端前端开发都想深入发展。但是也会纵观全栈,这不仅能提升自己对前后端开发的理解,也能提升自身的综合竞争力。待我工作经验足够丰富,我希望自己能够成为一名技术型的 leader ,去带领公司的技术团队为创造更多的价值。

学习计划

比较好的回答方式:
(需要分两种情况,进度是否很赶?需要快速且高效的学习。内在的不断补充深层次的学习,效率和速度较迟缓,但对自身的未来发展提升较大。总之要让老板觉得你的学习是对公司有用且能有效率的完成。)

如果是要学习和公司项目相关的知识我可能通过购买视频以及一些总结类的付费知识进行快速的入门,并通过多次练习训练尽可能清楚整个知识脉络,最后根据公司项目需要的知识点进行实践,最后将成果应用到项目中。如果是平时私下会更多的往深度方向进行学习,比如:框架内部运行方式、设计模式、语言的原理。主要是通过购买书籍的方式汲取知识,虽然短期的作用不大,但是未来一定会对公司和自己技术发展发挥重大作用。

点解要着草(别说前东家的坏话)

感觉在上家公司遇到自身技术的天花板,缺少提升的空间。工作内容的重复性较高。(经常有新项目)同时对在大城市工作充满了向往。

“你有什么问题”(很重要!!!如果你不想入职后就想溜了的话就要在这个环节问清楚你需要了解的东西)

1,是否有五险一金?什么时候交?(最好入职就交)

2,什么时候发工资(可以判断公司靠不靠谱)

3,项目代码管理(重要!!!不想被代码恶心的话,就要问清楚了,尽量不要让面试官回答 yes or no,尽可能得到一些详情)

1,项目历时多久?(经手的人有多少?尽量避开超过两年的陈年项目,多半代码管理很差的)

2,有代码规范吗?(怎么实施的?)

3,主要的工作内容是什么?(是做什么项目?)

未完待续。。。

4,上下班的时间,中午能休息多久(问 HR 就好了)

5,团队年龄分布?

6,公司是否配有电脑?

7,如果可以的话,最好看看工作的环境如何?(希望别太脏就好)

8,该岗位是否为新增岗位?如果不是?上一个人是为什么走的?

也是避坑的啦。



面试

本博客所有文章除特别声明外,均采用 CC BY-SA 3.0协议 。转载请注明出处!