1. 程式人生 > 實用技巧 >備戰金三銀四,前端面試知識點彙總,建議收藏

備戰金三銀四,前端面試知識點彙總,建議收藏

今天和大家聊聊前端面試的知識點,很快就要到金三銀四跳槽季了,希望這篇文章可以幫助到你,助你拿到心儀的offer。

前端基礎

瀏覽器

  • 瀏覽器的快取機制:強快取與協商快取,以及其區別是什麼?
  • 儲存相關:localstorage、sessionStorage、cookie等分別是做什麼用的,區別是什麼?
  • 瀏覽器的network面板裡面的東西,主要是timing下面的時間段代表的都是什麼意思?TTFB是什麼?
  • 瀏覽器的performance用過嗎,是用來幹什麼的?
  • 跨域的原理,跨域的實現方式有哪幾種?
  • 瀏覽器環境下的event loop是怎樣的?其實也就是巨集任務和微任務,可以看下這篇文章

JavaScript

基礎資料型別和引用資料型別

  • 基礎資料型別:Undefined、Null、Boolean、String、Number、Symbol
  • 引用資料型別:Object、Array、Date、RegExp、Function
  • 此處可能會考察,typeof、instanceof;包括手動實現以下typeof和instanceof
// 實現typeof
function type(obj) {
    return Object.prototype.toString.call(a).slice(8,-1).toLowerCase();
}
// 實現instanceof
function instance(left,right){
    left=left.__proto__
    right=right.prototype
    while(true){
       if(left==null)
          return false;
       if(left===right)
          return true;
       left=left.__proto__
    }
}

原型鏈

理解原型鏈是做什麼的,也就是:例項.proto=== 建構函式.prototype

Object.prototype.__proto__ === null // true
Function.prototype.__proto__ === Object.prototype // true
Object.__proto__ === Function.prototype // true

有個比較好的問題,可以思考下:

function F() {}
Object.prototype.b = 2;
F.prototype.a = 1;
var f = new F();
console.log(f.a) // 1
console.log(f.b) // 2
console.log(F.a) // undefined
console.log(F.b) // 2
複製程式碼

上面程式碼,為什麼F.a是undefined?

function F() {}
Object.prototype.b = 2;
Function.prototype.a = 1;
var f = new F();
console.log(f.a) // undefined
console.log(f.b) // 2
console.log(F.a) // 1
console.log(F.b) // 2

上面程式碼,為什麼f.a是undefined?

function F() {}
F.prototype.a = 1;
var f1 = new F()
F.prototype = {
    a: 2
}
var f2 = new F()
console.log(f1.a) // 1
console.log(f2.a) // 2

繼承

