70道JavaScript面試題

原文章鏈接
https://mp.weixin.qq.com/s?__biz=Mzg3ODAyMzkxOQ==&mid=2247484495&idx=1&sn=6ceec883a8ceaece5564ac929e2e7387&chksm=cf1b41cff86cc8d9d750aaca88a81551c4b463dd2868f6ef96ccad5f068889cc979fb86ace0a&mpshare=1&scene=23&srcid=0428MSXjZ1Yb74Qwg6VHRFmy&sharer_sharetime=1588040891126&sharer_shareid=5267396f169a074d1674498c0734d359#rd

1. undefined 和 null 有什么區(qū)別?

相似之處

它們屬于 JavaScript 的 7 種基本類型。

使用Boolean(value)或!!value將其轉換為布爾值時,值為false

    console.log(!!undefined);   // false
    console.log(!!null);    // false

    console.log(Boolean(null)); // false
    console.log(Boolean(undefined)); // false

區(qū)別

undefined是指未指定值的變量的默認值,或者函數沒有指定返回值,會默認返回undefined

    let _undefined;
    let fun = function () {}
    let obj = {
        name:'Aiva',
        age:1
    }

    console.log(_undefined);    // undefined
    console.log(fun()); // undefined
    console.log(obj.sex);   // undefined

null是“不代表任何值的值”。 null是已明確定義給變量的值。

在比較null和undefined時,我們使用==時得到true,使用===時得到false:

    console.log(undefined == null);     // true
    console.log(undefined === null);        // false

使用typeof判斷數據類型時,undefined會返回undefined,null會返回object。

    console.log(undefined);     // undefined
    console.log(null);      // object

2. && 運算符能做什么?

&& 也叫邏輯與,在表達式中從左往右查找,找到一個虛值表達式并返回它,如果沒有找到任何虛值表達式,則會返回最后一個虛值表達式。

    console.log(1 && 0 && true);    // 0
    console.log(true && 222 && 1);  // 1

在if語句中使用

    let flag;
    if (true && 22) {
        flag = true;
    }else {
        flag = true;
    }

    console.log(flag);      // true

3. || 運算符能做什么

||也叫或邏輯或,在其操作數中找到第一個真值表達式并返回它。

在支持 ES6 默認函數參數之前,它用于初始化函數中的默認參數值。

    console.log(1 || 0 || true);    // 1
    console.log('' || 0 || 12); // 12

    function fun(x) {
        var x = x || 0;
    }

4. 使用 + 或一元加運算符是將字符串轉換為數字的最快方法嗎?

+是將字符串轉換為數字的最快方法,因為如果值已經是數字,它不會執(zhí)行任何操作。

5. DOM 是什么?

DOM 代表文檔對象模型,是 HTML 和 XML 文檔的接口(API)。當瀏覽器第一次讀取(解析)HTML文檔時,它會創(chuàng)建一個大對象,一個基于 HTM L文檔的非常大的對象,這就是DOM。它是一個從 HTML 文檔中建模的樹狀結構。DOM 用于交互和修改DOM結構或特定元素或節(jié)點。

JS 中的document對象表示DOM。它為我們提供了許多方法,我們可以使用這些方法來選擇元素來更新元素內容,等等。

6. 什么是事件傳播?

當事件發(fā)生在DOM元素上時,該事件并不完全發(fā)生在那個元素上。在“冒泡階段”中,事件冒泡或向上傳播至父級,祖父母,祖父母或父級,直到到達window為止;而在“捕獲階段”中,事件從window開始向下觸發(fā)元素 事件或event.target。

事件傳播有三個階段:

  1. 捕獲階段–事件從 window 開始,然后向下到每個元素,直到到達目標元素。
  2. 目標階段–事件已達到目標元素。
  3. 冒泡階段–事件從目標元素冒泡,然后上升到每個元素,直到到達 window。

7. 什么是事件冒泡?

當事件發(fā)生在DOM元素上時,該事件并不完全發(fā)生在那個元素上。在冒泡階段,事件冒泡,或者事件會向上傳播,發(fā)生在它的父代,祖父母,祖父母的父代,直到到達window為止。

<div class="grandparent">
  <div class="parent">
    <div class="child">1</div>
  </div>
</div>

<script>
function addEvent(el, event, callback, isCapture = false) {
  if (!el || !event || !callback || typeof callback !== 'function') return;
  if (typeof el === 'string') {
    el = document.querySelector(el);
  };
  el.addEventListener(event, callback, isCapture);
}

addEvent(document, 'DOMContentLoaded', () => {
  const child = document.querySelector('.child');
  const parent = document.querySelector('.parent');
  const grandparent = document.querySelector('.grandparent');

  addEvent(child, 'click', function (e) {
    console.log('child');
  });

  addEvent(parent, 'click', function (e) {
    console.log('parent');
  });

  addEvent(grandparent, 'click', function (e) {
    console.log('grandparent');
  });

  addEvent(document, 'click', function (e) {
    console.log('document');
  });

  addEvent('html', 'click', function (e) {
    console.log('html');
  })

  addEvent(window, 'click', function (e) {
    console.log('window');
  })

});
</script>

addEventListener方法具有第三個可選參數useCapture,其默認值為false,事件將在冒泡階段中發(fā)生,如果為true,則事件將在捕獲階段中發(fā)生。如果單擊child元素,它將分別在控制臺上記錄child,parent,grandparent,html,document和window,這就是事件冒泡。

8. 什么是事件捕獲?

當事件發(fā)生在 DOM 元素上時,該事件并不完全發(fā)生在那個元素上。在捕獲階段,事件從window開始,一直到觸發(fā)事件的元素。

<div class="grandparent">
  <div class="parent">
    <div class="child">1</div>
  </div>
</div>

<script>
function addEvent(el, event, callback, isCapture = false) {
  if (!el || !event || !callback || typeof callback !== 'function') return;
  if (typeof el === 'string') {
    el = document.querySelector(el);
  };
  el.addEventListener(event, callback, isCapture);
}

addEvent(document, 'DOMContentLoaded', () => {
  const child = document.querySelector('.child');
  const parent = document.querySelector('.parent');
  const grandparent = document.querySelector('.grandparent');

  addEvent(child, 'click', function (e) {
    console.log('child');
  });

  addEvent(parent, 'click', function (e) {
    console.log('parent');
  });

  addEvent(grandparent, 'click', function (e) {
    console.log('grandparent');
  });

  addEvent(document, 'click', function (e) {
    console.log('document');
  });

  addEvent('html', 'click', function (e) {
    console.log('html');
  })

  addEvent(window, 'click', function (e) {
    console.log('window');
  })

});
</script>

addEventListener方法具有第三個可選參數useCapture,其默認值為false,事件將在冒泡階段中發(fā)生,如果為true,則事件將在捕獲階段中發(fā)生。如果單擊child元素,它將分別在控制臺上打印window,document,html,grandparent和parent,這就是事件捕獲。

9. event.preventDefault() 和 event.stopPropagation()方法之間有什么區(qū)別?

event.preventDefault() 方法可防止元素的默認行為。如果在表單元素中使用,它將阻止其提交。如果在錨元素中使用,它將阻止其導航。如果在上下文菜單中使用,它將阻止其顯示或顯示。 event.stopPropagation()方法用于阻止捕獲和冒泡階段中當前事件的進一步傳播。

10. 如何知道是否在元素中使用了event.preventDefault()方法?

我們可以在事件對象中使用event.defaultPrevented屬性。它返回一個布爾值用來表明是否在特定元素中調用了event.preventDefault()。

11. 為什么此代碼 obj.someprop.x 會引發(fā)錯誤?

const obj = {};
console.log(obj.someprop.x);

顯然,由于我們嘗試訪問someprop屬性中的x屬性,而 someprop 并沒有在對象中,所以值為 undefined。記住對象本身不存在的屬性,并且其原型的默認值為undefined。因為undefined沒有屬性x,所以試圖訪問將會報錯。

12. 什么是 event.target ?

簡單來說,event.target是發(fā)生事件的元素或觸發(fā)事件的元素。

假設有如下的 HTML 結構:

<div onclick="clickFunc(event)" style="text-align: center;margin:15px;
border:1px solid red;border-radius:3px;">
    <div style="margin: 25px; border:1px solid royalblue;border-radius:3px;">
        <div style="margin:25px;border:1px solid skyblue;border-radius:3px;">
          <button style="margin:10px">
             Button
          </button>
        </div>
    </div>
 </div>
<script>
function clickFunc(event) {
  console.log(event.target);
}
</script>

如果單擊 button,即使我們將事件附加在最外面的div上,它也將打印 button 標簽,因此我們可以得出結論event.target是觸發(fā)事件的元素。

13. 什么是 event.currentTarget?

event.currentTarget是我們在其上顯式附加事件處理程序的元素。

<div onclick="clickFunc(event)" style="text-align: center;margin:15px;
border:1px solid red;border-radius:3px;">
    <div style="margin: 25px; border:1px solid royalblue;border-radius:3px;">
        <div style="margin:25px;border:1px solid skyblue;border-radius:3px;">
          <button style="margin:10px">
             Button
          </button>
        </div>
    </div>
 </div>
<script>
function clickFunc(event) {
  console.log(event.currentTarget);
}
</script>

