【大前端攻城獅之路】面試集錦

TJYoung發表於2020-07-12

JS相關

1.變數提升

ES6之前我們一般使用var來宣告變數,提升簡單來說就是把我們所寫的類似於var a = 123;這樣的程式碼,宣告提升到它所在作用域的頂端去執行,到我們程式碼所在的位置來賦值。

function test() {
    console.log(a); // undefined
    a = 123;      
}

test();

  

執行順序如下:

function test() {
    var a;
    console.log(a); // undefined
    a = 123;      
}

test();

  

2.函式提升

javascript中不僅僅是變數宣告有提升的現象,函式的宣告也是一樣;具名函式的宣告有兩種方式:1. 函式宣告式    2. 函式字面量式

function test() {} // 函式式宣告
let test = function() {} // 字面量宣告

  

函式提升是整個程式碼塊提升到它所在的作用域的最開始執行

console.log(f);
function f() {
    console.log(1);
}

// 相當於以下程式碼
function f() {
    console.log(1);
}
console.log(f);

  

foo(); //1
 
var foo;
 
function foo () {
    console.log(1);
}
 
foo = function () {
    console.log(2);
}

  

根因分析:javascript引擎並將var a和a = 2看做是兩個單獨的宣告,第一個是編譯階段的任務,而第二個則是執行階段的任務。這意味著無論作用域中的宣告出現在什麼地方,都將在程式碼本身被執行前首先進行處理,可以將這個過程形象地想象成所有的宣告(變數和函式)都會被“移動”到各自作用域的最頂端,這個過程被稱為提升。

 

3.bind、call、apply

call和apply其實是同一個東西,區別只有引數不同,call是apply的語法糖,所以就放在一起說了,這兩個方法都是定義在函式物件的原型上的(Function.prototype),call和apply方法的作用都是改變函式的執行環境,第一個引數傳入上下文執行環境,然後傳入函式執行所需的引數。傳入call的引數只能是單個引數,不能是陣列。apply可傳入陣列。話不多說直接上程式碼,看下面的例子:

function ga() {

    let x=1;
} function gb(y) { return x+y; } gb(2) //呼叫發生報錯,因為拿不到x的值 gb.call(ga,2); //使gb在ga環境中執行,可以拿到x,執行正常

  

上面的程式碼中由於gb()函式執行依賴於ga()中的變數,所以我們使用了call將gb的執行環境變成了ga。

 

function gg(x,y,z){

    let a=Array.prototype.slice.call(arguments,1,2) //通過slice方法獲取到了第二個引數

    return a; //返回[2]

}

gg(1,2,3)
// arguments是一個類陣列物件,它本身不能呼叫陣列的slice方法,使用call將執行slice方法的物件由陣列變為了arguments。

使用apply改寫上面的方法

function gg(x,y,z){

    let d=[1,2]

    let a=Array.prototype.slice.apply(arguments,d) //通過slice方法獲取到了第二個引數

    return a; //返回[2]

}

gg(1,2,3)

  

使用apply和call實現繼承

function Parent(name) {

    this.name = name;

    this.sayHello = function() {
        alert(name);
    }
}

function Child(name) {
    // 子類的this傳給父類
    Parent.call(this, name);
}

let parent = new Parent("張三");

let child = new Child("李四");

parent.sayHello();

child.sayHello();

  

bind和apply區別是apply會立刻執行,而bind只是起一個繫結執行上下文的作用。看下面的例子:

function ga() {
    let x=1;

    (function gb(y) {
        return x+y;
    }).bind(this) //使用bind將gb函式的執行上下文繫結到ga上
}

gb(2) //執行正常,得到3

// 有些情況下為了方便我們可以直接將ga繫結,而不用在呼叫的時候再使用apply。

  

4.原型&原型鏈

