前端面試 JavaScript篇

2023-02-17 10:51 更新

一、數(shù)據(jù)類型


1. JavaScript有哪些數(shù)據(jù)類型,它們的區(qū)別?

JavaScript共有八種數(shù)據(jù)類型,分別是 Undefined、Null、Boolean、Number、String、Object、Symbol、BigInt。

其中 Symbol 和 BigInt 是ES6 中新增的數(shù)據(jù)類型:

  • Symbol 代表創(chuàng)建后獨一無二且不可變的數(shù)據(jù)類型,它主要是為了解決可能出現(xiàn)的全局變量沖突的問題。
  • BigInt 是一種數(shù)字類型的數(shù)據(jù),它可以表示任意精度格式的整數(shù),使用 BigInt 可以安全地存儲和操作大整數(shù),即使這個數(shù)已經(jīng)超出了 Number 能夠表示的安全整數(shù)范圍。

這些數(shù)據(jù)可以分為原始數(shù)據(jù)類型和引用數(shù)據(jù)類型:

  • 棧:原始數(shù)據(jù)類型(Undefined、Null、Boolean、Number、String)
  • 堆:引用數(shù)據(jù)類型(對象、數(shù)組和函數(shù))

兩種類型的區(qū)別在于存儲位置的不同:

  • 原始數(shù)據(jù)類型直接存儲在棧(stack)中的簡單數(shù)據(jù)段,占據(jù)空間小、大小固定,屬于被頻繁使用數(shù)據(jù),所以放入棧中存儲;
  • 引用數(shù)據(jù)類型存儲在堆(heap)中的對象,占據(jù)空間大、大小不固定。如果存儲在棧中,將會影響程序運行的性能;引用數(shù)據(jù)類型在棧中存儲了指針,該指針指向堆中該實體的起始地址。當(dāng)解釋器尋找引用值時,會首先檢索其在棧中的地址,取得地址后從堆中獲得實體。

堆和棧的概念存在于數(shù)據(jù)結(jié)構(gòu)和操作系統(tǒng)內(nèi)存中,在數(shù)據(jù)結(jié)構(gòu)中:

  • 在數(shù)據(jù)結(jié)構(gòu)中,棧中數(shù)據(jù)的存取方式為先進后出。
  • 堆是一個優(yōu)先隊列,是按優(yōu)先級來進行排序的,優(yōu)先級可以按照大小來規(guī)定。

在操作系統(tǒng)中,內(nèi)存被分為棧區(qū)和堆區(qū):

  • 棧區(qū)內(nèi)存由編譯器自動分配釋放,存放函數(shù)的參數(shù)值,局部變量的值等。其操作方式類似于數(shù)據(jù)結(jié)構(gòu)中的棧。
  • 堆區(qū)內(nèi)存一般由開發(fā)著分配釋放,若開發(fā)者不釋放,程序結(jié)束時可能由垃圾回收機制回收。

2. 數(shù)據(jù)類型檢測的方式有哪些

(1)typeof

console.log(typeof 2);               // number
console.log(typeof true);            // boolean
console.log(typeof 'str');           // string
console.log(typeof []);              // object  
console.log(typeof function(){});    // function
console.log(typeof {});              // object
console.log(typeof undefined);       // undefined
console.log(typeof null);            // object

其中數(shù)組、對象、null都會被判斷為object,其他判斷都正確。

(2)instanceof

instanceof可以正確判斷對象的類型,其內(nèi)部運行機制是判斷在其原型鏈中能否找到該類型的原型。

console.log(2 instanceof Number);                    // false
console.log(true instanceof Boolean);                // false 
console.log('str' instanceof String);                // false 
 
console.log([] instanceof Array);                    // true
console.log(function(){} instanceof Function);       // true
console.log({} instanceof Object);                   // true

可以看到,instanceof只能正確判斷引用數(shù)據(jù)類型,而不能判斷基本數(shù)據(jù)類型。instanceof 運算符可以用來測試一個對象在其原型鏈中是否存在一個構(gòu)造函數(shù)的 prototype 屬性。

(3) constructor

console.log((2).constructor === Number); // true
console.log((true).constructor === Boolean); // true
console.log(('str').constructor === String); // true
console.log(([]).constructor === Array); // true
console.log((function() {}).constructor === Function); // true
console.log(({}).constructor === Object); // true

constructor有兩個作用,一是判斷數(shù)據(jù)的類型,二是對象實例通過 constrcutor 對象訪問它的構(gòu)造函數(shù)。需要注意,如果創(chuàng)建一個對象來改變它的原型,constructor就不能用來判斷數(shù)據(jù)類型了:

function Fn(){};
 
Fn.prototype = new Array();
 
var f = new Fn();
 
console.log(f.constructor===Fn);    // false
console.log(f.constructor===Array); // true

(4)Object.prototype.toString.call()

Object.prototype.toString.call() 使用 Object 對象的原型方法 toString 來判斷數(shù)據(jù)類型:

var a = Object.prototype.toString;
 
console.log(a.call(2));
console.log(a.call(true));
console.log(a.call('str'));
console.log(a.call([]));
console.log(a.call(function(){}));
console.log(a.call({}));
console.log(a.call(undefined));
console.log(a.call(null));

同樣是檢測對象obj調(diào)用toString方法,obj.toString()的結(jié)果和Object.prototype.toString.call(obj)的結(jié)果不一樣,這是為什么?

這是因為toString是Object的原型方法,而Array、function等類型作為Object的實例,都重寫了toString方法。不同的對象類型調(diào)用toString方法時,根據(jù)原型鏈的知識,調(diào)用的是對應(yīng)的重寫之后的toString方法(function類型返回內(nèi)容為函數(shù)體的字符串,Array類型返回元素組成的字符串…),而不會去調(diào)用Object上原型toString方法(返回對象的具體類型),所以采用obj.toString()不能得到其對象類型,只能將obj轉(zhuǎn)換為字符串類型;因此,在想要得到對象的具體類型時,應(yīng)該調(diào)用Object原型上的toString方法。

3. 判斷數(shù)組的方式有哪些

  • 通過Object.prototype.toString.call()做判斷
Object.prototype.toString.call(obj).slice(8,-1) === 'Array';
  • 通過原型鏈做判斷
obj.__proto__ === Array.prototype;
  • 通過ES6的Array.isArray()做判斷
Array.isArrray(obj);
  • 通過instanceof做判斷
obj instanceof Array
  • 通過Array.prototype.isPrototypeOf
Array.prototype.isPrototypeOf(obj)

4. null和undefined區(qū)別

首先 Undefined 和 Null 都是基本數(shù)據(jù)類型,這兩個基本數(shù)據(jù)類型分別都只有一個值,就是 undefined 和 null。

undefined 代表的含義是未定義,null 代表的含義是空對象。一般變量聲明了但還沒有定義的時候會返回 undefined,null主要用于賦值給一些可能會返回對象的變量,作為初始化。

undefined 在 JavaScript 中不是一個保留字,這意味著可以使用 undefined 來作為一個變量名,但是這樣的做法是非常危險的,它會影響對 undefined 值的判斷。我們可以通過一些方法獲得安全的 undefined 值,比如說 void 0。

當(dāng)對這兩種類型使用 typeof 進行判斷時,Null 類型化會返回 “object”,這是一個歷史遺留的問題。當(dāng)使用雙等號對兩種類型的值進行比較時會返回 true,使用三個等號時會返回 false。

5. typeof null 的結(jié)果是什么,為什么?

typeof null 的結(jié)果是Object。

在 JavaScript 第一個版本中,所有值都存儲在 32 位的單元中,每個單元包含一個小的 類型標(biāo)簽(1-3 bits) 以及當(dāng)前要存儲值的真實數(shù)據(jù)。類型標(biāo)簽存儲在每個單元的低位中,共有五種數(shù)據(jù)類型:

000: object   - 當(dāng)前存儲的數(shù)據(jù)指向一個對象。
  1: int      - 當(dāng)前存儲的數(shù)據(jù)是一個 31 位的有符號整數(shù)。
010: double   - 當(dāng)前存儲的數(shù)據(jù)指向一個雙精度的浮點數(shù)。
100: string   - 當(dāng)前存儲的數(shù)據(jù)指向一個字符串。
110: boolean  - 當(dāng)前存儲的數(shù)據(jù)是布爾值。

如果最低位是 1,則類型標(biāo)簽標(biāo)志位的長度只有一位;如果最低位是 0,則類型標(biāo)簽標(biāo)志位的長度占三位,為存儲其他四種數(shù)據(jù)類型提供了額外兩個 bit 的長度。

有兩種特殊數(shù)據(jù)類型:

  • undefined的值是 (-2)30(一個超出整數(shù)范圍的數(shù)字);
  • null 的值是機器碼 NULL 指針(null 指針的值全是 0)

那也就是說null的類型標(biāo)簽也是000,和Object的類型標(biāo)簽一樣,所以會被判定為Object。

6. intanceof 操作符的實現(xiàn)原理及實現(xiàn)

instanceof 運算符用于判斷構(gòu)造函數(shù)的 prototype 屬性是否出現(xiàn)在對象的原型鏈中的任何位置。

function myInstanceof(left, right) {
  // 獲取對象的原型
  let proto = Object.getPrototypeOf(left)
  // 獲取構(gòu)造函數(shù)的 prototype 對象
  let prototype = right.prototype; 
 
  // 判斷構(gòu)造函數(shù)的 prototype 對象是否在對象的原型鏈上
  while (true) {
    if (!proto) return false;
    if (proto === prototype) return true;
    // 如果沒有找到,就繼續(xù)從其原型上找,Object.getPrototypeOf方法用來獲取指定對象的原型
    proto = Object.getPrototypeOf(proto);
  }
}

7. 為什么0.1+0.2 ! == 0.3,如何讓其相等

在開發(fā)過程中遇到類似這樣的問題:

let n1 = 0.1, n2 = 0.2
console.log(n1 + n2)  // 0.30000000000000004

這里得到的不是想要的結(jié)果,要想等于0.3,就要把它進行轉(zhuǎn)化:

(n1 + n2).toFixed(2) // 注意,toFixed為四舍五入

toFixed(num) 方法可把 Number 四舍五入為指定小數(shù)位數(shù)的數(shù)字。那為什么會出現(xiàn)這樣的結(jié)果呢?

計算機是通過二進制的方式存儲數(shù)據(jù)的,所以計算機計算0.1+0.2的時候,實際上是計算的兩個數(shù)的二進制的和。0.1的二進制是 0.0001100110011001100...(1100循環(huán)),0.2的二進制是:0.00110011001100...(1100循環(huán)),這兩個數(shù)的二進制都是無限循環(huán)的數(shù)。那JavaScript是如何處理無限循環(huán)的二進制小數(shù)呢?

一般我們認為數(shù)字包括整數(shù)和小數(shù),但是在 JavaScript 中只有一種數(shù)字類型:Number,它的實現(xiàn)遵循IEEE 754標(biāo)準(zhǔn),使用64位固定長度來表示,也就是標(biāo)準(zhǔn)的double雙精度浮點數(shù)。在二進制科學(xué)表示法中,雙精度浮點數(shù)的小數(shù)部分最多只能保留52位,再加上前面的1,其實就是保留53位有效數(shù)字,剩余的需要舍去,遵從“0舍1入”的原則。

根據(jù)這個原則,0.1和0.2的二進制數(shù)相加,再轉(zhuǎn)化為十進制數(shù)就是:0.30000000000000004

下面看一下雙精度數(shù)是如何保存的:


  • 第一部分(藍色):用來存儲符號位(sign),用來區(qū)分正負數(shù),0表示正數(shù),占用1位
  • 第二部分(綠色):用來存儲指數(shù)(exponent),占用11位
  • 第三部分(紅色):用來存儲小數(shù)(fraction),占用52位

對于0.1,它的二進制為:

0.00011001100110011001100110011001100110011001100110011001 10011...

轉(zhuǎn)為科學(xué)計數(shù)法(科學(xué)計數(shù)法的結(jié)果就是浮點數(shù)):

1.1001100110011001100110011001100110011001100110011001*2^-4

可以看出0.1的符號位為0,指數(shù)位為-4,小數(shù)位為:

1001100110011001100110011001100110011001100110011001

那么問題又來了,指數(shù)位是負數(shù),該如何保存呢?

IEEE標(biāo)準(zhǔn)規(guī)定了一個偏移量,對于指數(shù)部分,每次都加這個偏移量進行保存,這樣即使指數(shù)是負數(shù),那么加上這個偏移量也就是正數(shù)了。由于JavaScript的數(shù)字是雙精度數(shù),這里就以雙精度數(shù)為例,它的指數(shù)部分為11位,能表示的范圍就是0~2047,IEEE固定雙精度數(shù)的偏移量為1023。

  • 當(dāng)指數(shù)位不全是0也不全是1時(規(guī)格化的數(shù)值),IEEE規(guī)定,階碼計算公式為 e-Bias。 此時e最小值是1,則1-1023= -1022,e最大值是2046,則2046-1023=1023,可以看到,這種情況下取值范圍是 -1022~1013。
  • 當(dāng)指數(shù)位全部是0的時候(非規(guī)格化的數(shù)值),IEEE規(guī)定,階碼的計算公式為1-Bias,即1-1023= -1022。
  • 當(dāng)指數(shù)位全部是1的時候(特殊值),IEEE規(guī)定這個浮點數(shù)可用來表示3個特殊值,分別是正無窮,負無窮,NaN。 具體的,小數(shù)位不為0的時候表示NaN;小數(shù)位為0時,當(dāng)符號位s=0時表示正無窮,s=1時候表示負無窮。

對于上面的0.1的指數(shù)位為-4,-4+1023 = 1019 轉(zhuǎn)化為二進制就是:1111111011.

所以,0.1表示為:

0 1111111011 1001100110011001100110011001100110011001100110011001

說了這么多,是時候該最開始的問題了,如何實現(xiàn)0.1+0.2=0.3呢?

對于這個問題,一個直接的解決方法就是設(shè)置一個誤差范圍,通常稱為“機器精度”。對JavaScript來說,這個值通常為2-52,在ES6中,提供了 Number.EPSILON屬性,而它的值就是2-52,只要判斷 0.1+0.2-0.3是否小于 Number.EPSILON,如果小于,就可以判斷為0.1+0.2 ===0.3

function numberepsilon(arg1,arg2){                 
  return Math.abs(arg1 - arg2) < Number.EPSILON;      
}      

console.log(numberepsilon(0.1 + 0.2, 0.3)); // true

8. 如何獲取安全的 undefined 值?

因為 undefined 是一個標(biāo)識符,所以可以被當(dāng)作變量來使用和賦值,但是這樣會影響 undefined 的正常判斷。表達式 void ___ 沒有返回值,因此返回結(jié)果是 undefined。void 并不改變表達式的結(jié)果,只是讓表達式不返回值。因此可以用 void 0 來獲得 undefined。

9. typeof NaN 的結(jié)果是什么?

NaN 指“不是一個數(shù)字”(not a number),NaN 是一個“警戒值”(sentinel value,有特殊用途的常規(guī)值),用于指出數(shù)字類型中的錯誤情況,即“執(zhí)行數(shù)學(xué)運算沒有成功,這是失敗后返回的結(jié)果”。

typeof NaN; // "number"

NaN 是一個特殊值,它和自身不相等,是唯一一個非自反(自反,reflexive,即 x === x 不成立)的值。而 NaN !== NaN 為 true。

10. isNaN 和 Number.isNaN 函數(shù)的區(qū)別?

  • 函數(shù) isNaN 接收參數(shù)后,會嘗試將這個參數(shù)轉(zhuǎn)換為數(shù)值,任何不能被轉(zhuǎn)換為數(shù)值的的值都會返回 true,因此非數(shù)字值傳入也會返回 true ,會影響 NaN 的判斷。
  • 函數(shù) Number.isNaN 會首先判斷傳入?yún)?shù)是否為數(shù)字,如果是數(shù)字再繼續(xù)判斷是否為 NaN ,不會進行數(shù)據(jù)類型的轉(zhuǎn)換,這種方法對于 NaN 的判斷更為準(zhǔn)確。

11. == 操作符的強制類型轉(zhuǎn)換規(guī)則?

對于 == 來說,如果對比雙方的類型不一樣,就會進行類型轉(zhuǎn)換。假如對比 x 和 y 是否相同,就會進行如下判斷流程:

  1. 首先會判斷兩者類型是否相同,相同的話就比較兩者的大小;
  2. 類型不相同的話,就會進行類型轉(zhuǎn)換;
  3. 會先判斷是否在對比 ?null ?和 ?undefined?,是的話就會返回 ?true ?
  4. 判斷兩者類型是否為 ?string ?和 ?number?,是的話就會將字符串轉(zhuǎn)換為 ?number?
  5. 1 == '1'
          ↓
    1 ==  1
  6. 判斷其中一方是否為 ?boolean?,是的話就會把 ?boolean ?轉(zhuǎn)為 ?number ?再進行判斷
  7. '1' == true
            ↓
    '1' ==  1
            ↓
     1  ==  1
  8. 判斷其中一方是否為 ?object ?且另一方為 ?string?、?number ?或者 ?symbol?,是的話就會把 ?object ?轉(zhuǎn)為原始類型再進行判斷
  9. '1' == { name: 'js' }
            ↓
    '1' == '[object Object]'

其流程圖如下:

image

12. 其他值到字符串的轉(zhuǎn)換規(guī)則?

  • Null 和 Undefined 類型 ,null 轉(zhuǎn)換為 "null",undefined 轉(zhuǎn)換為 "undefined",
  • Boolean 類型,true 轉(zhuǎn)換為 "true",false 轉(zhuǎn)換為 "false"。
  • Number 類型的值直接轉(zhuǎn)換,不過那些極小和極大的數(shù)字會使用指數(shù)形式。
  • Symbol 類型的值直接轉(zhuǎn)換,但是只允許顯式強制類型轉(zhuǎn)換,使用隱式強制類型轉(zhuǎn)換會產(chǎn)生錯誤。
  • 對普通對象來說,除非自行定義 toString() 方法,否則會調(diào)用 toString()(Object.prototype.toString())來返回內(nèi)部屬性 [[Class]] 的值,如"[object Object]"。如果對象有自己的 toString() 方法,字符串化時就會調(diào)用該方法并使用其返回值。

13. 其他值到數(shù)字值的轉(zhuǎn)換規(guī)則?

  • Undefined 類型的值轉(zhuǎn)換為 NaN。
  • Null 類型的值轉(zhuǎn)換為 0。
  • Boolean 類型的值,true 轉(zhuǎn)換為 1,false 轉(zhuǎn)換為 0。
  • String 類型的值轉(zhuǎn)換如同使用 Number() 函數(shù)進行轉(zhuǎn)換,如果包含非數(shù)字值則轉(zhuǎn)換為 NaN,空字符串為 0。
  • Symbol 類型的值不能轉(zhuǎn)換為數(shù)字,會報錯。
  • 對象(包括數(shù)組)會首先被轉(zhuǎn)換為相應(yīng)的基本類型值,如果返回的是非數(shù)字的基本類型值,則再遵循以上規(guī)則將其強制轉(zhuǎn)換為數(shù)字。

為了將值轉(zhuǎn)換為相應(yīng)的基本類型值,抽象操作 ToPrimitive 會首先(通過內(nèi)部操作 DefaultValue)檢查該值是否有valueOf()方法。如果有并且返回基本類型值,就使用該值進行強制類型轉(zhuǎn)換。如果沒有就使用 toString() 的返回值(如果存在)來進行強制類型轉(zhuǎn)換。

如果 valueOf() 和 toString() 均不返回基本類型值,會產(chǎn)生 TypeError 錯誤。

14. 其他值到布爾類型的值的轉(zhuǎn)換規(guī)則?

以下這些是假值:

  • undefined
  • null
  • false
  • +0、-0 和 NaN
  •  ""

假值的布爾強制類型轉(zhuǎn)換結(jié)果為 false。從邏輯上說,假值列表以外的都應(yīng)該是真值。

15. || 和 && 操作符的返回值?

|| 和 && 首先會對第一個操作數(shù)執(zhí)行條件判斷,如果其不是布爾值就先強制轉(zhuǎn)換為布爾類型,然后再執(zhí)行條件判斷。

  • 對于 || 來說,如果條件判斷結(jié)果為 true 就返回第一個操作數(shù)的值,如果為 false 就返回第二個操作數(shù)的值。
  • && 則相反,如果條件判斷結(jié)果為 true 就返回第二個操作數(shù)的值,如果為 false 就返回第一個操作數(shù)的值。

|| 和 && 返回它們其中一個操作數(shù)的值,而非條件判斷的結(jié)果

16. Object.is() 與比較操作符 “===”、“==” 的區(qū)別?

  • 使用雙等號(==)進行相等判斷時,如果兩邊的類型不一致,則會進行強制類型轉(zhuǎn)化后再進行比較。
  • 使用三等號(===)進行相等判斷時,如果兩邊的類型不一致時,不會做強制類型準(zhǔn)換,直接返回 false。
  • 使用 Object.is 來進行相等判斷時,一般情況下和三等號的判斷相同,它處理了一些特殊的情況,比如 -0 和 +0 不再相等,兩個 NaN 是相等的。

17. 什么是 JavaScript 中的包裝類型?

在 JavaScript 中,基本類型是沒有屬性和方法的,但是為了便于操作基本類型的值,在調(diào)用基本類型的屬性或方法時 JavaScript 會在后臺隱式地將基本類型的值轉(zhuǎn)換為對象,如:

const a = "abc";
a.length; // 3
a.toUpperCase(); // "ABC"

在訪問 'abc'.length時,JavaScript 將 'abc'在后臺轉(zhuǎn)換成 String('abc'),然后再訪問其 length屬性。