如果單擊 button,即使我們單擊該 button,它也會打印最外面的div標簽。在此示例中,我們可以得出結論,event.currentTarget是附加事件處理程序的元素。

14. == 和 === 有什么區(qū)別?

==用于一般比較,===用于嚴格比較,==在比較的時候可以轉換數據類型,===嚴格比較,只要類型不匹配就返回flase。

先來看看 == :

強制是將值轉換為另一種類型的過程。在這種情況下,==會執(zhí)行隱式強制。在比較兩個值之前,==需要執(zhí)行一些規(guī)則。

假設我們要比較x == y的值。

  • 如果x和y的類型相同,則 JS 會換成===操作符進行比較。

  • 如果x為null, y為undefined,則返回true。

  • 如果x為undefined且y為null,則返回true。

  • 如果x的類型是number, y的類型是string,那么返回x == toNumber(y)。

  • 如果x的類型是string, y的類型是number,那么返回toNumber(x) == y。

  • 如果x為類型是boolean,則返回toNumber(x)== y。

  • 如果y為類型是boolean,則返回x == toNumber(y)。

  • 如果x是string、symbol或number,而y是object類型,則返回x == toPrimitive(y)。

  • 如果x是object,y是string,symbol 則返回toPrimitive(x) == y。

  • 剩下的 返回 false

注意:toPrimitive首先在對象中使用valueOf方法,然后使用toString方法來獲取該對象的原始值。

舉個例子

x y x == y
5 5 true
1 '1' true
null undefined true
0 false true
'1,2' [1,2] true
'[object Object]' {} true

這些例子都返回true。

第一個示例符合條件1,因為x和y具有相同的類型和值。

第二個示例符合條件4,在比較之前將y轉換為數字。

第三個例子符合條件2。

第四個例子符合條件7,因為y是boolean類型。

第五個示例符合條件8。使用toString()方法將數組轉換為字符串,該方法返回1,2。

最后一個示例符合條件8。使用toString()方法將對象轉換為字符串,該方法返回[object Object]。

x y x === y
5 5 true
1 '1' false
null undefined false
0 false false
'1,2' [1,2] false
'[object Object]' {} false

如果使用===運算符,則第一個示例以外的所有比較將返回false,因為它們的類型不同,而第一個示例將返回true,因為兩者的類型和值相同。

15. 為什么在 JS 中比較兩個相似的對象時返回 false?

let a = { a: 1 };
let b = { a: 1 };
let c = a;

console.log(a === b); // 打印 false,即使它們有相同的屬性
console.log(a === c); // true

JS 以不同的方式比較對象和基本類型。在基本類型中,JS 通過值對它們進行比較,而在對象中,JS 通過引用或存儲變量的內存中的地址對它們進行比較。這就是為什么第一個console.log語句返回false,而第二個console.log語句返回true。a和c有相同的引用地址,而a和b沒有。

16. !! 運算符能做什么?

!!運算符可以將右側的值強制轉換為布爾值,這也是將值轉換為布爾值的一種簡單方法。

console.log(!!null); // false
console.log(!!undefined); // false
console.log(!!''); // false
console.log(!!0); // false
console.log(!!NaN); // false
console.log(!!' '); // true
console.log(!!{}); // true
console.log(!![]); // true
console.log(!!1); // true
console.log(!![].length); // false

17. 如何在一行中計算多個表達式的值?

可以使用逗號運算符在一行中計算多個表達式。它從左到右求值,并返回右邊最后一個項目或最后一個操作數的值。

let x = 5;

x = (x++ , x = addFive(x), x *= 2, x -= 5, x += 10);

function addFive(num) {
  return num + 5;
}

上面的結果最后得到x的值為27。首先,我們將x的值增加到6,然后調用函數addFive(6)并將6作為參數傳遞并將結果重新分配給x,此時x的值為11。之后,將x的當前值乘以2并將其分配給x,x的更新值為22。然后,將x的當前值減去5并將結果分配給x x更新后的值為17。最后,我們將x的值增加10,然后將更新的值分配給x,最終x的值為27。

18. 什么是變量提升?

變量提升是用來描述變量和函數移動到其(全局或函數)作用域頂部的術語。

為了理解變量提升,需要來了解一下執(zhí)行上下文。執(zhí)行上下文是當前正在執(zhí)行的“代碼環(huán)境”。執(zhí)行上下文有兩個階段:編譯和執(zhí)行。

編譯:在此階段,JS 引薦獲取所有函數聲明并將其提升到其作用域的頂部,以便我們稍后可以引用它們并獲取所有變量聲明(使用var關鍵字進行聲明),還會為它們提供默認值: undefined。

執(zhí)行:在這個階段中,它將值賦給之前提升的變量,并執(zhí)行或調用函數(對象中的方法)。

注意:只有使用var聲明的變量,或者函數聲明才會被提升,相反,函數表達式或箭頭函數,let和const聲明的變量,這些都不會被提升。

假設在全局使用域,有如下的代碼:

console.log(y);
y = 1;
console.log(y);
console.log(greet("Mark"));

function greet(name){
  return 'Hello ' + name + '!';
}

var y;

上面分別打印:undefined,1, Hello Mark!。

上面代碼在編譯階段其實是這樣的:

function greet(name) {
  return 'Hello ' + name + '!';
}

var y; // 默認值 undefined

// 等待“編譯”階段完成,然后開始“執(zhí)行”階段

/*
console.log(y);
y = 1;
console.log(y);
console.log(greet("Mark"));
*/

編譯階段完成后,它將啟動執(zhí)行階段調用方法,并將值分配給變量。

function greet(name) {
  return 'Hello ' + name + '!';
}

var y;

//start "execution" phase

console.log(y);
y = 1;
console.log(y);
console.log(greet("Mark"));

什么是作用域?

JavaScript 中的作用域是我們可以有效訪問變量或函數的區(qū)域。JS 有三種類型的作用域:全局作用域、函數作用域和塊作用域(ES6)。

  • 全局作用域:在全局命名空間中聲明的變量或函數位于全局作用域中,因此在代碼中的任何地方都可以訪問它們。
var g = "global";

function globalFunc(){
  function innerFunc(){
    console.log(g); // global
  }
 innerFunc();
}

  • 函數作用域:在函數中聲明的變量、函數和參數可以在函數內部訪問,但不能在函數外部訪問。
function myFavoriteFunc(a) {
  if (true) {
    var b = "Hello " + a;
  }
  return b;
}

myFavoriteFunc("World");

console.log(a); // a is not defined
console.log(b); // b is not defined

  • 塊作用域:在塊{}中聲明的變量(let,const)只能在其中訪問。
 function testBlock(){
   if(true){
     let z = 5;
   }
   return z;
 }

 testBlock(); // z is not defined

作用域也是一組用于查找變量的規(guī)則。如果變量在當前作用域中不存在,它將向外部作用域中查找并搜索,如果該變量不存在,它將再次查找直到到達全局作用域,如果找到,則可以使用它,否則引發(fā)錯誤,這種查找過程也稱為作用域鏈。

/* 作用域鏈
     內部作用域->外部作用域-> 全局作用域
*/

  // 全局作用域
  var variable1 = "Comrades";
  var variable2 = "Sayonara";

  function outer(){
  // 外部作用域
    var variable1 = "World";
    function inner(){
    // 內部作用域
      var variable2 = "Hello";
      console.log(variable2 + " " + variable1);
    }
    inner();
  }
  outer(); // Hello World

20. 什么是閉包?

這可能是所有問題中最難的一個問題,因為閉包是一個有爭議的話題,這里從個人角度來談談,如果不妥,多多海涵。

閉包就是一個函數在聲明時能夠記住當前作用域、父函數作用域、及父函數作用域上的變量和參數的引用,直至通過作用域鏈上全局作用域,基本上閉包是在聲明函數時創(chuàng)建的作用域。

看看小例子:

   // 全局作用域
   var globalVar = "abc";

   function a(){
     console.log(globalVar);
   }

   a(); // "abc"

在此示例中,當我們聲明a函數時,全局作用域是a閉包的一部分。

來看一個更復雜的例子:

var globalVar = "global";
var outerVar = "outer"

function outerFunc(outerParam) {
  function innerFunc(innerParam) {
    console.log(globalVar, outerParam, innerParam);
  }
  return innerFunc;
}

const x = outerFunc(outerVar);
outerVar = "outer-2";
globalVar = "guess"
x("inner");

上面打印結果是 guess outer inner。

當我們調用outerFunc函數并將返回值innerFunc函數分配給變量x時,即使我們?yōu)閛uterVar變量分配了新值outer-2,outerParam也繼續(xù)保留outer值,因為重新分配是在調用outerFunc之后發(fā)生的,并且當我們調用outerFunc函數時,它會在作用域鏈中查找outerVar的值,此時的outerVar的值將為 "outer"。

現(xiàn)在,當我們調用引用了innerFunc的x變量時,innerParam將具有一個inner值,因為這是我們在調用中傳遞的值,而globalVar變量值為guess,因為在調用x變量之前,我們將一個新值分配給globalVar。

下面這個示例演示沒有理解好閉包所犯的錯誤:

const arrFuncs = [];
for(var i = 0; i < 5; i++){
  arrFuncs.push(function (){
    return i;
  });
}
console.log(i); // i is 5

for (let i = 0; i < arrFuncs.length; i++) {
  console.log(arrFuncs[i]()); // 都打印 5
}