繼承的幾種方式:

  • 原型鏈繼承:

    function SuperType() {
      this.name = 'Yvette';
      this.colors = ['red', 'blue', 'green'];
    }
    SuperType.prototype.getName = function () {
        return this.name;
    }
    function SubType() {
        this.age = 18;
    }
    SubType.prototype = new SuperType();
    SubType.prototype.constructor = SubType;
    
    let instance1 = new SubType();
    instance1.colors.push('yellow');
    console.log(instance1.getName());
    console.log(instance1.colors); // ['red', 'blue', 'green', 'yellow']
    
    let instance2 = new SubType();
    console.log(instance2.colors); // ['red', 'blue', 'green', 'yellow']
    

    缺點:

    • 通過原型來實現繼承時,原型會變成另一個型別的例項,原先的例項屬性變成了現在的原型屬性,該原型的引用型別屬性會被所有的例項共享。(引用型別值被所有例項共享)
    • 在建立子型別的例項時,沒有辦法在不影響所有物件例項的情況下給超型別的建構函式中傳遞引數
  • 建構函式繼承:

    function SuperType(name) {
        this.name = name;
        this.colors = ['red', 'blue', 'green'];
    }
    function SubType(name) {
        SuperType.call(this, name);
    }
    let instance1 = new SubType('draven');
    instance1.colors.push('yellow');
    console.log(instance1.colors);  // ['red', 'blue', 'green', 'yellow']
    
    let instance2 = new SubType('ben');
    console.log(instance2.colors);  // ['red', 'blue', 'green']
    

    優點:

    • 可以向超類傳遞引數
    • 解決了原型中包含引用型別值被所有例項共享的問題 缺點:
    • 方法都在建構函式中定義,函式複用無從談起。
    • 超型別原型中定義的方法對於子型別而言都是不可見的。
  • 組合繼承:

    function SuperType(name) {
        this.name = name;
        this.colors = ['red', 'blue', 'green'];
    }
    SuperType.prototype.sayName = function () {
        console.log(this.name);
    }
    function SuberType(name, age) {
        SuperType.call(this, name);
        this.age = age;
    }
    SuberType.prototype = new SuperType()
    SuberType.prototype.constructor = SuberType
    
    let instance1 = new SuberType('draven', 25);
    instance1.colors.push('yellow');
    console.log(instance1.colors); // ['red', 'blue', 'green', 'yellow']
    instance1.sayName(); //draven
    
    let instance2 = new SuberType('ben', 22);
    console.log(instance2.colors);  // ['red', 'blue', 'green']
    instance2.sayName();//ben
    

    缺點:

    • 無論什麼情況下,都會呼叫兩次超型別建構函式:一次是在建立子型別原型的時候,另一次是在子型別建構函式內部。優點:
    • 可以向超類傳遞引數
    • 每個例項都有自己的屬性
    • 實現了函式複用
  • 寄生組合式繼承,寄生組合繼承是引用型別最理性的繼承正規化,使用Object.create在組合繼承的基礎上進行優化:

    function SuperType(name) {
        this.name = name;
        this.colors = ['red', 'blue', 'green'];
    }
    SuperType.prototype.sayName = function () {
        console.log(this.name);
    }
    function SuberType(name, age) {
        SuperType.call(this, name);
        this.age = age;
    }
    SuberType.prototype = Object.create(SuperType.prototype)
    SuberType.prototype.constructor = SuberType
    let instance1 = new SuberType('draven', 25);
    instance1.colors.push('yellow');
    console.log(instance1.colors); //[ 'red', 'blue', 'green', 'yellow' ]
    instance1.sayName(); //draven
    
    let instance2 = new SuberType('ben', 22);
    console.log(instance2.colors); //[ 'red', 'blue', 'green' ]
    instance2.sayName();//ben
    
  • ES6繼承:

    class SuperType {
        constructor(age) {
            this.age = age;
        }
    
        getAge() {
            console.log(this.age);
        }
    }
    
    class SubType extends SuperType {
        constructor(age, name) {
            super(age); // 呼叫父類的constructor(age)
            this.name = name;
        }
    }
    
    let instance = new SubType(18, 'draven');
    instance.getAge(); // 18
    
    • 類的內部所有定義的方法,都是不可列舉的。(ES5原型上的方法預設是可列舉的)

閉包:

  • 柯理化:
// 實現固定引數的curry
function add(a, b, c, d) {
    return a + b + c + d
}

function curry(fn) {
    const length = fn.length
    let params = []
    return function func() {
        params = params.concat([].slice.call(arguments))
        if (params.length === length) {
            const res = fn.apply(null, params);
            params = [];
            return res;
        } else {
            return func;
        }
    }
}

const addCurry = curry(add);
console.log(addCurry(1, 2)(3, 4)); // 10
console.log(addCurry(2)(3)(4)(5)); // 14
// 實現隨意引數的柯理化
function add() {
    let params = [].slice.call(arguments);
    function func() {
        params = params.concat([].slice.call(arguments))
        return func;
    }
    func.toString = () => {
        return  params.reduce((a, b) => {
            return a + b;
        }, 0);
    }
    return func;
}

console.log(add(1, 2)(3, 4)); // 10
console.log(add(2)(3)(4)(5)); // 14
  • 防抖和節流:

函式防抖和節流,都是控制事件觸發頻率的方法。

