前端面試 JavaScript篇

2023-02-17 10:51 更新

一、數(shù)據(jù)類(lèi)型


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

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

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

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

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

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

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

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

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

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

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

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

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

(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ù)組、對(duì)象、null都會(huì)被判斷為object,其他判斷都正確。

(2)instanceof

instanceof可以正確判斷對(duì)象的類(lèi)型,其內(nèi)部運(yùn)行機(jī)制是判斷在其原型鏈中能否找到該類(lè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ù)類(lèi)型,而不能判斷基本數(shù)據(jù)類(lèi)型。instanceof 運(yùn)算符可以用來(lái)測(cè)試一個(gè)對(duì)象在其原型鏈中是否存在一個(gè)構(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有兩個(gè)作用,一是判斷數(shù)據(jù)的類(lèi)型,二是對(duì)象實(shí)例通過(guò) constrcutor 對(duì)象訪問(wèn)它的構(gòu)造函數(shù)。需要注意,如果創(chuàng)建一個(gè)對(duì)象來(lái)改變它的原型,constructor就不能用來(lái)判斷數(shù)據(jù)類(lèi)型了:

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 對(duì)象的原型方法 toString 來(lái)判斷數(shù)據(jù)類(lèi)型:

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));

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

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

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

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

4. null和undefined區(qū)別

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

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

undefined 在 JavaScript 中不是一個(gè)保留字,這意味著可以使用 undefined 來(lái)作為一個(gè)變量名,但是這樣的做法是非常危險(xiǎn)的,它會(huì)影響對(duì) undefined 值的判斷。我們可以通過(guò)一些方法獲得安全的 undefined 值,比如說(shuō) void 0。

當(dāng)對(duì)這兩種類(lèi)型使用 typeof 進(jìn)行判斷時(shí),Null 類(lèi)型化會(huì)返回 “object”,這是一個(gè)歷史遺留的問(wèn)題。當(dāng)使用雙等號(hào)對(duì)兩種類(lèi)型的值進(jìn)行比較時(shí)會(huì)返回 true,使用三個(gè)等號(hào)時(shí)會(huì)返回 false。

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

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

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

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

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

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

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

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

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

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

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

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

在開(kāi)發(fā)過(guò)程中遇到類(lèi)似這樣的問(wèn)題:

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

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

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

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

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

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

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

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


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

對(duì)于0.1,它的二進(jìn)制為:

0.00011001100110011001100110011001100110011001100110011001 10011...

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

1.1001100110011001100110011001100110011001100110011001*2^-4

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

1001100110011001100110011001100110011001100110011001

那么問(wèn)題又來(lái)了,指數(shù)位是負(fù)數(shù),該如何保存呢?

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

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

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

所以,0.1表示為:

0 1111111011 1001100110011001100110011001100110011001100110011001

說(shuō)了這么多,是時(shí)候該最開(kāi)始的問(wèn)題了,如何實(shí)現(xiàn)0.1+0.2=0.3呢?

對(duì)于這個(gè)問(wèn)題,一個(gè)直接的解決方法就是設(shè)置一個(gè)誤差范圍,通常稱(chēng)為“機(jī)器精度”。對(duì)JavaScript來(lái)說(shuō),這個(gè)值通常為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 值?

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

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

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

typeof NaN; // "number"

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

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

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

11. == 操作符的強(qiáng)制類(lèi)型轉(zhuǎn)換規(guī)則?

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

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

其流程圖如下:

image

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

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

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

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

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

如果 valueOf() 和 toString() 均不返回基本類(lèi)型值,會(huì)產(chǎn)生 TypeError 錯(cuò)誤。

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

以下這些是假值:

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

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

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

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

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

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

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

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

17. 什么是 JavaScript 中的包裝類(lèi)型?

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

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

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

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

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

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

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

看看如下代碼會(huì)打印出什么:

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

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

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

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

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

type的值為 number或者 string。

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

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

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

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

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

  • 如果對(duì)象為 Date 對(duì)象,則 ?type?默認(rèn)為 ?string?;
  • 其他情況下,?type?默認(rèn)為 ?number?。

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

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

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

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

  1. +操作符的兩邊有至少一個(gè) string類(lèi)型變量時(shí),兩邊的變量都會(huì)被隱式轉(zhuǎn)換為字符串;其他情況下兩邊的變量都會(huì)被轉(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也是一個(gè)數(shù)字
  4. 1 * '23' // 23
     1 * false // 0
     1 / 'aa' // NaN
  5. 對(duì)于==操作符
  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. 對(duì)于<和>比較符
  8. 如果兩邊都是字符串,則比較字母表順序:

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

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

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

    以上說(shuō)的是基本類(lèi)型的隱式轉(zhuǎn)換,而對(duì)象會(huì)被 ToPrimitive轉(zhuǎn)換為基本類(lèi)型再進(jìn)行轉(zhuǎn)換:

    var a = {}
    a > 2 // false

    其對(duì)比過(guò)程如下:

    a.valueOf() // {}, 上面提到過(guò),ToPrimitive默認(rèn)type為number,所以先valueOf,結(jié)果還是個(gè)對(duì)象,下一步
    a.toString() // "[object Object]",現(xiàn)在是一個(gè)字符串了
    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]"

    運(yùn)算過(guò)程如下:

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

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

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

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

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

20. 為什么會(huì)有BigInt的提案?

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

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

擴(kuò)展運(yùn)算符:

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)塊級(jí)作用域:塊作用域由 { }包括,let和const具有塊級(jí)作用域,var不存在塊級(jí)作用域。塊級(jí)作用域解決了ES5中的兩個(gè)問(wèn)題:

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

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

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

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

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

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

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

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