由于閉包,此代碼無法正常運行。var關鍵字創(chuàng)建一個全局變量,當我們 push 一個函數時,這里返回的全局變量i。因此,當我們在循環(huán)后在該數組中調用其中一個函數時,它會打印5,因為我們得到i的當前值為5,我們可以訪問它,因為它是全局變量。

因為閉包在創(chuàng)建變量時會保留該變量的引用而不是其值。我們可以使用IIFES或使用 let 來代替 var 的聲明。

21. JavaScript 中的虛值是什么?

const falsyValues = ['', 0, null, undefined, NaN, false];

簡單的來說虛值就是是在轉換為布爾值時變?yōu)?false 的值。

22. 如何檢查值是否虛值?

使用 Boolean 函數或者 !! 運算符。

23. 'use strict' 是干嘛用的?

"use strict" 是 ES5 特性,它使我們的代碼在函數或整個腳本中處于嚴格模式。嚴格模式幫助我們在代碼的早期避免 bug,并為其添加限制。

嚴格模式的一些限制:

  • 變量必須聲明后再使用

  • 函數的參數不能有同名屬性,否則報錯

  • 不能使用with語句

  • 不能對只讀屬性賦值,否則報錯

  • 不能使用前綴 0 表示八進制數,否則報錯

  • 不能刪除不可刪除的屬性,否則報錯

  • 不能刪除變量delete prop,會報錯,只能刪除屬性delete global[prop]

  • eval不能在它的外層作用域引入變量

  • eval和arguments不能被重新賦值

  • arguments不會自動反映函數參數的變化

  • 不能使用arguments.callee

  • 不能使用arguments.caller

  • 禁止this指向全局對象

  • 不能使用fn.caller和fn.arguments獲取函數調用的堆棧

  • 增加了保留字(比如protected、static和interface)

設立”嚴格模式”的目的,主要有以下幾個:

  • 消除Javascript語法的一些不合理、不嚴謹之處,減少一些怪異行為;

  • 消除代碼運行的一些不安全之處,保證代碼運行的安全;

  • 提高編譯器效率,增加運行速度;

  • 為未來新版本的Javascript做好鋪墊。

24. JavaScript 中 this 值是什么?

基本上,this指的是當前正在執(zhí)行或調用該函數的對象的值。this值的變化取決于我們使用它的上下文和我們在哪里使用它。

const carDetails = {
  name: "Ford Mustang",
  yearBought: 2005,
  getName(){
    return this.name;
  },
  isRegistered: true
};

console.log(carDetails.getName()); // Ford Mustang

這通常是我們期望的結果,因為在getName方法中我們返回this.name,在此上下文中,this指向的是carDetails對象,該對象當前是執(zhí)行函數的“所有者”對象。

接下我們做些奇怪的事情:

var name = "Ford Ranger";
var getCarName = carDetails.getName;

console.log(getCarName()); // Ford Ranger

上面打印Ford Ranger,這很奇怪,因為在第一個console.log語句中打印的是Ford Mustang。這樣做的原因是getCarName方法有一個不同的“所有者”對象,即window對象。在全局作用域中使用var關鍵字聲明變量會在window對象中附加與變量名稱相同的屬性。請記住,當沒有使用“use strict”時,在全局作用域中this指的是window對象。

console.log(getCarName === window.getCarName); // true
console.log(getCarName === this.getCarName); // true

本例中的this和window引用同一個對象。

解決這個問題的一種方法是在函數中使用apply和call方法。

console.log(getCarName.apply(carDetails)); // Ford Mustang
console.log(getCarName.call(carDetails));  // Ford Mustang

apply和call方法期望第一個參數是一個對象,該對象是函數內部this的值。

IIFE或立即執(zhí)行的函數表達式,在全局作用域內聲明的函數,對象內部方法中的匿名函數和內部函數的this具有默認值,該值指向window對象。

   (function (){
     console.log(this);
   })(); // 打印 "window" 對象

   function iHateThis(){
      console.log(this);
   }

   iHateThis(); // 打印 "window" 對象

   const myFavoriteObj = {
     guessThis(){
        function getName(){
          console.log(this.name);
        }
        getName();
     },
     name: 'Marko Polo',
     thisIsAnnoying(callback){
       callback();
     }
   };

   myFavoriteObj.guessThis(); // 打印 "window" 對象
   myFavoriteObj.thisIsAnnoying(function (){
     console.log(this); // 打印 "window" 對象
   });

如果我們要獲取myFavoriteObj對象中的name屬性(即Marko Polo)的值,則有兩種方法可以解決此問題。

一種是將 this 值保存在變量中。

const myFavoriteObj = {
 guessThis(){
  const self = this; // 把 this 值保存在 self 變量中
  function getName(){
    console.log(self.name);
  }
  getName();
 },
 name: 'Marko Polo',
 thisIsAnnoying(callback){
   callback();
  }
};

第二種方式是使用箭頭函數

   const myFavoriteObj = {
     guessThis(){
         const getName = () => {
           //copies the value of "this" outside of this arrow function
           console.log(this.name);
         }
         getName();
     },
     name: 'Marko Polo',
     thisIsAnnoying(callback){
       callback();
     }
   };

箭頭函數沒有自己的 this。它復制了這個封閉的詞法作用域中this值,在這個例子中,this值在getName內部函數之外,也就是myFavoriteObj對象。

25. 對象的 prototype(原型) 是什么?

簡單地說,原型就是對象的藍圖。如果它存在當前對象中,則將其用作屬性和方法的回退。它是在對象之間共享屬性和功能的方法,這也是JavaScript實現(xiàn)繼承的核心。

const o = {};
console.log(o.toString()); // [object Object]

即使o對象中不存在o.toString方法,它也不會引發(fā)錯誤,而是返回字符串[object Object]。當對象中不存在屬性時,它將查看其原型,如果仍然不存在,則將其查找到原型的原型,依此類推,直到在原型鏈中找到具有相同屬性的屬性為止。原型鏈的末尾是Object.prototype。

console.log(o.toString === Object.prototype.toString); // true

26. 什么是 IIFE,它的用途是什么?

IIFE或立即調用的函數表達式,是在創(chuàng)建或聲明后將被調用或執(zhí)行的函數。創(chuàng)建IIFE的語法是,將function (){}包裹在在括號()內,然后再用另一個括號()調用它,如:(function(){})()

(function(){
  ...
} ());

(function () {
  ...
})();

(function named(params) {
  ...
})();

(() => {

});

(function (global) {
  ...
})(window);

const utility = (function () {
  return {
    ...
  }
})

這些示例都是有效的IIFE。倒數第二個救命表明我們可以將參數傳遞給IIFE函數。最后一個示例表明,我們可以將IIFE的結果保存到變量中,以便稍后使用。

IIFE的一個主要作用是避免與全局作用域內的其他變量命名沖突或污染全局命名空間,來個例子。

<script src="https://cdnurl.com/somelibrary.js"></script>

假設我們引入了一個somelibr.js的鏈接,它提供了一些我們在代碼中使用的全局函數,但是這個庫有兩個方法我們沒有使用:createGraph和drawGraph,因為這些方法都有bug。我們想實現(xiàn)自己的createGraph和drawGraph方法。

解決此問題的一種方法是直接覆蓋:

<script src="https://cdnurl.com/somelibrary.js"></script>
<script>
   function createGraph() {
      // createGraph logic here
   }
   function drawGraph() {
      // drawGraph logic here
   }
</script>

當我們使用這個解決方案時,我們覆蓋了庫提供給我們的那兩個方法。

另一種方式是我們自己改名稱:

<script src="https://cdnurl.com/somelibrary.js"></script>
<script>
   function myCreateGraph() {
      // createGraph logic here
   }
   function myDrawGraph() {
      // drawGraph logic here
   }
</script>

當我們使用這個解決方案時,我們把那些函數調用更改為新的函數名。

還有一種方法就是使用IIFE:

<script src="https://cdnurl.com/somelibrary.js"></script>
<script>
   const graphUtility = (function () {
      function createGraph() {
         // createGraph logic here
      }
      function drawGraph() {
         // drawGraph logic here
      }
      return {
         createGraph,
         drawGraph
      }
   })
</script>

在此解決方案中,我們要聲明了graphUtility 變量,用來保存IIFE執(zhí)行的結果,該函數返回一個包含兩個方法createGraph和drawGraph的對象。

IIFE 還可以用來解決一個常見的面試題:

var li = document.querySelectorAll('.list-group > li');
for (var i = 0, len = li.length; i < len; i++) {
   li[i].addEventListener('click', function (e) {
      console.log(i);
})

假設我們有一個帶有l(wèi)ist-group類的ul元素,它有5個li子元素。當我們單擊單個li元素時,打印對應的下標值。但在此外上述代碼不起作用,這里每次點擊 li 打印 i 的值都是5,這是由于閉包的原因。

閉包只是函數記住其當前作用域,父函數作用域和全局作用域的變量引用的能力。當我們在全局作用域內使用var關鍵字聲明變量時,就創(chuàng)建全局變量i。因此,當我們單擊li元素時,它將打印5,因為這是稍后在回調函數中引用它時i的值。

使用 IIFE 可以解決此問題:

var li = document.querySelectorAll('.list-group > li');
for (var i = 0, len = li.length; i < len; i++) {
   (function (currentIndex) {
      li[currentIndex].addEventListener('click', function (e) {
         console.log(currentIndex);
      })
   })(i);
}

該解決方案之所以行的通,是因為IIFE會為每次迭代創(chuàng)建一個新的作用域,我們捕獲i的值并將其傳遞給currentIndex參數,因此調用IIFE時,每次迭代的currentIndex值都是不同的。

27. Function.prototype.apply 方法的用途是什么?

apply() 方法調用一個具有給定this值的函數,以及作為一個數組(或類似數組對象)提供的參數。

const details = {
  message: 'Hello World!'
};

function getMessage(){
  return this.message;
}

getMessage.apply(details); // 'Hello World!'

call()方法的作用和 apply() 方法類似,區(qū)別就是call()方法接受的是參數列表,而apply()方法接受的是一個參數數組。

const person = {
  name: "Marko Polo"
};

function greeting(greetingMessage) {
  return `${greetingMessage} ${this.name}`;
}

greeting.apply(person, ['Hello']); // "Hello Marko Polo!"

28. Function.prototype.call 方法的用途是什么?

call() 方法使用一個指定的 this 值和單獨給出的一個或多個參數來調用一個函數。

const details = {
  message: 'Hello World!'
};

function getMessage(){
  return this.message;
}

getMessage.call(details); // 'Hello World!'

注意:該方法的語法和作用與 apply() 方法類似,只有一個區(qū)別,就是 call() 方法接受的是一個參數列表,而 apply() 方法接受的是一個包含多個參數的數組。

const person = {
  name: "Marko Polo"
};

function greeting(greetingMessage) {
  return `${greetingMessage} ${this.name}`;
}

greeting.call(person, 'Hello'); // "Hello Marko Polo!"

29. Function.prototype.apply 和 Function.prototype.call 之間有什么區(qū)別?

apply()方法可以在使用一個指定的 this 值和一個參數數組(或類數組對象)的前提下調用某個函數或方法。call()方法類似于apply(),不同之處僅僅是call()接受的參數是參數列表。

const obj1 = {
 result:0
};

const obj2 = {
 result:0
};

function reduceAdd(){
   let result = 0;
   for(let i = 0, len = arguments.length; i < len; i++){
     result += arguments[i];
   }
   this.result = result;
}

reduceAdd.apply(obj1, [1, 2, 3, 4, 5]); // 15
reduceAdd.call(obj2, 1, 2, 3, 4, 5); // 15

30. Function.prototype.bind 的用途是什么?

bind() 方法創(chuàng)建一個新的函數,在 bind() 被調用時,這個新函數的 this 被指定為 bind() 的第一個參數,而其余參數將作為新函數的參數,供調用時使用。

import React from 'react';

class MyComponent extends React.Component {
     constructor(props){
          super(props);
          this.state = {
             value : ""
          }
          this.handleChange = this.handleChange.bind(this);
          // 將 “handleChange” 方法綁定到 “MyComponent” 組件
     }

     handleChange(e){
       //do something amazing here
     }

     render(){
        return (
              <>
                <input type={this.props.type}
                        value={this.state.value}
                     onChange={this.handleChange}
                  />
              </>
        )
     }
}

31. 什么是函數式編程? JavaScript 的哪些特性使其成為函數式語言的候選語言?

函數式編程(通??s寫為FP)是通過編寫純函數,避免共享狀態(tài)、可變數據、副作用 來構建軟件的過程。數式編程是聲明式 的而不是命令式 的,應用程序的狀態(tài)是通過純函數流動的。與面向對象編程形成對比,面向對象中應用程序的狀態(tài)通常與對象中的方法共享和共處。

函數式編程是一種編程范式 ,這意味著它是一種基于一些基本的定義原則(如上所列)思考軟件構建的方式。當然,編程范示的其他示例也包括面向對象編程和過程編程。

函數式的代碼往往比命令式或面向對象的代碼更簡潔,更可預測,更容易測試 - 但如果不熟悉它以及與之相關的常見模式,函數式的代碼也可能看起來更密集雜亂,并且 相關文獻對新人來說是不好理解的。

JavaScript支持閉包和高階函數是函數式編程語言的特點。

32. 什么是高階函數?

高階函數只是將函數作為參數或返回值的函數。

function higherOrderFunction(param,callback){
    return callback(param);
}

33. 為什么函數被稱為一等公民?

在JavaScript中,函數不僅擁有一切傳統(tǒng)函數的使用方式(聲明和調用),而且可以做到像簡單值一樣賦值(var func = function(){})、傳參(function func(x,callback){callback();})、返回(function(){return function(){}}),這樣的函數也稱之為第一級函數(First-class Function)。不僅如此,JavaScript中的函數還充當了類的構造函數的作用,同時又是一個Function類的實例(instance)。這樣的多重身份讓JavaScript的函數變得非常重要。

34. 手動實現(xiàn) Array.prototype.map 方法

map() 方法創(chuàng)建一個新數組,其結果是該數組中的每個元素都調用一個提供的函數后返回的結果。

function map(arr, mapCallback) {
  // 首先,檢查傳遞的參數是否正確。
  if (!Array.isArray(arr) || !arr.length || typeof mapCallback !== 'function') {
    return [];
  } else {
    let result = [];
    // 每次調用此函數時,我們都會創(chuàng)建一個 result 數組
    // 因為我們不想改變原始數組。
    for (let i = 0, len = arr.length; i < len; i++) {
      result.push(mapCallback(arr[i], i, arr));
      // 將 mapCallback 返回的結果 push 到 result 數組中
    }
    return result;
  }
}

35. 手動實現(xiàn)Array.prototype.filter方法

filter() 方法創(chuàng)建一個新數組, 其包含通過所提供函數實現(xiàn)的測試的所有元素。

function filter(arr, filterCallback) {
  // 首先,檢查傳遞的參數是否正確。
  if (!Array.isArray(arr) || !arr.length || typeof filterCallback !== 'function')
  {
    return [];
  } else {
    let result = [];
     // 每次調用此函數時,我們都會創(chuàng)建一個 result 數組
     // 因為我們不想改變原始數組。
    for (let i = 0, len = arr.length; i < len; i++) {
      // 檢查 filterCallback 的返回值是否是真值
      if (filterCallback(arr[i], i, arr)) {
      // 如果條件為真,則將數組元素 push 到 result 中
        result.push(arr[i]);
      }
    }
    return result; // return the result array
  }
}

36. 手動實現(xiàn)Array.prototype.reduce方法

reduce() 方法對數組中的每個元素執(zhí)行一個由您提供的reducer函數(升序執(zhí)行),將其結果匯總為單個返回值。

function reduce(arr, reduceCallback, initialValue) {
  // 首先,檢查傳遞的參數是否正確。
  if (!Array.isArray(arr) || !arr.length || typeof reduceCallback !== 'function')
  {
    return [];
  } else {
    // 如果沒有將initialValue傳遞給該函數,我們將使用第一個數組項作為initialValue
    let hasInitialValue = initialValue !== undefined;
    let value = hasInitialValue ? initialValue : arr[0];
   、

    // 如果有傳遞 initialValue,則索引從 1 開始,否則從 0 開始
    for (let i = hasInitialValue ? 0 : 1, len = arr.length; i < len; i++) {
      value = reduceCallback(value, arr[i], i, arr);
    }
    return value;
  }
}

37. arguments 的對象是什么?

arguments對象是函數中傳遞的參數值的集合。它是一個類似數組的對象,因為它有一個length屬性,我們可以使用數組索引表示法arguments[1]來訪問單個值,但它沒有數組中的內置方法,如:forEach、reduce、filter和map。

我們可以使用Array.prototype.slice將arguments對象轉換成一個數組。

function one() {
  return Array.prototype.slice.call(arguments);
}

注意:箭頭函數中沒有arguments對象。

function one() {
  return arguments;
}
const two = function () {
  return arguments;
}
const three = function three() {
  return arguments;
}

const four = () => arguments;

four(); // Throws an error  - arguments is not defined

當我們調用函數four時,它會拋出一個ReferenceError: arguments is not defined error。使用rest語法,可以解決這個問題。

const four = (...args) => args;

這會自動將所有參數值放入數組中。

38. 如何創(chuàng)建一個沒有 prototype(原型)的對象?

我們可以使用Object.create方法創(chuàng)建沒有原型的對象。

const o1 = {};
console.log(o1.toString()); // [object Object]

const o2 = Object.create(null);
console.log(o2.toString());
// throws an error o2.toString is not a function

39. 為什么在調用這個函數時,代碼中的b會變成一個全局變量?

function myFunc() {
  let a = b = 0;
}

myFunc();

原因是賦值運算符是從右到左的求值的。這意味著當多個賦值運算符出現(xiàn)在一個表達式中時,它們是從右向左求值的。所以上面代碼變成了這樣:

function myFunc() {
  let a = (b = 0);
}

myFunc();

首先,表達式b = 0求值,在本例中b沒有聲明。因此,JS引擎在這個函數外創(chuàng)建了一個全局變量b,之后表達式b = 0的返回值為0,并賦給新的局部變量a。

我們可以通過在賦值之前先聲明變量來解決這個問題。

function myFunc() {
  let a,b;
  a = b = 0;
}
myFunc();

40. ECMAScript 是什么?

ECMAScript 是編寫腳本語言的標準,這意味著JavaScript遵循ECMAScript標準中的規(guī)范變化,因為它是JavaScript的藍圖。

ECMAScript 和 Javascript,本質上都跟一門語言有關,一個是語言本身的名字,一個是語言的約束條件
只不過發(fā)明JavaScript的那個人(Netscape公司),把東西交給了ECMA(European Computer Manufacturers Association),這個人規(guī)定一下他的標準,因為當時有java語言了,又想強調這個東西是讓ECMA這個人定的規(guī)則,所以就這樣一個神奇的東西誕生了,這個東西的名稱就叫做ECMAScript。

javaScript = ECMAScript + DOM + BOM(自認為是一種廣義的JavaScript)

ECMAScript說什么JavaScript就得做什么!

JavaScript(狹義的JavaScript)做什么都要問問ECMAScript我能不能這樣干!如果不能我就錯了!能我就是對的!

突然感覺JavaScript好沒有尊嚴,為啥要搞個人出來約束自己,

那個人被創(chuàng)造出來也好委屈,自己被創(chuàng)造出來完全是因為要約束JavaScript。

41. ES6或ECMAScript 2015有哪些新特性?

  • 箭頭函數
  • 模板字符串
  • 加強的對象字面量
  • 對象解構
  • Promise
  • 生成器
  • 模塊
  • Symbol
  • 代理
  • Set
  • 函數默認參數
  • rest 和展開
  • 塊作用域

42. var,let和const的區(qū)別是什么?

var聲明的變量會掛載在window上,而let和const聲明的變量不會:

var a = 100;
console.log(a,window.a);    // 100 100

let b = 10;
console.log(b,window.b);    // 10 undefined

const c = 1;
console.log(c,window.c);    // 1 undefined

var聲明變量存在變量提升,let和const不存在變量提升:

console.log(a); // undefined  ===>  a已聲明還沒賦值,默認得到undefined值
var a = 100;

console.log(b); // 報錯:b is not defined  ===> 找不到b這個變量
let b = 10;

console.log(c); // 報錯:c is not defined  ===> 找不到c這個變量
const c = 10;

let和const聲明形成塊作用域

if(1){
  var a = 100;
  let b = 10;
}

console.log(a); // 100
console.log(b)  // 報錯:b is not defined  ===> 找不到b這個變量

-------------------------------------------------------------

if(1){
  var a = 100;
  const c = 1;
}
console.log(a); // 100
console.log(c)  // 報錯:c is not defined  ===> 找不到c這個變量

同一作用域下let和const不能聲明同名變量,而var可以

var a = 100;
console.log(a); // 100

var a = 10;
console.log(a); // 10
-------------------------------------
let a = 100;
let a = 10;

//  控制臺報錯:Identifier 'a' has already been declared  ===> 標識符a已經被聲明了。

暫存死區(qū)

var a = 100;

if(1){
    a = 10;
    //在當前塊作用域中存在a使用let/const聲明的情況下,給a賦值10時,只會在當前作用域找變量a,
    // 而這時,還未到聲明時候,所以控制臺Error:a is not defined
    let a = 1;
}
const

/*
*   1、一旦聲明必須賦值,不能使用null占位。
*
*   2、聲明后不能再修改
*
*   3、如果聲明的是復合類型數據,可以修改其屬性
*
* */

const a = 100;

const list = [];
list[0] = 10;
console.log(list);  // [10]

const obj = {a:100};
obj.name = 'apple';
obj.a = 10000;
console.log(obj);  // {a:10000,name:'apple'}

43. 什么是箭頭函數?

箭頭函數表達式的語法比函數表達式更簡潔,并且沒有自己的this,arguments,super或new.target。箭頭函數表達式更適用于那些本來需要匿名函數的地方,并且它不能用作構造函數。

//ES5 Version
var getCurrentDate = function (){
  return new Date();
}

//ES6 Version
const getCurrentDate = () => new Date();

在本例中,ES5 版本中有function(){}聲明和return關鍵字,這兩個關鍵字分別是創(chuàng)建函數和返回值所需要的。在箭頭函數版本中,我們只需要()括號,不需要 return 語句,因為如果我們只有一個表達式或值需要返回,箭頭函數就會有一個隱式的返回。

//ES5 Version
function greet(name) {
  return 'Hello ' + name + '!';
}

//ES6 Version
const greet = (name) => `Hello ${name}`;
const greet2 = name => `Hello ${name}`;

我們還可以在箭頭函數中使用與函數表達式和函數聲明相同的參數。如果我們在一個箭頭函數中有一個參數,則可以省略括號。

const getArgs = () => arguments

const getArgs2 = (...rest) => rest

箭頭函數不能訪問arguments對象。所以調用第一個getArgs函數會拋出一個錯誤。相反,我們可以使用rest參數來獲得在箭頭函數中傳遞的所有參數。

const data = {
  result: 0,
  nums: [1, 2, 3, 4, 5],
  computeResult() {
    // 這里的“this”指的是“data”對象
    const addAll = () => {
      return this.nums.reduce((total, cur) => total + cur, 0)
    };
    this.result = addAll();
  }
};

箭頭函數沒有自己的this值。它捕獲詞法作用域函數的this值,在此示例中,addAll函數將復制computeResult 方法中的this值,如果我們在全局作用域聲明箭頭函數,則this值為 window 對象。

44. 什么是類?

類(class)是在 JS 中編寫構造函數的新方法。它是使用構造函數的語法糖,在底層中使用仍然是原型和基于原型的繼承。

   //ES5 Version
   function Person(firstName, lastName, age, address){
      this.firstName = firstName;
      this.lastName = lastName;
      this.age = age;
      this.address = address;
   }

   Person.self = function(){
     return this;
   }

   Person.prototype.toString = function(){
     return "[object Person]";
   }

   Person.prototype.getFullName = function (){
     return this.firstName + " " + this.lastName;
   }

   //ES6 Version
   class Person {
        constructor(firstName, lastName, age, address){
            this.lastName = lastName;
            this.firstName = firstName;
            this.age = age;
            this.address = address;
        }

        static self() {
           return this;
        }

        toString(){
           return "[object Person]";
        }

        getFullName(){
           return `${this.firstName} ${this.lastName}`;
        }
   }

重寫方法并從另一個類繼承。

//ES5 Version
Employee.prototype = Object.create(Person.prototype);

function Employee(firstName, lastName, age, address, jobTitle, yearStarted) {
  Person.call(this, firstName, lastName, age, address);
  this.jobTitle = jobTitle;
  this.yearStarted = yearStarted;
}

Employee.prototype.describe = function () {
  return `I am ${this.getFullName()} and I have a position of ${this.jobTitle}
   and I started at ${this.yearStarted}`;
}

Employee.prototype.toString = function () {
  return "[object Employee]";
}

//ES6 Version
class Employee extends Person { //Inherits from "Person" class
  constructor(firstName, lastName, age, address, jobTitle, yearStarted) {
    super(firstName, lastName, age, address);
    this.jobTitle = jobTitle;
    this.yearStarted = yearStarted;
  }

  describe() {
    return `I am ${this.getFullName()} and I have a position of ${this.jobTitle}
     and I started at ${this.yearStarted}`;
  }

  toString() { // Overriding the "toString" method of "Person"
    return "[object Employee]";
  }
}

所以我們要怎么知道它在內部使用原型?

class Something {

}

function AnotherSomething(){

}
const as = new AnotherSomething();
const s = new Something();

console.log(typeof Something); // "function"
console.log(typeof AnotherSomething); // "function"
console.log(as.toString()); // "[object Object]"
console.log(as.toString()); // "[object Object]"
console.log(as.toString === Object.prototype.toString); // true
console.log(s.toString === Object.prototype.toString); // true

45. 什么是模板字符串?

模板字符串是在 JS 中創(chuàng)建字符串的一種新方法。我們可以通過使用反引號使模板字符串化。

//ES5 Version
var greet = 'Hi I\'m Mark';

//ES6 Version
let greet = `Hi I'm Mark`;

在 ES5 中我們需要使用一些轉義字符來達到多行的效果,在模板字符串不需要這么麻煩:

//ES5 Version
var lastWords = '\n'
  + '   I  \n'
  + '   Am  \n'
  + 'Iron Man \n';

//ES6 Version
let lastWords = `
    I
    Am
  Iron Man
`;

在ES5版本中,我們需要添加\n以在字符串中添加新行。在模板字符串中,我們不需要這樣做。

//ES5 Version
function greet(name) {
  return 'Hello ' + name + '!';
}

//ES6 Version
function greet(name) {
  return `Hello ${name} !`;
}

在 ES5 版本中,如果需要在字符串中添加表達式或值,則需要使用+運算符。在模板字符串s中,我們可以使用${expr}嵌入一個表達式,這使其比 ES5 版本更整潔。

46. 什么是對象解構?

對象析構是從對象或數組中獲取或提取值的一種新的、更簡潔的方法。假設有如下的對象:

const employee = {
  firstName: "Marko",
  lastName: "Polo",
  position: "Software Developer",
  yearHired: 2017
};

從對象獲取屬性,早期方法是創(chuàng)建一個與對象屬性同名的變量。這種方法很麻煩,因為我們要為每個屬性創(chuàng)建一個新變量。假設我們有一個大對象,它有很多屬性和方法,用這種方法提取屬性會很麻煩。

var firstName = employee.firstName;
var lastName = employee.lastName;
var position = employee.position;
var yearHired = employee.yearHired;

使用解構方式語法就變得簡潔多了:

{ firstName, lastName, position, yearHired } = employee;

我們還可以為屬性取別名:

let { firstName: fName, lastName: lName, position, yearHired } = employee;

當然如果屬性值為 undefined 時,我們還可以指定默認值:

let { firstName = "Mark", lastName: lName, position, yearHired } = employee;

47. 什么是 ES6 模塊?

模塊使我們能夠將代碼基礎分割成多個文件,以獲得更高的可維護性,并且避免將所有代碼放在一個大文件中。在 ES6 支持模塊之前,有兩個流行的模塊。

  • CommonJS-Node.js
  • AMD(異步模塊定義)-瀏覽器

基本上,使用模塊的方式很簡單,import用于從另一個文件中獲取功能或幾個功能或值,同時export用于從文件中公開功能或幾個功能或值。

導出

使用 ES5 (CommonJS)

// 使用 ES5 CommonJS - helpers.js
exports.isNull = function (val) {
  return val === null;
}

exports.isUndefined = function (val) {
  return val === undefined;
}

exports.isNullOrUndefined = function (val) {
  return exports.isNull(val) || exports.isUndefined(val);
}

使用 ES6 模塊

// 使用 ES6 Modules - helpers.js
export function isNull(val){
  return val === null;
}

export function isUndefined(val) {
  return val === undefined;
}

export function isNullOrUndefined(val) {
  return isNull(val) || isUndefined(val);
}

在另一個文件中導入函數

// 使用 ES5 (CommonJS) - index.js
const helpers = require('./helpers.js'); // helpers is an object
const isNull = helpers.isNull;
const isUndefined = helpers.isUndefined;
const isNullOrUndefined = helpers.isNullOrUndefined;

// or if your environment supports Destructuring
const { isNull, isUndefined, isNullOrUndefined } = require('./helpers.js');
-------------------------------------------------------

// ES6 Modules - index.js
import * as helpers from './helpers.js'; // helpers is an object

// or

import { isNull, isUndefined, isNullOrUndefined as isValid } from './helpers.js';

// using "as" for renaming named exports

在文件中導出單個功能或默認導出

使用 ES5 (CommonJS)

// 使用 ES5 (CommonJS) - index.js
class Helpers {
  static isNull(val) {
    return val === null;
  }

  static isUndefined(val) {
    return val === undefined;
  }

  static isNullOrUndefined(val) {
    return this.isNull(val) || this.isUndefined(val);
  }
}

module.exports = Helpers;

使用ES6 Modules

// 使用 ES6 Modules - helpers.js
class Helpers {
  static isNull(val) {
    return val === null;
  }

  static isUndefined(val) {
    return val === undefined;
  }

  static isNullOrUndefined(val) {
    return this.isNull(val) || this.isUndefined(val);
  }
}

export default Helpers

從另一個文件導入單個功能

使用ES5 (CommonJS)

// 使用 ES5 (CommonJS) - index.js
const Helpers = require('./helpers.js');
console.log(Helpers.isNull(null));

使用 ES6 Modules

import Helpers from '.helpers.js'
console.log(Helpers.isNull(null));

48. 什么是Set對象,它是如何工作的?

Set 對象允許你存儲任何類型的唯一值,無論是原始值或者是對象引用。

我們可以使用Set構造函數創(chuàng)建Set實例。

const set1 = new Set();
const set2 = new Set(["a","b","c","d","d","e"]);

我們可以使用add方法向Set實例中添加一個新值,因為add方法返回Set對象,所以我們可以以鏈式的方式再次使用add。如果一個值已經存在于Set對象中,那么它將不再被添加。

set2.add("f");
set2.add("g").add("h").add("i").add("j").add("k").add("k");
// 后一個“k”不會被添加到set對象中,因為它已經存在了

我們可以使用has方法檢查Set實例中是否存在特定的值。

set2.has("a") // true
set2.has("z") // true

我們可以使用size屬性獲得Set實例的長度。

set2.size // returns 10

可以使用clear方法刪除 Set 中的數據。

set2.clear();

我們可以使用Set對象來刪除數組中重復的元素。

const numbers = [1, 2, 3, 4, 5, 6, 6, 7, 8, 8, 5];
const uniqueNums = [...new Set(numbers)]; // [1,2,3,4,5,6,7,8]

49. 什么是回調函數?

回調函數是一段可執(zhí)行的代碼段,它作為一個參數傳遞給其他的代碼,其作用是在需要的時候方便調用這段(回調函數)代碼。

在JavaScript中函數也是對象的一種,同樣對象可以作為參數傳遞給函數,因此函數也可以作為參數傳遞給另外一個函數,這個作為參數的函數就是回調函數。

const btnAdd = document.getElementById('btnAdd');

btnAdd.addEventListener('click', function clickCallback(e) {
    // do something useless
});

在本例中,我們等待id為btnAdd的元素中的click事件,如果它被單擊,則執(zhí)行clickCallback函數?;卣{函數向某些數據或事件添加一些功能。

數組中的reduce、filter和map方法需要一個回調作為參數?;卣{的一個很好的類比是,當你打電話給某人,如果他們不接,你留下一條消息,你期待他們回調。調用某人或留下消息的行為是事件或數據,回調是你希望稍后發(fā)生的操作。

50. Promise 是什么?

Promise 是異步編程的一種解決方案:從語法上講,promise是一個對象,從它可以獲取異步操作的消息;從本意上講,它是承諾,承諾它過一段時間會給你一個結果。promise有三種狀態(tài):pending(等待態(tài)),fulfiled(成功態(tài)),rejected(失敗態(tài));狀態(tài)一旦改變,就不會再變。創(chuàng)造promise實例后,它會立即執(zhí)行。

fs.readFile('somefile.txt', function (e, data) {
  if (e) {
    console.log(e);
  }
  console.log(data);
});

如果我們在回調內部有另一個異步操作,則此方法存在問題。我們將有一個混亂且不可讀的代碼。此代碼稱為“回調地獄”。

// 回調地獄
fs.readFile('somefile.txt', function (e, data) {
  //your code here
  fs.readdir('directory', function (e, files) {
    //your code here
    fs.mkdir('directory', function (e) {
      //your code here
    })
  })
})

如果我們在這段代碼中使用promise,它將更易于閱讀、理解和維護。

promReadFile('file/path')
  .then(data => {
    return promReaddir('directory');
  })
  .then(data => {
    return promMkdir('directory');
  })
  .catch(e => {
    console.log(e);
  })

promise有三種不同的狀態(tài):

  • pending:初始狀態(tài),完成或失敗狀態(tài)的前一個狀態(tài)
  • fulfilled:操作成功完成
  • rejected:操作失敗

pending 狀態(tài)的 Promise 對象會觸發(fā) fulfilled/rejected 狀態(tài),在其狀態(tài)處理方法中可以傳入參數/失敗信息。當操作成功完成時,Promise 對象的 then 方法就會被調用;否則就會觸發(fā) catch。如:

const myFirstPromise = new Promise((resolve, reject) => {
    setTimeout(function(){
        resolve("成功!");
    }, 250);
});

myFirstPromise.then((data) => {
    console.log("Yay! " + data);
}).catch((e) => {...});

51. 什么是 async/await 及其如何工作?

async/await是 JS 中編寫異步或非阻塞代碼的新方法。它建立在Promises之上,相對于 Promise 和回調,它的可讀性和簡潔度都更高。但是,在使用此功能之前,我們必須先學習Promises的基礎知識,因為正如我之前所說,它是基于Promise構建的,這意味著幕后使用仍然是Promise。

使用 Promise

function callApi() {
  return fetch("url/to/api/endpoint")
    .then(resp => resp.json())
    .then(data => {
      //do something with "data"
    }).catch(err => {
      //do something with "err"
    });
}

使用async/await

在async/await中,我們使用 tru/catch 語法來捕獲異常。

async function callApi() {
  try {
    const resp = await fetch("url/to/api/endpoint");
    const data = await resp.json();
    //do something with "data"
  } catch (e) {
    //do something with "err"
  }
}

注意: 使用 async關鍵聲明函數會隱式返回一個Promise。

const giveMeOne = async () => 1;

giveMeOne()
  .then((num) => {
    console.log(num); // logs 1
  });

注意: await關鍵字只能在async function中使用。在任何非async function的函數中使用await關鍵字都會拋出錯誤。await關鍵字在執(zhí)行下一行代碼之前等待右側表達式(可能是一個Promise)返回。

const giveMeOne = async () => 1;

function getOne() {
  try {
    const num = await giveMeOne();
    console.log(num);
  } catch (e) {
    console.log(e);
  }
}

// Uncaught SyntaxError: await is only valid in async function

async function getTwo() {
  try {
    const num1 = await giveMeOne(); // 這行會等待右側表達式執(zhí)行完成
    const num2 = await giveMeOne();
    return num1 + num2;
  } catch (e) {
    console.log(e);
  }
}

await getTwo(); // 2

52. 展開(spread )運算符和 剩余(Rest) 運算符有什么區(qū)別?

展開運算符(spread)是三個點(...),可以將一個數組轉為用逗號分隔的參數序列。說的通俗易懂點,有點像化骨綿掌,把一個大元素給打散成一個個單獨的小元素。

剩余運算符也是用三個點(...)表示,它的樣子看起來和展開操作符一樣,但是它是用于解構數組和對象。在某種程度上,剩余元素和展開元素相反,展開元素會“展開”數組變成多個元素,剩余元素會收集多個元素和“壓縮”成一個單一的元素。

function add(a, b) {
  return a + b;
};

const nums = [5, 6];
const sum = add(...nums);
console.log(sum);

在本例中,我們在調用add函數時使用了展開操作符,對nums數組進行展開。所以參數a的值是5 ,b的值是6,所以sum 是11。

function add(...rest) {
  return rest.reduce((total,current) => total + current);
};

console.log(add(1, 2)); // 3
console.log(add(1, 2, 3, 4, 5)); // 15

在本例中,我們有一個add函數,它接受任意數量的參數,并將它們全部相加,然后返回總數。

const [first, ...others] = [1, 2, 3, 4, 5];
console.log(first); // 1
console.log(others); // [2,3,4,5]

這里,我們使用剩余操作符提取所有剩余的數組值,并將它們放入除第一項之外的其他數組中。

53. 什么是默認參數?

默認參數是在 JS 中定義默認變量的一種新方法,它在ES6或ECMAScript 2015版本中可用。

//ES5 Version
function add(a,b){
  a = a || 0;
  b = b || 0;
  return a + b;
}

//ES6 Version
function add(a = 0, b = 0){
  return a + b;
}
add(1); // returns 1

我們還可以在默認參數中使用解構。

function getFirst([first, ...rest] = [0, 1]) {
  return first;
}

getFirst();  // 0
getFirst([10,20,30]);  // 10

function getArr({ nums } = { nums: [1, 2, 3, 4] }){
    return nums;
}

getArr(); // [1, 2, 3, 4]
getArr({nums:[5,4,3,2,1]}); // [5,4,3,2,1]

我們還可以使用先定義的參數再定義它們之后的參數。

function doSomethingWithValue(value = "Hello World", callback = () => { console.log(value) }) {
  callback();
}
doSomethingWithValue(); //"Hello World"

54. 什么是包裝對象(wrapper object)?

我們現(xiàn)在復習一下JS的數據類型,JS數據類型被分為兩大類,基本類型和引用類型。

基本類型:Undefined,Null,Boolean,Number,String,Symbol,BigInt

引用類型:Object,Array,Date,RegExp等,說白了就是對象。

其中引用類型有方法和屬性,但是基本類型是沒有的,但我們經常會看到下面的代碼:

let name = "marko";

console.log(typeof name); // "string"
console.log(name.toUpperCase()); // "MARKO"

name類型是 string,屬于基本類型,所以它沒有屬性和方法,但是在這個例子中,我們調用了一個toUpperCase()方法,它不會拋出錯誤,還返回了對象的變量值。

原因是基本類型的值被臨時轉換或強制轉換為對象,因此name變量的行為類似于對象。除null和undefined之外的每個基本類型都有自己包裝對象。也就是:String,Number,Boolean,Symbol和BigInt。在這種情況下,name.toUpperCase()在幕后看起來如下:

console.log(new String(name).toUpperCase()); // "MARKO"

在完成訪問屬性或調用方法之后,新創(chuàng)建的對象將立即被丟棄。

55. 隱式和顯式轉換有什么區(qū)別)?

隱式強制轉換是一種將值轉換為另一種類型的方法,這個過程是自動完成的,無需我們手動操作。

假設我們下面有一個例子。

console.log(1 + '6'); // 16
console.log(false + true); // 1
console.log(6 * '2'); // 12

第一個console.log語句結果為16。在其他語言中,這會拋出編譯時錯誤,但在 JS 中,1被轉換成字符串,然后與+運算符連接。我們沒有做任何事情,它是由 JS 自動完成。

第二個console.log語句結果為1,JS 將false轉換為boolean 值為 0,,true為1,因此結果為1。

第三個console.log語句結果12,它將'2'轉換為一個數字,然后乘以6 * 2,結果是12。

而顯式強制是將值轉換為另一種類型的方法,我們需要手動轉換。

console.log(1 + parseInt('6'));

在本例中,我們使用parseInt函數將'6'轉換為number ,然后使用+運算符將1和6相加。

56. 什么是NaN?以及如何檢查值是否為NaN?

NaN表示“非數字”是 JS 中的一個值,該值是將數字轉換或執(zhí)行為非數字值的運算結果,因此結果為NaN。

let a;

console.log(parseInt('abc')); // NaN
console.log(parseInt(null)); // NaN
console.log(parseInt(undefined)); // NaN
console.log(parseInt(++a)); // NaN
console.log(parseInt({} * 10)); // NaN
console.log(parseInt('abc' - 2)); // NaN
console.log(parseInt(0 / 0)); // NaN
console.log(parseInt('10a' * 10)); // NaN

JS 有一個內置的isNaN方法,用于測試值是否為isNaN值,但是這個函數有一個奇怪的行為。

console.log(isNaN()); // true
console.log(isNaN(undefined)); // true
console.log(isNaN({})); // true
console.log(isNaN(String('a'))); // true
console.log(isNaN(() => { })); // true

所有這些console.log語句都返回true,即使我們傳遞的值不是NaN。

在ES6中,建議使用Number.isNaN方法,因為它確實會檢查該值(如果確實是NaN),或者我們可以使自己的輔助函數檢查此問題,因為在 JS 中,NaN是唯一的值,它不等于自己。

function checkIfNaN(value) {
  return value !== value;
}

57. 如何判斷值是否為數組?

我們可以使用Array.isArray方法來檢查值是否為數組。當傳遞給它的參數是數組時,它返回true,否則返回false。

console.log(Array.isArray(5));  // false
console.log(Array.isArray("")); // false
console.log(Array.isArray()); // false
console.log(Array.isArray(null)); // false
console.log(Array.isArray({ length: 5 })); // false

console.log(Array.isArray([])); // true

如果環(huán)境不支持此方法,則可以使用polyfill實現(xiàn)。

function isArray(value){
 return Object.prototype.toString.call(value) === "[object Array]"
}

當然還可以使用傳統(tǒng)的方法:

let a = []
if (a instanceof Array) {
  console.log('是數組')
} else {
  console.log('非數組')
}

58. 如何在不使用%模運算符的情況下檢查一個數字是否是偶數?

我們可以對這個問題使用按位&運算符,&對其操作數進行運算,并將其視為二進制值,然后執(zhí)行與運算。

function isEven(num) {
  if (num & 1) {
    return false
  } else {
    return true
  }
}

0 二進制數是 000
1 二進制數是 001
2 二進制數是 010
3 二進制數是 011
4 二進制數是 100
5 二進制數是 101
6 二進制數是 110
7 二進制數是 111

以此類推...

與運算的規(guī)則如下:

a b a & b
0 0 0
0 1 0
1 1 1

因此,當我們執(zhí)行console.log(5&1)這個表達式時,結果為1。首先,&運算符將兩個數字都轉換為二進制,因此5變?yōu)?01,1變?yōu)?01。

