2萬5千字各大廠前端程式員面試經歷-如何寫一個漂亮的簡歷

来源:https://www.cnblogs.com/lyy-1/archive/2020/07/02/13223572.html
-Advertisement-
Play Games

以下麵試題來自騰訊、阿裡、網易、餓了麽、美團、拼多多、百度等等大廠綜合起來常考的題目。 如何寫一個漂亮的簡歷 簡歷不是一份記流水賬的東西,而是讓用人方瞭解你的亮點的。平時有在做一些修改簡歷的收費服務,也算看過蠻多簡歷了。很多簡歷都有如下特征 喜歡說自己的特長、優點,用人方真的不關註你的性格是否陽光等 ...


以下麵試題來自騰訊、阿裡、網易、餓了麽、美團、拼多多、百度等等大廠綜合起來常考的題目。

如何寫一個漂亮的簡歷

簡歷不是一份記流水賬的東西,而是讓用人方瞭解你的亮點的。
平時有在做一些修改簡歷的收費服務,也算看過蠻多簡歷了。很多簡歷都有如下特征

喜歡說自己的特長、優點,用人方真的不關註你的性格是否陽光等等
個人技能能夠占半頁的篇幅,而且長得也都差不多
項目經驗流水賬,比如我會用這個 API 實現了某某功能
簡歷頁數過多,真心看不下去

以上類似簡歷可以說用人方也看了無數份,完全抓不到你的亮點。除非你呆過大廠或者教育背景不錯或者技術棧符合人家要求了,否則基本就是看運氣約面試了。
以下是我經常給別人修改簡歷的意見:
簡歷頁數控制在 2 頁以下

技術名詞註意大小寫

突出個人亮點,擴充內容。比如在項目中如何找到 Bug,解決 Bug 的過程;比如如何發現的性能問題,如何解決性能問題,最終提升了多少性能;比如為何如此選型,目的是什麼,較其他有什麼優點等等。總體思路就是不寫流水賬,突出你在項目中具有不錯的解決問題的能力和獨立思考的能力。
斟酌熟悉、精通等字眼,不要給自己挖坑
確保每一個寫上去的技術點自己都能說出點什麼,杜絕面試官問你一個技術點,你只能答出會用 API 這種減分的情況

做到以上內容,然後在投遞簡歷的過程中加上一份求職信,對你的求職之路相信能幫上很多忙。

JS 相關

談談變數提升?

當執行 JS 代碼時,會生成執行環境,只要代碼不是寫在函數中的,就是在全局執行環境中,函數中的代碼會產生函數執行環境,只此兩種執行環境。

接下來讓我們看一個老生常談的例子,var

b() // call b
console.log(a) // undefined
var a = 'Hello world'
function b() {
    console.log('call b')
}

想必以上的輸出大家肯定都已經明白了,這是因為函數和變數提升的原因。通常提升的解釋是說將聲明的代碼移動到了頂部,這其實沒有什麼錯誤,便於大家理解。但是更準確的解釋應該是:在生成執行環境時,會有兩個階段。第一個階段是創建的階段,JS 解釋器會找出需要提升的變數和函數,並且給他們提前在記憶體中開闢好空間,函數的話會將整個函數存入記憶體中,變數只聲明並且賦值為 undefined,所以在第二個階段,也就是代碼執行階段,我們可以直接提前使用。

在提升的過程中,相同的函數會覆蓋上一個函數,並且函數優先於變數提升

b() // call b second
function b() {
    console.log('call b fist')
}
function b() {
    console.log('call b second')
}
var b = 'Hello world'

var 會產生很多錯誤,所以在 ES6中引入了 let。let 不能在聲明前使用,但是這並不是常說的 let 不會提升,let 提升了,在第一階段記憶體也已經為他開闢好了空間,但是因為這個聲明的特性導致了並不能在聲明前使用。

bind、call、apply 區別

首先說下前兩者的區別。

call 和 apply 都是為瞭解決改變 this 的指向。作用都是相同的,只是傳參的方式不同。

除了第一個參數外,call 可以接收一個參數列表,apply 只接受一個參數數組。

let a = {
    value: 1
}
function getValue(name, age) {
    console.log(name)
    console.log(age)
    console.log(this.value)
}
getValue.call(a, 'yck', '24')
getValue.apply(a, ['yck', '24'])

bind 和其他兩個方法作用也是一致的,只是該方法會返回一個函數。並且我們可以通過 bind 實現柯里化。

如何實現一個 bind 函數

對於實現以下幾個函數,可以從幾個方面思考

  • [ ] 不傳入第一個參數,那麼預設為 window
  • [ ] 改變了 this 指向,讓新的對象可以執行該函數。那麼思路是否可以變成給新的對象添加一個函數,然後在執行完以後刪除?
Function.prototype.myBind = function (context) {
  if (typeof this !== 'function') {
    throw new TypeError('Error')
  }
  var _this = this
  var args = [...arguments].slice(1)
  // 返回一個函數
  return function F() {
    // 因為返回了一個函數,我們可以 new F(),所以需要判斷
    if (this instanceof F) {
      return new _this(...args, ...arguments)
    }
    return _this.apply(context, args.concat(...arguments))
  }
}

如何實現一個 call 函數

Function.prototype.myCall = function (context) {
  var context = context || window
  // 給 context 添加一個屬性
  // getValue.call(a, 'yck', '24') => a.fn = getValue
  context.fn = this
  // 將 context 後面的參數取出來
  var args = [...arguments].slice(1)
  // getValue.call(a, 'yck', '24') => a.fn('yck', '24')
  var result = context.fn(...args)
  // 刪除 fn
  delete context.fn
  return result
}
如果有想一起學習web前端,想製作酷炫的網頁,可以來一下我的前端qq群:851231348,從最基礎的HTML+CSS+JavaScript【炫酷特效,游戲,插件封裝,設計模式】到移動端HTML5的項目實戰的學習資料都有整理好友都會在裡面交流
分享一些學習的方法和需要註意的小細節,每天也會準時的講一些前端的項目實戰,及免費前端直播課程學習

如何實現一個 apply 函數

Function.prototype.myApply = function (context) {
  var context = context || window
  context.fn = this
  var result
  // 需要判斷是否存儲第二個參數
  // 如果存在,就將第二個參數展開
  if (arguments[1]) {
    result = context.fn(...arguments[1])
  } else {
    result = context.fn()
  }
  delete context.fn
  return result
}