2. const對(duì)象的屬性可以修改嗎

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

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

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

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

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

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

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

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

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

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

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

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

(3)箭頭函數(shù)繼承來(lái)的this指向永遠(yuǎn)不會(huì)改變

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

對(duì)象obj的方法b是使用箭頭函數(shù)定義的,這個(gè)函數(shù)中的this就永遠(yuǎn)指向它定義時(shí)所處的全局執(zhí)行環(huán)境中的this,即便這個(gè)函數(shù)是作為對(duì)象obj的方法調(diào)用,this依舊指向Window對(duì)象。需要注意,定義對(duì)象的大括號(hào) {}是無(wú)法形成一個(gè)單獨(dú)的執(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)說(shuō)過(guò)了,實(shí)際上第二步就是將函數(shù)中的this指向該對(duì)象。 但是由于箭頭函數(shù)時(shí)沒(méi)有自己的this的,且this指向外層的執(zhí)行環(huán)境,且不能改變指向,所以不能當(dāng)做構(gòu)造函數(shù)使用。

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

箭頭函數(shù)沒(méi)有自己的arguments對(duì)象。在箭頭函數(shù)中訪問(wèn)arguments實(shí)際上獲得的是它外層函數(shù)的arguments值。

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

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

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

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

可以?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. 擴(kuò)展運(yùn)算符的作用及使用場(chǎng)景

(1)對(duì)象擴(kuò)展運(yùn)算符

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

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

上述方法實(shí)際上等價(jià)于:

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

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

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

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

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

需要注意:擴(kuò)展運(yùn)算符對(duì)對(duì)象實(shí)例的拷貝屬于淺拷貝。

(2)數(shù)組擴(kuò)展運(yùn)算符

數(shù)組的擴(kuò)展運(yùn)算符可以將一個(gè)數(shù)組轉(zhuǎn)為用逗號(hào)分隔的參數(shù)序列,且每次只能展開(kāi)一層數(shù)組。

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