JavaScript也可以使用 Object函數(shù)顯式地將基本類型轉(zhuǎn)換為包裝類型:

var a = 'abc'
Object(a) // String {"abc"}

也可以使用 valueOf方法將包裝類型倒轉(zhuǎn)成基本類型:

var a = 'abc'
var b = Object(a)
var c = b.valueOf() // 'abc'

看看如下代碼會打印出什么:

var a = new Boolean( false );
if (!a) {
    console.log( "Oops" ); // never runs
}

答案是什么都不會打印,因為雖然包裹的基本類型是 false,但是 false被包裹成包裝類型后就成了對象,所以其非值為 false,所以循環(huán)體中的內(nèi)容不會運行。

18. JavaScript 中如何進行隱式類型轉(zhuǎn)換?

首先要介紹 ToPrimitive方法,這是 JavaScript 中每個值隱含的自帶的方法,用來將值 (無論是基本類型值還是對象)轉(zhuǎn)換為基本類型值。如果值為基本類型,則直接返回值本身;如果值為對象,其看起來大概是這樣:

/**
* @obj 需要轉(zhuǎn)換的對象
* @type 期望的結(jié)果類型
*/
ToPrimitive(obj,type)

type的值為 number或者 string。

(1)當(dāng) type number時規(guī)則如下:

  • 調(diào)用 ?obj?的 ?valueOf?方法,如果為原始值,則返回,否則下一步;
  • 調(diào)用 ?obj?的 ?toString?方法,后續(xù)同上;
  • 拋出 ?TypeError ?異常。

(2)當(dāng) type string時規(guī)則如下:

  • 調(diào)用 ?obj?的 ?toString?方法,如果為原始值,則返回,否則下一步;
  • 調(diào)用 ?obj?的 ?valueOf?方法,后續(xù)同上;
  • 拋出 ?TypeError?異常。

可以看出兩者的主要區(qū)別在于調(diào)用 toString和 valueOf的先后順序。默認情況下:

  • 如果對象為 Date 對象,則 ?type?默認為 ?string?;
  • 其他情況下,?type?默認為 ?number?。

總結(jié)上面的規(guī)則,對于 Date 以外的對象,轉(zhuǎn)換為基本類型的大概規(guī)則可以概括為一個函數(shù):

var objToNumber = value => Number(value.valueOf().toString())
objToNumber([]) === 0
objToNumber({}) === NaN

而 JavaScript 中的隱式類型轉(zhuǎn)換主要發(fā)生在 +、-、*、/以及 ==、>、<這些運算符之間。而這些運算符只能操作基本類型值,所以在進行這些運算前的第一步就是將兩邊的值用 ToPrimitive轉(zhuǎn)換成基本類型,再進行操作。

以下是基本類型的值在不同操作符的情況下隱式轉(zhuǎn)換的規(guī)則 (對于對象,其會被 ToPrimitive轉(zhuǎn)換成基本類型,所以最終還是要應(yīng)用基本類型轉(zhuǎn)換規(guī)則):

  1. +操作符的兩邊有至少一個 string類型變量時,兩邊的變量都會被隱式轉(zhuǎn)換為字符串;其他情況下兩邊的變量都會被轉(zhuǎn)換為數(shù)字。
  2. 1 + '23' // '123'
     1 + false // 1 
     1 + Symbol() // Uncaught TypeError: Cannot convert a Symbol value to a number
     '1' + false // '1false'
     false + true // 1
  3. -、*、\操作符NaN也是一個數(shù)字
  4. 1 * '23' // 23
     1 * false // 0
     1 / 'aa' // NaN
  5. 對于==操作符
  6. 操作符兩邊的值都盡量轉(zhuǎn)成 number

    3 == true // false, 3 轉(zhuǎn)為number為3,true轉(zhuǎn)為number為1
    '0' == false //true, '0'轉(zhuǎn)為number為0,false轉(zhuǎn)為number為0
    '0' == 0 // '0'轉(zhuǎn)為number為0
  7. 對于<和>比較符
  8. 如果兩邊都是字符串,則比較字母表順序:

    'ca' < 'bd' // false
    'a' < 'b' // true

    其他情況下,轉(zhuǎn)換為數(shù)字再比較:

    '12' < 13 // true
    false > -1 // true

    以上說的是基本類型的隱式轉(zhuǎn)換,而對象會被 ToPrimitive轉(zhuǎn)換為基本類型再進行轉(zhuǎn)換:

    var a = {}
    a > 2 // false

    其對比過程如下:

    a.valueOf() // {}, 上面提到過,ToPrimitive默認type為number,所以先valueOf,結(jié)果還是個對象,下一步
    a.toString() // "[object Object]",現(xiàn)在是一個字符串了
    Number(a.toString()) // NaN,根據(jù)上面 < 和 > 操作符的規(guī)則,要轉(zhuǎn)換成數(shù)字
    NaN > 2 //false,得出比較結(jié)果

    又比如:

    var a = {name:'Jack'}
    var b = {age: 18}
    a + b // "[object Object][object Object]"

    運算過程如下:

    a.valueOf() // {},上面提到過,ToPrimitive默認type為number,所以先valueOf,結(jié)果還是個對象,下一步
    a.toString() // "[object Object]"
    b.valueOf() // 同理
    b.toString() // "[object Object]"
    a + b // "[object Object][object Object]"

19. + 操作符什么時候用于字符串的拼接?

根據(jù) ES5 規(guī)范,如果某個操作數(shù)是字符串或者能夠通過以下步驟轉(zhuǎn)換為字符串的話,+ 將進行拼接操作。如果其中一個操作數(shù)是對象(包括數(shù)組),則首先對其調(diào)用 ToPrimitive 抽象操作,該抽象操作再調(diào)用 [[DefaultValue]],以數(shù)字作為上下文。如果不能轉(zhuǎn)換為字符串,則會將其轉(zhuǎn)換為數(shù)字類型來進行計算。

簡單來說就是,如果 + 的其中一個操作數(shù)是字符串(或者通過以上步驟最終得到字符串),則執(zhí)行字符串拼接,否則執(zhí)行數(shù)字加法。

那么對于除了加法的運算符來說,只要其中一方是數(shù)字,那么另一方就會被轉(zhuǎn)為數(shù)字。

20. 為什么會有BigInt的提案?

JavaScript中Number.MAX_SAFE_INTEGER表示最?安全數(shù)字,計算結(jié)果是9007199254740991,即在這個數(shù)范圍內(nèi)不會出現(xiàn)精度丟失(?數(shù)除外)。但是?旦超過這個范圍,js就會出現(xiàn)計算不準(zhǔn)確的情況,這在?數(shù)計算的時候不得不依靠?些第三?庫進?解決,因此官?提出了BigInt來解決此問題。

21. object.assign和擴展運算法是深拷貝還是淺拷貝,兩者區(qū)別

擴展運算符:

let outObj = {
  inObj: {a: 1, b: 2}
}
let newObj = {...outObj}
newObj.inObj.a = 2
console.log(outObj) // {inObj: {a: 2, b: 2}}

Object.assign():

let outObj = {
  inObj: {a: 1, b: 2}
}
let newObj = Object.assign({}, outObj)
newObj.inObj.a = 2
console.log(outObj) // {inObj: {a: 2, b: 2}}

二、ES6


1. let、const、var的區(qū)別

(1)塊級作用域:塊作用域由 { }包括,let和const具有塊級作用域,var不存在塊級作用域。塊級作用域解決了ES5中的兩個問題:

  • 內(nèi)層變量可能覆蓋外層變量
  • 用來計數(shù)的循環(huán)變量泄露為全局變量

(2)變量提升:var存在變量提升,let和const不存在變量提升,即在變量只能在聲明之后使用,否在會報錯。

(3)給全局添加屬性:瀏覽器的全局對象是window,Node的全局對象是global。var聲明的變量為全局變量,并且會將該變量添加為全局對象的屬性,但是let和const不會。

(4)重復(fù)聲明:var聲明變量時,可以重復(fù)聲明變量,后聲明的同名變量會覆蓋之前聲明的遍歷。const和let不允許重復(fù)聲明變量。

(5)暫時性死區(qū):在使用let、const命令聲明變量之前,該變量都是不可用的。這在語法上,稱為暫時性死區(qū)。使用var聲明的變量不存在暫時性死區(qū)。

(6)初始值設(shè)置:在變量聲明時,var 和 let 可以不用設(shè)置初始值。而const聲明變量必須設(shè)置初始值。

(7)指針指向:let和const都是ES6新增的用于創(chuàng)建變量的語法。 let創(chuàng)建的變量是可以更改指針指向(可以重新賦值)。但const聲明的變量是不允許改變指針的指向。

區(qū)別 var let const
是否有塊級作用域 × ?? ??
是否存在變量提升 ?? × ×
是否添加全局屬性 ?? × ×
能否重復(fù)聲明變量 ?? × ×
是否存在暫時性死區(qū) × ?? ??
是否必須設(shè)置初始值 × × ??
能否改變指針指向 ?? ?? ×

2. const對象的屬性可以修改嗎

const保證的并不是變量的值不能改動,而是變量指向的那個內(nèi)存地址不能改動。對于基本類型的數(shù)據(jù)(數(shù)值、字符串、布爾值),其值就保存在變量指向的那個內(nèi)存地址,因此等同于常量。

但對于引用類型的數(shù)據(jù)(主要是對象和數(shù)組)來說,變量指向數(shù)據(jù)的內(nèi)存地址,保存的只是一個指針,const只能保證這個指針是固定不變的,至于它指向的數(shù)據(jù)結(jié)構(gòu)是不是可變的,就完全不能控制了。

3. 如果new一個箭頭函數(shù)的會怎么樣

箭頭函數(shù)是ES6中的提出來的,它沒有prototype,也沒有自己的this指向,更不可以使用arguments參數(shù),所以不能New一個箭頭函數(shù)。

new操作符的實現(xiàn)步驟如下:

  1. 創(chuàng)建一個對象
  2. 將構(gòu)造函數(shù)的作用域賦給新對象(也就是將對象的__proto__屬性指向構(gòu)造函數(shù)的prototype屬性)
  3. 指向構(gòu)造函數(shù)中的代碼,構(gòu)造函數(shù)中的this指向該對象(也就是為這個對象添加屬性和方法)
  4. 返回新的對象

所以,上面的第二、三步,箭頭函數(shù)都是沒有辦法執(zhí)行的。

4. 箭頭函數(shù)與普通函數(shù)的區(qū)別

(1)箭頭函數(shù)比普通函數(shù)更加簡潔

  • 如果沒有參數(shù),就直接寫一個空括號即可
  • 如果只有一個參數(shù),可以省去參數(shù)的括號
  • 如果有多個參數(shù),用逗號分割
  • 如果函數(shù)體的返回值只有一句,可以省略大括號
  • 如果函數(shù)體不需要返回值,且只有一句話,可以給這個語句前面加一個void關(guān)鍵字。最常見的就是調(diào)用一個函數(shù):
let fn = () => void doesNotReturn();

(2)箭頭函數(shù)沒有自己的this

箭頭函數(shù)不會創(chuàng)建自己的this, 所以它沒有自己的this,它只會在自己作用域的上一層繼承this。所以箭頭函數(shù)中this的指向在它在定義時已經(jīng)確定了,之后不會改變。

(3)箭頭函數(shù)繼承來的this指向永遠不會改變

var id = 'GLOBAL';
var obj = {
  id: 'OBJ',
  a: function(){
    console.log(this.id);
  },
  b: () => {
    console.log(this.id);
  }
};
obj.a();    // 'OBJ'
obj.b();    // 'GLOBAL'
new obj.a()  // undefined
new obj.b()  // Uncaught TypeError: obj.b is not a constructor

對象obj的方法b是使用箭頭函數(shù)定義的,這個函數(shù)中的this就永遠指向它定義時所處的全局執(zhí)行環(huán)境中的this,即便這個函數(shù)是作為對象obj的方法調(diào)用,this依舊指向Window對象。需要注意,定義對象的大括號 {}是無法形成一個單獨的執(zhí)行環(huán)境的,它依舊是處于全局執(zhí)行環(huán)境中。

(4)call()、apply()、bind()等方法不能改變箭頭函數(shù)中this的指向

var id = 'Global';
let fun1 = () => {
    console.log(this.id)
};
fun1();                     // 'Global'
fun1.call({id: 'Obj'});     // 'Global'
fun1.apply({id: 'Obj'});    // 'Global'
fun1.bind({id: 'Obj'})();   // 'Global'

(5)箭頭函數(shù)不能作為構(gòu)造函數(shù)使用

構(gòu)造函數(shù)在new的步驟在上面已經(jīng)說過了,實際上第二步就是將函數(shù)中的this指向該對象。 但是由于箭頭函數(shù)時沒有自己的this的,且this指向外層的執(zhí)行環(huán)境,且不能改變指向,所以不能當(dāng)做構(gòu)造函數(shù)使用。

(6)箭頭函數(shù)沒有自己的arguments

箭頭函數(shù)沒有自己的arguments對象。在箭頭函數(shù)中訪問arguments實際上獲得的是它外層函數(shù)的arguments值。

(7)箭頭函數(shù)沒有prototype

(8)箭頭函數(shù)不能用作Generator函數(shù),不能使用yeild關(guān)鍵字

5. 箭頭函數(shù)的this指向哪??

箭頭函數(shù)不同于傳統(tǒng)JavaScript中的函數(shù),箭頭函數(shù)并沒有屬于??的this,它所謂的this是捕獲其所在上下?的 this 值,作為??的 this 值,并且由于沒有屬于??的this,所以是不會被new調(diào)?的,這個所謂的this也不會被改變。

可以?Babel理解?下箭頭函數(shù):

// ES6 
const obj = { 
  getArrow() { 
    return () => { 
      console.log(this === obj); 
    }; 
  } 
}

轉(zhuǎn)化后:

// ES5,由 Babel 轉(zhuǎn)譯
var obj = { 
   getArrow: function getArrow() { 
     var _this = this; 
     return function () { 
        console.log(_this === obj); 
     }; 
   } 
};

6. 擴展運算符的作用及使用場景

(1)對象擴展運算符

對象的擴展運算符(...)用于取出參數(shù)對象中的所有可遍歷屬性,拷貝到當(dāng)前對象之中。

let bar = { a: 1, b: 2 };
let baz = { ...bar }; // { a: 1, b: 2 }

上述方法實際上等價于:

let bar = { a: 1, b: 2 };
let baz = Object.assign({}, bar); // { a: 1, b: 2 }

Object.assign方法用于對象的合并,將源對象 (source)的所有可枚舉屬性,復(fù)制到目標(biāo)對象 (target)Object.assign方法的第一個參數(shù)是目標(biāo)對象,后面的參數(shù)都是源對象。(如果目標(biāo)對象與源對象有同名屬性,或多個源對象有同名屬性,則后面的屬性會覆蓋前面的屬性)。

同樣,如果用戶自定義的屬性,放在擴展運算符后面,則擴展運算符內(nèi)部的同名屬性會被覆蓋掉。

let bar = {a: 1, b: 2};
let baz = {...bar, ...{a:2, b: 4}};  // {a: 2, b: 4}

利用上述特性就可以很方便的修改對象的部分屬性。在 redux中的 reducer函數(shù)規(guī)定必須是一個純函數(shù),reducer中的 state對象要求不能直接修改,可以通過擴展運算符把修改路徑的對象都復(fù)制一遍,然后產(chǎn)生一個新的對象返回。

需要注意:擴展運算符對對象實例的拷貝屬于淺拷貝

(2)數(shù)組擴展運算符

數(shù)組的擴展運算符可以將一個數(shù)組轉(zhuǎn)為用逗號分隔的參數(shù)序列,且每次只能展開一層數(shù)組。

console.log(...[1, 2, 3])
// 1 2 3
console.log(...[1, [2, 3, 4], 5])
// 1 [2, 3, 4] 5

下面是數(shù)組的擴展運算符的應(yīng)用:

  • 將數(shù)組轉(zhuǎn)換為參數(shù)序列
function add(x, y) {
  return x + y;
}
const numbers = [1, 2];
add(...numbers) // 3
  • 復(fù)制數(shù)組
const arr1 = [1, 2];
const arr2 = [...arr1];
  • 合并數(shù)組
const arr1 = ['two', 'three'];
const arr2 = ['one', ...arr1, 'four', 'five'];
// ["one", "two", "three", "four", "five"]
  • 擴展運算符與解構(gòu)賦值結(jié)合起來,用于生成數(shù)組
const [first, ...rest] = [1, 2, 3, 4, 5];
first // 1
rest  // [2, 3, 4, 5]

需要注意:如果將擴展運算符用于數(shù)組賦值,只能放在參數(shù)的最后一位,否則會報錯。

const [...rest, last] = [1, 2, 3, 4, 5];         // 報錯
const [first, ...rest, last] = [1, 2, 3, 4, 5];  // 報錯
  • 將字符串轉(zhuǎn)為真正的數(shù)組
[...'hello']    // [ "h", "e", "l", "l", "o" ]
  • 任何 Iterator 接口的對象,都可以用擴展運算符轉(zhuǎn)為真正的數(shù)組

比較常見的應(yīng)用是可以將某些數(shù)據(jù)結(jié)構(gòu)轉(zhuǎn)為數(shù)組:

// arguments對象
function foo() {
  const args = [...arguments];
}

用于替換 es5中的 Array.prototype.slice.call(arguments)寫法。

  • 使用 Math 函數(shù)獲取數(shù)組中特定的值
const numbers = [9, 4, 7, 1];
Math.min(...numbers); // 1
Math.max(...numbers); // 9

7. Proxy 可以實現(xiàn)什么功能?

在 Vue3.0 中通過 Proxy 來替換原本的 Object.defineProperty 來實現(xiàn)數(shù)據(jù)響應(yīng)式。

Proxy 是 ES6 中新增的功能,它可以用來自定義對象中的操作。

let p = new Proxy(target, handler)

target 代表需要添加代理的對象,handler 用來自定義對象中的操作,比如可以用來自定義 set 或者 get 函數(shù)。

下面來通過 Proxy 來實現(xiàn)一個數(shù)據(jù)響應(yīng)式:

let onWatch = (obj, setBind, getLogger) => {
  let handler = {
    get(target, property, receiver) {
      getLogger(target, property)
      return Reflect.get(target, property, receiver)
    },
    set(target, property, value, receiver) {
      setBind(value, property)
      return Reflect.set(target, property, value)
    }
  }
  return new Proxy(obj, handler)
}
let obj = { a: 1 }
let p = onWatch(
  obj,
  (v, property) => {
    console.log(`監(jiān)聽到屬性${property}改變?yōu)?{v}`)
  },
  (target, property) => {
    console.log(`'${property}' = ${target[property]}`)
  }
)
p.a = 2 // 監(jiān)聽到屬性a改變
p.a // 'a' = 2

在上述代碼中,通過自定義 set 和 get 函數(shù)的方式,在原本的邏輯中插入了我們的函數(shù)邏輯,實現(xiàn)了在對對象任何屬性進行讀寫時發(fā)出通知。

當(dāng)然這是簡單版的響應(yīng)式實現(xiàn),如果需要實現(xiàn)一個 Vue 中的響應(yīng)式,需要在 get 中收集依賴,在 set 派發(fā)更新,之所以 Vue3.0 要使用 Proxy 替換原本的 API 原因在于 Proxy 無需一層層遞歸為每個屬性添加代理,一次即可完成以上操作,性能上更好,并且原本的實現(xiàn)有一些數(shù)據(jù)更新不能監(jiān)聽到,但是 Proxy 可以完美監(jiān)聽到任何方式的數(shù)據(jù)改變,唯一缺陷就是瀏覽器的兼容性不好。

8. 對對象與數(shù)組的解構(gòu)的理解

解構(gòu)是 ES6 提供的一種新的提取數(shù)據(jù)的模式,這種模式能夠從對象或數(shù)組里有針對性地拿到想要的數(shù)值。

1)數(shù)組的解構(gòu)

在解構(gòu)數(shù)組時,以元素的位置為匹配條件來提取想要的數(shù)據(jù)的:

const [a, b, c] = [1, 2, 3]

最終,a、b、c分別被賦予了數(shù)組第0、1、2個索引位的值:


數(shù)組里的0、1、2索引位的元素值,精準(zhǔn)地被映射到了左側(cè)的第0、1、2個變量里去,這就是數(shù)組解構(gòu)的工作模式。還可以通過給左側(cè)變量數(shù)組設(shè)置空占位的方式,實現(xiàn)對數(shù)組中某幾個元素的精準(zhǔn)提?。?

const [a,,c] = [1,2,3]

通過把中間位留空,可以順利地把數(shù)組第一位和最后一位的值賦給 a、c 兩個變量:


2)對象的解構(gòu)

對象解構(gòu)比數(shù)組結(jié)構(gòu)稍微復(fù)雜一些,也更顯強大。在解構(gòu)對象時,是以屬性的名稱為匹配條件,來提取想要的數(shù)據(jù)的?,F(xiàn)在定義一個對象:

const stu = {
  name: 'Bob',
  age: 24
}

假如想要解構(gòu)它的兩個自有屬性,可以這樣:

const { name, age } = stu

這樣就得到了 name 和 age 兩個和 stu 平級的變量:


注意,對象解構(gòu)嚴(yán)格以屬性名作為定位依據(jù),所以就算調(diào)換了 name 和 age 的位置,結(jié)果也是一樣的:

const { age, name } = stu

9. 如何提取高度嵌套的對象里的指定屬性?

有時會遇到一些嵌套程度非常深的對象:

const school = {
   classes: {
      stu: {
         name: 'Bob',
         age: 24,
      }
   }
}

像此處的 name 這個變量,嵌套了四層,此時如果仍然嘗試?yán)戏椒▉硖崛∷?