簡單說下原型鏈

[圖片上傳失敗...(image-e8ccc5-1546698256133)]

每個函數都有 prototype 屬性,除了 Function.prototype.bind(),該屬性指向原型。

每個對象都有  proto  屬性,指向了創建該對象的構造函數的原型。其實這個屬性指向了 [[prototype]],但是 [[prototype]] 是內部屬性,我們並不能訪問到,所以使用  proto  來訪問。

對象可以通過  proto  來尋找不屬於該對象的屬性, proto  將對象連接起來組成了原型鏈。
如果你想更進一步的瞭解原型,可以仔細閱讀 深度解析原型中的各個難點。

怎麼判斷對象類型?

可以通過 Object.prototype.toString.call(xx)。這樣我們就可以獲得類似 [object Type] 的字元串。
instanceof 可以正確的判斷對象的類型,因為內部機制是通過判斷對象的原型鏈中是不是能找到類型的 prototype。

箭頭函數的特點

function a() {
    return () => {
        return () => {
            console.log(this)
        }
    }
}
console.log(a()()())

箭頭函數其實是沒有 this 的,這個函數中的 this 只取決於他外面的第一個不是箭頭函數的函數的 this。在這個例子中,因為調用 a 符合前面代碼中的第一個情況,所以 this 是 window。並且 this 一旦綁定了上下文,就不會被任何代碼改變。

This

this 是很多人會混淆的概念,但是其實他一點都不難,你只需要記住幾個規則就可以了。

function foo() {
    console.log(this.a)
}
var a = 1
foo()
var obj = {
    a: 2,
    foo: foo
}
obj.foo()
// 以上兩者情況 `this` 只依賴於調用函數前的對象,優先順序是第二個情況大於第一個情況
// 以下情況是優先順序最高的,`this` 只會綁定在 `c` 上,不會被任何方式修改 `this` 指向
var c = new foo()
c.a = 3
console.log(c.a)
// 還有種就是利用 call,apply,bind 改變 this,這個優先順序僅次於 new

async、await 優缺點

async 和 await 相比直接使用 Promise 來說,優勢在於處理 then 的調用鏈,能夠更清晰準確的寫出代碼。缺點在於濫用 await 可能會導致性能問題,因為 await 會阻塞代碼,也許之後的非同步代碼並不依賴於前者,但仍然需要等待前者完成,導致代碼失去了併發性。

下麵來看一個使用 await 的代碼。

var a = 0
var b = async () => {
  a = a + await 10
  console.log('2', a) // -> '2' 10
  a = (await 10) + a
  console.log('3', a) // -> '3' 20
}
b()
a++
console.log('1', a) // -> '1' 1

對於以上代碼你可能會有疑惑,這裡說明下原理

  • [ ] 首先函數 b 先執行,在執行到 await 10 之前變數 a 還是 0,因為在 await 內部實現了 generators
    ,generators 會保留堆棧中東西,所以這時候 a = 0 被保存了下來
  • [ ] 因為 await是非同步操作,遇到await就會立即返回一個pending狀態的Promise對象,暫時返回執行代碼的控制權,使得函數外的代碼得以繼續執行,所以會先執行
    console.log('1', a)
  • [ ] 這時候同步代碼執行完畢,開始執行非同步代碼,將保存下來的值拿出來使用,這時候 a = 10
  • [ ] 然後後面就是常規執行代碼了

generator 原理

Generator 是 ES6 中新增的語法,和 Promise 一樣,都可以用來非同步編程

// 使用 * 表示這是一個 Generator 函數
// 內部可以通過 yield 暫停代碼
// 通過調用 next 恢復執行
function* test() {
  let a = 1 + 2;
  yield 2;
  yield 3;
}
let b = test();
console.log(b.next()); // >  { value: 2, done: false }
console.log(b.next()); // >  { value: 3, done: false }
console.log(b.next()); // >  { value: undefined, done: true }

從以上代碼可以發現,加上 * 的函數執行後擁有了 next 函數,也就是說函數執行後返回了一個對象。每次調用 next 函數可以繼續執行被暫停的代碼。以下是 Generator 函數的簡單實現

// cb 也就是編譯過的 test 函數
function generator(cb) {
  return (function() {
    var object = {
      next: 0,
      stop: function() {}
    };
    return {
      next: function() {
        var ret = cb(object);
        if (ret === undefined) return { value: undefined, done: true };
        return {
          value: ret,
          done: false
        };
      }
    };
  })();
}
// 如果你使用 babel 編譯後可以發現 test 函數變成了這樣
function test() {
  var a;
  return generator(function(_context) {
    while (1) {
      switch ((_context.prev = _context.next)) {
        // 可以發現通過 yield 將代碼分割成幾塊
        // 每次執行 next 函數就執行一塊代碼
        // 並且表明下次需要執行哪塊代碼
        case 0:
          a = 1 + 2;
          _context.next = 4;
          return 2;
        case 4:
          _context.next = 6;
          return 3;
        // 執行完畢
        case 6:
        case "end":
          return _context.stop();
      }
    }
  });
}

Promise

Promise 是 ES6 新增的語法,解決了回調地獄的問題。

可以把 Promise 看成一個狀態機。初始是 pending 狀態,可以通過函數 resolve 和 reject ,將狀態轉變為 resolved 或者 rejected 狀態,狀態一旦改變就不能再次變化。

then 函數會返回一個 Promise 實例,並且該返回值是一個新的實例而不是之前的實例。因為 Promise 規範規定除了 pending 狀態,其他狀態是不可以改變的,如果返回的是一個相同實例的話,多個 then 調用就失去意義了。

對於 then 來說,本質上可以把它看成是 flatMap

如何實現一個 Promise