然后,它使用按位懷運算符比較每個位(0和1)。 101&001,從表中可以看出,如果a & b為1,所以5&1結果為1。

101 & 001
101
001
001

首先我們比較最左邊的1&0,結果是0。

然后我們比較中間的0&0,結果是0。

然后我們比較最后1&1,結果是1。

最后,得到一個二進制數001,對應的十進制數,即1。

由此我們也可以算出console.log(4 & 1) 結果為0。知道4的最后一位是0,而0 & 1 將是0。如果你很難理解這一點,我們可以使用遞歸函數來解決此問題。

function isEven(num) {

    if (num < 0 || num === 1) return false;

    if (num == 0) return true;
    return isEven(num - 2);

}

59. 如何檢查對象中是否存在某個屬性?

檢查對象中是否存在屬性有三種方法。

第一種使用 in 操作符號:

const o = {
  "prop" : "bwahahah",
  "prop2" : "hweasa"
};

console.log("prop" in o); // true
console.log("prop1" in o); // false

第二種使用 hasOwnProperty 方法,hasOwnProperty() 方法會返回一個布爾值,指示對象自身屬性中是否具有指定的屬性(也就是,是否有指定的鍵)。

console.log(o.hasOwnProperty("prop2")); // true
console.log(o.hasOwnProperty("prop1")); // false

