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

JS相關

1.變量提高

ES6以前咱們通常使用var來聲明變量,提高簡單來講就是把咱們所寫的相似於var a = 123;這樣的代碼,聲明提高到它所在做用域的頂端去執行,到咱們代碼所在的位置來賦值。javascript

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

test();

  

執行順序以下:html

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

test();

  

2.函數提高

javascript中不只僅是變量聲明有提高的現象,函數的聲明也是同樣;具名函數的聲明有兩種方式:1. 函數聲明式    2. 函數字面量式java

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。promise

 

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的實例,是一個對象app

 

 

每一個對象(除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.堆和棧

這裏先說兩個概念:一、堆(heap)二、棧(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;
}

  

未完待續···
相關文章
相關標籤/搜索