const { name } = school

顯然是不奏效的,因為 school 這個對象本身是沒有 name 這個屬性的,name 位于 school 對象的“兒子的兒子”對象里面。要想把 name 提取出來,一種比較笨的方法是逐層解構(gòu):

const { classes } = school
const { stu } = classes
const { name } = stu
name // 'Bob'

但是還有一種更標(biāo)準(zhǔn)的做法,可以用一行代碼來解決這個問題:

const { classes: { stu: { name } }} = school
     
console.log(name)  // 'Bob'

可以在解構(gòu)出來的變量名右側(cè),通過冒號+{目標(biāo)屬性名}這種形式,進一步解構(gòu)它,一直解構(gòu)到拿到目標(biāo)數(shù)據(jù)為止。

10. 對 rest 參數(shù)的理解

擴展運算符被用在函數(shù)形參上時,它還可以把一個分離的參數(shù)序列整合成一個數(shù)組

function mutiple(...args) {
  let result = 1;
  for (var val of args) {
    result *= val;
  }
  return result;
}
mutiple(1, 2, 3, 4) // 24

這里,傳入 mutiple 的是四個分離的參數(shù),但是如果在 mutiple 函數(shù)里嘗試輸出 args 的值,會發(fā)現(xiàn)它是一個數(shù)組:

function mutiple(...args) {
  console.log(args)
}
mutiple(1, 2, 3, 4) // [1, 2, 3, 4]

這就是 … rest運算符的又一層威力了,它可以把函數(shù)的多個入?yún)⑹諗窟M一個數(shù)組里。這一點經(jīng)常用于獲取函數(shù)的多余參數(shù),或者像上面這樣處理函數(shù)參數(shù)個數(shù)不確定的情況。

11. ES6中模板語法與字符串處理

ES6 提出了“模板語法”的概念。在 ES6 以前,拼接字符串是很麻煩的事情:

var name = 'css'   
var career = 'coder' 
var hobby = ['coding', 'writing']
var finalString = 'my name is ' + name + ', I work as a ' + career + ', I love ' + hobby[0] + ' and ' + hobby[1]

僅僅幾個變量,寫了這么多加號,還要時刻小心里面的空格和標(biāo)點符號有沒有跟錯地方。但是有了模板字符串,拼接難度直線下降:

var name = 'css'   
var career = 'coder' 
var hobby = ['coding', 'writing']
var finalString = `my name is ${name}, I work as a ${career} I love ${hobby[0]} and ${hobby[1]}`

字符串不僅更容易拼了,也更易讀了,代碼整體的質(zhì)量都變高了。這就是模板字符串的第一個優(yōu)勢——允許用${}的方式嵌入變量。但這還不是問題的關(guān)鍵,模板字符串的關(guān)鍵優(yōu)勢有兩個:

  • 在模板字符串中,空格、縮進、換行都會被保留
  • 模板字符串完全支持“運算”式的表達式,可以在${}里完成一些計算

基于第一點,可以在模板字符串里無障礙地直接寫 html 代碼:

let list = `
    <ul>
        <li>列表項1</li>
        <li>列表項2</li>
    </ul>
`;
console.log(message); // 正確輸出,不存在報錯

基于第二點,可以把一些簡單的計算和調(diào)用丟進 ${} 來做:

function add(a, b) {
  const finalString = `${a} + $ = ${a+b}`
  console.log(finalString)
}
add(1, 2) // 輸出 '1 + 2 = 3'

除了模板語法外, ES6中還新增了一系列的字符串方法用于提升開發(fā)效率:

  • 存在性判定:在過去,當(dāng)判斷一個字符/字符串是否在某字符串中時,只能用 indexOf > -1 來做?,F(xiàn)在 ES6 提供了三個方法:includes、startsWith、endsWith,它們都會返回一個布爾值來告訴你是否存在。
  • includes:判斷字符串與子串的包含關(guān)系:
const son = 'haha' 
const father = 'xixi haha hehe'
father.includes(son) // true
  • startsWith:判斷字符串是否以某個/某串字符開頭:
const father = 'xixi haha hehe'
father.startsWith('haha') // false
father.startsWith('xixi') // true
  • endsWith:判斷字符串是否以某個/某串字符結(jié)尾:
const father = 'xixi haha hehe'
father.endsWith('hehe') // true
  • 自動重復(fù):可以使用 repeat 方法來使同一個字符串輸出多次(被連續(xù)復(fù)制多次):
const sourceCode = 'repeat for 3 times;'
const repeated = sourceCode.repeat(3) 
console.log(repeated) // repeat for 3 times;repeat for 3 times;repeat for 3 times;

三、JavaScript基礎(chǔ)


1. new操作符的實現(xiàn)原理

new操作符的執(zhí)行過程:

(1)首先創(chuàng)建了一個新的空對象

(2)設(shè)置原型,將對象的原型設(shè)置為函數(shù)的 prototype 對象。

(3)讓函數(shù)的 this 指向這個對象,執(zhí)行構(gòu)造函數(shù)的代碼(為這個新對象添加屬性)

(4)判斷函數(shù)的返回值類型,如果是值類型,返回創(chuàng)建的對象。如果是引用類型,就返回這個引用類型的對象。

具體實現(xiàn):

function objectFactory() {
  let newObject = null;
  let constructor = Array.prototype.shift.call(arguments);
  let result = null;
  // 判斷參數(shù)是否是一個函數(shù)
  if (typeof constructor !== "function") {
    console.error("type error");
    return;
  }
  // 新建一個空對象,對象的原型為構(gòu)造函數(shù)的 prototype 對象
  newObject = Object.create(constructor.prototype);
  // 將 this 指向新建對象,并執(zhí)行函數(shù)
  result = constructor.apply(newObject, arguments);
  // 判斷返回對象
  let flag = result && (typeof result === "object" || typeof result === "function");
  // 判斷返回結(jié)果
  return flag ? result : newObject;
}
// 使用方法
objectFactory(構(gòu)造函數(shù), 初始化參數(shù));

2. map和Object的區(qū)別

Map Object
意外的鍵 Map默認情況不包含任何鍵,只包含顯式插入的鍵。 Object 有一個原型, 原型鏈上的鍵名有可能和自己在對象上的設(shè)置的鍵名產(chǎn)生沖突。
鍵的類型 Map的鍵可以是任意值,包括函數(shù)、對象或任意基本類型。 Object 的鍵必須是 String 或是Symbol。
鍵的順序 Map 中的 key 是有序的。因此,當(dāng)?shù)臅r候, Map 對象以插入的順序返回鍵值。 Object 的鍵是無序的
Size Map 的鍵值對個數(shù)可以輕易地通過size 屬性獲取 Object 的鍵值對個數(shù)只能手動計算
迭代 Map 是 iterable 的,所以可以直接被迭代。 迭代Object需要以某種方式獲取它的鍵然后才能迭代。
性能 在頻繁增刪鍵值對的場景下表現(xiàn)更好。 在頻繁添加和刪除鍵值對的場景下未作出優(yōu)化。

3. map和weakMap的區(qū)別

(1)Map

map本質(zhì)上就是鍵值對的集合,但是普通的Object中的鍵值對中的鍵只能是字符串。而ES6提供的Map數(shù)據(jù)結(jié)構(gòu)類似于對象,但是它的鍵不限制范圍,可以是任意類型,是一種更加完善的Hash結(jié)構(gòu)。如果Map的鍵是一個原始數(shù)據(jù)類型,只要兩個鍵嚴(yán)格相同,就視為是同一個鍵。

實際上Map是一個數(shù)組,它的每一個數(shù)據(jù)也都是一個數(shù)組,其形式如下:

const map = [
     ["name","張三"],
     ["age",18],
]

Map數(shù)據(jù)結(jié)構(gòu)有以下操作方法:

  • size: ?map.size? 返回Map結(jié)構(gòu)的成員總數(shù)。
  • set(key,value):設(shè)置鍵名key對應(yīng)的鍵值value,然后返回整個Map結(jié)構(gòu),如果key已經(jīng)有值,則鍵值會被更新,否則就新生成該鍵。(因為返回的是當(dāng)前Map對象,所以可以鏈?zhǔn)秸{(diào)用)
  • get(key):該方法讀取key對應(yīng)的鍵值,如果找不到key,返回undefined。
  • has(key):該方法返回一個布爾值,表示某個鍵是否在當(dāng)前Map對象中。
  • delete(key):該方法刪除某個鍵,返回true,如果刪除失敗,返回false。
  • clear():map.clear()清除所有成員,沒有返回值。

Map結(jié)構(gòu)原生提供是三個遍歷器生成函數(shù)和一個遍歷方法

  • keys():返回鍵名的遍歷器。
  • values():返回鍵值的遍歷器。
  • entries():返回所有成員的遍歷器。
  • forEach():遍歷Map的所有成員。
const map = new Map([
     ["foo",1],
     ["bar",2],
])
for(let key of map.keys()){
    console.log(key);  // foo bar
}
for(let value of map.values()){
     console.log(value); // 1 2
}
for(let items of map.entries()){
    console.log(items);  // ["foo",1]  ["bar",2]
}
map.forEach( (value,key,map) => {
     console.log(key,value); // foo 1    bar 2
})

(2)WeakMap

WeakMap 對象也是一組鍵值對的集合,其中的鍵是弱引用的。其鍵必須是對象,原始數(shù)據(jù)類型不能作為key值,而值可以是任意的。

該對象也有以下幾種方法:

  • set(key,value):設(shè)置鍵名key對應(yīng)的鍵值value,然后返回整個Map結(jié)構(gòu),如果key已經(jīng)有值,則鍵值會被更新,否則就新生成該鍵。(因為返回的是當(dāng)前Map對象,所以可以鏈?zhǔn)秸{(diào)用)
  • get(key):該方法讀取key對應(yīng)的鍵值,如果找不到key,返回undefined。
  • has(key):該方法返回一個布爾值,表示某個鍵是否在當(dāng)前Map對象中。
  • delete(key):該方法刪除某個鍵,返回true,如果刪除失敗,返回false。

其clear()方法已經(jīng)被棄用,所以可以通過創(chuàng)建一個空的WeakMap并替換原對象來實現(xiàn)清除。

WeakMap的設(shè)計目的在于,有時想在某個對象上面存放一些數(shù)據(jù),但是這會形成對于這個對象的引用。一旦不再需要這兩個對象,就必須手動刪除這個引用,否則垃圾回收機制就不會釋放對象占用的內(nèi)存。

而WeakMap的鍵名所引用的對象都是弱引用,即垃圾回收機制不將該引用考慮在內(nèi)。因此,只要所引用的對象的其他引用都被清除,垃圾回收機制就會釋放該對象所占用的內(nèi)存。也就是說,一旦不再需要,WeakMap 里面的鍵名對象和所對應(yīng)的鍵值對會自動消失,不用手動刪除引用

總結(jié):

  • Map 數(shù)據(jù)結(jié)構(gòu)。它類似于對象,也是鍵值對的集合,但是“鍵”的范圍不限于字符串,各種類型的值(包括對象)都可以當(dāng)作鍵。
  • WeakMap 結(jié)構(gòu)與 Map 結(jié)構(gòu)類似,也是用于生成鍵值對的集合。但是 WeakMap 只接受對象作為鍵名( null 除外),不接受其他類型的值作為鍵名。而且 WeakMap 的鍵名所指向的對象,不計入垃圾回收機制。

4. JavaScript有哪些內(nèi)置對象

全局的對象( global objects )或稱標(biāo)準(zhǔn)內(nèi)置對象,不要和 "全局對象(global object)" 混淆。這里說的全局的對象是說在

全局作用域里的對象。全局作用域中的其他對象可以由用戶的腳本創(chuàng)建或由宿主程序提供。

標(biāo)準(zhǔn)內(nèi)置對象的分類:

(1)值屬性,這些全局屬性返回一個簡單值,這些值沒有自己的屬性和方法。

例如 Infinity、NaN、undefined、null 字面量

(2)函數(shù)屬性,全局函數(shù)可以直接調(diào)用,不需要在調(diào)用時指定所屬對象,執(zhí)行結(jié)束后會將結(jié)果直接返回給調(diào)用者。

例如 eval()、parseFloat()、parseInt() 等

(3)基本對象,基本對象是定義或使用其他對象的基礎(chǔ)?;緦ο蟀ㄒ话銓ο?、函數(shù)對象和錯誤對象。

例如 Object、Function、Boolean、Symbol、Error 等

(4)數(shù)字和日期對象,用來表示數(shù)字、日期和執(zhí)行數(shù)學(xué)計算的對象。

例如 Number、Math、Date

(5)字符串,用來表示和操作字符串的對象。

例如 String、RegExp

(6)可索引的集合對象,這些對象表示按照索引值來排序的數(shù)據(jù)集合,包括數(shù)組和類型數(shù)組,以及類數(shù)組結(jié)構(gòu)的對象。例如 Array

(7)使用鍵的集合對象,這些集合對象在存儲數(shù)據(jù)時會使用到鍵,支持按照插入順序來迭代元素。

例如 Map、Set、WeakMap、WeakSet

(8)矢量集合,SIMD 矢量集合中的數(shù)據(jù)會被組織為一個數(shù)據(jù)序列。

例如 SIMD 等

(9)結(jié)構(gòu)化數(shù)據(jù),這些對象用來表示和操作結(jié)構(gòu)化的緩沖區(qū)數(shù)據(jù),或使用 JSON 編碼的數(shù)據(jù)。

例如 JSON 等

(10)控制抽象對象

例如 Promise、Generator 等

(11)反射

例如 Reflect、Proxy

(12)國際化,為了支持多語言處理而加入 ECMAScript 的對象。

例如 Intl、Intl.Collator 等

(13)WebAssembly

(14)其他

例如 arguments

總結(jié):

js 中的內(nèi)置對象主要指的是在程序執(zhí)行前存在全局作用域里的由 js 定義的一些全局值屬性、函數(shù)和用來實例化其他對象的構(gòu)造函數(shù)對象。一般經(jīng)常用到的如全局變量值 NaN、undefined,全局函數(shù)如 parseInt()、parseFloat() 用來實例化對象的構(gòu)造函數(shù)如 Date、Object 等,還有提供數(shù)學(xué)計算的單體內(nèi)置對象如 Math 對象。

5. 常用的正則表達式有哪些?

// (1)匹配 16 進制顏色值
var regex = /#([0-9a-fA-F]{6}|[0-9a-fA-F]{3})/g;

// (2)匹配日期,如 yyyy-mm-dd 格式
var regex = /^[0-9]{4}-(0[1-9]|1[0-2])-(0[1-9]|[12][0-9]|3[01])$/;

// (3)匹配 qq 號
var regex = /^[1-9][0-9]{4,10}$/g;

// (4)手機號碼正則
var regex = /^1[34578]\d{9}$/g;

// (5)用戶名正則
var regex = /^[a-zA-Z\$][a-zA-Z0-9_\$]{4,16}$/;

6. 對JSON的理解

JSON 是一種基于文本的輕量級的數(shù)據(jù)交換格式。它可以被任何的編程語言讀取和作為數(shù)據(jù)格式來傳遞。

在項目開發(fā)中,使用 JSON 作為前后端數(shù)據(jù)交換的方式。在前端通過將一個符合 JSON 格式的數(shù)據(jù)結(jié)構(gòu)序列化為

JSON 字符串,然后將它傳遞到后端,后端通過 JSON 格式的字符串解析后生成對應(yīng)的數(shù)據(jù)結(jié)構(gòu),以此來實現(xiàn)前后端數(shù)據(jù)的一個傳遞。

因為 JSON 的語法是基于 js 的,因此很容易將 JSON 和 js 中的對象弄混,但是應(yīng)該注意的是 JSON 和 js 中的對象不是一回事,JSON 中對象格式更加嚴(yán)格,比如說在 JSON 中屬性值不能為函數(shù),不能出現(xiàn) NaN 這樣的屬性值等,因此大多數(shù)的 js 對象是不符合 JSON 對象的格式的。

在 js 中提供了兩個函數(shù)來實現(xiàn) js 數(shù)據(jù)結(jié)構(gòu)和 JSON 格式的轉(zhuǎn)換處理,

  • JSON.stringify 函數(shù),通過傳入一個符合 JSON 格式的數(shù)據(jù)結(jié)構(gòu),將其轉(zhuǎn)換為一個 JSON 字符串。如果傳入的數(shù)據(jù)結(jié)構(gòu)不符合 JSON 格式,那么在序列化的時候會對這些值進行對應(yīng)的特殊處理,使其符合規(guī)范。在前端向后端發(fā)送數(shù)據(jù)時,可以調(diào)用這個函數(shù)將數(shù)據(jù)對象轉(zhuǎn)化為 JSON 格式的字符串。
  • JSON.parse() 函數(shù),這個函數(shù)用來將 JSON 格式的字符串轉(zhuǎn)換為一個 js 數(shù)據(jù)結(jié)構(gòu),如果傳入的字符串不是標(biāo)準(zhǔn)的 JSON 格式的字符串的話,將會拋出錯誤。當(dāng)從后端接收到 JSON 格式的字符串時,可以通過這個方法來將其解析為一個 js 數(shù)據(jù)結(jié)構(gòu),以此來進行數(shù)據(jù)的訪問。

7. JavaScript腳本延遲加載的方式有哪些?

延遲加載就是等頁面加載完成之后再加載 JavaScript 文件。 js 延遲加載有助于提高頁面加載速度。

一般有以下幾種方式:

  • defer 屬性:給 js 腳本添加 defer 屬性,這個屬性會讓腳本的加載與文檔的解析同步解析,然后在文檔解析完成后再執(zhí)行這個腳本文件,這樣的話就能使頁面的渲染不被阻塞。多個設(shè)置了 defer 屬性的腳本按規(guī)范來說最后是順序執(zhí)行的,但是在一些瀏覽器中可能不是這樣。
  • async 屬性:給 js 腳本添加 async 屬性,這個屬性會使腳本異步加載,不會阻塞頁面的解析過程,但是當(dāng)腳本加載完成后立即執(zhí)行 js 腳本,這個時候如果文檔沒有解析完成的話同樣會阻塞。多個 async 屬性的腳本的執(zhí)行順序是不可預(yù)測的,一般不會按照代碼的順序依次執(zhí)行。
  • 動態(tài)創(chuàng)建 DOM 方式:動態(tài)創(chuàng)建 DOM 標(biāo)簽的方式,可以對文檔的加載事件進行監(jiān)聽,當(dāng)文檔加載完成后再動態(tài)的創(chuàng)建 script 標(biāo)簽來引入 js 腳本。
  • 使用 setTimeout 延遲方法:設(shè)置一個定時器來延遲加載js腳本文件
  • 讓 JS 最后加載:將 js 腳本放在文檔的底部,來使 js 腳本盡可能的在最后來加載執(zhí)行。

8. JavaScript 類數(shù)組對象的定義?

一個擁有 length 屬性和若干索引屬性的對象就可以被稱為類數(shù)組對象,類數(shù)組對象和數(shù)組類似,但是不能調(diào)用數(shù)組的方法。常見的類數(shù)組對象有 arguments 和 DOM 方法的返回結(jié)果,還有一個函數(shù)也可以被看作是類數(shù)組對象,因為它含有 length 屬性值,代表可接收的參數(shù)個數(shù)。

常見的類數(shù)組轉(zhuǎn)換為數(shù)組的方法有這樣幾種:

(1)通過 call 調(diào)用數(shù)組的 slice 方法來實現(xiàn)轉(zhuǎn)換

Array.prototype.slice.call(arrayLike);

(2)通過 call 調(diào)用數(shù)組的 splice 方法來實現(xiàn)轉(zhuǎn)換

Array.prototype.splice.call(arrayLike, 0);

(3)通過 apply 調(diào)用數(shù)組的 concat 方法來實現(xiàn)轉(zhuǎn)換

Array.prototype.concat.apply([], arrayLike);

(4)通過 Array.from 方法來實現(xiàn)轉(zhuǎn)換

Array.from(arrayLike);

9. 數(shù)組有哪些原生方法?

  • 數(shù)組和字符串的轉(zhuǎn)換方法:toString()、toLocalString()、join() 其中 join() 方法可以指定轉(zhuǎn)換為字符串時的分隔符。
  • 數(shù)組尾部操作的方法 pop() 和 push(),push 方法可以傳入多個參數(shù)。
  • 數(shù)組首部操作的方法 shift() 和 unshift() 重排序的方法 reverse() 和 sort(),sort() 方法可以傳入一個函數(shù)來進行比較,傳入前后兩個值,如果返回值為正數(shù),則交換兩個參數(shù)的位置。
  • 數(shù)組連接的方法 concat() ,返回的是拼接好的數(shù)組,不影響原數(shù)組。
  • 數(shù)組截取辦法 slice(),用于截取數(shù)組中的一部分返回,不影響原數(shù)組。
  • 數(shù)組插入方法 splice(),影響原數(shù)組查找特定項的索引的方法,indexOf() 和 lastIndexOf() 迭代方法 every()、some()、filter()、map() 和 forEach() 方法
  • 數(shù)組歸并方法 reduce() 和 reduceRight() 方法

10. Unicode、UTF-8、UTF-16、UTF-32的區(qū)別?

(1)Unicode

在說 Unicode之前需要先了解一下 ASCII碼:ASCII 碼(American Standard Code for Information Interchange)稱為美國標(biāo)準(zhǔn)信息交換碼。

  • 它是基于拉丁字母的一套電腦編碼系統(tǒng)。
  • 它定義了一個用于代表常見字符的字典。
  • 它包含了"A-Z"(包含大小寫),數(shù)據(jù)"0-9" 以及一些常見的符號。
  • 它是專門為英語而設(shè)計的,有128個編碼,對其他語言無能為力