第三種使用括號符號obj["prop"]。如果屬性存在,它將返回該屬性的值,否則將返回undefined。

console.log(o["prop"]); // "bwahahah"
console.log(o["prop1"]); // undefined

60. AJAX 是什么?

即異步的 JavaScript 和 XML,是一種用于創(chuàng)建快速動態(tài)網頁的技術,傳統(tǒng)的網頁(不使用 AJAX)如果需要更新內容,必需重載整個網頁面。使用AJAX則不需要加載更新整個網頁,實現(xiàn)部分內容更新

用到AJAX的技術:

  • HTML - 網頁結構
  • CSS - 網頁的樣式
  • JavaScript - 操作網頁的行為和更新DOM
  • XMLHttpRequest API - 用于從服務器發(fā)送和獲取數據
  • PHP,Python,Nodejs - 某些服務器端語言

61. 如何在 JS 中創(chuàng)建對象?

使用對象字面量:

  const o = {
   name: "Mark",
   greeting() {
      return `Hi, I'm ${this.name}`;
   }
  };

  o.greeting(); //returns "Hi, I'm Mark"

使用構造函數:

function Person(name) {
   this.name = name;
}

Person.prototype.greeting = function () {
   return `Hi, I'm ${this.name}`;
}

const mark = new Person("Mark");