// 三種狀態
const PENDING = "pending";
const RESOLVED = "resolved";
const REJECTED = "rejected";
// promise 接收一個函數參數,該函數會立即執行
function MyPromise(fn) {
  let _this = this;
  _this.currentState = PENDING;
  _this.value = undefined;
  // 用於保存 then 中的回調,只有當 promise
  // 狀態為 pending 時才會緩存,並且每個實例至多緩存一個
  _this.resolvedCallbacks = [];
  _this.rejectedCallbacks = [];
  _this.resolve = function (value) {
    if (value instanceof MyPromise) {
      // 如果 value 是個 Promise,遞歸執行
      return value.then(_this.resolve, _this.reject)
    }
    setTimeout(() => { // 非同步執行,保證執行順序
      if (_this.currentState === PENDING) {
        _this.currentState = RESOLVED;
        _this.value = value;
        _this.resolvedCallbacks.forEach(cb => cb());
      }
    })
  };
  _this.reject = function (reason) {
    setTimeout(() => { // 非同步執行,保證執行順序
      if (_this.currentState === PENDING) {
        _this.currentState = REJECTED;
        _this.value = reason;
        _this.rejectedCallbacks.forEach(cb => cb());
      }
    })
  }
  // 用於解決以下問題
  // new Promise(() => throw Error('error))
  try {
    fn(_this.resolve, _this.reject);
  } catch (e) {
    _this.reject(e);
  }
}
MyPromise.prototype.then = function (onResolved, onRejected) {
  var self = this;
  // 規範 2.2.7,then 必須返回一個新的 promise
  var promise2;
  // 規範 2.2.onResolved 和 onRejected 都為可選參數
  // 如果類型不是函數需要忽略,同時也實現了透傳
  // Promise.resolve(4).then().then((value) => console.log(value))
  onResolved = typeof onResolved === 'function' ? onResolved : v => v;
  onRejected = typeof onRejected === 'function' ? onRejected : r => throw r;
  if (self.currentState === RESOLVED) {
    return (promise2 = new MyPromise(function (resolve, reject) {
      // 規範 2.2.4,保證 onFulfilled,onRjected 非同步執行
      // 所以用了 setTimeout 包裹下
      setTimeout(function () {
        try {
          var x = onResolved(self.value);
          resolutionProcedure(promise2, x, resolve, reject);
        } catch (reason) {
          reject(reason);
        }
      });
    }));
  }
  if (self.currentState === REJECTED) {
    return (promise2 = new MyPromise(function (resolve, reject) {
      setTimeout(function () {
        // 非同步執行onRejected
        try {
          var x = onRejected(self.value);
          resolutionProcedure(promise2, x, resolve, reject);
        } catch (reason) {
          reject(reason);
        }
      });
    }));
  }
  if (self.currentState === PENDING) {
    return (promise2 = new MyPromise(function (resolve, reject) {
      self.resolvedCallbacks.push(function () {
        // 考慮到可能會有報錯,所以使用 try/catch 包裹
        try {
          var x = onResolved(self.value);
          resolutionProcedure(promise2, x, resolve, reject);
        } catch (r) {
          reject(r);
        }
      });
      self.rejectedCallbacks.push(function () {
        try {
          var x = onRejected(self.value);
          resolutionProcedure(promise2, x, resolve, reject);
        } catch (r) {
          reject(r);
        }
      });
    }));
  }
};
// 規範 2.3
function resolutionProcedure(promise2, x, resolve, reject) {
  // 規範 2.3.1,x 不能和 promise2 相同,避免迴圈引用
  if (promise2 === x) {
    return reject(new TypeError("Error"));
  }
  // 規範 2.3.2
  // 如果 x 為 Promise,狀態為 pending 需要繼續等待否則執行
  if (x instanceof MyPromise) {
    if (x.currentState === PENDING) {
      x.then(function (value) {
        // 再次調用該函數是為了確認 x resolve 的
        // 參數是什麼類型,如果是基本類型就再次 resolve
        // 把值傳給下個 then
        resolutionProcedure(promise2, value, resolve, reject);
      }, reject);
    } else {
      x.then(resolve, reject);
    }
    return;
  }
  // 規範 2.3.3.3.3
  // reject 或者 resolve 其中一個執行過得話,忽略其他的
  let called = false;
  // 規範 2.3.3,判斷 x 是否為對象或者函數
  if (x !== null && (typeof x === "object" || typeof x === "function")) {
    // 規範 2.3.3.2,如果不能取出 then,就 reject
    try {
      // 規範 2.3.3.1
      let then = x.then;
      // 如果 then 是函數,調用 x.then
      if (typeof then === "function") {
        // 規範 2.3.3.3
        then.call(
          x,
          y => {
            if (called) return;
            called = true;
            // 規範 2.3.3.3.1
            resolutionProcedure(promise2, y, resolve, reject);
          },
          e => {
            if (called) return;
            called = true;
            reject(e);
          }
        );
      } else {
        // 規範 2.3.3.4
        resolve(x);
      }
    } catch (e) {
      if (called) return;
      called = true;
      reject(e);
    }
  } else {
    // 規範 2.3.4,x 為基本類型
    resolve(x);
  }
}

==  和   === 區別,什麼情況用 ==

[圖片上傳失敗...(image-e8a801-1546698256133)]

上圖中的 toPrimitive 就是對象轉基本類型。

這裡來解析一道題目   [] == ![] // -> true  ,下麵是這個表達式為何為 true 的步驟

// [] 轉成 true,然後取反變成 false
[] == false
// 根據第 8 條得出
[] == ToNumber(false)
[] == 0
// 根據第 10 條得出
ToPrimitive([]) == 0
// [].toString() -> ''
'' == 0
// 根據第 6 條得出
0 == 0 // -> true

===  用於判斷兩者類型和值是否相同。 在開發中,對於後端返回的 code,可以通過 == 去判斷。

垃圾回收

V8 實現了準確式 GC,GC 演算法採用了分代式垃圾回收機制。因此,V8 將記憶體(堆)分為新生代和老生代兩部分。

新生代演算法

新生代中的對象一般存活時間較短,使用 Scavenge GC 演算法。

在新生代空間中,記憶體空間分為兩部分,分別為 From 空間和 To 空間。在這兩個空間中,必定有一個空間是使用的,另一個空間是空閑的。新分配的對象會被放入 From 空間中,當 From 空間被占滿時,新生代 GC 就會啟動了。演算法會檢查 From 空間中存活的對象並複製到 To 空間中,如果有失活的對象就會銷毀。當複製完成後將 From 空間和 To 空間互換,這樣 GC 就結束了。

老生代演算法

老生代中的對象一般存活時間較長且數量也多,使用了兩個演算法,分別是標記清除演算法和標記壓縮演算法。

在講演算法前,先來說下什麼情況下對象會出現在老生代空間中:

  • [ ] 新生代中的對象是否已經經歷過一次 Scavenge 演算法,如果經歷過的話,會將對象從新生代空間移到老生代空間中。
  • [ ] To 空間的對象占比大小超過 25 %。在這種情況下,為了不影響到記憶體分配,會將對象從新生代空間移到老生代空間中。

老生代中的空間很複雜,有如下幾個空間