在JavaScript中,每個函式都有一個prototype屬性,這個屬性指向函式的原型物件(原型就是一個Object的例項,是一個物件

 

 

每個物件(除null外)都會有的屬性,叫做__proto__,這個屬性會指向該物件的原型;絕大部分瀏覽器都支援這個非標準的方法訪問原型,然而它並不存在於 Person.prototype 中,實際上,它是來自於 Object.prototype ,與其說是一個屬性,不如說是一個 getter/setter,當使用 obj.__proto__ 時,可以理解成返回了 Object.getPrototypeOf(obj)。

 

 每個原型都有一個constructor屬性,指向該關聯的建構函式

 

 當讀取例項的屬性時,如果找不到,就會查詢與物件關聯的原型中的屬性,如果還查不到,就去找原型的原型,一直找到最頂層為止

 原型的原型是什麼?

其實原型物件就是通過 Object 建構函式生成的,結合之前所講,例項的 __proto__ 指向建構函式的 prototype

 

 簡單的回顧一下建構函式、原型和例項的關係:每個建構函式都有一個原型物件,原型物件都包含一個指向建構函式的指標,而例項都包含一個指向原型物件的內部指標。那麼假如我們讓原型物件等於另一個型別的例項,結果會怎樣?顯然,此時的原型物件將包含一個指向另一個原型的指標,相應地,另一個原型中也包含著一個指向另一個建構函式的指標。假如另一個原型又是另一個型別的例項,那麼上述關係依然成立。如此層層遞進,就構成了例項與原型的鏈條。這就是所謂的原型鏈的基本概念。

如圖所示:藍色即為原型鏈。

 5. this指向

面嚮物件語言中 this 表示當前物件的一個引用。

但在 JavaScript 中 this 不是固定不變的,它會隨著執行環境的改變而改變。

  • 在方法中,this 表示該方法所屬的物件。
  • 如果單獨使用,this 表示全域性物件。
  • 在函式中,this 表示全域性物件。
  • 在函式中,在嚴格模式下,this 是未定義的(undefined)。
  • 在事件中,this 表示接收事件的元素。
  • 類似 call() 和 apply() 方法可以將 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)

1
2
undefined
3

  

6.堆和棧

這裡先說兩個概念:1、堆(heap)2、棧(stack)
堆 是堆記憶體的簡稱。
棧 是棧記憶體的簡稱。
說到堆疊,我們講的就是記憶體的使用和分配了,沒有暫存器的事,也沒有硬碟的事。
各種語言在處理堆疊的原理上都大同小異。堆是動態分配記憶體,記憶體大小不一,也不會自動釋放。棧是自動分配相對固定大小的記憶體空間,並由系統自動釋放。

javascript的基本型別就5種:Undefined、Null、Boolean、Number和String,它們都是直接按值儲存在棧中的,每種型別的資料佔用的記憶體空間的大小是確定的,並由系統自動分配和自動釋放。這樣帶來的好處就是,記憶體可以及時得到回收,相對於堆來說,更加容易管理記憶體空間。

javascript中其他型別的資料被稱為引用型別的資料 : 如物件(Object)、陣列(Array)、函式(Function) …,它們是通過拷貝和new出來的,這樣的資料儲存於堆中。其實,說儲存於堆中,也不太準確,因為,引用型別的資料的地址指標是儲存於棧中的,當我們想要訪問引用型別的值的時候,需要先從棧中獲得物件的地址指標,然後,在通過地址指標找到堆中的所需要的資料。

說來也是形象,棧,線性結構,後進先出,便於管理。堆,一個混沌,雜亂無章,方便儲存和開闢記憶體空間;

 

 

 7.generate,async, await 參考https://blog.csdn.net/qdmoment/article/details/86672907

generator生成器的設計原理:

  1. 狀態機,簡化函式內部狀態儲存;
  2. 半協程實現
  3. 上下文凍結

應用場景:

  1. 非同步操作的同步化表達
  2. 控制流管理
  3. 部署 Iterator 介面
  4. 作為資料結構

整個 Generator 函式就是一個封裝的非同步任務,或者說是非同步任務的容器。非同步操作需要暫停的地方,都用yield語句註明

Generator 函式是協程在 ES6 的實現,最大特點就是可以交出函式的執行權(即暫停執行)

generator生成器和iterator遍歷器是對應的,我們知道iterator遍歷器是給不同資料結構提供統一的資料介面機制,那麼相對的generator生成器是生成這樣一個遍歷器,進而使資料結構擁有iterator遍歷器介面。換一種方法來說,generator函式提供了可供遍歷的狀態,所以generator是一個狀態機,在其內部封裝了多個狀態,這些狀態可以使用iterator遍歷器遍歷