ASCII碼可以表示的編碼有限,要想表示其他語言的編碼,還是要使用 Unicode來表示,可以說 Unicode是 ASCII 的超集。

Unicode全稱 Unicode Translation Format,又叫做統(tǒng)一碼、萬國碼、單一碼。Unicode 是為了解決傳統(tǒng)的字符編碼方案的局限而產(chǎn)生的,它為每種語言中的每個字符設(shè)定了統(tǒng)一并且唯一的二進制編碼,以滿足跨語言、跨平臺進行文本轉(zhuǎn)換、處理的要求。

Unicode的實現(xiàn)方式(也就是編碼方式)有很多種,常見的是UTF-8UTF-16、UTF-32USC-2。

(2)UTF-8

UTF-8是使用最廣泛的 Unicode編碼方式,它是一種可變長的編碼方式,可以是1—4個字節(jié)不等,它可以完全兼容 ASCII碼的128個字符。

注意: UTF-8 是一種編碼方式,Unicode是一個字符集合。

UTF-8的編碼規(guī)則:

  • 對于單字節(jié)的符號,字節(jié)的第一位為0,后面的7位為這個字符的 Unicode編碼,因此對于英文字母,它的 Unicode編碼和 ACSII編碼一樣。
  • 對于n字節(jié)的符號,第一個字節(jié)的前n位都是1,第n+1位設(shè)為0,后面字節(jié)的前兩位一律設(shè)為10,剩下的沒有提及的二進制位,全部為這個符號的 Unicode碼 。

來看一下具體的 Unicode編號范圍與對應(yīng)的 UTF-8二進制格式 :

編碼范圍(編號對應(yīng)的十進制數(shù)) 二進制格式
0x00—0x7F (0-127) 0xxxxxxx
0x80—0x7FF (128-2047) 110xxxxx 10xxxxxx
0x800—0xFFFF (2048-65535) 1110xxxx 10xxxxxx 10xxxxxx
0x10000—0x10FFFF (65536以上) 11110xxx 10xxxxxx 10xxxxxx 10xxxxxx

那該如何通過具體的 Unicode編碼,進行具體的 UTF-8編碼呢?步驟如下:

  • 找到該 ?Unicode?編碼的所在的編號范圍,進而找到與之對應(yīng)的二進制格式
  • 將 ?Unicode?編碼轉(zhuǎn)換為二進制數(shù)(去掉最高位的0)
  • 將二進制數(shù)從右往左一次填入二進制格式的 ?X?中,如果有 ?X?未填,就設(shè)為0

來看一個實際的例子:

” 字的 Unicode編碼是:0x9A6C,整數(shù)編號是 39532

(1)首選確定了該字符在第三個范圍內(nèi),它的格式是 1110xxxx 10xxxxxx 10xxxxxx

(2)39532對應(yīng)的二進制數(shù)為 1001 1010 0110 1100

(3)將二進制數(shù)填入X中,結(jié)果是:11101001 10101001 10101100

(3)UTF-16

1. 平面的概念

在了解 UTF-16之前,先看一下平面的概念:

Unicode編碼中有很多很多的字符,它并不是一次性定義的,而是分區(qū)進行定義的,每個區(qū)存放65536(216)個字符,這稱為一個平面,目前總共有17 個平面。

最前面的一個平面稱為基本平面,它的碼點從0 — 216-1,寫成16進制就是 U+0000 — U+FFFF,那剩下的16個平面就是輔助平面,碼點范圍是 U+10000—U+10FFFF。

2. UTF-16 概念:

UTF-16也是 Unicode編碼集的一種編碼形式,把 Unicode字符集的抽象碼位映射為16位長的整數(shù)(即碼元)的序列,用于數(shù)據(jù)存儲或傳遞。Unicode字符的碼位需要1個或者2個16位長的碼元來表示,因此 UTF-16也是用變長字節(jié)表示的。

3. UTF-16 編碼規(guī)則:

  • 編號在 ?U+0000—U+FFFF? 的字符(常用字符集),直接用兩個字節(jié)表示。
  • 編號在 ?U+10000—U+10FFFF? 之間的字符,需要用四個字節(jié)表示。

4. 編碼識別

那么問題來了,當(dāng)遇到兩個字節(jié)時,怎么知道是把它當(dāng)做一個字符還是和后面的兩個字節(jié)一起當(dāng)做一個字符呢?

UTF-16 編碼肯定也考慮到了這個問題,在基本平面內(nèi),從 U+D800 — U+DFFF 是一個空段,也就是說這個區(qū)間的碼點不對應(yīng)任何的字符,因此這些空段就可以用來映射輔助平面的字符。

輔助平面共有 220 個字符位,因此表示這些字符至少需要 20 個二進制位。UTF-16 將這 20 個二進制位分成兩半,前 10 位映射在 U+D800 — U+DBFF,稱為高位(H),后 10 位映射在 U+DC00 — U+DFFF,稱為低位(L)。這就相當(dāng)于,將一個輔助平面的字符拆成了兩個基本平面的字符來表示。

因此,當(dāng)遇到兩個字節(jié)時,發(fā)現(xiàn)它的碼點在 U+D800 —U+DBFF之間,就可以知道,它后面的兩個字節(jié)的碼點應(yīng)該在 U+DC00 — U+DFFF 之間,這四個字節(jié)必須放在一起進行解讀。

5. 舉例說明

以 "" 字為例,它的 Unicode 碼點為 0x21800,該碼點超出了基本平面的范圍,因此需要用四個字節(jié)來表示,步驟如下:

  • 首先計算超出部分的結(jié)果:?0x21800 - 0x10000 ?
  • 將上面的計算結(jié)果轉(zhuǎn)為20位的二進制數(shù),不足20位就在前面補0,結(jié)果為:?0001000110 0000000000 ?
  • 將得到的兩個10位二進制數(shù)分別對應(yīng)到兩個區(qū)間中
  • ?U+D800? 對應(yīng)的二進制數(shù)為 ?1101100000000000?, 將 ?0001000110?填充在它的后10 個二進制位,得到 ?1101100001000110?,轉(zhuǎn)成 16 進制數(shù)為 ?0xD846?。同理,低位為 ?0xDC00?,所以這個字的 ?UTF-16? 編碼為 ?0xD846 0xDC00?

(4) UTF-32

UTF-32 就是字符所對應(yīng)編號的整數(shù)二進制形式,每個字符占四個字節(jié),這個是直接進行轉(zhuǎn)換的。該編碼方式占用的儲存空間較多,所以使用較少。

比如“” 字的Unicode編號是:U+9A6C,整數(shù)編號是 39532,直接轉(zhuǎn)化為二進制:1001 1010 0110 1100,這就是它的UTF-32編碼。

(5)總結(jié)

Unicode、UTF-8、UTF-16、UTF-32有什么區(qū)別?

  • ?Unicode ?是編碼字符集(字符集),而 ?UTF-8?、?UTF-16?、?UTF-32?是字符集編碼(編碼規(guī)則);
  • ?UTF-16? 使用變長碼元序列的編碼方式,相較于定長碼元序列的 ?UTF-32?算法更復(fù)雜,甚至比同樣是變長碼元序列的 ?UTF-8?也更為復(fù)雜,因為其引入了獨特的代理對這樣的代理機制;
  • ?UTF-8?需要判斷每個字節(jié)中的開頭標(biāo)志信息,所以如果某個字節(jié)在傳送過程中出錯了,就會導(dǎo)致后面的字節(jié)也會解析出錯;而 ?UTF-16?不會判斷開頭標(biāo)志,即使錯也只會錯一個字符,所以容錯能力教強;
  • 如果字符內(nèi)容全部英文或英文與其他文字混合,但英文占絕大部分,那么用 ?UTF-8?就比 ?UTF-16?節(jié)省了很多空間;而如果字符內(nèi)容全部是中文這樣類似的字符或者混合字符中中文占絕大多數(shù),那么 ?UTF-16?就占優(yōu)勢了,可以節(jié)省很多空間;

11. 常見的位運算符有哪些?其計算規(guī)則是什么?

現(xiàn)代計算機中數(shù)據(jù)都是以二進制的形式存儲的,即0、1兩種狀態(tài),計算機對二進制數(shù)據(jù)進行的運算加減乘除等都是叫位運算,即將符號位共同參與運算的運算。

常見的位運算有以下幾種:

運算符 描述 運算規(guī)則
& 兩個位都為1時,結(jié)果才為1
` `
^ 異或 兩個位相同為0,相異為1
~ 取反 0變1,1變0
<< 左移 各二進制位全部左移若干位,高位丟棄,低位補0
>> 右移 各二進制位全部右移若干位,正數(shù)左補0,負數(shù)左補1,右邊丟棄

1. 按位與運算符(&)

定義: 參加運算的兩個數(shù)據(jù)按二進制位進行“與”運算。

運算規(guī)則:

0 & 0 = 0  
0 & 1 = 0  
1 & 0 = 0  
1 & 1 = 1

總結(jié):兩位同時為1,結(jié)果才為1,否則結(jié)果為0。

例如:3&5 即:

0000 0011 
   0000 0101 
 = 0000 0001

因此 3&5 的值為1。

注意:負數(shù)按補碼形式參加按位與運算。

用途:

(1)判斷奇偶

只要根據(jù)最未位是0還是1來決定,為0就是偶數(shù),為1就是奇數(shù)。因此可以用 if ((i & 1) == 0)代替 if (i % 2 == 0)來判斷a是不是偶數(shù)。

(2)清零

如果想將一個單元清零,即使其全部二進制位為0,只要與一個各位都為零的數(shù)值相與,結(jié)果為零。

2. 按位或運算符(|)

定義: 參加運算的兩個對象按二進制位進行“或”運算。

運算規(guī)則:

0 | 0 = 0
0 | 1 = 1  
1 | 0 = 1  
1 | 1 = 1

總結(jié):參加運算的兩個對象只要有一個為1,其值為1。

例如:3|5即:

0000 0011
  0000 0101 
= 0000 0111

因此,3|5的值為7。

注意:負數(shù)按補碼形式參加按位或運算。

3. 異或運算符(^)

定義: 參加運算的兩個數(shù)據(jù)按二進制位進行“異或”運算。

運算規(guī)則:

0 ^ 0 = 0  
0 ^ 1 = 1  
1 ^ 0 = 1  
1 ^ 1 = 0

總結(jié):參加運算的兩個對象,如果兩個相應(yīng)位相同為0,相異為1。

例如:3|5即:

0000 0011
  0000 0101 
= 0000 0110

因此,3^5的值為6。

異或運算的性質(zhì):

  • 交換律:?(a^b)^c == a^(b^c) ?
  • 結(jié)合律:?(a + b)^c == a^b + b^c ?
  • 對于任何數(shù)x,都有 ?x^x=0,x^0=x ?
  • 自反性: ?a^b^b=a^0=a;?

4. 取反運算符 (~)

定義: 參加運算的一個數(shù)據(jù)按二進制進行“取反”運算。

運算規(guī)則:

~ 1 = 0
~ 0 = 1

總結(jié):對一個二進制數(shù)按位取反,即將0變1,1變0。

例如:~6 即:

0000 0110
= 1111 1001

在計算機中,正數(shù)用原碼表示,負數(shù)使用補碼存儲,首先看最高位,最高位1表示負數(shù),0表示正數(shù)。此計算機二進制碼為負數(shù),最高位為符號位。

當(dāng)發(fā)現(xiàn)按位取反為負數(shù)時,就直接取其補碼,變?yōu)槭M制:

0000 0110
   = 1111 1001
反碼:1000 0110
補碼:1000 0111

因此,~6的值為-7。

5. 左移運算符(<<)

定義: 將一個運算對象的各二進制位全部左移若干位,左邊的二進制位丟棄,右邊補0。

設(shè) a=1010 1110,a = a<< 2 將a的二進制位左移2位、右補0,即得a=1011 1000。

若左移時舍棄的高位不包含1,則每左移一位,相當(dāng)于該數(shù)乘以2。

6. 右移運算符(>>)

定義: 將一個數(shù)的各二進制位全部右移若干位,正數(shù)左補0,負數(shù)左補1,右邊丟棄。

例如:a=a>>2 將a的二進制位右移2位,左補0 或者 左補1得看被移數(shù)是正還是負。

操作數(shù)每右移一位,相當(dāng)于該數(shù)除以2。

7. 原碼、補碼、反碼

上面提到了補碼、反碼等知識,這里就補充一下。

計算機中的有符號數(shù)有三種表示方法,即原碼、反碼和補碼。三種表示方法均有符號位和數(shù)值位兩部分,符號位都是用0表示“正”,用1表示“負”,而數(shù)值位,三種表示方法各不相同。

(1)原碼

原碼就是一個數(shù)的二進制數(shù)。

例如:10的原碼為0000 1010

(2)反碼

  • 正數(shù)的反碼與原碼相同,如:10 反碼為 0000 1010
  • 負數(shù)的反碼為除符號位,按位取反,即0變1,1變0。

例如:-10

原碼:1000 1010
反碼:1111 0101

(3)補碼

  • 正數(shù)的補碼與原碼相同,如:10 補碼為 0000 1010
  • 負數(shù)的補碼是原碼除符號位外的所有位取反即0變1,1變0,然后加1,也就是反碼加1。

例如:-10

原碼:1000 1010
反碼:1111 0101
補碼:1111 0110

12. 為什么函數(shù)的 arguments 參數(shù)是類數(shù)組而不是數(shù)組?如何遍歷類數(shù)組?

arguments是一個對象,它的屬性是從 0 開始依次遞增的數(shù)字,還有 callee和 length等屬性,與數(shù)組相似;但是它卻沒有數(shù)組常見的方法屬性,如 forEachreduce等,所以叫它們類數(shù)組。

要遍歷類數(shù)組,有三個方法:

(1)將數(shù)組的方法應(yīng)用到類數(shù)組上,這時候就可以使用 call和 apply方法,如:

function foo(){ 
  Array.prototype.forEach.call(arguments, a => console.log(a))
}

(2)使用Array.from方法將類數(shù)組轉(zhuǎn)化成數(shù)組:?

function foo(){ 
  const arrArgs = Array.from(arguments) 
  arrArgs.forEach(a => console.log(a))
}

(3)使用展開運算符將類數(shù)組轉(zhuǎn)化成數(shù)組

function foo(){ 
    const arrArgs = [...arguments] 
    arrArgs.forEach(a => console.log(a)) 
}

13. 什么是 DOM 和 BOM?

  • DOM 指的是文檔對象模型,它指的是把文檔當(dāng)做一個對象,這個對象主要定義了處理網(wǎng)頁內(nèi)容的方法和接口。
  • BOM 指的是瀏覽器對象模型,它指的是把瀏覽器當(dāng)做一個對象來對待,這個對象主要定義了與瀏覽器進行交互的法和接口。BOM的核心是 window,而 window 對象具有雙重角色,它既是通過 js 訪問瀏覽器窗口的一個接口,又是一個 Global(全局)對象。這意味著在網(wǎng)頁中定義的任何對象,變量和函數(shù),都作為全局對象的一個屬性或者方法存在。window 對象含有 location 對象、navigator 對象、screen 對象等子對象,并且 DOM 的最根本的對象 document 對象也是 BOM 的 window 對象的子對象。

14. 對類數(shù)組對象的理解,如何轉(zhuǎn)化為數(shù)組

一個擁有 length 屬性和若干索引屬性的對象就可以被稱為類數(shù)組對象,類數(shù)組對象和數(shù)組類似,但是不能調(diào)用數(shù)組的方法。常見的類數(shù)組對象有 arguments 和 DOM 方法的返回結(jié)果,函數(shù)參數(shù)也可以被看作是類數(shù)組對象,因為它含有 length屬性值,代表可接收的參數(shù)個數(shù)。

常見的類數(shù)組轉(zhuǎn)換為數(shù)組的方法有這樣幾種:

  • 通過 call 調(diào)用數(shù)組的 slice 方法來實現(xiàn)轉(zhuǎn)換
Array.prototype.slice.call(arrayLike);
  • 通過 call 調(diào)用數(shù)組的 splice 方法來實現(xiàn)轉(zhuǎn)換
Array.prototype.splice.call(arrayLike, 0);
  • 通過 apply 調(diào)用數(shù)組的 concat 方法來實現(xiàn)轉(zhuǎn)換
Array.prototype.concat.apply([], arrayLike);
  • 通過 Array.from 方法來實現(xiàn)轉(zhuǎn)換
Array.from(arrayLike);

15. escape、encodeURI、encodeURIComponent 的區(qū)別

  • encodeURI 是對整個 URI 進行轉(zhuǎn)義,將 URI 中的非法字符轉(zhuǎn)換為合法字符,所以對于一些在 URI 中有特殊意義的字符不會進行轉(zhuǎn)義。
  • encodeURIComponent 是對 URI 的組成部分進行轉(zhuǎn)義,所以一些特殊字符也會得到轉(zhuǎn)義。
  • escape 和 encodeURI 的作用相同,不過它們對于 unicode 編碼為 0xff 之外字符的時候會有區(qū)別,escape 是直接在字符的 unicode 編碼前加上 %u,而 encodeURI 首先會將字符轉(zhuǎn)換為 UTF-8 的格式,再在每個字節(jié)前加上 %。

16. 對AJAX的理解,實現(xiàn)一個AJAX請求

AJAX是 Asynchronous JavaScript and XML 的縮寫,指的是通過 JavaScript 的 異步通信,從服務(wù)器獲取 XML 文檔從中提取數(shù)據(jù),再更新當(dāng)前網(wǎng)頁的對應(yīng)部分,而不用刷新整個網(wǎng)頁。

創(chuàng)建AJAX請求的步驟:

  • 創(chuàng)建一個 XMLHttpRequest 對象。
  • 在這個對象上使用 open 方法創(chuàng)建一個 HTTP 請求,open 方法所需要的參數(shù)是請求的方法、請求的地址、是否異步和用戶的認證信息。
  • 在發(fā)起請求前,可以為這個對象添加一些信息和監(jiān)聽函數(shù)。比如說可以通過 setRequestHeader 方法來為請求添加頭信息。還可以為這個對象添加一個狀態(tài)監(jiān)聽函數(shù)。一個 XMLHttpRequest 對象一共有 5 個狀態(tài),當(dāng)它的狀態(tài)變化時會觸發(fā)onreadystatechange 事件,可以通過設(shè)置監(jiān)聽函數(shù),來處理請求成功后的結(jié)果。當(dāng)對象的 readyState 變?yōu)?nbsp;4 的時候,代表服務(wù)器返回的數(shù)據(jù)接收完成,這個時候可以通過判斷請求的狀態(tài),如果狀態(tài)是 2xx 或者 304 的話則代表返回正常。這個時候就可以通過 response 中的數(shù)據(jù)來對頁面進行更新了。
  • 當(dāng)對象的屬性和監(jiān)聽函數(shù)設(shè)置完成后,最后調(diào)用 sent 方法來向服務(wù)器發(fā)起請求,可以傳入?yún)?shù)作為發(fā)送的數(shù)據(jù)體。
const SERVER_URL = "/server";
let xhr = new XMLHttpRequest();
// 創(chuàng)建 Http 請求
xhr.open("GET", url, true);
// 設(shè)置狀態(tài)監(jiān)聽函數(shù)
xhr.onreadystatechange = function() {
  if (this.readyState !== 4) return;
  // 當(dāng)請求成功時
  if (this.status === 200) {
    handle(this.response);
  } else {
    console.error(this.statusText);
  }
};
// 設(shè)置請求失敗時的監(jiān)聽函數(shù)
xhr.onerror = function() {
  console.error(this.statusText);
};
// 設(shè)置請求頭信息
xhr.responseType = "json";
xhr.setRequestHeader("Accept", "application/json");
// 發(fā)送 Http 請求
xhr.send(null);

使用Promise封裝AJAX:

// promise 封裝實現(xiàn):
function getJSON(url) {
  // 創(chuàng)建一個 promise 對象
  let promise = new Promise(function(resolve, reject) {
    let xhr = new XMLHttpRequest();
    // 新建一個 http 請求
    xhr.open("GET", url, true);
    // 設(shè)置狀態(tài)的監(jiān)聽函數(shù)
    xhr.onreadystatechange = function() {
      if (this.readyState !== 4) return;
      // 當(dāng)請求成功或失敗時,改變 promise 的狀態(tài)
      if (this.status === 200) {
        resolve(this.response);
      } else {
        reject(new Error(this.statusText));
      }
    };
    // 設(shè)置錯誤監(jiān)聽函數(shù)
    xhr.onerror = function() {
      reject(new Error(this.statusText));
    };
    // 設(shè)置響應(yīng)的數(shù)據(jù)類型
    xhr.responseType = "json";
    // 設(shè)置請求頭信息
    xhr.setRequestHeader("Accept", "application/json");
    // 發(fā)送 http 請求
    xhr.send(null);
  });
  return promise;
}

17. JavaScript為什么要進行變量提升,它導(dǎo)致了什么問題?

變量提升的表現(xiàn)是,無論在函數(shù)中何處位置聲明的變量,好像都被提升到了函數(shù)的首部,可以在變量聲明前訪問到而不會報錯。

造成變量聲明提升的本質(zhì)原因是 js 引擎在代碼執(zhí)行前有一個解析的過程,創(chuàng)建了執(zhí)行上下文,初始化了一些代碼執(zhí)行時需要用到的對象。當(dāng)訪問一個變量時,會到當(dāng)前執(zhí)行上下文中的作用域鏈中去查找,而作用域鏈的首端指向的是當(dāng)前執(zhí)行上下文的變量對象,這個變量對象是執(zhí)行上下文的一個屬性,它包含了函數(shù)的形參、所有的函數(shù)和變量聲明,這個對象的是在代碼解析的時候創(chuàng)建的。