enum AllocationSpace {
  // TODO(v8:7464): Actually map this space's memory as read-only.
  RO_SPACE,    // 不變的對象空間
  NEW_SPACE,   // 新生代用於 GC 複製演算法的空間
  OLD_SPACE,   // 老生代常駐對象空間
  CODE_SPACE,  // 老生代代碼對象空間
  MAP_SPACE,   // 老生代 map 對象
  LO_SPACE,    // 老生代大空間對象
  NEW_LO_SPACE,  // 新生代大空間對象
  FIRST_SPACE = RO_SPACE,
  LAST_SPACE = NEW_LO_SPACE,
  FIRST_GROWABLE_PAGED_SPACE = OLD_SPACE,
  LAST_GROWABLE_PAGED_SPACE = MAP_SPACE
};

在老生代中,以下情況會先啟動標記清除演算法:

  • [ ] 某一個空間沒有分塊的時候
  • [ ] 空間中被對象超過一定限制
  • [ ] 空間不能保證新生代中的對象移動到老生代中

在這個階段中,會遍歷堆中所有的對象,然後標記活的對象,在標記完成後,銷毀所有沒有被標記的對象。在標記大型對記憶體時,可能需要幾百毫秒才能完成一次標記。這就會導致一些性能上的問題。為瞭解決這個問題,2011 年,V8 從 stop-the-world 標記切換到增量標誌。在增量標記期間,GC 將標記工作分解為更小的模塊,可以讓 JS 應用邏輯在模塊間隙執行一會,從而不至於讓應用出現停頓情況。但在 2018 年,GC 技術又有了一個重大突破,這項技術名為併發標記。該技術可以讓 GC 掃描和標記對象時,同時允許 JS 運行

清除對象後會造成堆記憶體出現碎片的情況,當碎片超過一定限制後會啟動壓縮演算法。在壓縮過程中,將活的對象像一端移動,直到所有對象都移動完成然後清理掉不需要的記憶體。

閉包

閉包的定義很簡單:函數 A 返回了一個函數 B,並且函數 B 中使用了函數 A 的變數,函數 B 就被稱為閉包。

function A() {
  let a = 1
  function B() {
      console.log(a)
  }
  return B
}

你是否會疑惑,為什麼函數 A 已經彈出調用棧了,為什麼函數 B 還能引用到函數 A 中的變數。因為函數 A 中的變數這時候是存儲在堆上的。現在的 JS 引擎可以通過逃逸分析辨別出哪些變數需要存儲在堆上,哪些需要存儲在棧上。

經典面試題,迴圈中使用閉包解決 var 定義函數的問題

for ( var i=1; i<=5; i++) {
    setTimeout( function timer() {
        console.log( i );
    }, i*1000 );
}

首先因為 setTimeout 是個非同步函數,所有會先把迴圈全部執行完畢,這時候 i 就是 6 了,所以會輸出一堆 6。

解決辦法兩種,第一種使用閉包

for (var i = 1; i <= 5; i++) {
  (function(j) {
    setTimeout(function timer() {
      console.log(j);
    }, j * 1000);
  })(i);
}

第二種就是使用 setTimeout 的第三個參數

for ( var i=1; i<=5; i++) {
    setTimeout( function timer(j) {
        console.log( j );
    }, i*1000, i);
}

第三種就是使用 let 定義 i 了

for ( let i=1; i<=5; i++) {
    setTimeout( function timer() {
        console.log( i );
    }, i*1000 );
}

因為對於 let 來說,他會創建一個塊級作用域,相當於

{ // 形成塊級作用域
  let i = 0
  {
    let ii = i
    setTimeout( function timer() {
        console.log( ii );
    }, i*1000 );
  }
  i++
  {
    let ii = i
  }
  i++
  {
    let ii = i
  }
  ...
}

基本數據類型和引⽤類型在存儲上的差別

前者存儲在棧上,後者存儲在堆上

瀏覽器 Eventloop 和 Node 中的有什麼區別

眾所周知 JS 是門非阻塞單線程語言,因為在最初 JS 就是為了和瀏覽器交互而誕生的。如果 JS 是門多線程的語言話,我們在多個線程中處理 DOM 就可能會發生問題(一個線程中新加節點,另一個線程中刪除節點),當然可以引入讀寫鎖解決這個問題。

JS 在執行的過程中會產生執行環境,這些執行環境會被順序的加入到執行棧中。如果遇到非同步的代碼,會被掛起並加入到 Task(有多種 task) 隊列中。一旦執行棧為空,Event Loop 就會從 Task 隊列中拿出需要執行的代碼並放入執行棧中執行,所以本質上來說 JS 中的非同步還是同步行為。

console.log('script start');
setTimeout(function() {
  console.log('setTimeout');
}, 0);
console.log('script end');

以上代碼雖然 setTimeout 延時為 0,其實還是非同步。這是因為 HTML5 標準規定這個函數第二個參數不得小於 4 毫秒,不足會自動增加。所以 setTimeout 還是會在 script end 之後列印。

不同的任務源會被分配到不同的 Task 隊列中,任務源可以分為 微任務(microtask) 和 巨集任務(macrotask)。在 ES6 規範中,microtask 稱為 jobs,macrotask 稱為 task。

console.log('script start');
setTimeout(function() {
  console.log('setTimeout');
}, 0);
new Promise((resolve) => {
    console.log('Promise')
    resolve()
}).then(function() {
  console.log('promise1');
}).then(function() {
  console.log('promise2');
});
console.log('script end');
// script start => Promise => script end => promise1 => promise2 => setTimeout

以上代碼雖然 setTimeout 寫在 Promise 之前,但是因為 Promise 屬於微任務而 setTimeout 屬於巨集任務,所以會有以上的列印。

微任務包括 process.nextTick ,promise ,Object.observe ,MutationObserver
巨集任務包括 script , setTimeout ,setInterval ,setImmediate ,I/O ,UI rendering

很多人有個誤區,認為微任務快於巨集任務,其實是錯誤的。因為巨集任務中包括了 script ,瀏覽器會先執行一個巨集任務,接下來有非同步代碼的話就先執行微任務。
所以正確的一次 Event loop 順序是這樣的

1.執行同步代碼,這屬於巨集任務
2.執行棧為空,查詢是否有微任務需要執行
3.執行所有微任務
4.必要的話渲染 UI
5.然後開始下一輪 Event loop,執行巨集任務中的非同步代碼

