web前端進階篇(一 )JS

a1322674015發表於2019-10-18

1談談變數提升

當執行JS程式碼時,會生成執行環境,只要程式碼不是寫在函式中的,就是在堆疊執行環境中,函式中的程式碼會產生函式執行環境,僅此兩種執行環境。


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了,在第一階段記憶體也已經為他開闢好了空間,但是因為這個宣告的特性導致了並不能在宣告前使用


2繫結,呼叫,應用區別

  • 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實現柯里化


3如何實現一個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))
  }
}

4如何實現一個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
}

5如何實現一個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
}

6簡單說下原型鏈?

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

  • 每個物件都有__proto__屬性,指向了建立該物件的建構函式的原型。其實這個屬性指向了[[prototype]],但是[[prototype]]是內部屬性,我們並不能訪問到,所以使用_proto_來訪問。

  • 物件可以透過__proto__來尋找不屬於該物件的屬性,__proto__將物件連線起來組成了原型鏈。

7怎麼判斷物件型別

  • 可以透過Object.prototype.toString.call(xx)。這樣我們就可以獲得類似[object Type]的字串。

  • instanceof 可以正確的判斷物件的型別,因為內部機制是透過判斷物件的原型鏈中是不是能找到型別的 prototype

8箭頭函式的特點

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

函式箭頭的英文其實沒有this的,函式這個中的this只取決於他外面的第一個不是箭頭函式的函式的this。在這個例子中,呼叫因為a符合前面程式碼中的第一個情況,所以this的英文window。並且this一旦繫結了一部分,就不會被任何程式碼改變


9這個

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

10 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

  • 然後後面就是常規執行程式碼了

11 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();
      }
    }
  });
}

12承諾

  • Promise是ES6補充的語法,解決了備選地獄的問題。

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

  • then函式會返回一個Promise例項,並且該返回值是一個新的例項而不是之前的例項。因為Promise規範規定pending已有狀態,其他狀態是不可以改變的,如果返回的是一個相同例項的話,多個then呼叫就失去了意義了。對於then來說,本質上可以把它看成是flatMap

13如何實現一個承諾

// 三種狀態
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);
  }
}

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


這裡來解析一道[] == ![] // -> true譯文,下面是這個表示式為何為true的步驟

// [] 轉成 true,然後取反變成 false
[] == false
// 根據第 8 條得出
[] == ToNumber(false)
[] == 0
// 根據第 10 條得出
ToPrimitive([]) == 0
// [].toString() -> ''
'' == 0
// 根據第 6 條得出
0 == 0 // -> true
===在開發中,對於預先返回的code,可以透過==去判斷

15基本資料型別和引薦型別在儲存上的差異


前者儲存在棧上,另外儲存在堆上


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

如果JS是門多執行緒的語言話,我們在多個執行緒中處理DOM就可能會發生問題(一個執行緒),因為JS是門非雙向單執行緒語言,因為在最初是JS就是為了和瀏覽器互動而產生的。中新加二進位制,另一個執行緒中刪除例程),當然可以約會讀寫鎖解決這個問題。


  • 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稱為工作,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 renderin

很多人有個誤區,認為微任務快於宏任務,其實是錯誤的。因為宏任務中包括了script,瀏覽器會先執行一個宏任務,然後有非同步程式碼的話就先執行微任務


所以正確的一次Event loop順序是這樣的


  • 執行同步程式碼,這屬於宏任務

  • 執行棧為空,查詢是否有微任務需要執行

  • 執行所有微任務

  • 必要的話渲染 UI

  • 然後開始下一輪Event loop,執行宏任務中的非同步程式碼

  • 透過上述的 Event loop順序可知,如果宏任務中的非同步程式碼有大量的計算和需要操作DOM的話,為了更換的介面響應,我們可以把操作DOM放入微任務中


17 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)

18片段降維

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


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


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

19深複製

這個問題通常可以透過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)

在遇到函式,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"}



但是在通常情況下,複雜資料都是可以序列化的,所以這個函式可以解決大部分問題,並且該函式是內建函式中處理深複製效能移動的。當然如果你的資料中含有以上某種情況下,可以使用lodash的深複製函式


20 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'



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__
    }
}






來自 “ ITPUB部落格 ” ,連結:http://blog.itpub.net/69946034/viewspace-2660584/,如需轉載,請註明出處,否則將追究法律責任。

相關文章