// 防抖
export function debounce(func, wait, immediate) {
    let timeout, args, context, timestamp, result;

    let nowTime = Date.now || function () {
        return new Date().getTime();
    };

    const later = function () {
        let last = nowTime() - timestamp;

        if (last < wait && last >= 0) {
            timeout = setTimeout(later, wait - last);
        } else {
            timeout = null;
            if (!immediate) {
                result = func.apply(context, args);
                if (!timeout) context = args = null;
            }
        }
    };

    return function () {
        context = this;
        args = arguments;
        timestamp = nowTime();
        let callNow = immediate && !timeout;
        if (!timeout) timeout = setTimeout(later, wait);
        if (callNow) {
            result = func.apply(context, args);
            context = args = null;
        }

        return result;
    };
};
// 節流
function throttle(fn, threshhold) {
    let timeout
    let start = new Date;
    threshhold = threshhold || 160
    return function () {
        const context = this, args = arguments, curr = new Date() - 0
        clearTimeout(timeout)//總是幹掉事件回撥
        if (curr - start >= threshhold) {
            fn.apply(context, args)
            start = curr
        } else {
            //讓方法在脫離事件後也能執行一次
            timeout = setTimeout(function(){
                fn.apply(context, args)
            }, threshhold);
        }
    }
}

var/let/const

這部分主要考查對let和var的理解,變數提升等。

看下面這個程式碼的執行結果是什麼?

var foo = {n: 1};
var bar = foo;
foo.x = foo = {n: 2};

bar = ?
foo = ?

上面的執行結果是:bar = {n:1,x:{n:2}}; foo={n:2};

a();
var a=3;
function a(){
alert(10)
}
alert(a)
a=6;
a()

上面的執行結果是:10 3 error;最後的error是因為a不是個function;

== 與 ===

隱式轉換的步驟: 主要搞明白在強等和雙等的時候做了什麼事情,也就好理解了。

強等(===)會首先比較兩邊的型別是否相同,如果不同則直接返回false;如果型別相同的話,則是按照==來判斷的,我們來看下==所引起的隱式轉換。

雙等號引起的隱式轉換

一、首先看雙等號前後有沒有NaN,如果存在NaN,一律返回false。

二、再看雙等號前後有沒有布林,有布林就將布林轉換為數字。(false是0,true是1)

三、接著看雙等號前後有沒有字串, 有三種情況:

1、對方是物件,物件使用toString()或者valueOf()進行轉換;

2、對方是數字,字串轉數字;(前面已經舉例)

3、對方是字串,直接比較;

4、其他返回false

四、如果是數字,對方是物件,物件取valueOf()或者toString()進行比較, 其他一律返回false

五、null, undefined不會進行型別轉換, 但它們倆相等

.toString()方法和.valueOf()方法數值轉換

通常情況下我們認為,將一個物件轉換為字串要呼叫toString()方法,轉換為數字要呼叫valueOf()方法,但是真正應用的時候並沒有這麼簡單,看如下程式碼例項:

let obj = {
 name: "draven",
 age: 28
}
console.log(obj.toString()); //[object Object]

同理,我們再看valueOf()方法:

let arr = [1, 2, 3];
console.log(arr.valueOf());//[1, 2, 3]

從上面的程式碼可以看出,valueOf()方法並沒有將物件轉換為能夠反映此物件的一個數字。相反,我們用toString()

let arr = [1, 2, 3];
console.log(arr.toString());//1,2,3

注:很多朋友認為,轉換為字串首先要呼叫toString()方法, 其實這是錯誤的認識,我們應該這麼理解,呼叫toString()方法可以轉換為字串,但不一定轉換字串就是首先呼叫toString()方法。

我們看下下面程式碼:

let arr = {};
arr.valueOf = function () { return 1; }
arr.toString = function () { return 2; }
console.log(arr == 1);//true

let arr = {};
arr.valueOf = function () { return []; }
arr.toString = function () { return 1; }
console.log(arr == 1);//true

上面程式碼我們可以看出,轉換首先呼叫的是valueOf(),假如valueOf()不是數值,那就會呼叫toString進行轉換!

let arr = {};
arr.valueOf = function () { return "1"; }
arr.toString = function () { return "2"; }
console.log(arr == "1");//true

假如"1"是字串,那麼它首先呼叫的還是valueOf()。

let arr = [2];
console.log(arr + "1");//21

上面的例子,呼叫的是toString();因為arr.toString()之後是2。

轉換過程是這樣的,首先arr會首先呼叫valueOf()方法,但是數字的此方法是簡單繼承而來,並沒有重寫(當然這個重寫不是我們實現),返回值是陣列物件本身,並不是一個值型別,所以就轉而呼叫toString()方法,於是就實現了轉換為字串的目的。

說明

大多數物件隱式轉換為值型別都是首先嚐試呼叫valueOf()方法。但是Date物件是個例外,此物件的valueOf()和toString()方法都經過精心重寫,預設是呼叫toString()方法,比如使用+運算子,如果在其他算數運算環境中,則會轉而呼叫valueOf()方法。