通過上述的 Event loop 順序可知,如果巨集任務中的非同步代碼有大量的計算並且需要操作 DOM 的話,為了更快的 界面響應,我們可以把操作 DOM 放入微任務中。

Node 中的 Event loop

Node 中的 Event loop 和瀏覽器中的不相同。

Node 的 Event loop 分為6個階段,它們會按照順序反覆運行

┌───────────────────────┐
┌─>│        timers         │
│  └──────────┬────────────┘
│  ┌──────────┴────────────┐
│  │     I/O callbacks     │
│  └──────────┬────────────┘
│  ┌──────────┴────────────┐
│  │     idle, prepare     │
│  └──────────┬────────────┘      ┌───────────────┐
│  ┌──────────┴────────────┐      │   incoming:   │
│  │         poll          │<──connections───     │
│  └──────────┬────────────┘      │   data, etc.  │
│  ┌──────────┴────────────┐      └───────────────┘
│  │        check          │
│  └──────────┬────────────┘
│  ┌──────────┴────────────┐
└──┤    close callbacks    │
   └───────────────────────┘

timer

timers 階段會執行 setTimeout 和 setInterval

一個 timer 指定的時間並不是準確時間,而是在達到這個時間後儘快執行回調,可能會因為系統正在執行別的事務而延遲。

下限的時間有一個範圍:[1, 2147483647] ,如果設定的時間不在這個範圍,將被設置為1。

**I/O **

I/O 階段會執行除了 close 事件,定時器和 setImmediate 的回調

idle, prepare

idle, prepare 階段內部實現

poll

poll 階段很重要,這一階段中,系統會做兩件事情

執行到點的定時器
執行 poll 隊列中的事件
並且當 poll 中沒有定時器的情況下,會發現以下兩件事情

如果 poll 隊列不為空,會遍歷回調隊列並同步執行,直到隊列為空或者系統限制
如果 poll 隊列為空,會有兩件事發生

如果有 setImmediate 需要執行,poll 階段會停止並且進入到 check 階段執行 setImmediate
如果沒有 setImmediate 需要執行,會等待回調被加入到隊列中並立即執行回調

如果有別的定時器需要被執行,會回到 timer 階段執行回調。

check

check 階段執行 setImmediate

close callbacks

close callbacks 階段執行 close 事件

並且在 Node 中,有些情況下的定時器執行順序是隨機的

setTimeout(() => {
    console.log('setTimeout');
}, 0);
setImmediate(() => {
    console.log('setImmediate');
})
// 這裡可能會輸出 setTimeout,setImmediate
// 可能也會相反的輸出,這取決於性能
// 因為可能進入 event loop 用了不到 1 毫秒,這時候會執行 setImmediate
// 否則會執行 setTimeout

當然在這種情況下,執行順序是相同的

var fs = require('fs')
fs.readFile(__filename, () => {
    setTimeout(() => {
        console.log('timeout');
    }, 0);
    setImmediate(() => {
        console.log('immediate');
    });
});
// 因為 readFile 的回調在 poll 中執行
// 發現有 setImmediate ,所以會立即跳到 check 階段執行回調
// 再去 timer 階段執行 setTimeout
// 所以以上輸出一定是 setImmediate,setTimeout

上面介紹的都是 macrotask 的執行情況,microtask 會在以上每個階段完成後立即執行。

setTimeout(()=>{
    console.log('timer1')
    Promise.resolve().then(function() {
        console.log('promise1')
    })
}, 0)
setTimeout(()=>{
    console.log('timer2')
    Promise.resolve().then(function() {
        console.log('promise2')
    })
}, 0)
// 以上代碼在瀏覽器和 node 中列印情況是不同的
// 瀏覽器中一定列印 timer1, promise1, timer2, promise2
// node 中可能列印 timer1, timer2, promise1, promise2
// 也可能列印 timer1, promise1, timer2, promise2

Node 中的 process.nextTick 會先於其他 microtask 執行。

setTimeout(() => {
  console.log("timer1");
  Promise.resolve().then(function() {
    console.log("promise1");
  });
}, 0);
process.nextTick(() => {
  console.log("nextTick");
});
// nextTick, timer1, promise1

setTimeout 倒計時誤差

JS 是單線程的,所以 setTimeout 的誤差其實是無法被完全解決的,原因有很多,可能是回調中的,有可能是瀏覽器中的各種事件導致。這也是為什麼頁面開久了,定時器會不准的原因,當然我們可以通過一定的辦法去減少這個誤差。

以下是一個相對準備的倒計時實現

var period = 60 * 1000 * 60 * 2
var startTime = new Date().getTime();
var count = 0
var end = new Date().getTime() + period
var interval = 1000
var currentInterval = interval
function loop() {
  count++
  var offset = new Date().getTime() - (startTime + count * interval); // 代碼執行所消耗的時間
  var diff = end - new Date().getTime()
  var h = Math.floor(diff / (60 * 1000 * 60))
  var hdiff = diff % (60 * 1000 * 60)
  var m = Math.floor(hdiff / (60 * 1000))
  var mdiff = hdiff % (60 * 1000)
  var s = mdiff / (1000)
  var sCeil = Math.ceil(s)
  var sFloor = Math.floor(s)
  currentInterval = interval - offset // 得到下一次迴圈所消耗的時間
  console.log('時:'+h, '分:'+m, '毫秒:'+s, '秒向上取整:'+sCeil, '代碼執行時間:'+offset, '下次迴圈間隔'+currentInterval) // 列印 時 分 秒 代碼執行時間 下次迴圈間隔
  setTimeout(loop, currentInterval)
}
setTimeout(loop, currentInterval)

防抖

你是否在日常開發中遇到一個問題,在滾動事件中需要做個複雜計算或者實現一個按鈕的防二次點擊操作。

這些需求都可以通過函數防抖動來實現。尤其是第一個需求,如果在頻繁的事件回調中做複雜計算,很有可能導致頁面卡頓,不如將多次計算合併為一次計算,只在一個精確點做操作。

PS:防抖和節流的作用都是防止函數多次調用。區別在於,假設一個用戶一直觸發這個函數,且每次觸發函數的間隔小於wait,防抖的情況下只會調用一次,而節流的 情況會每隔一定時間(參數wait)調用函數。

我們先來看一個袖珍版的防抖理解一下防抖的實現:

// func是用戶傳入需要防抖的函數
// wait是等待時間
const debounce = (func, wait = 50) => {
  // 緩存一個定時器id
  let timer = 0
  // 這裡返回的函數是每次用戶實際調用的防抖函數
  // 如果已經設定過定時器了就清空上一次的定時器
  // 開始一個新的定時器,延遲執行用戶傳入的方法
  return function(...args) {
    if (timer) clearTimeout(timer)
    timer = setTimeout(() => {
      func.apply(this, args)
    }, wait)
  }
}
// 不難看出如果用戶調用該函數的間隔小於wait的情況下,上一次的時間還未到就被清除了,並不會執行函數

這是一個簡單版的防抖,但是有缺陷,這個防抖只能在最後調用。一般的防抖會有immediate選項,表示是否立即調用。這兩者的區別,舉個慄子來說:

  • [ ] 例如在搜索引擎搜索問題的時候,我們當然是希望用戶輸入完最後一個字才調用查詢介面,這個時候適用延遲執行的防抖函數,它總是在一連串(間隔小於wait的)函數觸發之後調用。

  • [ ] 例如用戶給interviewMap點star的時候,我們希望用戶點第一下的時候就去調用介面,並且成功之後改變star按鈕的樣子,用戶就可以立馬得到反饋是否star成功了,這個情況適用立即執行的防抖函數,它總是在第一次調用,並且下一次調用必須與前一次調用的時間間隔大於wait才會觸發。

下麵我們來實現一個帶有立即執行選項的防抖函數

// 這個是用來獲取當前時間戳的
function now() {
  return +new Date()
}
/**
 * 防抖函數,返回函數連續調用時,空閑時間必須大於或等於 wait,func 才會執行
 *
 * @param  {function} func        回調函數
 * @param  {number}   wait        表示時間視窗的間隔
 * @param  {boolean}  immediate   設置為ture時,是否立即調用函數
 * @return {function}             返回客戶調用函數
 */
function debounce (func, wait = 50, immediate = true) {
  let timer, context, args
  
  // 延遲執行函數
  const later = () => setTimeout(() => {
    // 延遲函數執行完畢,清空緩存的定時器序號
    timer = null
    // 延遲執行的情況下,函數會在延遲函數中執行
    // 使用到之前緩存的參數和上下文
    if (!immediate) {
      func.apply(context, args)
      context = args = null
    }
  }, wait)
  // 這裡返回的函數是每次實際調用的函數
  return function(...params) {
    // 如果沒有創建延遲執行函數(later),就創建一個
    if (!timer) {
      timer = later()
      // 如果是立即執行,調用函數
      // 否則緩存參數和調用上下文
      if (immediate) {
        func.apply(this, params)
      } else {
        context = this
        args = params
      }
    // 如果已有延遲執行函數(later),調用的時候清除原來的並重新設定一個
    // 這樣做延遲函數會重新計時
    } else {
      clearTimeout(timer)
      timer = later()
    }
  }
}

這是一個簡單版的防抖,但是有缺陷,這個防抖只能在最後調用。一般的防抖會有immediate選項,表示是否立即調用。這兩者的區別,舉個慄子來說:

  • [ ] 例如在搜索引擎搜索問題的時候,我們當然是希望用戶輸入完最後一個字才調用查詢介面,這個時候適用延遲執行的防抖函數,它總是在一連串(間隔小於wait的)函數觸發之後調用。
  • [ ] 例如用戶給interviewMap點star的時候,我們希望用戶點第一下的時候就去調用介面,並且成功之後改變star按鈕的樣子,用戶就可以立馬得到反饋是否star成功了,這個情況適用立即執行的防抖函數,它總是在第一次調用,並且下一次調用必須與前一次調用的時間間隔大於wait才會觸發。

下麵我們來實現一個帶有立即執行選項的防抖函數

// 這個是用來獲取當前時間戳的
function now() {
  return +new Date()
}
/**
 * 防抖函數,返回函數連續調用時,空閑時間必須大於或等於 wait,func 才會執行
 *
 * @param  {function} func        回調函數
 * @param  {number}   wait        表示時間視窗的間隔
 * @param  {boolean}  immediate   設置為ture時,是否立即調用函數
 * @return {function}             返回客戶調用函數
 */
function debounce (func, wait = 50, immediate = true) {
  let timer, context, args
  
  // 延遲執行函數
  const later = () => setTimeout(() => {
    // 延遲函數執行完畢,清空緩存的定時器序號
    timer = null
    // 延遲執行的情況下,函數會在延遲函數中執行
    // 使用到之前緩存的參數和上下文
    if (!immediate) {
      func.apply(context, args)
      context = args = null
    }
  }, wait)
  // 這裡返回的函數是每次實際調用的函數
  return function(...params) {
    // 如果沒有創建延遲執行函數(later),就創建一個
    if (!timer) {
      timer = later()
      // 如果是立即執行,調用函數
      // 否則緩存參數和調用上下文
      if (immediate) {
        func.apply(this, params)
      } else {
        context = this
        args = params
      }
    // 如果已有延遲執行函數(later),調用的時候清除原來的並重新設定一個
    // 這樣做延遲函數會重新計時
    } else {
      clearTimeout(timer)
      timer = later()
    }
  }
}

整體函數實現的不難,總結一下。

對於按鈕防點擊來說的實現:如果函數是立即執行的,就立即調用,如果函數是延遲執行的,就緩存上下文和參數,放到延遲函數中去執行。一旦我開始一個定時器,只要我定時器還在,你每次點擊我都重新計時。一旦你點累了,定時器時間到,定時器重置為 null,就可以再次點擊了。

對於延時執行函數來說的實現:清除定時器ID,如果是延遲調用就調用函數

數組降維

[1, [2], 3].flatMap((v) => v + 1)
// -> [2, 3, 4]

如果想將一個多維數組徹底的降維,可以這樣實現

const flattenDeep = (arr) => Array.isArray(arr)
  ? arr.reduce( (a, b) => [...a, ...flattenDeep(b)] , [])
  : [arr]
flattenDeep([1, [[2], [3, [4]], 5]])

深拷貝

這個問題通常可以通過 JSON.parse(JSON.stringify(object)) 來解決。

let a = {
    age: 1,
    jobs: {
        first: 'FE'
    }
}
let b = JSON.parse(JSON.stringify(a))
a.jobs.first = 'native'
console.log(b.jobs.first) // FE

但是該方法也是有局限性的:

  • [ ] 會忽略 undefined
  • [ ] 會忽略 symbol
  • [ ] 不能序列化函數
  • [ ] 不能解決迴圈引用的對象