mark.greeting(); //returns "Hi, I'm Mark"

使用 Object.create 方法:

const n = {
   greeting() {
      return `Hi, I'm ${this.name}`;
   }
};

const o = Object.create(n); // sets the prototype of "o" to be "n"

o.name = "Mark";

console.log(o.greeting()); // logs "Hi, I'm Mark"

62. Object.seal 和 Object.freeze 方法之間有什么區(qū)別?

這兩種方法之間的區(qū)別在于,當我們對一個對象使用Object.freeze方法時,該對象的屬性是不可變的,這意味著我們不能更改或編輯這些屬性的值。而在Obj.Engor方法中,我們可以改變現(xiàn)有的屬性。

Object.freeze()

Object.freeze() 方法可以凍結一個對象。一個被凍結的對象再也不能被修改;凍結了一個對象則不能向這個對象添加新的屬性,不能刪除已有屬性,不能修改該對象已有屬性的可枚舉性、可配置性、可寫性,以及不能修改已有屬性的值。此外,凍結一個對象后該對象的原型也不能被修改。freeze() 返回和傳入的參數相同的對象。

Object.seal()

Object.seal()方法封閉一個對象,阻止添加新屬性并將所有現(xiàn)有屬性標記為不可配置。當前屬性的值只要可寫就可以改變。