let date = new Date();
console.log(date + "1"); //Sun Apr 17 2014 17:54:48 GMT+0800 (CST)1
console.log(date + 1);//Sun Apr 17 2014 17:54:48 GMT+0800 (CST)1
console.log(date - 1);//1460886888556
console.log(date * 1);//1460886888557

舉例鞏固提高 下面我們一起來做做下面的題目吧!

let a;
console.dir(0 == false);//true
console.dir(1 == true);//true
console.dir(2 == {valueOf: function(){return 2}});//true

console.dir(a == NaN);//false
console.dir(NaN == NaN);//false

console.dir(8 == undefined);//false
console.dir(1 == undefined);//false
console.dir(2 == {toString: function(){return 2}});//true

console.dir(undefined == null);//true

console.dir(null == 1);//false

console.dir({ toString:function(){ return 1 } , valueOf:function(){ return [] }} == 1);//true

console.dir(1=="1");//true
console.dir(1==="1");//false

[] == 0 // true

上面的都可以理解了嗎?最後一行程式碼結果是true的原因是什麼?

es6

這部分考查對es6的掌握熟練度,新增的一些型別,語法,等等。推薦大家看一看阮一峰老師的es6的文章

手寫實現

js實現bind

// 實現bind
Function.prototype.myBind = function (context,...args) {
    let self = this;
    let params = args;
    return function (...newArgs) {
        self.call(context, ...params.concat(...newArgs))
    }
}
var a = {
    name: 'this is a'
}

function sayName() {
    console.log(this.name, arguments)
}

let newfn = sayName.myBind(a, '1234', '5678')
newfn('1000', '2000')

js實現call

// 實現call
Function.prototype.myCall = function (context,...args) {
    context.fn = this;
    context.fn(...args)
    delete context.fn;
}
var a = {
    name: 'this is a'
}
function sayName() {
    console.log(this.name, arguments)
}
sayName.myCall(a, '1234', '5678')

js實現setInterval

// setTimeout 實現setInterval
function mySetInterval(fn, time) {
    let timer = {};
    function timeout() {
        timer.t = setTimeout(() => {
            fn();
            timeout()
        }, time)
    }
    timeout();
    return timer;
}

function clearMyInterval(timer) {
    clearTimeout(timer.t)
}

promise

promise考察點比較多,包括實現自己的promise和一些呼叫的知識點

推薦兩篇文章:實現PromisePromise題

css

  • 盒模型,盒模型的margin、padding有什麼特點?
  • flex佈局的屬性都有什麼,都代表什麼含義?
  • 左右居中佈局、上下居中佈局、上下左右居中佈局,實現方式是什麼?
  • 單行超出省略...,多行超出省略...
  • 自適應佈局
  • 響應式佈局
  • less、scss、stylus
  • rem、em、vw等
  • 移動端1px如何實現?
  • css如何實現三角形?
  • css的link和import區別是什麼?

html

  • meta用來幹嘛的
  • 塊元素、行元素區別和舉例
  • html5新增的標籤有哪些?
  • video標籤的使用,事件等

前端框架

vue

vue基本

一、 vue的生命週期: beforeCreate、created、beforeMounte、mounted、beforeUpdate、updated、beforeDestory、destroyed;

二、 Vue元件通訊:

  • props(emit);
  • listeners,
  • 事件bus物件(bus.emit),
  • provide(inject),
  • v-model(props:value, emit:input ),
  • $children,
  • vuex

三、keep-alive使用及原理,LRU演算法

四、vue的v-show和v-if的區別;vue的watch和computed的區別;

五、其他:vue的服務端渲染,例如框架nuxt的使用;前端元件庫的使用,如element-ui;

vue2與vue3

3是Proxy+Reflect,2是Object.defineProperty;dom-diff的優化;componentApi等

vue-router

  • 實現的模式:hash & history;兩者的區別和分析
  • 事件:全域性:beforeEach、afterEach;路由:beforeEnter;元件內:beforeRouteEnter、beforeRouteUpdate、beforeRouteLeave
  • 實現原理,原始碼