注意:既然generator是一個狀態機,所以直接執行generator()函式,並不會執行,相反的是生成一個指向內部狀態的指標物件,即一個可供遍歷的遍歷器

想執行generator,必須呼叫遍歷器物件的next方法,使得指標移向下一個狀態,直到遇到下一個yield表示式(或return語句)為止。Generator 函式是分段執行的,yield表示式是暫停執行的標記,而next方法可以恢復執行。

const test = testGen();
 
test.next()
// { value: '1', done: false }
 
test.next()
// { value: '2', done: false }
 
test.next()
// { value: 'ending', done: true }
 
test.next()
// { value: undefined, done: true }

// 函式有三個狀態 1,2,return
function* testGen() {
    yield '1';
    yield '2';
    return 'end';
}

  

Generator的原型方法:
Generator.prototype.throw(),Generator.prototype.return()
throw() 在函式體外丟擲錯誤,然後在 Generator 函式體內捕獲

return():返回給定的值,並且終結遍歷 Generator 函式

next()、throw()、return() 的共同點
作用都是讓 Generator 函式恢復執行,並且使用不同的語句替換yield表示式(帶入參)

next()是將yield表示式替換成一個值
throw()是將yield表示式替換成一個throw語句
return()是將yield表示式替換成一個return語句

 

async函式

async 函式的實現原理,就是將 Generator 函式和自動執行器,包裝在一個函式裡。

(看了很多遍還不是很明白~)

async function fn(args) {
  // ...
}
 
// 等同於
 
function fn(args) {
  return spawn(function* () {
    // ...
  });
}
 
function spawn(genF) {
  return new Promise(function(resolve, reject) {
    const gen = genF();
    function step(nextF) {
      let next;
      try {
        next = nextF();
      } catch(e) {
        return reject(e);
      }
      if(next.done) {
        return resolve(next.value);
      }
      Promise.resolve(next.value).then(function(v) {
        step(function() { return gen.next(v); });
      }, function(e) {
        step(function() { return gen.throw(e); });
      });
    }
    step(function() { return gen.next(undefined); });
  });
}

  

8.如何實現一個 Promise

promise的核心原理其實就是釋出訂閱模式,通過兩個佇列來快取成功的回撥(onResolve)和失敗的回撥(onReject)。

promise的特點:

  1. new Promise時需要傳遞一個executor執行器,執行器會立刻執行(是在主執行緒執行,區別於then)
  2. 執行器中傳遞了兩個引數:resolve成功的函式、reject失敗的函式,他們呼叫時可以接受任何值的引數value
  3. promise狀態只能從pending態轉onfulfilled,onrejected到resolved或者rejected,然後執行相應快取佇列中的任務
  4. promise例項,每個例項都有一個then方法,這個方法傳遞兩個引數,一個是成功回撥onfulfilled,另一個是失敗回撥onrejected
  5. promise例項呼叫then時,如果狀態resolved,會讓onfulfilled執行並且把成功的內容當作引數傳遞到函式中
  6. promise中可以同一個例項then多次,如果狀態是pengding 需要將函式存放起來 等待狀態確定後 在依次將對應的函式執行 (釋出訂閱)

(1) 建構函式

function Promise(resolver) {}

(2) 原型方法

Promise.prototype.then = function() {}
Promise.prototype.catch = function() {}

(3) 靜態方法

Promise.resolve = function() {}
Promise.reject = function() {}
Promise.all = function() {}
Promise.race = function() {}

function Promise (executor) {
  var self = this;//resolve和reject中的this指向不是promise例項,需要用self快取
  self.state = 'padding';
  self.value = '';//快取成功回撥onfulfilled的引數
  self.reson = '';//快取失敗回撥onrejected的引數
  self.onResolved = []; // 專門存放成功的回撥onfulfilled的集合
  self.onRejected = []; // 專門存放失敗的回撥onrejected的集合
  function resolve (value) {
    if(self.state==='padding'){
      self.state==='resolved';
      self.value=value;
      self.onResolved.forEach(fn=>fn())
    }
  }
  function reject (reason) {
    self.state = 'rejected';
    self.value = reason;
    self.onRejected.forEach(fn=>fn())
  }
  try{
    executor(resolve,reject)
  }catch(e){
    reject(e)
  }
}