let obj = {
  a: 1,
  b: {
    c: 2,
    d: 3,
  },
}
obj.c = obj.b
obj.e = obj.a
obj.b.c = obj.c
obj.b.d = obj.b
obj.b.e = obj.b.c
let newObj = JSON.parse(JSON.stringify(obj))
console.log(newObj)

如果你有這麼一個迴圈引用對象,你會發現你不能通過該方法深拷貝

image

在遇到函數、   undefined  或者   symbol  的時候,該對象也不能正常的序列化

let a = {
    age: undefined,
    sex: Symbol('male'),
    jobs: function() {},
    name: 'yck'
}
let b = JSON.parse(JSON.stringify(a))
console.log(b) // {name: "yck"}

你會發現在上述情況中,該方法會忽略掉函數和 undefined 。

但是在通常情況下,複雜數據都是可以序列化的,所以這個函數可以解決大部分問題,並且該函數是內置函數中處理深拷貝性能最快的。當然如果你的數據中含有以上三種情況下,可以使用 lodash 的深拷貝函數。

如果你所需拷貝的對象含有內置類型並且不包含函數,可以使用

MessageChannel
function structuralClone(obj) {
  return new Promise(resolve => {
    const {port1, port2} = new MessageChannel();
    port2.onmessage = ev => resolve(ev.data);
    port1.postMessage(obj);
  });
}
var obj = {a: 1, b: {
    c: b
}}
// 註意該方法是非同步的
// 可以處理 undefined 和迴圈引用對象
(async () => {
  const clone = await structuralClone(obj)
})()

typeof 於 instanceof 區別

typeof 對於基本類型,除了 null 都可以顯示正確的類型

typeof 1 // 'number'
typeof '1' // 'string'
typeof undefined // 'undefined'
typeof true // 'boolean'
typeof Symbol() // 'symbol'
typeof b // b 沒有聲明,但是還會顯示 undefined

typeof 對於對象,除了函數都會顯示 object

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

對於 null 來說,雖然它是基本類型,但是會顯示 object,這是一個存在很久了的 Bug

typeof null // 'object'

 

PS:為什麼會出現這種情況呢?因為在 JS 的最初版本中,使用的是 32 位系統,為了性能考慮使用低位存儲了變數的類型信息,000 開頭代表是對象,然而 null 表示為全零,所以將它錯誤的判斷為 object 。雖然現在的內部類型判斷代碼已經改變了,但是對於這個 Bug 卻是一直流傳下來。

instanceof 可以正確的判斷對象的類型,因為內部機制是通過判斷對象的原型鏈中是不是能找到類型的 prototype。

我們也可以試著實現一下 instanceof

function instanceof(left, right) {
    // 獲得類型的原型
    let prototype = right.prototype
    // 獲得對象的原型
    left = left.__proto__
    // 判斷對象的類型是否等於類型的原型
    while (true) {
        if (left === null)
            return false
        if (prototype === left)
            return true
        left = left.__proto__
    }
}

Webpack

優化打包速度
減少文件搜索範圍
比如通過別名
loader 的 test,include & exclude
Webpack4 預設壓縮並行
Happypack 併發調用
babel 也可以緩存編譯

Babel 原理

本質就是編譯器,當代碼轉為字元串生成 AST,對 AST 進行轉變最後再生成新的代碼

分為三步:詞法分析生成 Token,語法分析生成 AST,遍歷 AST,根據插件變換相應的節點,最後把 AST 轉換為代碼

如何實現一個插件

  • [ ] 調用插件 apply 函數傳入 compiler 對象
  • [ ] 通過 compiler 對象監聽事件

比如你想實現一個編譯結束退出命令的插件

class BuildEndPlugin {
  apply (compiler) {
    const afterEmit = (compilation, cb) => {
      cb()
      setTimeout(function () {
        process.exit(0)
      }, 1000)
    }
    compiler.plugin('after-emit', afterEmit)
  }
}
module.exports = BuildEndPlugin

框架

React 生命周期

在 V16 版本中引入了 Fiber 機制。這個機制一定程度上的影響了部分生命周期的調用,並且也引入了新的 2 個 API 來解決問題。

在之前的版本中,如果你擁有一個很複雜的複合組件,然後改動了最上層組件的 state,那麼調用棧可能會很長

[圖片上傳失敗...(image-2744ce-1546698256133)]

調用棧過長,再加上中間進行了複雜的操作,就可能導致長時間阻塞主線程,帶來不好的用戶體驗。Fiber 就是為瞭解決該問題而生。

Fiber 本質上是一個虛擬的堆棧幀,新的調度器會按照優先順序自由調度這些幀,從而將之前的同步渲染改成了非同步渲染,在不影響體驗的情況下去分段計算更新。

對於如何區別優先順序,React 有自己的一套邏輯。對於動畫這種實時性很高的東西,也就是 16 ms 必須渲染一次保證不卡頓的情況下,React 會每 16 ms(以內) 暫停一下更新,返回來繼續渲染動畫。

對於非同步渲染,現在渲染有兩個階段:reconciliation 和 commit 。前者過程是可以打斷的,後者不能暫停,會一直更新界面直到完成。

Reconciliation 階段

componentWillMount
componentWillReceiveProps
shouldComponentUpdate
componentWillUpdate

Commit 階段

componentDidMount
componentDidUpdate
componentWillUnmount

因為 reconciliation 階段是可以被打斷的,所以 reconciliation 階段會執行的生命周期函數就可能會出現調用多次的情況,從而引起 Bug。所以對於 reconciliation 階段調用的幾個函數,除了 shouldComponentUpdate 以外,其他都應該避免去使用,並且 V16 中也引入了新的 API 來解決這個問題。

getDerivedStateFromProps 用於替換 componentWillReceiveProps ,該函數會在初始化和 update 時被調用