下面是數(shù)組的擴(kuò)展運(yùn)算符的應(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"]
  • 擴(kuò)展運(yùn)算符與解構(gòu)賦值結(jié)合起來(lái),用于生成數(shù)組
const [first, ...rest] = [1, 2, 3, 4, 5];
first // 1
rest  // [2, 3, 4, 5]

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

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

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

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

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

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

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

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

Proxy 是 ES6 中新增的功能,它可以用來(lái)自定義對(duì)象中的操作。

let p = new Proxy(target, handler)

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

下面來(lái)通過(guò) Proxy 來(lái)實(shí)現(xiàn)一個(gè)數(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)聽(tīng)到屬性${property}改變?yōu)?{v}`)
  },
  (target, property) => {
    console.log(`'${property}' = ${target[property]}`)
  }
)
p.a = 2 // 監(jiān)聽(tīng)到屬性a改變
p.a // 'a' = 2

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

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

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

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

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

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

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

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


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

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

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


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

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

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

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

const { name, age } = stu

這樣就得到了 name 和 age 兩個(gè)和 stu 平級(jí)的變量:


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

const { age, name } = stu

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

有時(shí)會(huì)遇到一些嵌套程度非常深的對(duì)象:

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

像此處的 name 這個(gè)變量,嵌套了四層,此時(shí)如果仍然嘗試?yán)戏椒▉?lái)提取它:

const { name } = school

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

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

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

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

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

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

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

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

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

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

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

11. ES6中模板語(yǔ)法與字符串處理

ES6 提出了“模板語(yǔ)法”的概念。在 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]

僅僅幾個(gè)變量,寫(xiě)了這么多加號(hào),還要時(shí)刻小心里面的空格和標(biāo)點(diǎn)符號(hào)有沒(méi)有跟錯(cuò)地方。但是有了模板字符串,拼接難度直線下降:

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ì)量都變高了。這就是模板字符串的第一個(gè)優(yōu)勢(shì)——允許用${}的方式嵌入變量。但這還不是問(wèn)題的關(guān)鍵,模板字符串的關(guān)鍵優(yōu)勢(shì)有兩個(gè):

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

基于第一點(diǎn),可以在模板字符串里無(wú)障礙地直接寫(xiě) html 代碼:

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

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

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

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

  • 存在性判定:在過(guò)去,當(dāng)判斷一個(gè)字符/字符串是否在某字符串中時(shí),只能用 indexOf > -1 來(lái)做?,F(xiàn)在 ES6 提供了三個(gè)方法:includes、startsWith、endsWith,它們都會(huì)返回一個(gè)布爾值來(lái)告訴你是否存在。
  • includes:判斷字符串與子串的包含關(guān)系:
const son = 'haha' 
const father = 'xixi haha hehe'
father.includes(son) // true
  • startsWith:判斷字符串是否以某個(gè)/某串字符開(kāi)頭:
const father = 'xixi haha hehe'
father.startsWith('haha') // false
father.startsWith('xixi') // true
  • endsWith:判斷字符串是否以某個(gè)/某串字符結(jié)尾:
const father = 'xixi haha hehe'
father.endsWith('hehe') // true
  • 自動(dòng)重復(fù):可以使用 repeat 方法來(lái)使同一個(gè)字符串輸出多次(被連續(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操作符的實(shí)現(xiàn)原理

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

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

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

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

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

具體實(shí)現(xiàn):

function objectFactory() {
  let newObject = null;
  let constructor = Array.prototype.shift.call(arguments);
  let result = null;
  // 判斷參數(shù)是否是一個(gè)函數(shù)
  if (typeof constructor !== "function") {
    console.error("type error");
    return;
  }
  // 新建一個(gè)空對(duì)象,對(duì)象的原型為構(gòu)造函數(shù)的 prototype 對(duì)象
  newObject = Object.create(constructor.prototype);
  // 將 this 指向新建對(duì)象,并執(zhí)行函數(shù)
  result = constructor.apply(newObject, arguments);
  // 判斷返回對(duì)象
  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默認(rèn)情況不包含任何鍵,只包含顯式插入的鍵。 Object 有一個(gè)原型, 原型鏈上的鍵名有可能和自己在對(duì)象上的設(shè)置的鍵名產(chǎn)生沖突。
鍵的類(lèi)型 Map的鍵可以是任意值,包括函數(shù)、對(duì)象或任意基本類(lèi)型。 Object 的鍵必須是 String 或是Symbol。
鍵的順序 Map 中的 key 是有序的。因此,當(dāng)?shù)臅r(shí)候, Map 對(duì)象以插入的順序返回鍵值。 Object 的鍵是無(wú)序的
Size Map 的鍵值對(duì)個(gè)數(shù)可以輕易地通過(guò)size 屬性獲取 Object 的鍵值對(duì)個(gè)數(shù)只能手動(dòng)計(jì)算
迭代 Map 是 iterable 的,所以可以直接被迭代。 迭代Object需要以某種方式獲取它的鍵然后才能迭代。
性能 在頻繁增刪鍵值對(duì)的場(chǎng)景下表現(xiàn)更好。 在頻繁添加和刪除鍵值對(duì)的場(chǎng)景下未作出優(yōu)化。

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

(1)Map

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

實(shí)際上Map是一個(gè)數(shù)組,它的每一個(gè)數(shù)據(jù)也都是一個(gè)數(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對(duì)應(yīng)的鍵值value,然后返回整個(gè)Map結(jié)構(gòu),如果key已經(jīng)有值,則鍵值會(huì)被更新,否則就新生成該鍵。(因?yàn)榉祷氐氖钱?dāng)前Map對(duì)象,所以可以鏈?zhǔn)秸{(diào)用)
  • get(key):該方法讀取key對(duì)應(yīng)的鍵值,如果找不到key,返回undefined。
  • has(key):該方法返回一個(gè)布爾值,表示某個(gè)鍵是否在當(dāng)前Map對(duì)象中。
  • delete(key):該方法刪除某個(gè)鍵,返回true,如果刪除失敗,返回false。
  • clear():map.clear()清除所有成員,沒(méi)有返回值。

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

  • 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 對(duì)象也是一組鍵值對(duì)的集合,其中的鍵是弱引用的。其鍵必須是對(duì)象,原始數(shù)據(jù)類(lèi)型不能作為key值,而值可以是任意的。

該對(duì)象也有以下幾種方法:

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

其clear()方法已經(jīng)被棄用,所以可以通過(guò)創(chuàng)建一個(gè)空的WeakMap并替換原對(duì)象來(lái)實(shí)現(xiàn)清除。

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

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

總結(jié):

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

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

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

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

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

(1)值屬性,這些全局屬性返回一個(gè)簡(jiǎn)單值,這些值沒(méi)有自己的屬性和方法。

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

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

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

(3)基本對(duì)象,基本對(duì)象是定義或使用其他對(duì)象的基礎(chǔ)?;緦?duì)象包括一般對(duì)象、函數(shù)對(duì)象和錯(cuò)誤對(duì)象。

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

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

例如 Number、Math、Date

(5)字符串,用來(lái)表示和操作字符串的對(duì)象。

例如 String、RegExp

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

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

例如 Map、Set、WeakMap、WeakSet

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

例如 SIMD 等

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

例如 JSON 等

(10)控制抽象對(duì)象

例如 Promise、Generator 等

(11)反射

例如 Reflect、Proxy

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

例如 Intl、Intl.Collator 等

(13)WebAssembly

(14)其他

例如 arguments

總結(jié):

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

5. 常用的正則表達(dá)式有哪些?

// (1)匹配 16 進(jìn)制顏色值
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 號(hào)
var regex = /^[1-9][0-9]{4,10}$/g;

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

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

6. 對(duì)JSON的理解

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

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

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

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

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

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

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

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

一般有以下幾種方式:

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

8. JavaScript 類(lèi)數(shù)組對(duì)象的定義?

一個(gè)擁有 length 屬性和若干索引屬性的對(duì)象就可以被稱(chēng)為類(lèi)數(shù)組對(duì)象,類(lèi)數(shù)組對(duì)象和數(shù)組類(lèi)似,但是不能調(diào)用數(shù)組的方法。常見(jiàn)的類(lèi)數(shù)組對(duì)象有 arguments 和 DOM 方法的返回結(jié)果,還有一個(gè)函數(shù)也可以被看作是類(lèi)數(shù)組對(duì)象,因?yàn)樗?length 屬性值,代表可接收的參數(shù)個(gè)數(shù)。

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

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

Array.prototype.slice.call(arrayLike);

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

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

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

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

(4)通過(guò) Array.from 方法來(lái)實(shí)現(xiàn)轉(zhuǎn)換

Array.from(arrayLike);

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

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

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

(1)Unicode

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

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

ASCII碼可以表示的編碼有限,要想表示其他語(yǔ)言的編碼,還是要使用 Unicode來(lái)表示,可以說(shuō) Unicode是 ASCII 的超集。

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

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

(2)UTF-8

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

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

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

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

來(lái)看一下具體的 Unicode編號(hào)范圍與對(duì)應(yīng)的 UTF-8二進(jìn)制格式 :

編碼范圍(編號(hào)對(duì)應(yīng)的十進(jìn)制數(shù)) 二進(jìn)制格式
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

那該如何通過(guò)具體的 Unicode編碼,進(jìn)行具體的 UTF-8編碼呢?步驟如下:

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

來(lái)看一個(gè)實(shí)際的例子:

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

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

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

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

(3)UTF-16

1. 平面的概念

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

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

最前面的一個(gè)平面稱(chēng)為基本平面,它的碼點(diǎn)從0 — 216-1,寫(xiě)成16進(jìn)制就是 U+0000 — U+FFFF,那剩下的16個(gè)平面就是輔助平面,碼點(diǎn)范圍是 U+10000—U+10FFFF。

2. UTF-16 概念:

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

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

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

4. 編碼識(shí)別

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

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

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

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

5. 舉例說(shuō)明

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

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

(4) UTF-32

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

比如“” 字的Unicode編號(hào)是:U+9A6C,整數(shù)編號(hào)是 39532,直接轉(zhuǎn)化為二進(jì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? 使用變長(zhǎng)碼元序列的編碼方式,相較于定長(zhǎng)碼元序列的 ?UTF-32?算法更復(fù)雜,甚至比同樣是變長(zhǎng)碼元序列的 ?UTF-8?也更為復(fù)雜,因?yàn)槠湟肓霜?dú)特的代理對(duì)這樣的代理機(jī)制;
  • ?UTF-8?需要判斷每個(gè)字節(jié)中的開(kāi)頭標(biāo)志信息,所以如果某個(gè)字節(jié)在傳送過(guò)程中出錯(cuò)了,就會(huì)導(dǎo)致后面的字節(jié)也會(huì)解析出錯(cuò);而 ?UTF-16?不會(huì)判斷開(kāi)頭標(biāo)志,即使錯(cuò)也只會(huì)錯(cuò)一個(gè)字符,所以容錯(cuò)能力教強(qiáng);
  • 如果字符內(nèi)容全部英文或英文與其他文字混合,但英文占絕大部分,那么用 ?UTF-8?就比 ?UTF-16?節(jié)省了很多空間;而如果字符內(nèi)容全部是中文這樣類(lèi)似的字符或者混合字符中中文占絕大多數(shù),那么 ?UTF-16?就占優(yōu)勢(shì)了,可以節(jié)省很多空間;

11. 常見(jiàn)的位運(yùn)算符有哪些?其計(jì)算規(guī)則是什么?

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

常見(jiàn)的位運(yùn)算有以下幾種:

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

1. 按位與運(yùn)算符(&)

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

運(yùn)算規(guī)則:

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

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

例如:3&5 即:

0000 0011 
   0000 0101 
 = 0000 0001

因此 3&5 的值為1。

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

用途:

(1)判斷奇偶

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

(2)清零

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

2. 按位或運(yùn)算符(|)

定義: 參加運(yùn)算的兩個(gè)對(duì)象按二進(jìn)制位進(jìn)行“或”運(yùn)算。

運(yùn)算規(guī)則:

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

總結(jié):參加運(yùn)算的兩個(gè)對(duì)象只要有一個(gè)為1,其值為1。

例如:3|5即:

0000 0011
  0000 0101 
= 0000 0111

因此,3|5的值為7。

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

3. 異或運(yùn)算符(^)

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

運(yùn)算規(guī)則:

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

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

例如:3|5即:

0000 0011
  0000 0101 
= 0000 0110

因此,3^5的值為6。

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

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

4. 取反運(yùn)算符 (~)

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

運(yùn)算規(guī)則:

~ 1 = 0
~ 0 = 1

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

例如:~6 即:

0000 0110
= 1111 1001

在計(jì)算機(jī)中,正數(shù)用原碼表示,負(fù)數(shù)使用補(bǔ)碼存儲(chǔ),首先看最高位,最高位1表示負(fù)數(shù),0表示正數(shù)。此計(jì)算機(jī)二進(jìn)制碼為負(fù)數(shù),最高位為符號(hào)位。

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

0000 0110
   = 1111 1001
反碼:1000 0110
補(bǔ)碼:1000 0111

因此,~6的值為-7。

5. 左移運(yùn)算符(<<)

定義: 將一個(gè)運(yùn)算對(duì)象的各二進(jìn)制位全部左移若干位,左邊的二進(jìn)制位丟棄,右邊補(bǔ)0。

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

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

6. 右移運(yùn)算符(>>)

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

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

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

7. 原碼、補(bǔ)碼、反碼

上面提到了補(bǔ)碼、反碼等知識(shí),這里就補(bǔ)充一下。

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

(1)原碼

原碼就是一個(gè)數(shù)的二進(jìn)制數(shù)。

例如:10的原碼為0000 1010

(2)反碼

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

例如:-10

原碼:1000 1010
反碼:1111 0101

(3)補(bǔ)碼

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

例如:-10

原碼:1000 1010
反碼:1111 0101
補(bǔ)碼:1111 0110

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

arguments是一個(gè)對(duì)象,它的屬性是從 0 開(kāi)始依次遞增的數(shù)字,還有 callee和 length等屬性,與數(shù)組相似;但是它卻沒(méi)有數(shù)組常見(jiàn)的方法屬性,如 forEachreduce等,所以叫它們類(lèi)數(shù)組。

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

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

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

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

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

(3)使用展開(kāi)運(yùn)算符將類(lèi)數(shù)組轉(zhuǎn)化成數(shù)組

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

13. 什么是 DOM 和 BOM?

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

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

一個(gè)擁有 length 屬性和若干索引屬性的對(duì)象就可以被稱(chēng)為類(lèi)數(shù)組對(duì)象,類(lèi)數(shù)組對(duì)象和數(shù)組類(lèi)似,但是不能調(diào)用數(shù)組的方法。常見(jiàn)的類(lèi)數(shù)組對(duì)象有 arguments 和 DOM 方法的返回結(jié)果,函數(shù)參數(shù)也可以被看作是類(lèi)數(shù)組對(duì)象,因?yàn)樗?length屬性值,代表可接收的參數(shù)個(gè)數(shù)。

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

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

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

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

16. 對(duì)AJAX的理解,實(shí)現(xiàn)一個(gè)AJAX請(qǐng)求

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

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

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

使用Promise封裝AJAX:

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

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

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

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

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

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

那為什么會(huì)進(jìn)行變量提升呢?主要有以下兩個(gè)原因:

  • 提高性能
  • 容錯(cuò)性更好

(1)提高性能

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

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

(2)容錯(cuò)性更好

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

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

如果沒(méi)有變量提升,這兩行代碼就會(huì)報(bào)錯(cuò),但是因?yàn)橛辛俗兞刻嵘?,這段代碼就可以正常執(zhí)行。

雖然,在可以開(kāi)發(fā)過(guò)程中,可以完全避免這樣寫(xiě),但是有時(shí)代碼很復(fù)雜的時(shí)候。可能因?yàn)槭韬龆仁褂煤蠖x了,這樣也不會(huì)影響正常使用。由于變量提升的存在,而會(huì)正常運(yùn)行。

總結(jié):

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

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

var tmp = new Date();

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

fn();  // undefined

在這個(gè)函數(shù)中,原本是要打印出外層的tmp變量,但是因?yàn)樽兞刻嵘膯?wèn)題,內(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

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

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

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

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

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

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

ES6 Module和CommonJS模塊的共同點(diǎn):

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

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

CommonJS

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

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

3.當(dāng)使用require命令加載某個(gè)模塊時(shí),就會(huì)運(yùn)行整個(gè)模塊的代碼。

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

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

ES6模塊

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

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

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

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

20. 常見(jiàn)的DOM操作有哪些

1)DOM 節(jié)點(diǎn)的獲取

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

getElementById // 按照 id 查詢
getElementsByTagName // 按照標(biāo)簽名查詢
getElementsByClassName // 按照類(lèi)名查詢
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])
// 按照類(lèi)名查詢
var moocList = document.getElementsByClassName('mooc') // 查詢到類(lèi)名為 mooc 的集合
// 按照 css 選擇器查詢
var pList = document.querySelectorAll('.mooc') // 查詢到類(lèi)名為 mooc 的集合

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

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

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

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

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

3)DOM 節(jié)點(diǎn)的刪除

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

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

需要?jiǎng)h除 id 為 title 的元素,做法是:

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

或者通過(guò)子節(jié)點(diǎn)數(shù)組來(lái)完成刪除:

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

4)修改 DOM 元素

修改 DOM 元素這個(gè)動(dòng)作可以分很多維度,比如說(shuō)移動(dòng) DOM 元素的位置,修改 DOM 元素的屬性等。

將指定的兩個(gè) 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')   
 
// 獲取兩個(gè)需要被交換的元素
var title = document.getElementById('title')
var content = document.getElementById('content')
// 交換兩個(gè)元素,把 content 置于 title 前面
container.insertBefore(content, title)

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

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

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

區(qū)別:

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

22. 如何判斷一個(gè)對(duì)象是否屬于某個(gè)類(lèi)?

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

23. 強(qiáng)類(lèi)型語(yǔ)言和弱類(lèi)型語(yǔ)言的區(qū)別

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

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

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

(1)解釋型語(yǔ)言

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

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

(2)編譯型語(yǔ)言

使用專(zhuān)門(mén)的編譯器,針對(duì)特定的平臺(tái),將高級(jí)語(yǔ)言源代碼一次性的編譯成可被該平臺(tái)硬件執(zhí)行的機(jī)器碼,并包裝成該平臺(tái)所能識(shí)別的可執(zhí)行性程序的格式。在編譯型語(yǔ)言寫(xiě)的程序執(zhí)行之前,需要一個(gè)專(zhuān)門(mén)的編譯過(guò)程,把源代碼編譯成機(jī)器語(yǔ)言的文件,如exe格式的文件,以后要再運(yùn)行時(shí),直接使用編譯結(jié)果即可,如直接運(yùn)行exe文件。因?yàn)橹恍杈幾g一次,以后運(yùn)行時(shí)不需要編譯,所以編譯型語(yǔ)言執(zhí)行效率高。其特點(diǎn)總結(jié)如下:

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

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

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

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

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

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

26. 如何使用for...of遍歷對(duì)象

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

如果需要遍歷的對(duì)象是類(lèi)數(shù)組對(duì)象,用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)
}

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

//方法一:
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è)應(yīng)用的網(wǎng)頁(yè)開(kāi)發(fā)技術(shù)。它是一種在無(wú)需重新加載整個(gè)網(wǎng)頁(yè)的情況下,能夠更新部分網(wǎng)頁(yè)的技術(shù)。通過(guò)在后臺(tái)與服務(wù)器進(jìn)行少量數(shù)據(jù)交換,Ajax 可以使網(wǎng)頁(yè)實(shí)現(xiàn)異步更新。這意味著可以在不重新加載整個(gè)網(wǎng)頁(yè)的情況下,對(duì)網(wǎng)頁(yè)的某部分進(jìn)行更新。傳統(tǒng)的網(wǎng)頁(yè)(不使用 Ajax)如果需要更新內(nèi)容,必須重載整個(gè)網(wǎng)頁(yè)頁(yè)面。其缺點(diǎn)如下:

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

(2)Fetch

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

fetch的優(yōu)點(diǎn):

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

fetch的缺點(diǎn):

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

(3)Axios

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

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

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

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

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

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

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

四、原型與原型鏈


1. 對(duì)原型、原型鏈的理解

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

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

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


2. 原型修改、重寫(xiě)

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
// 重寫(xiě)原型
Person.prototype = {
    getName: function() {}
}
var p = new Person('hello')
console.log(p.__proto__ === Person.prototype)        // true
console.log(p.__proto__ === p.constructor.prototype) // false

可以看到重寫(xiě)原型的時(shí)候p的構(gòu)造函數(shù)不是指向Person了,因?yàn)橹苯咏oPerson的原型對(duì)象直接用對(duì)象賦值時(shí),它的構(gòu)造函數(shù)指向的了根構(gòu)造函數(shù)Object,所以這時(shí)候 p.constructor === Object ,而不是 p.constructor === Person。要想成立,就要用constructor指回來(lái):

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. 原型鏈的終點(diǎn)是什么?如何打印出原型鏈的終點(diǎn)?

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


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

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

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

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


1. 對(duì)閉包的理解

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

閉包有兩個(gè)常用的用途;

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

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

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

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

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

首先因?yàn)?nbsp;setTimeout 是個(gè)異步函數(shù),所以會(huì)先把循環(huán)全部執(zhí)行完畢,這時(shí)候 i 就是 6 了,所以會(huì)輸出一堆 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)部,這個(gè)時(shí)候值就被固定在了參數(shù) j 上面不會(huì)改變,當(dāng)下次執(zhí)行 timer 這個(gè)閉包的時(shí)候,就可以使用外部函數(shù)的變量 j,從而達(dá)到目的。

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

2. 對(duì)作用域、作用域鏈的理解

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

(1)全局作用域

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

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

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

作用域鏈:

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

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

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

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

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

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

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

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

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

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

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

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

2. 執(zhí)行上下文棧
  • JavaScript引擎使用執(zhí)行上下文棧來(lái)管理執(zhí)行上下文
  • 當(dāng)JavaScript執(zhí)行代碼時(shí),首先遇到全局代碼,會(huì)創(chuàng)建一個(gè)全局執(zhí)行上下文并且壓入執(zhí)行棧中,每當(dāng)遇到一個(gè)函數(shù)調(diào)用,就會(huì)為該函數(shù)創(chuàng)建一個(gè)新的執(zhí)行上下文并壓入棧頂,引擎會(huì)執(zhí)行位于執(zhí)行上下文棧頂?shù)暮瘮?shù),當(dāng)函數(shù)執(zhí)行完成之后,執(zhí)行上下文從棧中彈出,繼續(xù)執(zhí)行下一個(gè)上下文。當(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í)行上下文有兩個(gè)階段:創(chuàng)建階段執(zhí)行階段

1)創(chuàng)建階段

(1)this綁定

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

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

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

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

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

2)執(zhí)行階段

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

簡(jiǎn)單來(lái)說(shuō)執(zhí)行上下文就是指:

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

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

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

六、this/call/apply/bind


1. 對(duì)this對(duì)象的理解

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

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

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

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

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

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

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

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

  • 判斷調(diào)用對(duì)象是否為函數(shù),即使是定義在函數(shù)的原型上的,但是可能出現(xiàn)使用 call 等方式調(diào)用的情況。
  • 判斷傳入上下文對(duì)象是否存在,如果不存在,則設(shè)置為 window 。
  • 處理傳入的參數(shù),截取第一個(gè)參數(shù)后的所有參數(shù)。
  • 將函數(shù)作為上下文對(duì)象的一個(gè)屬性。
  • 使用上下文對(duì)象來(lái)調(diào)用這個(gè)方法,并保存返回結(jié)果。
  • 刪除剛才新增的屬性。
  • 返回結(jié)果。
Function.prototype.myCall = function(context) {
  // 判斷調(diào)用對(duì)象
  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è)為對(duì)象的方法
  context.fn = this;
  // 調(diào)用函數(shù)
  result = context.fn(...args);
  // 將屬性刪除
  delete context.fn;
  return result;
};

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

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

  • 判斷調(diào)用對(duì)象是否為函數(shù),即使是定義在函數(shù)的原型上的,但是可能出現(xiàn)使用 call 等方式調(diào)用的情況。
  • 保存當(dāng)前函數(shù)的引用,獲取其余傳入?yún)?shù)值。
  • 創(chuàng)建一個(gè)函數(shù)返回
  • 函數(shù)內(nèi)部使用 apply 來(lái)綁定函數(shù)調(diào)用,需要判斷函數(shù)作為構(gòu)造函數(shù)的情況,這個(gè)時(shí)候需要傳入當(dāng)前函數(shù)的 this 給 apply 調(diào)用,其余情況都傳入指定的上下文對(duì)象。
Function.prototype.myBind = function(context) {
  // 判斷調(diào)用對(duì)象是否為函數(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. 異步編程的實(shí)現(xiàn)方式?

JavaScript中的異步機(jī)制可以分為以下幾種:

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

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對(duì)象時(shí):

  • promise1.then() 的回調(diào)就是一個(gè) task
  • promise1 是 resolved或rejected: 那這個(gè) task 就會(huì)放入當(dāng)前事件循環(huán)回合的 microtask queue
  • promise1 是 pending: 這個(gè) task 就會(huì)放入 事件循環(huán)的未來(lái)的某個(gè)(可能下一個(gè))回合的 microtask queue 中
  • setTimeout 的回調(diào)也是個(gè) task ,它會(huì)被放入 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ù)返回一個(gè) Promise 對(duì)象,當(dāng)函數(shù)執(zhí)行的時(shí)候,一旦遇到 await 就會(huì)先返回,等到觸發(fā)的異步操作完成,再執(zhí)行函數(shù)體內(nèi)后面的語(yǔ)句??梢岳斫鉃椋亲尦隽司€程,跳出了 async 函數(shù)體。

例如:

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

img

func1的運(yùn)行結(jié)果其實(shí)就是一個(gè)Promise對(duì)象。因此也可以使用then來(lái)處理后續(xù)邏輯。

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

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

3. 對(duì)Promise的理解

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

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

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

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

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

(2)Promise的實(shí)例有兩個(gè)過(guò)程

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

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

Promise的特點(diǎn):

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

Promise的缺點(diǎn):

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

總結(jié):

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

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

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

4. Promise的基本用法

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

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

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

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

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

  • Promise.resolve

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

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

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

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

  • Promise.reject

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

Promise.reject(new Error(“我錯(cuò)了,請(qǐng)?jiān)彴常?!?);

就是下面的代碼new Promise的簡(jiǎn)單形式:

new Promise(function(resolve,reject){
   reject(new Error("我錯(cuò)了,請(qǐng)?jiān)彴常。?));
});

下面是使用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方法傳遞一個(gè)參數(shù),返回一個(gè)promise對(duì)象,如果為 true的話,那么調(diào)用promise對(duì)象中的 resolve()方法,并且把其中的參數(shù)傳遞給后面的 then第一個(gè)函數(shù)內(nèi),因此打印出 “hello world”, 如果為 false的話,會(huì)調(diào)用promise對(duì)象中的 reject()方法,則會(huì)進(jìn)入 then的第二個(gè)函數(shù)內(nèi),會(huì)打印 No thanks;

(2)Promise方法

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

  • then()

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

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

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

then方法返回的是一個(gè)新的Promise實(shí)例(不是原來(lái)那個(gè)Promise實(shí)例)。因此可以采用鏈?zhǔn)綄?xiě)法,即 then方法后面再調(diào)用另一個(gè)then方法。

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

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)要寫(xiě)的事件沒(méi)有順序或者關(guān)系時(shí),還如何寫(xiě)呢?可以使用 all 方法來(lái)解決。

  • catch()

Promise對(duì)象除了有then方法,還有一個(gè)catch方法,該方法相當(dāng)于 then方法的第二個(gè)參數(shù),指向 reject的回調(diào)函數(shù)。不過(guò) catch方法還有一個(gè)作用,就是在執(zhí)行 resolve回調(diào)函數(shù)時(shí),如果出現(xiàn)錯(cuò)誤,拋出異常,不會(huì)停止運(yùn)行,而是進(jì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ù), 它接收一個(gè)數(shù)組,數(shù)組的每一項(xiàng)都是一個(gè) promise對(duì)象。當(dāng)數(shù)組中所有的 promise的狀態(tài)都達(dá)到 resolved的時(shí)候,all方法的狀態(tài)就會(huì)變成 resolved,如果有一個(gè)狀態(tài)變成了 rejected,那么 all方法的狀態(tài)就會(huì)變成 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方法時(shí)的結(jié)果成功的時(shí)候是回調(diào)函數(shù)的參數(shù)也是一個(gè)數(shù)組,這個(gè)數(shù)組按順序保存著每一個(gè)promise對(duì)象 resolve執(zhí)行時(shí)的值。

  • race()

race方法和 all一樣,接受的參數(shù)是一個(gè)每項(xiàng)都是 promise的數(shù)組,但是與 all不同的是,當(dāng)最先執(zhí)行完的事件執(zhí)行完之后,就直接返回該 promise對(duì)象的值。如果第一個(gè) promise對(duì)象狀態(tài)變成 resolved,那自身的狀態(tài)變成了 resolved;反之第一個(gè) promise變成 rejected,那自身狀態(tài)就會(huì)變成 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方法有什么實(shí)際作用呢?當(dāng)要做一件事,超過(guò)多長(zhǎng)時(shí)間就不做了,可以用這個(gè)方法來(lái)解決:

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

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

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

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

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

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

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

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

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

5. Promise解決了什么問(wèn)題

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

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)
    })
  })
})

上面的代碼有如下缺點(diǎn):

  • 后一個(gè)請(qǐng)求需要依賴(lài)于前一個(gè)請(qǐng)求成功后,將數(shù)據(jù)往下傳遞,會(huì)導(dǎo)致多個(gè)ajax請(qǐng)求嵌套的情況,代碼不夠直觀。
  • 如果前后兩個(gè)請(qǐng)求不需要傳遞參數(shù)的情況下,那么后一個(gè)請(qǐng)求也需要前一個(gè)請(qǐng)求成功后再執(zhí)行下一步操作,這種情況下,那么也需要如上編寫(xiě)代碼,導(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)
})

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

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

(1)Promise.all

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

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

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

(2)Promise.race

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

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

7. 對(duì)async/await 的理解

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

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

img

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

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

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

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

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

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

8. await 到底在等啥?

await 在等待什么呢?一般來(lái)說(shuō),都認(rèn)為 await 是在等待一個(gè) async 函數(shù)完成。不過(guò)按語(yǔ)法說(shuō)明,await 等待的是一個(gè)表達(dá)式,這個(gè)表達(dá)式的計(jì)算結(jié)果是 Promise 對(duì)象或者其它值(換句話說(shuō),就是沒(méi)有特殊限定)。

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

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 表達(dá)式的運(yùn)算結(jié)果取決于它等的是什么。

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

來(lái)看一個(gè)例子:

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)用不會(huì)造成阻塞,它內(nèi)部所有的阻塞都被封裝在一個(gè) Promise 對(duì)象中異步執(zhí)行。await暫停當(dāng)前async的執(zhí)行,所以'cug''最先輸出,hello world'和‘cuger’是3秒鐘后同時(shí)出現(xiàn)的。

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

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

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

/**
 * 傳入?yún)?shù) n,表示這個(gè)函數(shù)執(zhí)行的時(shí)間(毫秒)
 * 執(zhí)行的結(jié)果是 n + 200,這個(gè)值將用于下一步驟
 */
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 方式來(lái)實(shí)現(xiàn)這三個(gè)步驟的處理:

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 來(lái)實(shí)現(xiàn)呢,會(huì)是這樣:

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 實(shí)現(xiàn)是一樣的,但是這個(gè)代碼看起來(lái)是不是清晰得多,幾乎跟同步代碼一樣

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

  • 代碼讀起來(lái)更加同步,Promise雖然擺脫了回調(diào)地獄,但是then的鏈?zhǔn)秸{(diào)?也會(huì)帶來(lái)額外的閱讀負(fù)擔(dān)
  • Promise傳遞中間值?常麻煩,?async/await?乎是同步的寫(xiě)法,?常優(yōu)雅
  • 錯(cuò)誤處理友好,async/await可以?成熟的try/catch,Promise的錯(cuò)誤捕獲?常冗余
  • 調(diào)試友好,Promise的調(diào)試很差,由于沒(méi)有代碼塊,你不能在?個(gè)返回表達(dá)式的箭頭函數(shù)中設(shè)置斷點(diǎn),如果你在?個(gè).then代碼塊中使?調(diào)試器的步進(jìn)(step-over)功能,調(diào)試器并不會(huì)進(jìn)?后續(xù)的.then代碼塊,因?yàn)檎{(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,在一段時(shí)間內(nèi)通過(guò)任務(wù)間的切換完成了這兩個(gè)任務(wù),這種情況就可以稱(chēng)之為并發(fā)。
  • 并行是微觀概念,假設(shè) CPU 中存在兩個(gè)核心,那么我就可以同時(shí)完成任務(wù) A、B。同時(shí)完成多個(gè)任務(wù)的情況就可以稱(chēng)之為并行。

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

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

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

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

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

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

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

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

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

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

14. setTimeout、setInterval、requestAnimationFrame 各有什么特點(diǎn)?

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

其實(shí)這個(gè)觀點(diǎn)是錯(cuò)誤的,因?yàn)?JS 是單線程執(zhí)行的,如果前面的代碼影響了性能,就會(huì)導(dǎo)致 setTimeout 不會(huì)按期執(zhí)行。當(dāng)然了,可以通過(guò)代碼去修正 setTimeout,從而使定時(shí)器相對(duì)準(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í)行所消耗的時(shí)間
  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)所消耗的時(shí)間
  currentInterval = interval - offset 
  console.log('時(shí):'+h, '分:'+m, '毫秒:'+s, '秒向上取整:'+sCeil, '代碼執(zhí)行時(shí)間:'+offset, '下次循環(huán)間隔'+currentInterval) 
  setTimeout(loop, currentInterval)
}
setTimeout(loop, currentInterval)

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

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

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

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

如果有循環(huán)定時(shí)器的需求,其實(shí)完全可以通過(guò) requestAnimationFrame 來(lái)實(shí)現(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ù)的延時(shí)效果是精確的,沒(méi)有其他定時(shí)器時(shí)間不準(zhǔn)的問(wèn)題,當(dāng)然你也可以通過(guò)該函數(shù)來(lái)實(shí)現(xiàn) setTimeout

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

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

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

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

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

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

(4)第四種模式是組合使用構(gòu)造函數(shù)模式和原型模式,這是創(chuàng)建自定義類(lèi)型的最常見(jiàn)方式。因?yàn)闃?gòu)造函數(shù)模式和原型模式分開(kāi)使用都存在一些問(wèn)題,因此可以組合使用這兩種模式,通過(guò)構(gòu)造函數(shù)來(lái)初始化對(duì)象的屬性,通過(guò)原型對(duì)象來(lái)實(shí)現(xiàn)函數(shù)方法的復(fù)用。這種方法很好的解決了兩種模式單獨(dú)使用時(shí)的缺點(diǎn),但是有一點(diǎn)不足的就是,因?yàn)槭褂昧藘煞N不同的模式,所以對(duì)于代碼的封裝性不夠好。

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

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

2. 對(duì)象繼承的方式有哪些?

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

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

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

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

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

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

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


1. 瀏覽器的垃圾回收機(jī)制

(1)垃圾回收的概念

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

回收機(jī)制

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

(2)垃圾回收的方式

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

1)標(biāo)記清除

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

2)引用計(jì)數(shù)

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

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

obj1.a =  null
 obj2.a =  null

(3)減少垃圾回收

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

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

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

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

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


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

掃描二維碼

下載編程獅App

公眾號(hào)
微信公眾號(hào)

編程獅公眾號(hào)