首先要知道,JS在拿到一個變量或者一個函數(shù)的時候,會有兩步操作,即解析和執(zhí)行。

  • 在解析階段,JS會檢查語法,并對函數(shù)進行預(yù)編譯。解析的時候會先創(chuàng)建一個全局執(zhí)行上下文環(huán)境,先把代碼中即將執(zhí)行的變量、函數(shù)聲明都拿出來,變量先賦值為undefined,函數(shù)先聲明好可使用。在一個函數(shù)執(zhí)行之前,也會創(chuàng)建一個函數(shù)執(zhí)行上下文環(huán)境,跟全局執(zhí)行上下文類似,不過函數(shù)執(zhí)行上下文會多出this、arguments和函數(shù)的參數(shù)。
    • 全局上下文:變量定義,函數(shù)聲明
    • 函數(shù)上下文:變量定義,函數(shù)聲明,this,arguments
  • 在執(zhí)行階段,就是按照代碼的順序依次執(zhí)行。

那為什么會進行變量提升呢?主要有以下兩個原因:

  • 提高性能
  • 容錯性更好

(1)提高性能

在JS代碼執(zhí)行之前,會進行語法檢查和預(yù)編譯,并且這一操作只進行一次。這么做就是為了提高性能,如果沒有這一步,那么每次執(zhí)行代碼前都必須重新解析一遍該變量(函數(shù)),而這是沒有必要的,因為變量(函數(shù))的代碼并不會改變,解析一遍就夠了。

在解析的過程中,還會為函數(shù)生成預(yù)編譯代碼。在預(yù)編譯時,會統(tǒng)計聲明了哪些變量、創(chuàng)建了哪些函數(shù),并對函數(shù)的代碼進行壓縮,去除注釋、不必要的空白等。這樣做的好處就是每次執(zhí)行函數(shù)時都可以直接為該函數(shù)分配棧空間(不需要再解析一遍去獲取代碼中聲明了哪些變量,創(chuàng)建了哪些函數(shù)),并且因為代碼壓縮的原因,代碼執(zhí)行也更快了。

(2)容錯性更好

變量提升可以在一定程度上提高JS的容錯性,看下面的代碼:

a = 1;
var a;
console.log(a);

如果沒有變量提升,這兩行代碼就會報錯,但是因為有了變量提升,這段代碼就可以正常執(zhí)行。

雖然,在可以開發(fā)過程中,可以完全避免這樣寫,但是有時代碼很復(fù)雜的時候??赡芤驗槭韬龆仁褂煤蠖x了,這樣也不會影響正常使用。由于變量提升的存在,而會正常運行。

總結(jié):

  • 解析和預(yù)編譯過程中的聲明提升可以提高性能,讓函數(shù)可以在執(zhí)行時預(yù)先為變量分配??臻g
  • 聲明提升還可以提高JS代碼的容錯性,使一些不規(guī)范的代碼也可以正常執(zhí)行

變量提升雖然有一些優(yōu)點,但是他也會造成一定的問題,在ES6中提出了let、const來定義變量,它們就沒有變量提升的機制。下面看一下變量提升可能會導(dǎo)致的問題:

var tmp = new Date();

function fn(){
    console.log(tmp);
    if(false){
        var tmp = 'hello world';
    }
}

fn();  // undefined

在這個函數(shù)中,原本是要打印出外層的tmp變量,但是因為變量提升的問題,內(nèi)層定義的tmp被提到函數(shù)內(nèi)部的最頂部,相當(dāng)于覆蓋了外層的tmp,所以打印結(jié)果為undefined。

var tmp = 'hello world';

for (var i = 0; i < tmp.length; i++) {
    console.log(tmp[i]);
}

console.log(i); // 11

由于遍歷時定義的i會變量提升成為一個全局變量,在函數(shù)結(jié)束之后不會被銷毀,所以打印出來11。

18. 什么是尾調(diào)用,使用尾調(diào)用有什么好處?

尾調(diào)用指的是函數(shù)的最后一步調(diào)用另一個函數(shù)。代碼執(zhí)行是基于執(zhí)行棧的,所以當(dāng)在一個函數(shù)里調(diào)用另一個函數(shù)時,會保留當(dāng)前的執(zhí)行上下文,然后再新建另外一個執(zhí)行上下文加入棧中。使用尾調(diào)用的話,因為已經(jīng)是函數(shù)的最后一步,所以這時可以不必再保留當(dāng)前的執(zhí)行上下文,從而節(jié)省了內(nèi)存,這就是尾調(diào)用優(yōu)化。但是 ES6 的尾調(diào)用優(yōu)化只在嚴(yán)格模式下開啟,正常模式是無效的。

19. ES6模塊與CommonJS模塊有什么異同?

ES6 Module和CommonJS模塊的區(qū)別:

  • CommonJS是對模塊的淺拷?,ES6 Module是對模塊的引?,即ES6 Module只存只讀,不能改變其值,也就是指針指向不能變,類似const;
  • import的接?是read-only(只讀狀態(tài)),不能修改其變量值。 即不能修改其變量的指針指向,但可以改變變量內(nèi)部指針指向,可以對commonJS對重新賦值(改變指針指向),但是對ES6 Module賦值會編譯報錯。

ES6 Module和CommonJS模塊的共同點:

  • CommonJS和ES6 Module都可以對引?的對象進?賦值,即對對象內(nèi)部屬性的值進?改變。
  • CommonJS 模塊輸出的是一個值的拷貝,ES6 模塊輸出的是值的引用。CommonJS 模塊是運行時加載,ES6 模塊是編譯時輸出接口。
  • CommonJs 是單個值導(dǎo)出,ES6 Module可以導(dǎo)出多個
  • CommonJs 是動態(tài)語法可以寫在判斷里,ES6 Module 靜態(tài)語法只能寫在頂層
  • CommonJs 的 this 是當(dāng)前模塊,ES6 Module的 this 是 undefined

CommonJs require后執(zhí)行整個模塊代碼 且加載同一個模塊只會執(zhí)行一次,執(zhí)行完進行緩存 ES6 Module 像const一樣指定 import {xx} from 'xxx' 無法給xx賦值 但可以改變xx的屬性和方法

CommonJS

1.對于基本數(shù)據(jù)類型,屬于復(fù)制。即會被模塊緩存。同時,在另一個模塊可以對該模塊輸出的變量重新賦值。

2.對于復(fù)雜數(shù)據(jù)類型,屬于淺拷貝。由于兩個模塊引用的對象指向同一個內(nèi)存空間,因此對該模塊的值做修改時會影響另一個模塊。

3.當(dāng)使用require命令加載某個模塊時,就會運行整個模塊的代碼。

4.當(dāng)使用require命令加載同一個模塊時,不會再執(zhí)行該模塊,而是取到緩存之中的值。也就是說,CommonJS模塊無論加載多少次,都只會在第一次加載時運行一次,以后再加載,就返回第一次運行的結(jié)果,除非手動清除系統(tǒng)緩存。

5.循環(huán)加載時,屬于加載時執(zhí)行。即腳本代碼在require的時候,就會全部執(zhí)行。一旦出現(xiàn)某個模塊被"循環(huán)加載",就只輸出已經(jīng)執(zhí)行的部分,還未執(zhí)行的部分不會輸出。

ES6模塊

1.ES6模塊中的值屬于【動態(tài)只讀引用】

2.對于只讀來說,即不允許修改引入變量的值,import的變量是只讀的,不論是基本數(shù)據(jù)類型還是復(fù)雜數(shù)據(jù)類型。當(dāng)模塊遇到import命令時,就會生成一個只讀引用。等到腳本真正執(zhí)行時,再根據(jù)這個只讀引用,到被加載的那個模塊里面去取值。

3.對于動態(tài)來說,原始值發(fā)生變化,import加載的值也會發(fā)生變化。不論是基本數(shù)據(jù)類型還是復(fù)雜數(shù)據(jù)類型。

4.循環(huán)加載時,ES6模塊是動態(tài)引用。只要兩個模塊之間存在某個引用,代碼就能夠執(zhí)行。

20. 常見的DOM操作有哪些

1)DOM 節(jié)點的獲取

DOM 節(jié)點的獲取的API及使用:

getElementById // 按照 id 查詢
getElementsByTagName // 按照標(biāo)簽名查詢
getElementsByClassName // 按照類名查詢
querySelectorAll // 按照 css 選擇器查詢

// 按照 id 查詢
var imooc = document.getElementById('imooc') // 查詢到 id 為 imooc 的元素
// 按照標(biāo)簽名查詢
var pList = document.getElementsByTagName('p')  // 查詢到標(biāo)簽為 p 的集合
console.log(divList.length)
console.log(divList[0])
// 按照類名查詢
var moocList = document.getElementsByClassName('mooc') // 查詢到類名為 mooc 的集合
// 按照 css 選擇器查詢
var pList = document.querySelectorAll('.mooc') // 查詢到類名為 mooc 的集合

2)DOM 節(jié)點的創(chuàng)建

創(chuàng)建一個新節(jié)點,并把它添加到指定節(jié)點的后面。已知的 HTML 結(jié)構(gòu)如下:

<html>
  <head>
    <title>DEMO</title>
  </head>
  <body>
    <div id="container"> 
      <h1 id="title">我是標(biāo)題</h1>
    </div>   
  </body>
</html>

要求添加一個有內(nèi)容的 span 節(jié)點到 id 為 title 的節(jié)點后面,做法就是:

// 首先獲取父節(jié)點
var container = document.getElementById('container')
// 創(chuàng)建新節(jié)點
var targetSpan = document.createElement('span')
// 設(shè)置 span 節(jié)點的內(nèi)容
targetSpan.innerHTML = 'hello world'
// 把新創(chuàng)建的元素塞進父節(jié)點里去
container.appendChild(targetSpan)

3)DOM 節(jié)點的刪除

刪除指定的 DOM 節(jié)點,已知的 HTML 結(jié)構(gòu)如下:

<html>
  <head>
    <title>DEMO</title>
  </head>
  <body>
    <div id="container"> 
      <h1 id="title">我是標(biāo)題</h1>
    </div>   
  </body>
</html>

需要刪除 id 為 title 的元素,做法是:

// 獲取目標(biāo)元素的父元素
var container = document.getElementById('container')
// 獲取目標(biāo)元素
var targetNode = document.getElementById('title')
// 刪除目標(biāo)元素
container.removeChild(targetNode)

或者通過子節(jié)點數(shù)組來完成刪除:

// 獲取目標(biāo)元素的父元素
var container = document.getElementById('container')
// 獲取目標(biāo)元素
var targetNode = container.childNodes[1]
// 刪除目標(biāo)元素
container.removeChild(targetNode)

4)修改 DOM 元素

修改 DOM 元素這個動作可以分很多維度,比如說移動 DOM 元素的位置,修改 DOM 元素的屬性等。

將指定的兩個 DOM 元素交換位置,已知的 HTML 結(jié)構(gòu)如下:

<html>
  <head>
    <title>DEMO</title>
  </head>
  <body>
    <div id="container"> 
      <h1 id="title">我是標(biāo)題</h1>
      <p id="content">我是內(nèi)容</p>
    </div>   
  </body>
</html>

現(xiàn)在需要調(diào)換 title 和 content 的位置,可以考慮 insertBefore 或者 appendChild:

// 獲取父元素
var container = document.getElementById('container')   
 
// 獲取兩個需要被交換的元素
var title = document.getElementById('title')
var content = document.getElementById('content')
// 交換兩個元素,把 content 置于 title 前面
container.insertBefore(content, title)

21. use strict是什么意思 ? 使用它區(qū)別是什么?

use strict 是一種 ECMAscript5 添加的(嚴(yán)格模式)運行模式,這種模式使得 Javascript 在更嚴(yán)格的條件下運行。設(shè)立嚴(yán)格模式的目的如下:

  • 消除 Javascript 語法的不合理、不嚴(yán)謹之處,減少怪異行為;
  • 消除代碼運行的不安全之處,保證代碼運行的安全;
  • 提高編譯器效率,增加運行速度;
  • 為未來新版本的 Javascript 做好鋪墊。

區(qū)別:

  • 禁止使用 with 語句。
  • 禁止 this 關(guān)鍵字指向全局對象。
  • 對象不能有重名的屬性。

22. 如何判斷一個對象是否屬于某個類?

  • 第一種方式,使用 instanceof 運算符來判斷構(gòu)造函數(shù)的 prototype 屬性是否出現(xiàn)在對象的原型鏈中的任何位置。
  • 第二種方式,通過對象的 constructor 屬性來判斷,對象的 constructor 屬性指向該對象的構(gòu)造函數(shù),但是這種方式不是很安全,因為 constructor 屬性可以被改寫。
  • 第三種方式,如果需要判斷的是某個內(nèi)置的引用類型的話,可以使用 Object.prototype.toString() 方法來打印對象的[[Class]] 屬性來進行判斷。

23. 強類型語言和弱類型語言的區(qū)別

  • 強類型語言:強類型語言也稱為強類型定義語言,是一種總是強制類型定義的語言,要求變量的使用要嚴(yán)格符合定義,所有變量都必須先定義后使用。Java和C++等語言都是強制類型定義的,也就是說,一旦一個變量被指定了某個數(shù)據(jù)類型,如果不經(jīng)過強制轉(zhuǎn)換,那么它就永遠是這個數(shù)據(jù)類型了。例如你有一個整數(shù),如果不顯式地進行轉(zhuǎn)換,你不能將其視為一個字符串。
  • 弱類型語言:弱類型語言也稱為弱類型定義語言,與強類型定義相反。JavaScript語言就屬于弱類型語言。簡單理解就是一種變量類型可以被忽略的語言。比如JavaScript是弱類型定義的,在JavaScript中就可以將字符串'12'和整數(shù)3進行連接得到字符串'123',在相加的時候會進行強制類型轉(zhuǎn)換。

兩者對比:強類型語言在速度上可能略遜色于弱類型語言,但是強類型語言帶來的嚴(yán)謹性可以有效地幫助避免許多錯誤。

24. 解釋性語言和編譯型語言的區(qū)別

(1)解釋型語言

使用專門的解釋器對源程序逐行解釋成特定平臺的機器碼并立即執(zhí)行。是代碼在執(zhí)行時才被解釋器一行行動態(tài)翻譯和執(zhí)行,而不是在執(zhí)行之前就完成翻譯。解釋型語言不需要事先編譯,其直接將源代碼解釋成機器碼并立即執(zhí)行,所以只要某一平臺提供了相應(yīng)的解釋器即可運行該程序。其特點總結(jié)如下

  • 解釋型語言每次運行都需要將源代碼解釋稱機器碼并執(zhí)行,效率較低;
  • 只要平臺提供相應(yīng)的解釋器,就可以運行源代碼,所以可以方便源程序移植;
  • JavaScript、Python等屬于解釋型語言。

(2)編譯型語言

使用專門的編譯器,針對特定的平臺,將高級語言源代碼一次性的編譯成可被該平臺硬件執(zhí)行的機器碼,并包裝成該平臺所能識別的可執(zhí)行性程序的格式。在編譯型語言寫的程序執(zhí)行之前,需要一個專門的編譯過程,把源代碼編譯成機器語言的文件,如exe格式的文件,以后要再運行時,直接使用編譯結(jié)果即可,如直接運行exe文件。因為只需編譯一次,以后運行時不需要編譯,所以編譯型語言執(zhí)行效率高。其特點總結(jié)如下:

  • 一次性的編譯成平臺相關(guān)的機器語言文件,運行時脫離開發(fā)環(huán)境,運行效率高;
  • 與特定平臺相關(guān),一般無法移植到其他平臺;
  • C、C++等屬于編譯型語言。

兩者主要區(qū)別在于:前者源程序編譯后即可在該平臺運行,后者是在運行期間才編譯。所以前者運行速度快,后者跨平臺性好。

25. for...in和for...of的區(qū)別

for…of 是ES6新增的遍歷方式,允許遍歷一個含有iterator接口的數(shù)據(jù)結(jié)構(gòu)(數(shù)組、對象等)并且返回各項的值,和ES3中的for…in的區(qū)別如下

  • for…of 遍歷獲取的是對象的鍵值,for…in 獲取的是對象的鍵名;
  • for… in 會遍歷對象的整個原型鏈,性能非常差不推薦使用,而 for … of 只遍歷當(dāng)前對象不會遍歷原型鏈;
  • 對于數(shù)組的遍歷,for…in 會返回數(shù)組中所有可枚舉的屬性(包括原型鏈上可枚舉的屬性),for…of 只返回數(shù)組的下標(biāo)對應(yīng)的屬性值;

總結(jié):for...in 循環(huán)主要是為了遍歷對象而生,不適用于遍歷數(shù)組;for...of 循環(huán)可以用來遍歷數(shù)組、類數(shù)組對象,字符串、Set、Map 以及 Generator 對象。

26. 如何使用for...of遍歷對象

for…of是作為ES6新增的遍歷方式,允許遍歷一個含有iterator接口的數(shù)據(jù)結(jié)構(gòu)(數(shù)組、對象等)并且返回各項的值,普通的對象用for..of遍歷是會報錯的。

如果需要遍歷的對象是類數(shù)組對象,用Array.from轉(zhuǎn)成數(shù)組即可。

var obj = {
    0:'one',
    1:'two',
    length: 2
};
obj = Array.from(obj);
for(var k of obj){
    console.log(k)
}

如果不是類數(shù)組對象,就給對象添加一個[Symbol.iterator]屬性,并指向一個迭代器即可。

//方法一:
var obj = {
    a:1,
    b:2,
    c:3
};

obj[Symbol.iterator] = function(){
    var keys = Object.keys(this);
    var count = 0;
    return {
        next(){
            if(count<keys.length){
                return {value: obj[keys[count++]],done:false};
            }else{
                return {value:undefined,done:true};
            }
        }
    }
};

for(var k of obj){
    console.log(k);
}


// 方法二
var obj = {
    a:1,
    b:2,
    c:3
};
obj[Symbol.iterator] = function*(){
    var keys = Object.keys(obj);
    for(var k of keys){
        yield [k,obj[k]]
    }
};

for(var [k,v] of obj){
    console.log(k,v);
}

27. ajax、axios、fetch的區(qū)別

(1)AJAX

Ajax 即“AsynchronousJavascriptAndXML”(異步 JavaScript 和 XML),是指一種創(chuàng)建交互式網(wǎng)頁應(yīng)用的網(wǎng)頁開發(fā)技術(shù)。它是一種在無需重新加載整個網(wǎng)頁的情況下,能夠更新部分網(wǎng)頁的技術(shù)。通過在后臺與服務(wù)器進行少量數(shù)據(jù)交換,Ajax 可以使網(wǎng)頁實現(xiàn)異步更新。這意味著可以在不重新加載整個網(wǎng)頁的情況下,對網(wǎng)頁的某部分進行更新。傳統(tǒng)的網(wǎng)頁(不使用 Ajax)如果需要更新內(nèi)容,必須重載整個網(wǎng)頁頁面。其缺點如下:

  • 本身是針對MVC編程,不符合前端MVVM的浪潮
  • 基于原生XHR開發(fā),XHR本身的架構(gòu)不清晰
  • 不符合關(guān)注分離(Separation of Concerns)的原則
  • 配置和調(diào)用方式非?;靵y,而且基于事件的異步模型不友好。

(2)Fetch

fetch號稱是AJAX的替代品,是在ES6出現(xiàn)的,使用了ES6中的promise對象。Fetch是基于promise設(shè)計的。Fetch的代碼結(jié)構(gòu)比起ajax簡單多。fetch不是ajax的進一步封裝,而是原生js,沒有使用XMLHttpRequest對象。

fetch的優(yōu)點:

  • 語法簡潔,更加語義化
  • 基于標(biāo)準(zhǔn) Promise 實現(xiàn),支持 async/await
  • 更加底層,提供的API豐富(request, response)
  • 脫離了XHR,是ES規(guī)范里新的實現(xiàn)方式

fetch的缺點:

  • fetch只對網(wǎng)絡(luò)請求報錯,對400,500都當(dāng)做成功的請求,服務(wù)器返回 400,500 錯誤碼時并不會 reject,只有網(wǎng)絡(luò)錯誤這些導(dǎo)致請求不能完成時,fetch 才會被 reject。
  • fetch默認不會帶cookie,需要添加配置項: fetch(url, {credentials: 'include'})
  • fetch不支持abort,不支持超時控制,使用setTimeout及Promise.reject的實現(xiàn)的超時控制并不能阻止請求過程繼續(xù)在后臺運行,造成了流量的浪費
  • fetch沒有辦法原生監(jiān)測請求的進度,而XHR可以

(3)Axios

Axios 是一種基于Promise封裝的HTTP客戶端,其特點如下:

  • 瀏覽器端發(fā)起XMLHttpRequests請求
  • node端發(fā)起http請求
  • 支持Promise API
  • 監(jiān)聽請求和返回
  • 對請求和返回進行轉(zhuǎn)化
  • 取消請求
  • 自動轉(zhuǎn)換json數(shù)據(jù)
  • 客戶端支持抵御XSRF攻擊

28. 數(shù)組的遍歷方法有哪些