vuex

  • vuex是什麼?vuex官網
  • state、getters、mutations(commit)、actions(dispatch)、module
  • mapState、mapMutations、mapGetters、mapActions;subscribe,subscribeAction
  • 實現原理,原始碼等

react

一、生命週期

  1.react16之前的生命週期是什麼?
  2.react16之後的生命週期是什麼?
  3.react16和react16之前的版本有什麼區別?
  4.requestAnimationFrame是什麼?requestIdleCallback是什麼?如何實現requestAnimationFrame
// 實現requestAnimationFrame
var lastTime = 0
window.requestAnimationFrame = function (callback) {
    let now = Date().now;
    let timeCall = Math.max(0, 16 - (lastTime - now));

    let id = setTimeout(function () {
        callback(now + timeCall)
    }, timeCall)

    lastTime = now + timeCall;
    return id;
}

二、react-hooks

1.常用的reacthooks都有哪些?
2.使用useEffect模擬componentDidMount和componentDidUpdate
3.useEffect返回的是什麼?做什麼用的?
4.useEffect和useLayoutEffect的區別是什麼?
5、useMemo和useCallback是什麼?做什麼用的?有什麼區別?

三、react-router

1.如何實現路由切換,有幾種方式?
2.有哪幾個鉤子函式?onEnter和routerWillLeave
3.link與a標籤的區別什麼?

四、redux

1.redux是什麼?做什麼的?與vuex有什麼區別?
2.redux包含哪幾塊?state,reducers,actions

五、其他:

1.服務端渲染next;
2.元件庫antd;
3.PureComponent與Component的區別是什麼?
4.react的效能優化有什麼建議?
5.封裝一個promise的setState
// 使用promise封裝setState
function setStateP(state) {
    return new Promise(resolve => {
        this.setState(state, resolve)
    })
}

工具型-webpack

  • 1.webpack是什麼?
  • 2.webpack的工作原理是什麼?
  • 3.寫過plugin嗎?怎麼實現的?
  • 4.loader是做什麼的?loader和plugin的區別什麼?
  • 5.關於webpack的優化建議等

聊聊webpack

nodeJs

  • event-loop:可以看下這篇文章nodejs的event loop

  • egg:宣告週期,目錄結構,常用外掛

  • koa:Koa的中介軟體與express的中介軟體的區別,實現一個洋蔥圈模型的函式

    // 洋蔥圈模型
    function compose(middleware) {
        return function (context, next) {
            let index = -1;
            function dispatch(i) {
                if (i <= index) {
                    return Promise.reject('err')
                }
                index = i;
                let fn = middleware[i];
                if(i === middleware.length) {
                    fn = next;
                }
                if (!fn) {
                    return Promise.resolve();
                }
                try {
                    return Promise.resolve(fn(context, function next() {
                        return dispatch(i + 1);
                    }))
                } catch (e) {
                    return Promise.reject(e);
                }
            }
            dispatch(0);
        }
    }
    
    

child_process

    1. spawn、exec、execFile、fork、
    1. fork與spawn類似,不同在於fork建立子程序需要執行js檔案;
    1. spawn與exec和execFile不同的是,後兩者建立時可以指定timeout屬性設定超時時間,一旦程序超時就會被殺死;
    1. exec與execFile不同的是,exec執行的是已有命令,execFile執行的是檔案。

pm2

  • pm2常用命令:start、stop、delete、logs、restart、list
  • -i 引數,啟動多執行緒;watch,-w,監聽檔案改變
  • pm2配置檔案,可以配置多個app,apps陣列,啟動 pm2 start pm2.connfig.js —only=one-app-name

計算機基礎

http系列

  • 三次握手是什麼?為什麼需要三次?
  • 四次揮手是什麼?為何需要四次?
  • http1、http2、https的區別是什麼?
  • https是如何進行加密的?
  • 請求如何取消?AbortController

排序

  • 氣泡排序
// 從小到大排序:
function bubblingSort(list){
     let temp;
     for(let i=0; i<list.length; i++){
          for(let j=i; j<list.length; j++){
               if(list[i] > list[j]){
                    temp = list[i];
                    list[i] = list[j];
                    list[j] = temp;
               }
          }
     }
     return list;
}
let res = bubblingSort([10, 8, 2, 23, 30, 4, 7, 1])
console.log(res); // [1, 2, 4, 7, 8, 10, 23, 30]
  • 直接選擇排序