方法的相同點:

  • ES5新增。

  • 對象不可能擴展,也就是不能再添加新的屬性或者方法。

  • 對象已有屬性不允許被刪除。

  • 對象屬性特性不可以重新配置。

方法不同點:

  • Object.seal方法生成的密封對象,如果屬性是可寫的,那么可以修改屬性值。

  • Object.freeze方法生成的凍結對象,屬性都是不可寫的,也就是屬性值無法更改。

63. in 運算符和 Object.hasOwnProperty 方法有什么區(qū)別?

如你所知,這兩個特性都檢查對象中是否存在屬性,它將返回true或false。它們之間的區(qū)別在于,in操作符還會檢查對象的原型鏈,如果屬性在當前對象中沒有找到,而hasOwnProperty方法只檢查屬性是否存在于當前對象中,而忽略原型鏈。

hasOwnPropert方法

hasOwnPropert()方法返回值是一個布爾值,指示對象自身屬性中是否具有指定的屬性,因此這個方法會忽略掉那些從原型鏈上繼承到的屬性。

看下面的例子:

Object.prototype.phone= '15345025546';

let obj = {
    name: '西門大官人',
    age: '28'
}
console.log(obj.hasOwnProperty('phone')) // false
console.log(obj.hasOwnProperty('name')) // true

可以看到,如果在函數原型上定義一個變量phone,hasOwnProperty方法會直接忽略掉。

in 運算符

如果指定的屬性在指定的對象或其原型鏈中,則in 運算符返回true。

還是用上面的例子來演示:

console.log('phone' in obj) // true

可以看到in運算符會檢查它或者其原型鏈是否包含具有指定名稱的屬性。

64. 有哪些方法可以處理 JS 中的異步代碼?

  • 回調
  • Promise
  • async/await

還有一些庫:async.js, bluebird, q, co

65. 函數表達式和函數聲明之間有什么區(qū)別?

看下面的例子:

hoistedFunc();
notHoistedFunc();

function hoistedFunc(){
  console.log("注意:我會被提升");
}

var notHoistedFunc = function(){
  console.log("注意:我沒有被提升");
}

notHoistedFunc調用拋出異常:Uncaught TypeError: notHoistedFunc is not a function,而hoistedFunc調用不會,因為hoistedFunc會被提升到作用域的頂部,而notHoistedFunc 不會。

66. 調用函數,可以使用哪些方法?

在 JS 中有4種方法可以調用函數。

作為函數調用——如果一個函數沒有作為方法、構造函數、apply、call 調用時,此時 this 指向的是 window 對象(非嚴格模式)

  //Global Scope

  function add(a,b){
    console.log(this);
    return a + b;
  }

  add(1,5); // 打印 "window" 對象和 6

  const o = {
    method(callback){
      callback();
    }
  }

  o.method(function (){
      console.log(this); // 打印 "window" 對象
  });

作為方法調用——如果一個對象的屬性有一個函數的值,我們就稱它為方法。調用該方法時,該方法的this值指向該對象。

const details = {
  name : "Marko",
  getName(){
    return this.name;
  }
}

details.getName(); // Marko
// the "this" value inside "getName" method will be the "details" object

作為構造函數的調用-如果在函數之前使用new關鍵字調用了函數,則該函數稱為構造函數。構造函數里面會默認創(chuàng)建一個空對象,并將this指向該對象。

function Employee(name, position, yearHired) {
  // creates an empty object {}
  // then assigns the empty object to the "this" keyword
  // this = {};
  this.name = name;
  this.position = position;
  this.yearHired = yearHired;
  // inherits from Employee.prototype
  // returns the "this" value implicitly if no
  // explicit return statement is specified
};

const emp = new Employee("Marko Polo", "Software Developer", 2017);

使用apply和call方法調用——如果我們想顯式地指定一個函數的this值,我們可以使用這些方法,這些方法對所有函數都可用。

const obj1 = {
 result:0
};

const obj2 = {
 result:0
};

function reduceAdd(){
   let result = 0;
   for(let i = 0, len = arguments.length; i < len; i++){
     result += arguments[i];
   }
   this.result = result;
}

reduceAdd.apply(obj1, [1, 2, 3, 4, 5]);  // reduceAdd 函數中的 this 對象將是 obj1
reduceAdd.call(obj2, 1, 2, 3, 4, 5); // reduceAdd 函數中的 this 對象將是 obj2

67. 什么是緩存及它有什么作用?

緩存是建立一個函數的過程,這個函數能夠記住之前計算的結果或值。使用緩存函數是為了避免在最后一次使用相同參數的計算中已經執(zhí)行的函數的計算。這節(jié)省了時間,但也有不利的一面,即我們將消耗更多的內存來保存以前的結果。

68. 手動實現(xiàn)緩存方法

function memoize(fn) {
  const cache = {};
  return function (param) {
    if (cache[param]) {
      console.log('cached');
      return cache[param];
    } else {
      let result = fn(param);
      cache[param] = result;
      console.log(`not cached`);
      return result;
    }
  }
}

const toUpper = (str ="")=> str.toUpperCase();

const toUpperMemoized = memoize(toUpper);

toUpperMemoized("abcdef");
toUpperMemoized("abcdef");

這個緩存函數適用于接受一個參數。我們需要改變下,讓它接受多個參數。

const slice = Array.prototype.slice;
function memoize(fn) {
  const cache = {};
  return (...args) => {
    const params = slice.call(args);
    console.log(params);
    if (cache[params]) {
      console.log('cached');
      return cache[params];
    } else {
      let result = fn(...args);
      cache[params] = result;
      console.log(`not cached`);
      return result;
    }
  }
}
const makeFullName = (fName, lName) => `${fName} ${lName}`;
const reduceAdd = (numbers, startingValue = 0) =>
 numbers.reduce((total, cur) => total + cur, startingValue);

const memoizedMakeFullName = memoize(makeFullName);
const memoizedReduceAdd = memoize(reduceAdd);

memoizedMakeFullName("Marko", "Polo");
memoizedMakeFullName("Marko", "Polo");

memoizedReduceAdd([1, 2, 3, 4, 5], 5);
memoizedReduceAdd([1, 2, 3, 4, 5], 5);

69. 為什么typeof null 返回 object?如何檢查一個值是否為 null?

typeof null == 'object'總是返回true,因為這是自 JS 誕生以來null的實現(xiàn)。曾經有人提出將typeof null == 'object'修改為typeof null == 'null',但是被拒絕了,因為這將導致更多的bug。

我們可以使用嚴格相等運算符===來檢查值是否為null。

function isNull(value){
  return value === null;
}

70. new 關鍵字有什么作用?

new關鍵字與構造函數一起使用以創(chuàng)建對象在JavaScript中。

下面看看例子:

function Employee(name, position, yearHired) {
  this.name = name;
  this.position = position;
  this.yearHired = yearHired;
};

const emp = new Employee("Marko Polo", "Software Developer", 2017);

  1. new關鍵字做了4件事:
  2. 創(chuàng)建空對象 {}
  3. 將空對象分配給 this 值
  4. 將空對象的proto指向構造函數的prototype
  5. 如果沒有使用顯式return語句,則返回this

根據上面描述的,它將首先創(chuàng)建一個空對象{},然后它將this值賦給這個空對象this={},并向這個對象添加屬性。因為我們沒有顯式的return語句,所以它會自動為我們返回this。

?著作權歸作者所有,轉載或內容合作請聯(lián)系作者
【社區(qū)內容提示】社區(qū)部分內容疑似由AI輔助生成,瀏覽時請結合常識與多方信息審慎甄別。
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發(fā)布,文章內容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。

友情鏈接更多精彩內容