前端面試之JavaScript篇

之前準備面試的時候整理的,希望能帶給大家一點幫助~

js爲什麼是單線程,有什麼好處?

js最初被設計用在瀏覽器中,假如js是多線程的,當第一個線程上的js與第二個線程上的js同時對一個dom進行操作時,這個dom就不知道該執行哪個線程上的指令。

js異步加載的方式

  1. 使用async屬性
  2. 使用defer屬性
  3. onload時動態添加script標籤

Microtasks、Macrotasks(事件循環event loop、任務隊列task queues)

常見macro-task:整體的script、setTimeout、setInterval

常見micro-task:promise、process.nextTick

js通過事件循環實現異步,具體過程爲:
執行一個宏任務,過程中如果遇到微任務,就將其放到微任務的事件隊列裏,當前宏任務執行完成後,會查看微任務的事件隊列,並將裏面全部的微任務依次執行完。然後在執行一個宏任務,這樣一直循環下去。

原型和原型鏈

構造函數、原型、實例的關係

構造函數中有一個prototype指針指向原型,原型中也有一個constructor指針指向構造函數。實例中有一個內部屬性__proto__指向原型。構造函數和實例間通過原型產生聯繫,他們本身沒有直接的關聯。

new的基本原理(當let fun = new Fun()時發生了什麼?)

  1. 在構造函數內部聲明瞭一個對象
  2. 將構造函數的作用域賦給這個對象(obj.__proto__ = Fun.prototype
  3. 執行構造函數(給對象添加屬性)
  4. 返回這個對象

這裏同時附上實現new的代碼:

// new的內部機制
 function Person() {
    this.name = "I'm from Person!"
  }
  let person = new Person()
  console.log(person);
  // new
  function myNew(Constructor) {
    let obj = {};
    obj.__proto__ = Constructor.prototype;
    obj.name = "I'm new!";
    return obj;
  }
  console.log(myNew(Person));

繼承

知道了構造函數、原型、實例三者之間的關係後,可以試想一下,當一個構造函數的原型等於另一個對象實例時,會發生什麼?對,此時就形成了一個鏈式連接,原型鏈。

可以結合這個圖理解一下:
在這裏插入圖片描述
我們在查找一個對象的屬性時,如果在對象本身身上沒有找到,js就會沿着這個對象的原型鏈繼續向上查找,直到找到這個屬性。那如果一直查到了最頂部(最頂部對象的原型爲null)都沒有找到,就說明這個對象上沒有這個屬性。因此我們可以用繼承來實現屬性和函數的共享。

繼承也就是用原型鏈實現的,關於繼承,常考es5和es6的繼承(es5中組合繼承,es6中extends)。具體在代碼中的實現如下:

ES5:

/**
 *es5繼承 Student繼承Person 組合繼承
 */
function Person(name, gender) {
  this.name = name;
  this.gender = gender;
}
Person.prototype.sayName = function() {
  console.log(this.name)
}
function Student(name, gender, grade) {
  Person.call(this, name, gender);
  this.grade = grade;
}
Student.prototype = new Person();
Student.prototype.constructor = Student;
const student = new Student('張三', '男', '大三');
student.sayName();
console.log(student);

ES6:

/**
 * es6繼承 Student繼承Person
 */ 
class Person {
  constructor(name, gender) {
    this.name = name;
    this.gender = gender;
  }
  sayName() {
    console.log(this.name);
  }
}
class Student extends Person {
  constructor(name, gender, grade) {
    super(name, gender);
    this.grade = grade;
  }
}
const student = new Student('張三', '男', '大三');
student.sayName();
console.log(student);

prototype和__proto__的關係

所有的對象都擁有__proto__屬性,它指向構造函數的prototype原型對象,最後指向Object.prototype(Object是一個原生函數,所有的對象都是Object的實例)。

所有的函數都同時擁有__proto__和prototype屬性,函數的__proto__指向自己的函數實現,函數的prototype是一個對象,所以函數的prototype也有__proto__屬性,指向Object.prototype。

Object.prototype.__proto__指向null(原型鏈的終點指向null)。
在這裏插入圖片描述

typeof和instanceof

typeof用於檢測基本類型,當他檢測引用類型array和object時,得到的都是“object”。因此,我們需要instanceof。

instanceof用於檢測引用類型,它可以區分出array和object。其內部是通過原型鏈來實現的,比如 arr1 instanceof Array ,他會在arr1的原型鏈上查找,這裏只查找一層,arr1.__proto__ == Array.prototype,返回true。

具體的實現:

// instanceof的內部機制
function myInstanceof(obj, Constructor) {
  while(obj.__proto__ !== null) {
    if(obj.__proto__ == Constructor.prototype) break;
    obj.__proto__ = obj.__proto__.__proto__;
  }
  if(obj.__proto__ == null) return false;
  return true;
}

const arr = [1,3,2,1,4];
console.log(myInstanceof(arr, Array));
console.log(myInstanceof(arr, Object));

堆內存和棧內存

棧內存:存放基本數據類型String、Number、undefined、Boolean、null

堆內存:存放引用類型Object、Array、Function、Date、RegExp、包裝類型(Boolean、Number、String)

這個可以跟深拷貝聯繫一下~

什麼是事件委託(事件代理),事件委託有哪些優點?

事件委託就是將事件綁定到父元素上,根據事件的冒泡,當子元素處理事件時會自動觸發父元素的事件。通過判斷事件對象event的target可以找到時間實際發生的子元素。

優點:提高性能、動態監聽。提高性能是因爲減少了事件監聽的數量,動態監聽是指當增加一個子元素時,該子元素自動擁有父級元素上綁定的事件。

舉例:最經典的就是ul和li標籤的事件監聽,比如我們在添加事件時候,採用事件委託機制,不會在li標籤上直接添加,而是在ul父元素上添加。

作用域、作用域鏈

js沒有塊作用域,只有函數作用域。函數內部的函數可以訪問到外函數中的變量,他們都可以訪問到全局作用域中的變量,全局執行環境的變量對象始終是作用域鏈中的最後一個對象。

es6中的let、const可以達到塊級作用域的效果。

提升

js存在變量提升。變量提升包括函數聲明提升和變量聲明提升,函數聲明提升優先於變量聲明提升,函數表達式不提升。
要點就這些,具體的可以看我的博客:關於JavaScript中的聲明提升

閉包

什麼是閉包

一個持有外部環境變量的函數就是閉包

哪些地方用到了閉包

回調函數、私有屬性、柯里化

閉包的缺陷

內存泄漏,博客:關於JavaScript的內存泄漏

this指向

普通函數,this指向調用它的對象
箭頭函數,this指向上下文對象

當this指向全局對象時也可能引起內存泄漏。

bind、call和apply

相同點:都可以改變this指向

區別:

  1. bind不調用函數,返回一個新的函數,只有一個參數,指明this的指向
  2. call會直接調用函數,call只有一個參數,指明this的指向
  3. apply有兩個參數,第二個參數一般爲數組,apply將數組展開傳給函數

深拷貝和淺拷貝,實現深拷貝

淺拷貝和深拷貝都只針對於引用數據類型,淺拷貝只複製指向某個對象的指針,而不復制對象本身,新舊對象還是共享同一塊內存,所以當一個對象發生變化時,另一個對象隨之改變;

深拷貝會另外創造一個一模一樣的對象,新對象跟原對象不共享內存,修改新對象不會改到原對象;

區別:淺拷貝只複製對象的第一層屬性、深拷貝可以對對象的屬性進行遞歸複製;

具體的實現:
兩種方法 ~
第一種,遞歸實現:

/**
 *遞歸實現對象深拷貝
 *
 * @param {Object || Array} source
 * @returns
 */
function deepClone(source) {
  if(typeof source !== "object") return source; // 淺拷貝
  let target = source instanceof Array ? [] : {};
  for(let key in source) {
    // 數組索引,對象鍵值
    target[key] = typeof source[key] === 'object' ? deepClone(source[key]) : source[key];
  }
  return target;
}
// 對象淺拷貝
let obj1 = {a:1, b:2};
let easyObj = obj1;
easyObj.a = 3;
console.log(obj1, easyObj);
// 對象深拷貝
let obj2 = {a:1, b:2, c: new Date()};
let deepObj = deepClone(obj2);
deepObj.a = 3;
console.log(obj2, deepObj);
// 數組淺拷貝
let arr1 = [1, 2, {a: 1}];
let easyArr = arr1;
easyArr[2].a = 2;
console.log(arr1, easyArr);
// 數組深拷貝
let arr2 = [1, 2, {a: 1}];
let deepArr = deepClone(arr2);
deepArr[2].a = 2;
console.log(arr2, deepArr);

第二種,用json的內置方法:

/**
 *json實現對象深拷貝
 *
 * @param {Object || Array} source
 * @returns
 */
function deepClone(source) {
  return JSON.parse(JSON.stringify(source));
}
// 對象淺拷貝
let obj1 = {a:1, b:2};
let easyObj = obj1;
easyObj.a = 3;
console.log(obj1, easyObj);
// 對象深拷貝
let obj2 = {a:1, b:2};
let deepObj = deepClone(obj2);
deepObj.a = 3;
console.log(obj2, deepObj);
// 數組淺拷貝
let arr1 = [1, 2, {a: 1}];
let easyArr = arr1;
easyArr[2].a = 2;
console.log(arr1, easyArr);
// 數組深拷貝
let arr2 = [1, 2, {a: 1}];
let deepArr = deepClone(arr2);
deepArr[2].a = 2;
console.log(arr2, deepArr);

函數的防抖和節流

目的:防止在事件持續觸發的過程中頻繁執行函數。

防抖,指觸發事件後在 n 秒內函數只能執行一次,如果在 n 秒內又觸發了事件,則會重新計算函數執行時間。防抖函數分爲非立即執行版和立即執行版。

/**
 * @desc 函數防抖
 * @param func 函數
 * @param wait 延遲執行毫秒數
 * @param immediate true 表立即執行,false 表非立即執行
 */
function debounce(func,wait,immediate) {
  let timeout;
  return function () {
      let context = this;
      let args = arguments;

      if (timeout) clearTimeout(timeout);
      if (immediate) {
          var callNow = !timeout;
          timeout = setTimeout(() => {
              timeout = null;
          }, wait)
          if (callNow) func.apply(context, args)
      }
      else {
          timeout = setTimeout(function(){
              func.apply(context, args)
          }, wait);
      }
  }
}

節流,就是指連續觸發事件但是在 n 秒中只執行一次函數。節流會稀釋函數的執行頻率。

對於節流,一般有兩種方式可以實現,分別是時間戳版和定時器版。

/**
 * @desc 函數節流
 * @param func 函數
 * @param wait 延遲執行毫秒數
 * @param type 1 表時間戳版,2 表定時器版
 */
function throttle(func, wait ,type) {
    if(type===1){
        let previous = 0;
    }else if(type===2){
        let timeout;
    }
    return function() {
        let context = this;
        let args = arguments;
        if(type===1){
            let now = Date.now();
            if (now - previous > wait) {
                func.apply(context, args);
                previous = now;
            }
        }else if(type===2){
            if (!timeout) {
                timeout = setTimeout(() => {
                    timeout = null;
                    func.apply(context, args)
                }, wait)
            }
        }
    }
}

冒泡和捕獲

冒泡是從內向外,捕獲是從外向內。
冒泡一般會在講事件委託的時候提到。
具體的可以看我博客:關於事件冒泡和事件捕獲

原生Ajax

var xhr = new XMLHttpRequest();
xhr.onreadystatechange = function() {
  if(xhr.readyState == 4) {
    if(xhr.status >= 200 && xhr.status < 300 || xhr.status == 304>) {
      console.log(xhr.responseText);
    } else {
      console.error('error:::', xhr.status);
    }
  }
};
xhr.open("get", "demo.txt", true);
xhr.setRequestHeader("MyHeader", "MyValue");
xhr.send(null);

注意點:

  1. send()方法接收一個參數,作爲請求主體發送的數據。如果不需要發送數據最好傳入null,因爲參數對有些瀏覽器來說是必須的
  2. 必須在調用open()之前指定onreadystatechange事件處理程序才能確保跨瀏覽器 兼容性
  3. 要成功發送請求頭部信息,必須在調用open()方法之後且調用send()方法之前調用setRequestHeader()

模塊化

CommonJs

用於後端node和前端webpack

接口:module.exports和require

特點:

  1. 模塊輸出的是一個值的拷貝,模塊是運行時加載,同步加載
  2. CommonJS 模塊的頂層this指向當前模塊
    AMD(Asynchronous Module Definition,異步模塊定義)
    瀏覽器端模塊化開發的規範,require.js爲AMD規範的實現
    接口:define、require、config
    特點:異步加載,不阻塞頁面的加載,能並行加載多個模塊,但是不能按需加載,必須提前加載所需依賴

ES6 module

接口:import、export、export default
內嵌在網頁中的用法:

<script type="module">
  import utils from "./utils.js";
  // other code
</script>

此時不能用file協議,否則會報跨域的錯誤
特點:

  1. ES6 模塊之中,頂層的this指向undefined,即不應該在頂層代碼使用this
  2. 自動採用嚴格模式"use strict"。須遵循嚴格模式的要求
  3. ES6 模塊的設計思想是儘量的靜態化,編譯時加載”或者靜態加載,編譯時輸出接口
  4. ES6 模塊export、import命令可以出現在模塊的任何位置,但是必須處於模塊頂層。如果處於塊級作用域內,就會報錯
  5. ES6模塊輸出的是值的引用

Tree-Shaking

介紹:消除無用的代碼,減少js包的大小,從而減少頁面的加載時間。

原理:找到有用的代碼打包進去。依賴es6的module模塊,tree shaking會分析文件項目裏具體哪些代碼被引入了,哪些沒有引入,然後將真正引入的代碼打包進去,最後沒有使用到的代碼自然就不會存在了。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章