從小到大排序:
function selectSort(list){
     let r,temp;
     for(let j=0; j<list.length; j++){
          for(let i = j+1; i<list.length; i++){
               if(list[j] > list[i]){
                   temp = list[j];
                   list[j] = list[i];
                   list[i] = temp;
               }
          }
     }
     return list;
}
let res = selectSort([10, 8, 2, 23, 30, 4, 7, 1])
console.log(res); // [1, 2, 4, 7, 8, 10, 23, 30]
  • 直接插入排序

整個排序過程為n-1趟插入,即先將序列中第1個記錄看成是一個有序子序列,然後從第2個記錄開始,逐個進行插入,直至整個序列有序。

function insertSort(list) {
    let flag;
    for(let index = 1; index < list.length; index++) {
        flag = list[index];
        let j = index - 1;
        while (flag < list[j]) {
            list[j + 1] = list[j]
            j--;
        }
        list[j + 1] = flag;
    }
     return list;
}
let res = insertSort([10, 8, 2, 23, 30, 4, 7, 1])
console.log(res); // [1, 2, 4, 7, 8, 10, 23, 30]
  • 希爾排序

排序過程:先取一個正整數d1<n,把所有相隔d1的記錄放一組,組內進行直接插入排序;然後取d2<d1,重複上述分組和排序操作;直至di=1,即所有記錄放進一個組中排序為止

function shellSort(list) {
    const length = list.length;
    let j, temp;
    for (let d = parseInt(length / 2); d >= 1; d = parseInt(d / 2)) {
        for (let i = d; i < length; i++) {
            temp = list[i];
            j = i - d;
            while (j >= 0 && temp < list[j]) {
                list[j + d] = list[j];
                j -= d;
            }
            list[j + d] = temp;
        }
    }
    return list;
}
let res = shellSort([10, 8, 2, 23, 30, 4, 7, 1])
console.log(res); // [1, 2, 4, 7, 8, 10, 23, 30]
  • 快速排序

通過一次排序,將待排序記錄分割成獨立的兩部分,其中一部分記錄的關鍵字均比另一部分記錄的關鍵字小,則可對這兩部分記錄進行排序,以達到整個序列有序。

function quickSort(v,left,right){
    if(left < right){
        var key = v[left];
        var low = left;
        var high = right;
        while(low < high){
            while(low < high && v[high] > key){
                high--;
            }
            v[low] = v[high];
            while(low < high && v[low] < key){
                low++;
            }
            v[high] = v[low];
        }
        v[low] = key;
        quickSort(v,left,low-1);
        quickSort(v,low+1,right);
    }
}
let list = [10, 8, 2, 23, 30, 4, 7, 1]
quickSort(list, 0, 7)
console.log(list); // [1, 2, 4, 7, 8, 10, 23, 30]

其他

  • tcp/ip協議的五層模型:應用層、傳輸層、網路層、資料鏈路層、物理層
  • 演算法相關,leetcode上面刷吧
  • 二叉樹等的遍歷,前中後序遍歷,深度優先,廣度優先;
  • 棧、佇列的使用
  • 連結串列的使用

其他

  • hybird
1、什麼是hybrid?
2、jsbridge是什麼?如何實現?
3、hybrid開發需要注意什麼?
  • 預載入和懶載入:
1.包括圖片視訊等內容的懶載入(IntersectionObserver的使用封裝)
2.資料的預載入,純h5的prefetch && 與端結合的預載入方案
3.js的按需載入(配合webpack的import().then()的split實現)
  • dom文件的載入步驟是什麼?
1、 解析HTML結構。
2、 載入外部指令碼和樣式表文件。
3、 解析並執行指令碼程式碼。
4、 構造HTML DOM模型。//ready
5、 載入圖片等外部檔案。
6、 頁面載入完畢。//load
  • 從瀏覽器輸入url,到展現完成,經過了什麼?
此問題網上有很多回答,屬於自由發揮問題;回答的深度和廣度能夠看出本人的知識面。此處就不多說了。
  • 前端效能優化等方方面面

總結

更多的面試知識點我這整理成了PDF

篇幅有限,僅展示部分內容

如果你需要這份完整版的面試題+解析,【點選我】免費獲取。

碼字不易,動動手指點個贊,再走唄~~~

最後祝大家換工作都可以找到心儀的工作。