方法 是否改變原數(shù)組 特點
forEach() 數(shù)組方法,不改變原數(shù)組,沒有返回值
map() 數(shù)組方法,不改變原數(shù)組,有返回值,可鏈?zhǔn)秸{(diào)用
filter() 數(shù)組方法,過濾數(shù)組,返回包含符合條件的元素的數(shù)組,可鏈?zhǔn)秸{(diào)用
for...of for...of遍歷具有Iterator迭代器的對象的屬性,返回的是數(shù)組的元素、對象的屬性值,不能遍歷普通的obj對象,將異步循環(huán)變成同步循環(huán)
every() 和 some() 數(shù)組方法,some()只要有一個是true,便返回true;而every()只要有一個是false,便返回false.
find() 和 findIndex() 數(shù)組方法,find()返回的是第一個符合條件的值;findIndex()返回的是第一個返回條件的值的索引值
reduce() 和 reduceRight() 數(shù)組方法,reduce()對數(shù)組正序操作;reduceRight()對數(shù)組逆序操作

29. forEach和map方法有什么區(qū)別

這方法都是用來遍歷數(shù)組的,兩者區(qū)別如下:

  • forEach()方法會針對每一個元素執(zhí)行提供的函數(shù),對數(shù)據(jù)的操作不會改變原數(shù)組,該方法沒有返回值;
  • map()方法不會改變原數(shù)組的值,返回一個新數(shù)組,新數(shù)組中的值為原數(shù)組調(diào)用函數(shù)處理之后的值;

四、原型與原型鏈


1. 對原型、原型鏈的理解

在JavaScript中是使用構(gòu)造函數(shù)來新建一個對象的,每一個構(gòu)造函數(shù)的內(nèi)部都有一個 prototype 屬性,它的屬性值是一個對象,這個對象包含了可以由該構(gòu)造函數(shù)的所有實例共享的屬性和方法。當(dāng)使用構(gòu)造函數(shù)新建一個對象后,在這個對象的內(nèi)部將包含一個指針,這個指針指向構(gòu)造函數(shù)的 prototype 屬性對應(yīng)的值,在 ES5 中這個指針被稱為對象的原型。一般來說不應(yīng)該能夠獲取到這個值的,但是現(xiàn)在瀏覽器中都實現(xiàn)了 proto 屬性來訪問這個屬性,但是最好不要使用這個屬性,因為它不是規(guī)范中規(guī)定的。ES5 中新增了一個 Object.getPrototypeOf() 方法,可以通過這個方法來獲取對象的原型。

當(dāng)訪問一個對象的屬性時,如果這個對象內(nèi)部不存在這個屬性,那么它就會去它的原型對象里找這個屬性,這個原型對象又會有自己的原型,于是就這樣一直找下去,也就是原型鏈的概念。原型鏈的盡頭一般來說都是 Object.prototype 所以這就是新建的對象為什么能夠使用 toString() 等方法的原因。

特點:JavaScript 對象是通過引用來傳遞的,創(chuàng)建的每個新對象實體中并沒有一份屬于自己的原型副本。當(dāng)修改原型時,與之相關(guān)的對象也會繼承這一改變。


2. 原型修改、重寫

function Person(name) {
    this.name = name
}
// 修改原型
Person.prototype.getName = function() {}
var p = new Person('hello')
console.log(p.__proto__ === Person.prototype) // true
console.log(p.__proto__ === p.constructor.prototype) // true
// 重寫原型
Person.prototype = {
    getName: function() {}
}
var p = new Person('hello')
console.log(p.__proto__ === Person.prototype)        // true
console.log(p.__proto__ === p.constructor.prototype) // false

可以看到重寫原型的時候p的構(gòu)造函數(shù)不是指向Person了,因為直接給Person的原型對象直接用對象賦值時,它的構(gòu)造函數(shù)指向的了根構(gòu)造函數(shù)Object,所以這時候 p.constructor === Object ,而不是 p.constructor === Person。要想成立,就要用constructor指回來:

Person.prototype = {
    getName: function() {}
}
var p = new Person('hello')
p.constructor = Person
console.log(p.__proto__ === Person.prototype)        // true
console.log(p.__proto__ === p.constructor.prototype) // true

3. 原型鏈指向

p.__proto__  // Person.prototype
Person.prototype.__proto__  // Object.prototype
p.__proto__.__proto__ //Object.prototype
p.__proto__.constructor.prototype.__proto__ // Object.prototype
Person.prototype.constructor.prototype.__proto__ // Object.prototype
p1.__proto__.constructor // Person
Person.prototype.constructor  // Person

4. 原型鏈的終點是什么?如何打印出原型鏈的終點?

由于 Object是構(gòu)造函數(shù),原型鏈終點是 Object.prototype.__proto__,而 Object.prototype.__proto__=== null // true,所以,原型鏈的終點是 null。原型鏈上的所有原型都是對象,所有的對象最終都是由 Object構(gòu)造的,而 Object.prototype的下一級是 Object.prototype.__proto__。


5. 如何獲得對象非原型鏈上的屬性?

使用后 hasOwnProperty()方法來判斷屬性是否屬于原型鏈的屬性:

function iterate(obj){
   var res=[];
   for(var key in obj){
        if(obj.hasOwnProperty(key))
           res.push(key+': '+obj[key]);
   }
   return res;
} 

五、執(zhí)行上下文/作用域鏈/閉包


1. 對閉包的理解

閉包是指有權(quán)訪問另一個函數(shù)作用域中變量的函數(shù),創(chuàng)建閉包的最常見的方式就是在一個函數(shù)內(nèi)創(chuàng)建另一個函數(shù),創(chuàng)建的函數(shù)可以訪問到當(dāng)前函數(shù)的局部變量。

閉包有兩個常用的用途;

  • 閉包的第一個用途是使我們在函數(shù)外部能夠訪問到函數(shù)內(nèi)部的變量。通過使用閉包,可以通過在外部調(diào)用閉包函數(shù),從而在外部訪問到函數(shù)內(nèi)部的變量,可以使用這種方法來創(chuàng)建私有變量。
  • 閉包的另一個用途是使已經(jīng)運行結(jié)束的函數(shù)上下文中的變量對象繼續(xù)留在內(nèi)存中,因為閉包函數(shù)保留了這個變量對象的引用,所以這個變量對象不會被回收。

比如,函數(shù) A 內(nèi)部有一個函數(shù) B,函數(shù) B 可以訪問到函數(shù) A 中的變量,那么函數(shù) B 就是閉包。

function A() {
  let a = 1
  window.B = function () {
      console.log(a)
  }
}
A()
B() // 1

在 JS 中,閉包存在的意義就是讓我們可以間接訪問函數(shù)內(nèi)部的變量。經(jīng)典面試題:循環(huán)中使用閉包解決 var 定義函數(shù)的問題

for (var i = 1; i <= 5; i++) {
  setTimeout(function timer() {
    console.log(i)
  }, i * 1000)
}

首先因為 setTimeout 是個異步函數(shù),所以會先把循環(huán)全部執(zhí)行完畢,這時候 i 就是 6 了,所以會輸出一堆 6。解決辦法有三種:

  • 第一種是使用閉包的方式
for (var i = 1; i <= 5; i++) {
  ;(function(j) {
    setTimeout(function timer() {
      console.log(j)
    }, j * 1000)
  })(i)
}

在上述代碼中,首先使用了立即執(zhí)行函數(shù)將 i 傳入函數(shù)內(nèi)部,這個時候值就被固定在了參數(shù) j 上面不會改變,當(dāng)下次執(zhí)行 timer 這個閉包的時候,就可以使用外部函數(shù)的變量 j,從而達到目的。

  • 第二種就是使用 ?setTimeout ?的第三個參數(shù),這個參數(shù)會被當(dāng)成 ?timer ?函數(shù)的參數(shù)傳入。
for (var i = 1; i <= 5; i++) {
  setTimeout(
    function timer(j) {
      console.log(j)
    },
    i * 1000,
    i
  )
}
  • 第三種就是使用 ?let ?定義 ?i? 了來解決問題了,這個也是最為推薦的方式
for (let i = 1; i <= 5; i++) {
  setTimeout(function timer() {
    console.log(i)
  }, i * 1000)
}

2. 對作用域、作用域鏈的理解

1)全局作用域和函數(shù)作用域

(1)全局作用域

  • 最外層函數(shù)和最外層函數(shù)外面定義的變量擁有全局作用域
  • 所有未定義直接賦值的變量自動聲明為全局作用域
  • 所有window對象的屬性擁有全局作用域
  • 全局作用域有很大的弊端,過多的全局作用域變量會污染全局命名空間,容易引起命名沖突。

(2)函數(shù)作用域

  • 函數(shù)作用域聲明在函數(shù)內(nèi)部的變零,一般只有固定的代碼片段可以訪問到
  • 作用域是分層的,內(nèi)層作用域可以訪問外層作用域,反之不行
2)塊級作用域
  • 使用ES6中新增的let和const指令可以聲明塊級作用域,塊級作用域可以在函數(shù)中創(chuàng)建也可以在一個代碼塊中的創(chuàng)建(由 ?{ }?包裹的代碼片段)
  • let和const聲明的變量不會有變量提升,也不可以重復(fù)聲明
  • 在循環(huán)中比較適合綁定塊級作用域,這樣就可以把聲明的計數(shù)器變量限制在循環(huán)內(nèi)部。

作用域鏈:

在當(dāng)前作用域中查找所需變量,但是該作用域沒有這個變量,那這個變量就是自由變量。如果在自己作用域找不到該變量就去父級作用域查找,依次向上級作用域查找,直到訪問到window對象就被終止,這一層層的關(guān)系就是作用域鏈。

作用域鏈的作用是保證對執(zhí)行環(huán)境有權(quán)訪問的所有變量和函數(shù)的有序訪問,通過作用域鏈,可以訪問到外層環(huán)境的變量和函數(shù)。

作用域鏈的本質(zhì)上是一個指向變量對象的指針列表。變量對象是一個包含了執(zhí)行環(huán)境中所有變量和函數(shù)的對象。作用域鏈的前端始終都是當(dāng)前執(zhí)行上下文的變量對象。全局執(zhí)行上下文的變量對象(也就是全局對象)始終是作用域鏈的最后一個對象。

當(dāng)查找一個變量時,如果當(dāng)前執(zhí)行環(huán)境中沒有找到,可以沿著作用域鏈向后查找。

3. 對執(zhí)行上下文的理解

1. 執(zhí)行上下文類型

(1)全局執(zhí)行上下文

任何不在函數(shù)內(nèi)部的都是全局執(zhí)行上下文,它首先會創(chuàng)建一個全局的window對象,并且設(shè)置this的值等于這個全局對象,一個程序中只有一個全局執(zhí)行上下文。

(2)函數(shù)執(zhí)行上下文

當(dāng)一個函數(shù)被調(diào)用時,就會為該函數(shù)創(chuàng)建一個新的執(zhí)行上下文,函數(shù)的上下文可以有任意多個。

(3)eval函數(shù)執(zhí)行上下文

執(zhí)行在eval函數(shù)中的代碼會有屬于他自己的執(zhí)行上下文,不過eval函數(shù)不常使用,不做介紹。

2. 執(zhí)行上下文棧
  • JavaScript引擎使用執(zhí)行上下文棧來管理執(zhí)行上下文
  • 當(dāng)JavaScript執(zhí)行代碼時,首先遇到全局代碼,會創(chuàng)建一個全局執(zhí)行上下文并且壓入執(zhí)行棧中,每當(dāng)遇到一個函數(shù)調(diào)用,就會為該函數(shù)創(chuàng)建一個新的執(zhí)行上下文并壓入棧頂,引擎會執(zhí)行位于執(zhí)行上下文棧頂?shù)暮瘮?shù),當(dāng)函數(shù)執(zhí)行完成之后,執(zhí)行上下文從棧中彈出,繼續(xù)執(zhí)行下一個上下文。當(dāng)所有的代碼都執(zhí)行完畢之后,從棧中彈出全局執(zhí)行上下文。
let a = 'Hello World!';
function first() {
  console.log('Inside first function');
  second();
  console.log('Again inside first function');
}
function second() {
  console.log('Inside second function');
}
first();
//執(zhí)行順序
//先執(zhí)行second(),在執(zhí)行first()
3. 創(chuàng)建執(zhí)行上下文

創(chuàng)建執(zhí)行上下文有兩個階段:創(chuàng)建階段執(zhí)行階段

1)創(chuàng)建階段

(1)this綁定

  • 在全局執(zhí)行上下文中,this指向全局對象(window對象)
  • 在函數(shù)執(zhí)行上下文中,this指向取決于函數(shù)如何調(diào)用。如果它被一個引用對象調(diào)用,那么 this 會被設(shè)置成那個對象,否則 this 的值被設(shè)置為全局對象或者 undefined

(2)創(chuàng)建詞法環(huán)境組件

  • 詞法環(huán)境是一種有標(biāo)識符——變量映射的數(shù)據(jù)結(jié)構(gòu),標(biāo)識符是指變量/函數(shù)名,變量是對實際對象或原始數(shù)據(jù)的引用。
  • 詞法環(huán)境的內(nèi)部有兩個組件:環(huán)境記錄器:用來儲存變量和函數(shù)聲明的實際位置外部環(huán)境的引用:可以訪問外部詞法環(huán)境 let const

(3)創(chuàng)建變量環(huán)境組件

  • 變量環(huán)境也是一個詞法環(huán)境,其環(huán)境記錄器持有變量聲明語句在執(zhí)行上下文中創(chuàng)建的綁定關(guān)系。 

2)執(zhí)行階段

此階段會完成對變量的分配,最后執(zhí)行完代碼。

簡單來說執(zhí)行上下文就是指:

在執(zhí)行一點JS代碼之前,需要先解析代碼。解析的時候會先創(chuàng)建一個全局執(zhí)行上下文環(huán)境,先把代碼中即將執(zhí)行的變量、函數(shù)聲明都拿出來,變量先賦值為undefined,函數(shù)先聲明好可使用。這一步執(zhí)行完了,才開始正式的執(zhí)行程序。

在一個函數(shù)執(zhí)行之前,也會創(chuàng)建一個函數(shù)執(zhí)行上下文環(huán)境,跟全局執(zhí)行上下文類似,不過函數(shù)執(zhí)行上下文會多出this、arguments和函數(shù)的參數(shù)。

  • 全局上下文:變量定義,函數(shù)聲明
  • 函數(shù)上下文:變量定義,函數(shù)聲明,?this?,?arguments?

六、this/call/apply/bind


1. 對this對象的理解

this 是執(zhí)行上下文中的一個屬性,它指向最后一次調(diào)用這個方法的對象。在實際開發(fā)中,this 的指向可以通過四種調(diào)用模式來判斷。

  • 第一種是函數(shù)調(diào)用模式,當(dāng)一個函數(shù)不是一個對象的屬性時,直接作為函數(shù)來調(diào)用時,this 指向全局對象。
  • 第二種是方法調(diào)用模式,如果一個函數(shù)作為一個對象的方法來調(diào)用時,this 指向這個對象。
  • 第三種是構(gòu)造器調(diào)用模式,如果一個函數(shù)用 new 調(diào)用時,函數(shù)執(zhí)行前會新創(chuàng)建一個對象,this 指向這個新創(chuàng)建的對象。
  • 第四種是 apply 、 call 和 bind 調(diào)用模式,這三個方法都可以顯示的指定調(diào)用函數(shù)的 this 指向。其中 apply 方法接收兩個參數(shù):一個是 this 綁定的對象,一個是參數(shù)數(shù)組。call 方法接收的參數(shù),第一個是 this 綁定的對象,后面的其余參數(shù)是傳入函數(shù)執(zhí)行的參數(shù)。也就是說,在使用 call() 方法時,傳遞給函數(shù)的參數(shù)必須逐個列舉出來。bind 方法通過傳入一個對象,返回一個 this 綁定了傳入對象的新函數(shù)。這個函數(shù)的 this 指向除了使用 new 時會被改變,其他情況下都不會改變。

這四種方式,使用構(gòu)造器調(diào)用模式的優(yōu)先級最高,然后是 apply、call 和 bind 調(diào)用模式,然后是方法調(diào)用模式,然后是函數(shù)調(diào)用模式。

2. call() 和 apply() 的區(qū)別?

它們的作用一模一樣,區(qū)別僅在于傳入?yún)?shù)的形式的不同。

  • apply 接受兩個參數(shù),第一個參數(shù)指定了函數(shù)體內(nèi) this 對象的指向,第二個參數(shù)為一個帶下標(biāo)的集合,這個集合可以為數(shù)組,也可以為類數(shù)組,apply 方法把這個集合中的元素作為參數(shù)傳遞給被調(diào)用的函數(shù)。
  • call 傳入的參數(shù)數(shù)量不固定,跟 apply 相同的是,第一個參數(shù)也是代表函數(shù)體內(nèi)的 this 指向,從第二個參數(shù)開始往后,每個參數(shù)被依次傳入函數(shù)。

3. 實現(xiàn)call、apply 及 bind 函數(shù)

(1)call 函數(shù)的實現(xiàn)步驟:

  • 判斷調(diào)用對象是否為函數(shù),即使是定義在函數(shù)的原型上的,但是可能出現(xiàn)使用 call 等方式調(diào)用的情況。
  • 判斷傳入上下文對象是否存在,如果不存在,則設(shè)置為 window 。
  • 處理傳入的參數(shù),截取第一個參數(shù)后的所有參數(shù)。
  • 將函數(shù)作為上下文對象的一個屬性。
  • 使用上下文對象來調(diào)用這個方法,并保存返回結(jié)果。
  • 刪除剛才新增的屬性。
  • 返回結(jié)果。
Function.prototype.myCall = function(context) {
  // 判斷調(diào)用對象
  if (typeof this !== "function") {
    console.error("type error");
  }
  // 獲取參數(shù)
  let args = [...arguments].slice(1),
    result = null;
  // 判斷 context 是否傳入,如果未傳入則設(shè)置為 window
  context = context || window;
  // 將調(diào)用函數(shù)設(shè)為對象的方法
  context.fn = this;
  // 調(diào)用函數(shù)
  result = context.fn(...args);
  // 將屬性刪除
  delete context.fn;
  return result;
};

(2)apply 函數(shù)的實現(xiàn)步驟:

  • 判斷調(diào)用對象是否為函數(shù),即使是定義在函數(shù)的原型上的,但是可能出現(xiàn)使用 call 等方式調(diào)用的情況。
  • 判斷傳入上下文對象是否存在,如果不存在,則設(shè)置為 window 。
  • 將函數(shù)作為上下文對象的一個屬性。
  • 判斷參數(shù)值是否傳入
  • 使用上下文對象來調(diào)用這個方法,并保存返回結(jié)果。
  • 刪除剛才新增的屬性
  • 返回結(jié)果
Function.prototype.myApply = function(context) {
  // 判斷調(diào)用對象是否為函數(shù)
  if (typeof this !== "function") {
    throw new TypeError("Error");
  }
  let result = null;
  // 判斷 context 是否存在,如果未傳入則為 window
  context = context || window;
  // 將函數(shù)設(shè)為對象的方法
  context.fn = this;
  // 調(diào)用方法
  if (arguments[1]) {
    result = context.fn(...arguments[1]);
  } else {
    result = context.fn();
  }
  // 將屬性刪除
  delete context.fn;
  return result;
};

(3)bind 函數(shù)的實現(xiàn)步驟:

  • 判斷調(diào)用對象是否為函數(shù),即使是定義在函數(shù)的原型上的,但是可能出現(xiàn)使用 call 等方式調(diào)用的情況。
  • 保存當(dāng)前函數(shù)的引用,獲取其余傳入?yún)?shù)值。
  • 創(chuàng)建一個函數(shù)返回
  • 函數(shù)內(nèi)部使用 apply 來綁定函數(shù)調(diào)用,需要判斷函數(shù)作為構(gòu)造函數(shù)的情況,這個時候需要傳入當(dāng)前函數(shù)的 this 給 apply 調(diào)用,其余情況都傳入指定的上下文對象。
Function.prototype.myBind = function(context) {
  // 判斷調(diào)用對象是否為函數(shù)
  if(typeof this !== 'function'){
    console.error('err')
  }
  let args = [...arguments].slice(1)
  let fn = this
  const func = function(){
    return fn.apply(this instanceof fn ? this:context, args.concat([...arguments]))
  }
  func.prototype = Object.create(fn.prototype)
  func.prototype.constructor = func
  return func
};

七、異步編程


1. 異步編程的實現(xiàn)方式?

JavaScript中的異步機制可以分為以下幾種:

  • 回調(diào)函數(shù) 的方式,使用回調(diào)函數(shù)的方式有一個缺點是,多個回調(diào)函數(shù)嵌套的時候會造成回調(diào)函數(shù)地獄,上下兩層的回調(diào)函數(shù)間的代碼耦合度太高,不利于代碼的可維護。
  • Promise 的方式,使用 Promise 的方式可以將嵌套的回調(diào)函數(shù)作為鏈?zhǔn)秸{(diào)用。但是使用這種方法,有時會造成多個 then 的鏈?zhǔn)秸{(diào)用,可能會造成代碼的語義不夠明確。
  • generator 的方式,它可以在函數(shù)的執(zhí)行過程中,將函數(shù)的執(zhí)行權(quán)轉(zhuǎn)移出去,在函數(shù)外部還可以將執(zhí)行權(quán)轉(zhuǎn)移回來。當(dāng)遇到異步函數(shù)執(zhí)行的時候,將函數(shù)執(zhí)行權(quán)轉(zhuǎn)移出去,當(dāng)異步函數(shù)執(zhí)行完畢時再將執(zhí)行權(quán)給轉(zhuǎn)移回來。因此在 generator 內(nèi)部對于異步操作的方式,可以以同步的順序來書寫。使用這種方式需要考慮的問題是何時將函數(shù)的控制權(quán)轉(zhuǎn)移回來,因此需要有一個自動執(zhí)行 generator 的機制,比如說 co 模塊等方式來實現(xiàn) generator 的自動執(zhí)行。
  • async 函數(shù) 的方式,async 函數(shù)是 generator 和 promise 實現(xiàn)的一個自動執(zhí)行的語法糖,它內(nèi)部自帶執(zhí)行器,當(dāng)函數(shù)內(nèi)部執(zhí)行到一個 await 語句的時候,如果語句返回一個 promise 對象,那么函數(shù)將會等待 promise 對象的狀態(tài)變?yōu)?nbsp;resolve 后再繼續(xù)向下執(zhí)行。因此可以將異步邏輯,轉(zhuǎn)化為同步的順序來書寫,并且這個函數(shù)可以自動執(zhí)行。