Promise.prototype.then=function (onfulfilled,onrejected) {
  var self=this;
  if(this.state==='resolved'){
    onfulfilled(self.value)
  }
  if(this.state==='rejected'){
    onrejected(self.value)
  }
  if(this.state==='padding'){
    this.onResolved.push(function () {
      onfulfilled(self.value)
    })
  }
}

Promise.prototype.catch = function (onrejected) {
  return this.then(null, onrejected)
};

Promise.reject = function (reason) {
  return new Promise((resolve, reject) => {
    reject(reason)
  })
};
Promise.resolve = function (value) {
  return new Promise((resolve, reject) => {
    resolve(value);
  })
};

Promise.all=function (promises) {
  return new Promise((resolve,reject)=>{
    let results=[],i=0;
    for(let i=0;i<promises.length;i++){
      let p=promises[i];
      p.then((data)=>{
        processData(i,data)
      },reject)
    }
    function processData (index,data) {
      results[index]=data;
      if(++i==promises.length){
        resolve(results)
      }
    }
  })
};
//在每個promise的回撥中新增一個resolve(就是在當前的promise.then中新增),有一個狀態改變,就讓race的狀態改變
Promise.race=function (promises) {
  return new promises((resolve,reject)=>{
    for(let i=0;i<promises.length;i++){
      let p=promises[i];
      p.then(resolve,reject)
    }
  })

  

9.垃圾回收機制

一般來說沒有被引用的物件就是垃圾,就是要被清除, 有個例外如果幾個物件引用形成一個環,互相引用,但根訪問不到它們,這幾個物件也是垃圾,也要被清除。

JS中最常見的垃圾回收方式是標記清除。

工作原理:是當變數進入環境時,將這個變數標記為“進入環境”。當變數離開環境時,則將其標記為“離開環境”。標記“離開環境”的就回收記憶體。

工作流程:

1.    垃圾回收器,在執行的時候會給儲存在記憶體中的所有變數都加上標記。

2.    去掉環境中的變數以及被環境中的變數引用的變數的標記。

3.    再被加上標記的會被視為準備刪除的變數。

4.    垃圾回收器完成記憶體清除工作,銷燬那些帶標記的值並回收他們所佔用的記憶體空間。

引用計數 方式

工作原理:跟蹤記錄每個值被引用的次數。

工作流程:

1.    宣告瞭一個變數並將一個引用型別的值賦值給這個變數,這個引用型別值的引用次數就是1。

2.    同一個值又被賦值給另一個變數,這個引用型別值的引用次數加1.

3.    當包含這個引用型別值的變數又被賦值成另一個值了,那麼這個引用型別值的引用次數減1.

4.    當引用次數變成0時,說明沒辦法訪問這個值了。

5.    當垃圾收集器下一次執行時,它就會釋放引用次數是0的值所佔的記憶體。

新生代演算法(http://newhtml.net/v8-garbage-collection/

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

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

老生代演算法

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

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

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

10. 深拷貝

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

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

  • 會忽略 undefined
  • 會忽略 symbol
  • 不能序列化函式
  • 不能解決迴圈引用的物件

手動實現:

// 定義一個深拷貝函式  接收目標target引數
function deepClone(target) {
    // 定義一個變數
    let result;
    // 如果當前需要深拷貝的是一個物件的話
    if (typeof target === 'object') {
    // 如果是一個陣列的話
        if (Array.isArray(target)) {
            result = []; // 將result賦值為一個陣列,並且執行遍歷
            for (let i in target) {
                // 遞迴克隆陣列中的每一項
                result.push(deepClone(target[i]))
            }
         // 判斷如果當前的值是null的話;直接賦值為null
        } else if(target===null) {
            result = null;
         // 判斷如果當前的值是一個RegExp物件的話,直接賦值    
        } else if(target.constructor===RegExp){
            result = target;
        }else {
         // 否則是普通物件,直接for in迴圈,遞迴賦值物件的所有值
            result = {};
            for (let i in target) {
                result[i] = deepClone(target[i]);
            }
        }
     // 如果不是物件的話,就是基本資料型別,那麼直接賦值
    } else {
        result = target;
    }
     // 返回最終結果
    return result;
}

  

未完待續···

相關文章