class ExampleComponent extends React.Component {
  // Initialize state in constructor,
  // Or with a property initializer.
  state = {};
  static getDerivedState

您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • 元編程是一種強大的技術,使你能夠編寫可以創建其他程式的程式。ES6藉助代理和許多類似功能,使在JavaScript中利用元編程變得更加容易。ES6 Proxy(代理) 有助於重新定義對象的基本操作,從而為各種可能性打開了大門。 本指南可以幫助您理解為什麼ES6代理如此之好,尤其是對於元編程而言: 什 ...
  • 1.HTML的head標簽 head標簽中主要配置瀏覽器的配置信息 常用的配置信息: 1.網頁解析編碼格式 2.網頁標題標簽 3.網頁關鍵字 4.網頁描述 5.作者 6.自動跳轉 7.其他(引入css,js) 註:其中第三,第四和第五項,提升在瀏覽器中的搜索概率 例: 2.HTML的body標簽(文 ...
  • 憑藉應用廣泛、入門簡單的優勢,Web前端吸引了人們的廣泛關註。學習Web前端就業薪資高,因此很多人都想入門前端開發行業。 零基礎自學Web前端,你需要具備以下幾點: 1、耐性。要成為優秀的web前端開發者,要調整好心態。拋開一切的方法和技術知識,最重要的就是你的耐性。 2、學會延伸。對於新手來說,新 ...
  • 目錄判斷 js 類型的方式ES5 和 ES6 分別幾種方式聲明變數閉包的概念?優缺點?淺拷貝和深拷貝數組去重的方法DOM 事件有哪些階段?談談對事件代理的理解js 執行機制、事件迴圈介紹下 promise.allasync 和 awaitES6 的 class 和構造函數的區別transform、t ...
  • 不知道大家有沒有註意過對象中的一些通用方法,例如所有所有的對象都有 toString、constructor 等等一些方法。 當然如果要仔細看的話,大家可以: var a = {}; console.log(a); 我們可以清晰的看到他有很多的內置方法。當然,也可以看到最下麵有兩個比較怪的方法 ge ...
  • 應屆生:阿姨,我不想努力了在學校用React + antd做過後臺管理系統,熟悉React技術棧。兩年前端:公司技術棧是React,都用了一年了,我React賊六。五年前端:帶團隊把公司的糞坑項目用React重構了。React對我來說就跟呼吸一樣容易。:要不學學React源碼吧。......%……& ...
  • 1.HTML的學習內容 1.HTML的概念 2.互聯網的三大基石 3.HTML的頭標簽 4.HTML的主體標簽 5.HTML的圖片標簽 6.HTML的超鏈接標簽7.HTML的表格標簽 8.HTML的內嵌和框架標簽 9.HTML的表單 2.HTML的概念 HTML:超文本標記語言 作用:需要將java ...
  • 在前面隨筆《循序漸進VUE+Element 前端應用開發(12)--- 整合ABP框架的前端登錄處理》簡單的介紹了一個結合ABP後端的登陸介面實現前端系統登陸的功能,本篇隨筆繼續深化這一主題,著重介紹基於ABP後端介面信息,實現對前端界面的開發工作。 ABP(ASP.NET Boilerplate)... ...
一周排行
    -Advertisement-
    Play Games
  • GoF之工廠模式 @目錄GoF之工廠模式每博一文案1. 簡單說明“23種設計模式”1.2 介紹工廠模式的三種形態1.3 簡單工廠模式(靜態工廠模式)1.3.1 簡單工廠模式的優缺點:1.4 工廠方法模式1.4.1 工廠方法模式的優缺點:1.5 抽象工廠模式1.6 抽象工廠模式的優缺點:2. 總結:3 ...
  • 新改進提供的Taurus Rpc 功能,可以簡化微服務間的調用,同時可以不用再手動輸出模塊名稱,或調用路徑,包括負載均衡,這一切,由框架實現並提供了。新的Taurus Rpc 功能,將使得服務間的調用,更加輕鬆、簡約、高效。 ...
  • 本章將和大家分享ES的數據同步方案和ES集群相關知識。廢話不多說,下麵我們直接進入主題。 一、ES數據同步 1、數據同步問題 Elasticsearch中的酒店數據來自於mysql資料庫,因此mysql數據發生改變時,Elasticsearch也必須跟著改變,這個就是Elasticsearch與my ...
  • 引言 在我們之前的文章中介紹過使用Bogus生成模擬測試數據,今天來講解一下功能更加強大自動生成測試數據的工具的庫"AutoFixture"。 什麼是AutoFixture? AutoFixture 是一個針對 .NET 的開源庫,旨在最大程度地減少單元測試中的“安排(Arrange)”階段,以提高 ...
  • 經過前面幾個部分學習,相信學過的同學已經能夠掌握 .NET Emit 這種中間語言,並能使得它來編寫一些應用,以提高程式的性能。隨著 IL 指令篇的結束,本系列也已經接近尾聲,在這接近結束的最後,會提供幾個可供直接使用的示例,以供大伙分析或使用在項目中。 ...
  • 當從不同來源導入Excel數據時,可能存在重覆的記錄。為了確保數據的準確性,通常需要刪除這些重覆的行。手動查找並刪除可能會非常耗費時間,而通過編程腳本則可以實現在短時間內處理大量數據。本文將提供一個使用C# 快速查找並刪除Excel重覆項的免費解決方案。 以下是實現步驟: 1. 首先安裝免費.NET ...
  • C++ 異常處理 C++ 異常處理機制允許程式在運行時處理錯誤或意外情況。它提供了捕獲和處理錯誤的一種結構化方式,使程式更加健壯和可靠。 異常處理的基本概念: 異常: 程式在運行時發生的錯誤或意外情況。 拋出異常: 使用 throw 關鍵字將異常傳遞給調用堆棧。 捕獲異常: 使用 try-catch ...
  • 優秀且經驗豐富的Java開發人員的特征之一是對API的廣泛瞭解,包括JDK和第三方庫。 我花了很多時間來學習API,尤其是在閱讀了Effective Java 3rd Edition之後 ,Joshua Bloch建議在Java 3rd Edition中使用現有的API進行開發,而不是為常見的東西編 ...
  • 框架 · 使用laravel框架,原因:tp的框架路由和orm沒有laravel好用 · 使用強制路由,方便介面多時,分多版本,分文件夾等操作 介面 · 介面開發註意欄位類型,欄位是int,查詢成功失敗都要返回int(對接java等強類型語言方便) · 查詢介面用GET、其他用POST 代碼 · 所 ...
  • 正文 下午找企業的人去鎮上做貸後。 車上聽同事跟那個司機對罵,火星子都快出來了。司機跟那同事更熟一些,連我在內一共就三個人,同事那一手指桑罵槐給我都聽愣了。司機也是老社會人了,馬上聽出來了,為那個無辜的企業經辦人辯護,實際上是為自己辯護。 “這個事情你不能怪企業。”“但他們總不能讓銀行的人全權負責, ...