2. setTimeout、Promise、Async/Await 的區(qū)別

(1)setTimeout

console.log('script start') //1. 打印 script start
setTimeout(function(){
    console.log('settimeout')   // 4. 打印 settimeout
})  // 2. 調(diào)用 setTimeout 函數(shù),并定義其完成后執(zhí)行的回調(diào)函數(shù)
console.log('script end')   //3. 打印 script start
// 輸出順序:script start->script end->settimeout

(2)Promise

Promise本身是同步的立即執(zhí)行函數(shù), 當(dāng)在executor中執(zhí)行resolve或者reject的時候, 此時是異步操作, 會先執(zhí)行then/catch等,當(dāng)主棧完成后,才會去調(diào)用resolve/reject中存放的方法執(zhí)行,打印p的時候,是打印的返回結(jié)果,一個Promise實例。

console.log('script start')
let promise1 = new Promise(function (resolve) {
    console.log('promise1')
    resolve()
    console.log('promise1 end')
}).then(function () {
    console.log('promise2')
})
setTimeout(function(){
    console.log('settimeout')
})
console.log('script end')
// 輸出順序: script start->promise1->promise1 end->script end->promise2->settimeout

當(dāng)JS主線程執(zhí)行到Promise對象時:

  • promise1.then() 的回調(diào)就是一個 task
  • promise1 是 resolved或rejected: 那這個 task 就會放入當(dāng)前事件循環(huán)回合的 microtask queue
  • promise1 是 pending: 這個 task 就會放入 事件循環(huán)的未來的某個(可能下一個)回合的 microtask queue 中
  • setTimeout 的回調(diào)也是個 task ,它會被放入 macrotask queue 即使是 0ms 的情況

(3)async/await

async function async1(){
   console.log('async1 start');
    await async2();
    console.log('async1 end')
}
async function async2(){
    console.log('async2')
}
console.log('script start');
async1();
console.log('script end')
// 輸出順序:script start->async1 start->async2->script end->async1 end

async 函數(shù)返回一個 Promise 對象,當(dāng)函數(shù)執(zhí)行的時候,一旦遇到 await 就會先返回,等到觸發(fā)的異步操作完成,再執(zhí)行函數(shù)體內(nèi)后面的語句??梢岳斫鉃椋亲尦隽司€程,跳出了 async 函數(shù)體。

例如:

async function func1() {
    return 1
}
console.log(func1())

img

func1的運行結(jié)果其實就是一個Promise對象。因此也可以使用then來處理后續(xù)邏輯。

func1().then(res => {
    console.log(res);  // 30
})

await的含義為等待,也就是 async 函數(shù)需要等待await后的函數(shù)執(zhí)行完成并且有了返回結(jié)果(Promise對象)之后,才能繼續(xù)執(zhí)行下面的代碼。await通過返回一個Promise對象來實現(xiàn)同步的效果。

3. 對Promise的理解

Promise是異步編程的一種解決方案,它是一個對象,可以獲取異步操作的消息,他的出現(xiàn)大大改善了異步編程的困境,避免了地獄回調(diào),它比傳統(tǒng)的解決方案回調(diào)函數(shù)和事件更合理和更強大。

所謂Promise,簡單說就是一個容器,里面保存著某個未來才會結(jié)束的事件(通常是一個異步操作)的結(jié)果。從語法上說,Promise 是一個對象,從它可以獲取異步操作的消息。Promise 提供統(tǒng)一的 API,各種異步操作都可以用同樣的方法進行處理。

(1)Promise的實例有三個狀態(tài):

  • Pending(進行中)
  • Resolved(已完成)
  • Rejected(已拒絕)

當(dāng)把一件事情交給promise時,它的狀態(tài)就是Pending,任務(wù)完成了狀態(tài)就變成了Resolved、沒有完成失敗了就變成了Rejected。

(2)Promise的實例有兩個過程

  • pending -> fulfilled : Resolved(已完成)
  • pending -> rejected:Rejected(已拒絕)

注意:一旦從進行狀態(tài)變成為其他狀態(tài)就永遠不能更改狀態(tài)了。

Promise的特點:

  • 對象的狀態(tài)不受外界影響。promise對象代表一個異步操作,有三種狀態(tài),?pending?(進行中)、?fulfilled?(已成功)、?rejected?(已失敗)。只有異步操作的結(jié)果,可以決定當(dāng)前是哪一種狀態(tài),任何其他操作都無法改變這個狀態(tài),這也是promise這個名字的由來——“承諾”;
  • 一旦狀態(tài)改變就不會再變,任何時候都可以得到這個結(jié)果。promise對象的狀態(tài)改變,只有兩種可能:從 ?pending?變?yōu)?nbsp;?fulfilled?,從 ?pending?變?yōu)?nbsp;?rejected?。這時就稱為 ?resolved?(已定型)。如果改變已經(jīng)發(fā)生了,你再對promise對象添加回調(diào)函數(shù),也會立即得到這個結(jié)果。這與事件(event)完全不同,事件的特點是:如果你錯過了它,再去監(jiān)聽是得不到結(jié)果的。

Promise的缺點:

  • 無法取消Promise,一旦新建它就會立即執(zhí)行,無法中途取消。
  • 如果不設(shè)置回調(diào)函數(shù),Promise內(nèi)部拋出的錯誤,不會反應(yīng)到外部。
  • 當(dāng)處于pending狀態(tài)時,無法得知目前進展到哪一個階段(剛剛開始還是即將完成)。

總結(jié):

Promise 對象是異步編程的一種解決方案,最早由社區(qū)提出。Promise 是一個構(gòu)造函數(shù),接收一個函數(shù)作為參數(shù),返回一個 Promise 實例。一個 Promise 實例有三種狀態(tài),分別是pending、resolved 和 rejected,分別代表了進行中、已成功和已失敗。實例的狀態(tài)只能由 pending 轉(zhuǎn)變 resolved 或者rejected 狀態(tài),并且狀態(tài)一經(jīng)改變,就凝固了,無法再被改變了。

狀態(tài)的改變是通過 resolve() 和 reject() 函數(shù)來實現(xiàn)的,可以在異步操作結(jié)束后調(diào)用這兩個函數(shù)改變 Promise 實例的狀態(tài),它的原型上定義了一個 then 方法,使用這個 then 方法可以為兩個狀態(tài)的改變注冊回調(diào)函數(shù)。這個回調(diào)函數(shù)屬于微任務(wù),會在本輪事件循環(huán)的末尾執(zhí)行。

注意:在構(gòu)造 Promise 的時候,構(gòu)造函數(shù)內(nèi)部的代碼是立即執(zhí)行的

4. Promise的基本用法

(1)創(chuàng)建Promise對象

Promise對象代表一個異步操作,有三種狀態(tài):pending(進行中)、fulfilled(已成功)和rejected(已失?。?。

Promise構(gòu)造函數(shù)接受一個函數(shù)作為參數(shù),該函數(shù)的兩個參數(shù)分別是 resolve和 reject

const promise = new Promise(function(resolve, reject) {
  // ... some code
  if (/* 異步操作成功 */){
    resolve(value);
  } else {
    reject(error);
  }
});

一般情況下都會使用 new Promise()來創(chuàng)建promise對象,但是也可以使用 promise.resolve promise.reject這兩個方法:

  • Promise.resolve

Promise.resolve(value)的返回值也是一個promise對象,可以對返回值進行.then調(diào)用,代碼如下:

Promise.resolve(11).then(function(value){
  console.log(value); // 打印出11
});

resolve(11)代碼中,會讓promise對象進入確定(resolve狀態(tài)),并將參數(shù) 11傳遞給后面的 then所指定的 onFulfilled 函數(shù);

創(chuàng)建promise對象可以使用 new Promise的形式創(chuàng)建對象,也可以使用 Promise.resolve(value)的形式創(chuàng)建promise對象;

  • Promise.reject

Promise.reject 也是 new Promise的快捷形式,也創(chuàng)建一個promise對象。代碼如下:

Promise.reject(new Error(“我錯了,請原諒俺!!”));

就是下面的代碼new Promise的簡單形式:

new Promise(function(resolve,reject){
   reject(new Error("我錯了,請原諒俺??!"));
});

下面是使用resolve方法和reject方法:

function testPromise(ready) {
  return new Promise(function(resolve,reject){
    if(ready) {
      resolve("hello world");
    }else {
      reject("No thanks");
    }
  });
};
// 方法調(diào)用
testPromise(true).then(function(msg){
  console.log(msg);
},function(error){
  console.log(error);
});

上面的代碼的含義是給 testPromise方法傳遞一個參數(shù),返回一個promise對象,如果為 true的話,那么調(diào)用promise對象中的 resolve()方法,并且把其中的參數(shù)傳遞給后面的 then第一個函數(shù)內(nèi),因此打印出 “hello world”, 如果為 false的話,會調(diào)用promise對象中的 reject()方法,則會進入 then的第二個函數(shù)內(nèi),會打印 No thanks;

(2)Promise方法

Promise有五個常用的方法:then()、catch()、all()、race()、finally。下面就來看一下這些方法。

  • then()

當(dāng)Promise執(zhí)行的內(nèi)容符合成功條件時,調(diào)用 resolve函數(shù),失敗就調(diào)用 reject函數(shù)。Promise創(chuàng)建完了,那該如何調(diào)用呢?

promise.then(function(value) {
  // success
}, function(error) {
  // failure
});

then方法可以接受兩個回調(diào)函數(shù)作為參數(shù)。第一個回調(diào)函數(shù)是Promise對象的狀態(tài)變?yōu)?nbsp;resolved時調(diào)用,第二個回調(diào)函數(shù)是Promise對象的狀態(tài)變?yōu)?nbsp;rejected時調(diào)用。其中第二個參數(shù)可以省略。

then方法返回的是一個新的Promise實例(不是原來那個Promise實例)。因此可以采用鏈?zhǔn)綄懛ǎ?nbsp;then方法后面再調(diào)用另一個then方法。

當(dāng)要寫有順序的異步事件時,需要串行時,可以這樣寫:

let promise = new Promise((resolve,reject)=>{
    ajax('first').success(function(res){
        resolve(res);
    })
})
promise.then(res=>{
    return new Promise((resovle,reject)=>{
        ajax('second').success(function(res){
            resolve(res)
        })
    })
}).then(res=>{
    return new Promise((resovle,reject)=>{
        ajax('second').success(function(res){
            resolve(res)
        })
    })
}).then(res=>{
  
})

那當(dāng)要寫的事件沒有順序或者關(guān)系時,還如何寫呢?可以使用 all 方法來解決。

  • catch()

Promise對象除了有then方法,還有一個catch方法,該方法相當(dāng)于 then方法的第二個參數(shù),指向 reject的回調(diào)函數(shù)。不過 catch方法還有一個作用,就是在執(zhí)行 resolve回調(diào)函數(shù)時,如果出現(xiàn)錯誤,拋出異常,不會停止運行,而是進入 catch方法中。

p.then((data) => {
     console.log('resolved',data);
},(err) => {
     console.log('rejected',err);
     }
); 
p.then((data) => {
    console.log('resolved',data);
}).catch((err) => {
    console.log('rejected',err);
});
  • all()

all方法可以完成并行任務(wù), 它接收一個數(shù)組,數(shù)組的每一項都是一個 promise對象。當(dāng)數(shù)組中所有的 promise的狀態(tài)都達到 resolved的時候,all方法的狀態(tài)就會變成 resolved,如果有一個狀態(tài)變成了 rejected,那么 all方法的狀態(tài)就會變成 rejected。

javascript
let promise1 = new Promise((resolve,reject)=>{
    setTimeout(()=>{
       resolve(1);
    },2000)
});
let promise2 = new Promise((resolve,reject)=>{
    setTimeout(()=>{
       resolve(2);
    },1000)
});
let promise3 = new Promise((resolve,reject)=>{
    setTimeout(()=>{
       resolve(3);
    },3000)
});
Promise.all([promise1,promise2,promise3]).then(res=>{
    console.log(res);
    //結(jié)果為:[1,2,3] 
})
function all(promises){
  return new Promise((resolve,reject) => {
    let lens = promises.length;
    let count = 0;
    let res = [];
    for(let i = 0; i < lens; i++){
      promises[i].then(val => {
        count++;
        res.push(val);
        if(count === lens){
          resolve(res)
        }
      }).catch(error => {
        reject(error)
      })
    }
  })
}

調(diào)用 all方法時的結(jié)果成功的時候是回調(diào)函數(shù)的參數(shù)也是一個數(shù)組,這個數(shù)組按順序保存著每一個promise對象 resolve執(zhí)行時的值。

  • race()

race方法和 all一樣,接受的參數(shù)是一個每項都是 promise的數(shù)組,但是與 all不同的是,當(dāng)最先執(zhí)行完的事件執(zhí)行完之后,就直接返回該 promise對象的值。如果第一個 promise對象狀態(tài)變成 resolved,那自身的狀態(tài)變成了 resolved;反之第一個 promise變成 rejected,那自身狀態(tài)就會變成 rejected。

let promise1 = new Promise((resolve,reject)=>{
    setTimeout(()=>{
       reject(1);
    },2000)
});
let promise2 = new Promise((resolve,reject)=>{
    setTimeout(()=>{
       resolve(2);
    },1000)
});
let promise3 = new Promise((resolve,reject)=>{
    setTimeout(()=>{
       resolve(3);
    },3000)
});
Promise.race([promise1,promise2,promise3]).then(res=>{
    console.log(res);
    //結(jié)果:2
},rej=>{
    console.log(rej)};
)

那么 race方法有什么實際作用呢?當(dāng)要做一件事,超過多長時間就不做了,可以用這個方法來解決:

Promise.race([promise1,timeOutPromise(5000)]).then(res=>{})
  • finally()

finally方法用于指定不管 Promise 對象最后狀態(tài)如何,都會執(zhí)行的操作。該方法是 ES2018 引入標(biāo)準(zhǔn)的。

promise
.then(result => {···})
.catch(error => {···})
.finally(() => {···});

上面代碼中,不管 promise最后的狀態(tài),在執(zhí)行完 then或 catch指定的回調(diào)函數(shù)以后,都會執(zhí)行 finally方法指定的回調(diào)函數(shù)。

下面是一個例子,服務(wù)器使用 Promise 處理請求,然后使用 finally方法關(guān)掉服務(wù)器。

server.listen(port)
  .then(function () {
    // ...
  })
  .finally(server.stop);

finally方法的回調(diào)函數(shù)不接受任何參數(shù),這意味著沒有辦法知道,前面的 Promise 狀態(tài)到底是 fulfilled還是 rejected。這表明,finally方法里面的操作,應(yīng)該是與狀態(tài)無關(guān)的,不依賴于 Promise 的執(zhí)行結(jié)果。finally本質(zhì)上是 then方法的特例:

promise
.finally(() => {
  // 語句
});
// 等同于
promise
.then(
  result => {
    // 語句
    return result;
  },
  error => {
    // 語句
    throw error;
  }
);

上面代碼中,如果不使用 finally方法,同樣的語句需要為成功和失敗兩種情況各寫一次。有了 finally方法,則只需要寫一次。

5. Promise解決了什么問題

在工作中經(jīng)常會碰到這樣一個需求,比如我使用ajax發(fā)一個A請求后,成功后拿到數(shù)據(jù),需要把數(shù)據(jù)傳給B請求;那么需要如下編寫代碼:

let fs = require('fs')
fs.readFile('./a.txt','utf8',function(err,data){
  fs.readFile(data,'utf8',function(err,data){
    fs.readFile(data,'utf8',function(err,data){
      console.log(data)
    })
  })
})

上面的代碼有如下缺點:

  • 后一個請求需要依賴于前一個請求成功后,將數(shù)據(jù)往下傳遞,會導(dǎo)致多個ajax請求嵌套的情況,代碼不夠直觀。
  • 如果前后兩個請求不需要傳遞參數(shù)的情況下,那么后一個請求也需要前一個請求成功后再執(zhí)行下一步操作,這種情況下,那么也需要如上編寫代碼,導(dǎo)致代碼不夠直觀。

Promise出現(xiàn)之后,代碼變成這樣:

let fs = require('fs')
function read(url){
  return new Promise((resolve,reject)=>{
    fs.readFile(url,'utf8',function(error,data){
      error && reject(error)
      resolve(data)
    })
  })
}
read('./a.txt').then(data=>{
  return read(data) 
}).then(data=>{
  return read(data)  
}).then(data=>{
  console.log(data)
})

這樣代碼看起了就簡潔了很多,解決了地獄回調(diào)的問題。

6. Promise.all和Promise.race的區(qū)別的使用場景

(1)Promise.all

Promise.all可以將多個 Promise實例包裝成一個新的Promise實例。同時,成功和失敗的返回值是不同的,成功的時候返回的是一個結(jié)果數(shù)組,而失敗的時候則返回最先被reject失敗狀態(tài)的值。

Promise.all中傳入的是數(shù)組,返回的也是是數(shù)組,并且會將進行映射,傳入的promise對象返回的值是按照順序在數(shù)組中排列的,但是注意的是他們執(zhí)行的順序并不是按照順序的,除非可迭代對象為空。

需要注意,Promise.all獲得的成功結(jié)果的數(shù)組里面的數(shù)據(jù)順序和Promise.all接收到的數(shù)組順序是一致的,這樣當(dāng)遇到發(fā)送多個請求并根據(jù)請求順序獲取和使用數(shù)據(jù)的場景,就可以使用Promise.all來解決。

(2)Promise.race

顧名思義,Promse.race就是賽跑的意思,意思就是說,Promise.race([p1, p2, p3])里面哪個結(jié)果獲得的快,就返回那個結(jié)果,不管結(jié)果本身是成功狀態(tài)還是失敗狀態(tài)。當(dāng)要做一件事,超過多長時間就不做了,可以用這個方法來解決:

Promise.race([promise1,timeOutPromise(5000)]).then(res=>{})

7. 對async/await 的理解

async/await其實是 Generator 的語法糖,它能實現(xiàn)的效果都能用then鏈來實現(xiàn),它是為優(yōu)化then鏈而開發(fā)出來的。從字面上來看,async是“異步”的簡寫,await則為等待,所以很好理解async 用于申明一個 function 是異步的,而 await 用于等待一個異步方法執(zhí)行完成。當(dāng)然語法上強制規(guī)定await只能出現(xiàn)在asnyc函數(shù)中,先來看看async函數(shù)返回了什么:

async function testAsy(){
   return 'hello world';
}
let result = testAsy(); 
console.log(result)

img

所以,async 函數(shù)返回的是一個 Promise 對象。async 函數(shù)(包含函數(shù)語句、函數(shù)表達式、Lambda表達式)會返回一個 Promise 對象,如果在函數(shù)中 return 一個直接量,async 會把這個直接量通過 Promise.resolve() 封裝成 Promise 對象。

async 函數(shù)返回的是一個 Promise 對象,所以在最外層不能用 await 獲取其返回值的情況下,當(dāng)然應(yīng)該用原來的方式:then() 鏈來處理這個 Promise 對象,就像這樣:

async function testAsy(){
   return 'hello world'
}
let result = testAsy() 
console.log(result)
result.then(v=>{
    console.log(v)   // hello world
})

那如果 async 函數(shù)沒有返回值,又該如何?很容易想到,它會返回 Promise.resolve(undefined)。

聯(lián)想一下 Promise 的特點——無等待,所以在沒有 await 的情況下執(zhí)行 async 函數(shù),它會立即執(zhí)行,返回一個 Promise 對象,并且,絕不會阻塞后面的語句。這和普通返回 Promise 對象的函數(shù)并無二致。

注意:Promise.resolve(x) 可以看作是 new Promise(resolve => resolve(x)) 的簡寫,可以用于快速封裝字面量對象或其他對象,將其封裝成 Promise 實例。

8. await 到底在等啥?

await 在等待什么呢?一般來說,都認為 await 是在等待一個 async 函數(shù)完成。不過按語法說明,await 等待的是一個表達式,這個表達式的計算結(jié)果是 Promise 對象或者其它值(換句話說,就是沒有特殊限定)。

因為 async 函數(shù)返回一個 Promise 對象,所以 await 可以用于等待一個 async 函數(shù)的返回值——這也可以說是 await 在等 async 函數(shù),但要清楚,它等的實際是一個返回值。注意到 await 不僅僅用于等 Promise 對象,它可以等任意表達式的結(jié)果,所以,await 后面實際是可以接普通函數(shù)調(diào)用或者直接量的。所以下面這個示例完全可以正確運行:

function getSomething() {
    return "something";
}
async function testAsync() {
    return Promise.resolve("hello async");
}
async function test() {
    const v1 = await getSomething();
    const v2 = await testAsync();
    console.log(v1, v2);
}
test();

await 表達式的運算結(jié)果取決于它等的是什么。

  • 如果它等到的不是一個 Promise 對象,那 await 表達式的運算結(jié)果就是它等到的東西。
  • 如果它等到的是一個 Promise 對象,await 就忙起來了,它會阻塞后面的代碼,等著 Promise 對象 resolve,然后得到 resolve 的值,作為 await 表達式的運算結(jié)果。

來看一個例子:

function testAsy(x){
   return new Promise(resolve=>{setTimeout(() => {
       resolve(x);
     }, 3000)
    }
   )
}
async function testAwt(){  
  let result =  await testAsy('hello world');
  console.log(result);    // 3秒鐘之后出現(xiàn)hello world
  console.log('cuger')   // 3秒鐘之后出現(xiàn)cug
}
testAwt();
console.log('cug')  //立即輸出cug

這就是 await 必須用在 async 函數(shù)中的原因。async 函數(shù)調(diào)用不會造成阻塞,它內(nèi)部所有的阻塞都被封裝在一個 Promise 對象中異步執(zhí)行。await暫停當(dāng)前async的執(zhí)行,所以'cug''最先輸出,hello world'和‘cuger’是3秒鐘后同時出現(xiàn)的。

9. async/await的優(yōu)勢

單一的 Promise 鏈并不能發(fā)現(xiàn) async/await 的優(yōu)勢,但是,如果需要處理由多個 Promise 組成的 then 鏈的時候,優(yōu)勢就能體現(xiàn)出來了(很有意思,Promise 通過 then 鏈來解決多層回調(diào)的問題,現(xiàn)在又用 async/await 來進一步優(yōu)化它)。

假設(shè)一個業(yè)務(wù),分多個步驟完成,每個步驟都是異步的,而且依賴于上一個步驟的結(jié)果。仍然用 setTimeout 來模擬異步操作:

/**
 * 傳入?yún)?shù) n,表示這個函數(shù)執(zhí)行的時間(毫秒)
 * 執(zhí)行的結(jié)果是 n + 200,這個值將用于下一步驟
 */
function takeLongTime(n) {
    return new Promise(resolve => {
        setTimeout(() => resolve(n + 200), n);
    });
}
function step1(n) {
    console.log(`step1 with ${n}`);
    return takeLongTime(n);
}
function step2(n) {
    console.log(`step2 with ${n}`);
    return takeLongTime(n);
}
function step3(n) {
    console.log(`step3 with ${n}`);
    return takeLongTime(n);
}

現(xiàn)在用 Promise 方式來實現(xiàn)這三個步驟的處理:

function doIt() {
    console.time("doIt");
    const time1 = 300;
    step1(time1)
        .then(time2 => step2(time2))
        .then(time3 => step3(time3))
        .then(result => {
            console.log(`result is ${result}`);
            console.timeEnd("doIt");
        });
}
doIt();
// c:\var\test>node --harmony_async_await .
// step1 with 300
// step2 with 500
// step3 with 700
// result is 900
// doIt: 1507.251ms

如果用 async/await 來實現(xiàn)呢,會是這樣:

async function doIt() {
    console.time("doIt");
    const time1 = 300;
    const time2 = await step1(time1);
    const time3 = await step2(time2);
    const result = await step3(time3);
    console.log(`result is ${result}`);
    console.timeEnd("doIt");
}
doIt();

結(jié)果和之前的 Promise 實現(xiàn)是一樣的,但是這個代碼看起來是不是清晰得多,幾乎跟同步代碼一樣

10. async/await對比Promise的優(yōu)勢

  • 代碼讀起來更加同步,Promise雖然擺脫了回調(diào)地獄,但是then的鏈?zhǔn)秸{(diào)?也會帶來額外的閱讀負擔(dān)
  • Promise傳遞中間值?常麻煩,?async/await?乎是同步的寫法,?常優(yōu)雅
  • 錯誤處理友好,async/await可以?成熟的try/catch,Promise的錯誤捕獲?常冗余
  • 調(diào)試友好,Promise的調(diào)試很差,由于沒有代碼塊,你不能在?個返回表達式的箭頭函數(shù)中設(shè)置斷點,如果你在?個.then代碼塊中使?調(diào)試器的步進(step-over)功能,調(diào)試器并不會進?后續(xù)的.then代碼塊,因為調(diào)試器只能跟蹤同步代碼的每?步。

11. async/await 如何捕獲異常

async function fn(){
    try{
        let a = await Promise.reject('error')
    }catch(error){
        console.log(error)
    }
}

12. 并發(fā)與并行的區(qū)別?

  • 并發(fā)是宏觀概念,我分別有任務(wù) A 和任務(wù) B,在一段時間內(nèi)通過任務(wù)間的切換完成了這兩個任務(wù),這種情況就可以稱之為并發(fā)。
  • 并行是微觀概念,假設(shè) CPU 中存在兩個核心,那么我就可以同時完成任務(wù) A、B。同時完成多個任務(wù)的情況就可以稱之為并行。

13. 什么是回調(diào)函數(shù)?回調(diào)函數(shù)有什么缺點?如何解決回調(diào)地獄問題?

以下代碼就是一個回調(diào)函數(shù)的例子:

ajax(url, () => {
    // 處理邏輯
})

回調(diào)函數(shù)有一個致命的弱點,就是容易寫出回調(diào)地獄(Callback hell)。假設(shè)多個請求存在依賴性,可能會有如下代碼:

ajax(url, () => {
    // 處理邏輯
    ajax(url1, () => {
        // 處理邏輯
        ajax(url2, () => {
            // 處理邏輯
        })
    })
})

以上代碼看起來不利于閱讀和維護,當(dāng)然,也可以把函數(shù)分開來寫:

function firstAjax() {
  ajax(url1, () => {
    // 處理邏輯
    secondAjax()
  })
}
function secondAjax() {
  ajax(url2, () => {
    // 處理邏輯
  })
}
ajax(url, () => {
  // 處理邏輯
  firstAjax()
})

以上的代碼雖然看上去利于閱讀了,但是還是沒有解決根本問題?;卣{(diào)地獄的根本問題就是:

  1. 嵌套函數(shù)存在耦合性,一旦有所改動,就會牽一發(fā)而動全身
  2. 嵌套函數(shù)一多,就很難處理錯誤

當(dāng)然,回調(diào)函數(shù)還存在著別的幾個缺點,比如不能使用 try catch 捕獲錯誤,不能直接 return。

14. setTimeout、setInterval、requestAnimationFrame 各有什么特點?

異步編程當(dāng)然少不了定時器了,常見的定時器函數(shù)有 setTimeout、setInterval、requestAnimationFrame。最常用的是 setTimeout,很多人認為 setTimeout 是延時多久,那就應(yīng)該是多久后執(zhí)行。

其實這個觀點是錯誤的,因為 JS 是單線程執(zhí)行的,如果前面的代碼影響了性能,就會導(dǎo)致 setTimeout 不會按期執(zhí)行。當(dāng)然了,可以通過代碼去修正 setTimeout,從而使定時器相對準(zhǔn)確:

let period = 60 * 1000 * 60 * 2
let startTime = new Date().getTime()
let count = 0
let end = new Date().getTime() + period
let interval = 1000
let currentInterval = interval
function loop() {
  count++
  // 代碼執(zhí)行所消耗的時間
  let offset = new Date().getTime() - (startTime + count * interval);
  let diff = end - new Date().getTime()
  let h = Math.floor(diff / (60 * 1000 * 60))
  let hdiff = diff % (60 * 1000 * 60)
  let m = Math.floor(hdiff / (60 * 1000))
  let mdiff = hdiff % (60 * 1000)
  let s = mdiff / (1000)
  let sCeil = Math.ceil(s)
  let sFloor = Math.floor(s)
  // 得到下一次循環(huán)所消耗的時間
  currentInterval = interval - offset 
  console.log('時:'+h, '分:'+m, '毫秒:'+s, '秒向上取整:'+sCeil, '代碼執(zhí)行時間:'+offset, '下次循環(huán)間隔'+currentInterval) 
  setTimeout(loop, currentInterval)
}
setTimeout(loop, currentInterval)

接下來看 setInterval,其實這個函數(shù)作用和 setTimeout 基本一致,只是該函數(shù)是每隔一段時間執(zhí)行一次回調(diào)函數(shù)。

通常來說不建議使用 setInterval。第一,它和 setTimeout 一樣,不能保證在預(yù)期的時間執(zhí)行任務(wù)。第二,它存在執(zhí)行累積的問題,請看以下偽代碼

function demo() {
  setInterval(function(){
    console.log(2)
  },1000)
  sleep(2000)
}
demo()

以上代碼在瀏覽器環(huán)境中,如果定時器執(zhí)行過程中出現(xiàn)了耗時操作,多個回調(diào)函數(shù)會在耗時操作結(jié)束以后同時執(zhí)行,這樣可能就會帶來性能上的問題。

如果有循環(huán)定時器的需求,其實完全可以通過 requestAnimationFrame 來實現(xiàn):

function setInterval(callback, interval) {
  let timer
  const now = Date.now
  let startTime = now()
  let endTime = startTime
  const loop = () => {
    timer = window.requestAnimationFrame(loop)
    endTime = now()
    if (endTime - startTime >= interval) {
      startTime = endTime = now()
      callback(timer)
    }
  }
  timer = window.requestAnimationFrame(loop)
  return timer
}
let a = 0
setInterval(timer => {
  console.log(1)
  a++
  if (a === 3) cancelAnimationFrame(timer)
}, 1000)

首先 requestAnimationFrame 自帶函數(shù)節(jié)流功能,基本可以保證在 16.6 毫秒內(nèi)只執(zhí)行一次(不掉幀的情況下),并且該函數(shù)的延時效果是精確的,沒有其他定時器時間不準(zhǔn)的問題,當(dāng)然你也可以通過該函數(shù)來實現(xiàn) setTimeout。

八、面向?qū)ο?/h2>

1. 對象創(chuàng)建的方式有哪些?

一般使用字面量的形式直接創(chuàng)建對象,但是這種創(chuàng)建方式對于創(chuàng)建大量相似對象的時候,會產(chǎn)生大量的重復(fù)代碼。但 js和一般的面向?qū)ο蟮恼Z言不同,在 ES6 之前它沒有類的概念。但是可以使用函數(shù)來進行模擬,從而產(chǎn)生出可復(fù)用的對象創(chuàng)建方式,常見的有以下幾種:

(1)第一種是工廠模式,工廠模式的主要工作原理是用函數(shù)來封裝創(chuàng)建對象的細節(jié),從而通過調(diào)用函數(shù)來達到復(fù)用的目的。但是它有一個很大的問題就是創(chuàng)建出來的對象無法和某個類型聯(lián)系起來,它只是簡單的封裝了復(fù)用代碼,而沒有建立起對象和類型間的關(guān)系。

(2)第二種是構(gòu)造函數(shù)模式。js 中每一個函數(shù)都可以作為構(gòu)造函數(shù),只要一個函數(shù)是通過 new 來調(diào)用的,那么就可以把它稱為構(gòu)造函數(shù)。執(zhí)行構(gòu)造函數(shù)首先會創(chuàng)建一個對象,然后將對象的原型指向構(gòu)造函數(shù)的 prototype 屬性,然后將執(zhí)行上下文中的 this 指向這個對象,最后再執(zhí)行整個函數(shù),如果返回值不是對象,則返回新建的對象。因為 this 的值指向了新建的對象,因此可以使用 this 給對象賦值。構(gòu)造函數(shù)模式相對于工廠模式的優(yōu)點是,所創(chuàng)建的對象和構(gòu)造函數(shù)建立起了聯(lián)系,因此可以通過原型來識別對象的類型。但是構(gòu)造函數(shù)存在一個缺點就是,造成了不必要的函數(shù)對象的創(chuàng)建,因為在 js 中函數(shù)也是一個對象,因此如果對象屬性中如果包含函數(shù)的話,那么每次都會新建一個函數(shù)對象,浪費了不必要的內(nèi)存空間,因為函數(shù)是所有的實例都可以通用的。

(3)第三種模式是原型模式,因為每一個函數(shù)都有一個 prototype 屬性,這個屬性是一個對象,它包含了通過構(gòu)造函數(shù)創(chuàng)建的所有實例都能共享的屬性和方法。因此可以使用原型對象來添加公用屬性和方法,從而實現(xiàn)代碼的復(fù)用。這種方式相對于構(gòu)造函數(shù)模式來說,解決了函數(shù)對象的復(fù)用問題。但是這種模式也存在一些問題,一個是沒有辦法通過傳入?yún)?shù)來初始化值,另一個是如果存在一個引用類型如 Array 這樣的值,那么所有的實例將共享一個對象,一個實例對引用類型值的改變會影響所有的實例。

(4)第四種模式是組合使用構(gòu)造函數(shù)模式和原型模式,這是創(chuàng)建自定義類型的最常見方式。因為構(gòu)造函數(shù)模式和原型模式分開使用都存在一些問題,因此可以組合使用這兩種模式,通過構(gòu)造函數(shù)來初始化對象的屬性,通過原型對象來實現(xiàn)函數(shù)方法的復(fù)用。這種方法很好的解決了兩種模式單獨使用時的缺點,但是有一點不足的就是,因為使用了兩種不同的模式,所以對于代碼的封裝性不夠好。

(5)第五種模式是動態(tài)原型模式,這一種模式將原型方法賦值的創(chuàng)建過程移動到了構(gòu)造函數(shù)的內(nèi)部,通過對屬性是否存在的判斷,可以實現(xiàn)僅在第一次調(diào)用函數(shù)時對原型對象賦值一次的效果。這一種方式很好地對上面的混合模式進行了封裝。

(6)第六種模式是寄生構(gòu)造函數(shù)模式,這一種模式和工廠模式的實現(xiàn)基本相同,我對這個模式的理解是,它主要是基于一個已有的類型,在實例化時對實例化的對象進行擴展。這樣既不用修改原來的構(gòu)造函數(shù),也達到了擴展對象的目的。它的一個缺點和工廠模式一樣,無法實現(xiàn)對象的識別。

2. 對象繼承的方式有哪些?

(1)第一種是以原型鏈的方式來實現(xiàn)繼承,但是這種實現(xiàn)方式存在的缺點是,在包含有引用類型的數(shù)據(jù)時,會被所有的實例對象所共享,容易造成修改的混亂。還有就是在創(chuàng)建子類型的時候不能向超類型傳遞參數(shù)。

(2)第二種方式是使用借用構(gòu)造函數(shù)的方式,這種方式是通過在子類型的函數(shù)中調(diào)用超類型的構(gòu)造函數(shù)來實現(xiàn)的,這一種方法解決了不能向超類型傳遞參數(shù)的缺點,但是它存在的一個問題就是無法實現(xiàn)函數(shù)方法的復(fù)用,并且超類型原型定義的方法子類型也沒有辦法訪問到。

(3)第三種方式是組合繼承,組合繼承是將原型鏈和借用構(gòu)造函數(shù)組合起來使用的一種方式。通過借用構(gòu)造函數(shù)的方式來實現(xiàn)類型的屬性的繼承,通過將子類型的原型設(shè)置為超類型的實例來實現(xiàn)方法的繼承。這種方式解決了上面的兩種模式單獨使用時的問題,但是由于我們是以超類型的實例來作為子類型的原型,所以調(diào)用了兩次超類的構(gòu)造函數(shù),造成了子類型的原型中多了很多不必要的屬性。

(4)第四種方式是原型式繼承,原型式繼承的主要思路就是基于已有的對象來創(chuàng)建新的對象,實現(xiàn)的原理是,向函數(shù)中傳入一個對象,然后返回一個以這個對象為原型的對象。這種繼承的思路主要不是為了實現(xiàn)創(chuàng)造一種新的類型,只是對某個對象實現(xiàn)一種簡單繼承,ES5 中定義的 Object.create() 方法就是原型式繼承的實現(xiàn)。缺點與原型鏈方式相同。

(5)第五種方式是寄生式繼承,寄生式繼承的思路是創(chuàng)建一個用于封裝繼承過程的函數(shù),通過傳入一個對象,然后復(fù)制一個對象的副本,然后對象進行擴展,最后返回這個對象。這個擴展的過程就可以理解是一種繼承。這種繼承的優(yōu)點就是對一個簡單對象實現(xiàn)繼承,如果這個對象不是自定義類型時。缺點是沒有辦法實現(xiàn)函數(shù)的復(fù)用。

(6)第六種方式是寄生式組合繼承,組合繼承的缺點就是使用超類型的實例做為子類型的原型,導(dǎo)致添加了不必要的原型屬性。寄生式組合繼承的方式是使用超類型的原型的副本來作為子類型的原型,這樣就避免了創(chuàng)建不必要的屬性。

九、垃圾回收與內(nèi)存泄漏


1. 瀏覽器的垃圾回收機制

(1)垃圾回收的概念

垃圾回收:JavaScript代碼運行時,需要分配內(nèi)存空間來儲存變量和值。當(dāng)變量不在參與運行時,就需要系統(tǒng)收回被占用的內(nèi)存空間,這就是垃圾回收。

回收機制

  • Javascript 具有自動垃圾回收機制,會定期對那些不再使用的變量、對象所占用的內(nèi)存進行釋放,原理就是找到不再使用的變量,然后釋放掉其占用的內(nèi)存。
  • JavaScript中存在兩種變量:局部變量和全局變量。全局變量的生命周期會持續(xù)要頁面卸載;而局部變量聲明在函數(shù)中,它的生命周期從函數(shù)執(zhí)行開始,直到函數(shù)執(zhí)行結(jié)束,在這個過程中,局部變量會在堆或棧中存儲它們的值,當(dāng)函數(shù)執(zhí)行結(jié)束后,這些局部變量不再被使用,它們所占有的空間就會被釋放。
  • 不過,當(dāng)局部變量被外部函數(shù)使用時,其中一種情況就是閉包,在函數(shù)執(zhí)行結(jié)束后,函數(shù)外部的變量依然指向函數(shù)內(nèi)部的局部變量,此時局部變量依然在被使用,所以不會回收。

(2)垃圾回收的方式

瀏覽器通常使用的垃圾回收方法有兩種:標(biāo)記清除,引用計數(shù)。

1)標(biāo)記清除

  • 標(biāo)記清除是瀏覽器常見的垃圾回收方式,當(dāng)變量進入執(zhí)行環(huán)境時,就標(biāo)記這個變量“進入環(huán)境”,被標(biāo)記為“進入環(huán)境”的變量是不能被回收的,因為他們正在被使用。當(dāng)變量離開環(huán)境時,就會被標(biāo)記為“離開環(huán)境”,被標(biāo)記為“離開環(huán)境”的變量會被內(nèi)存釋放。
  • 垃圾收集器在運行的時候會給存儲在內(nèi)存中的所有變量都加上標(biāo)記。然后,它會去掉環(huán)境中的變量以及被環(huán)境中的變量引用的標(biāo)記。而在此之后再被加上標(biāo)記的變量將被視為準(zhǔn)備刪除的變量,原因是環(huán)境中的變量已經(jīng)無法訪問到這些變量了。最后。垃圾收集器完成內(nèi)存清除工作,銷毀那些帶標(biāo)記的值,并回收他們所占用的內(nèi)存空間。

2)引用計數(shù)

  • 另外一種垃圾回收機制就是引用計數(shù),這個用的相對較少。引用計數(shù)就是跟蹤記錄每個值被引用的次數(shù)。當(dāng)聲明了一個變量并將一個引用類型賦值給該變量時,則這個值的引用次數(shù)就是1。相反,如果包含對這個值引用的變量又取得了另外一個值,則這個值的引用次數(shù)就減1。當(dāng)這個引用次數(shù)變?yōu)?時,說明這個變量已經(jīng)沒有價值,因此,在在機回收期下次再運行時,這個變量所占有的內(nèi)存空間就會被釋放出來。
  • 這種方法會引起循環(huán)引用的問題:例如: obj1和 obj2通過屬性進行相互引用,兩個對象的引用次數(shù)都是2。當(dāng)使用循環(huán)計數(shù)時,由于函數(shù)執(zhí)行完后,兩個對象都離開作用域,函數(shù)執(zhí)行結(jié)束,obj1和 obj2還將會繼續(xù)存在,因此它們的引用次數(shù)永遠不會是0,就會引起循環(huán)引用。
function fun() {
    let obj1 = {};
    let obj2 = {};
    obj1.a = obj2; // obj1 引用 obj2
    obj2.a = obj1; // obj2 引用 obj1
}

這種情況下,就要手動釋放變量占用的內(nèi)存:

obj1.a =  null
 obj2.a =  null

(3)減少垃圾回收

雖然瀏覽器可以進行垃圾自動回收,但是當(dāng)代碼比較復(fù)雜時,垃圾回收所帶來的代價比較大,所以應(yīng)該盡量減少垃圾回收。

  • 對數(shù)組進行優(yōu)化:在清空一個數(shù)組時,最簡單的方法就是給其賦值為[ ],但是與此同時會創(chuàng)建一個新的空對象,可以將數(shù)組的長度設(shè)置為0,以此來達到清空數(shù)組的目的。
  • 對object進行優(yōu)化:對象盡量復(fù)用,對于不再使用的對象,就將其設(shè)置為null,盡快被回收。
  • 對函數(shù)進行優(yōu)化:在循環(huán)中的函數(shù)表達式,如果可以復(fù)用,盡量放在函數(shù)的外面。

2. 哪些情況會導(dǎo)致內(nèi)存泄漏

以下四種情況會造成內(nèi)存的泄漏:

  • 意外的全局變量:由于使用未聲明的變量,而意外的創(chuàng)建了一個全局變量,而使這個變量一直留在內(nèi)存中無法被回收。
  • 被遺忘的計時器或回調(diào)函數(shù):設(shè)置了 setInterval 定時器,而忘記取消它,如果循環(huán)函數(shù)有對外部變量的引用的話,那么這個變量會被一直留在內(nèi)存中,而無法被回收。
  • 脫離 DOM 的引用:獲取一個 DOM 元素的引用,而后面這個元素被刪除,由于一直保留了對這個元素的引用,所以它也無法被回收。
  • 閉包:不合理的使用閉包,從而導(dǎo)致某些變量一直被留在內(nèi)存當(dāng)中。


以上內(nèi)容是否對您有幫助:
在線筆記
App下載
App下載

掃描二維碼

下載編程獅App

公眾號
微信公眾號

編程獅公眾號