1. 程式人生 > 其它 >前端面試——JS進階

前端面試——JS進階

技術標籤:前端面試javascript

JS進階

1. ES6 基礎知識點

變數提升

問:什麼是變數提升?為什麼存在變數提升

  • 函式及變數的宣告都將被提升到函式的最頂部。也就是變數可以先使用再宣告。
  • 變數提升的根本原因就是為了解決函式之間互相呼叫的情況。

問:變數和函式怎麼進行提升的? 優先順序是怎麼樣的?

  • 第一階段:對所有的函式宣告進行提升(忽略表示式和箭頭函式),引用型別的賦值分為三步:
    • 開闢堆空間
    • 儲存內容
    • 將地址賦值給變數
  • 第二階段:對所有的變數進行提升,全部賦值為undefined,然後依次順序執行程式碼(let和const時,不能在宣告之前使用變數,這叫做暫時性死區)

var、let、const

問:var、let、const 三者的區別是什麼

  • var 存在變數提升,而let、const則不會
  • var 在瀏覽器環境下宣告的變數會掛載到window上,而其他兩者不會。
  • let 和 const 的作用基本一致,後者宣告的變數不能再次賦值(但是能改變值)

2. map、filter、reduce、foreach區別

map()

map(function(element,index,arr), thisValue)

傳入一個函式,該函式會遍歷陣列,對每一個元素做變換之後,返回一個新陣列

  • element: 對應陣列的每個元素
  • index: 陣列元素的下標
  • arr: 原陣列
  • 可選。用作 “this” 的值。如果省略了 thisValue,或者傳入 null、undefined,那麼回撥函式的 this 為全域性物件。
  let arr = [2, 3, 4]
  let arr1 = arr.map(function (element, index, arr) {
    return arr[index] + 1
  }) 
  let arr2 = arr.map(function (element, index, arr) {
    return element + 1
  }) 
  console.log(arr); //  [2, 3, 4]
  console.log(arr1); // [3, 4, 5]
  console.log(arr2); // [3, 4, 5]

filter()

filter(function(currentValue,index,arr), thisValue)

傳入一個函式,函式返回值為布林型別,將返回值為真的元素放入新陣列,返回這個新陣列

  • element: 對應陣列的每個元素
  • index: 陣列元素的下標
  • arr: 原陣列
  • thisValue: 可選。用作 “this” 的值。如果省略了 thisValue ,“this” 的值為 “undefined”
let arr = [1, 2, 3, 4]
let arr1 = arr.filter(function(element) {
  return element < 3
})
console.log(arr); //  [1, 2, 3, 4]
console.log(arr1); // [1, 2]

reduce()

reduce(function(total, element, index, arr), initialValue)

傳入一個函式,返回一個值

  • total: 累計值(第一次的值代表初始化的值)

  • element: 對應陣列的每個元素

  • index: 陣列元素的下標

  • arr: 原陣列

  • initialValue: 可選。傳遞給函式的初始值

let arr = [1, 2, 3]
let sum = arr.reduce(function(acc, element) {
  return acc + element
}, 1)
console.log(arr); // [1, 2, 3]
console.log(sum); // 7

forEach()

forEach(function(element, index, arr), thisValue)

  • element: 對應陣列的每個元素
  • index: 陣列元素的下標
  • arr: 原陣列
  • initialValue: 可選。如果省略了 thisValue ,“this” 的值為 “undefined”

傳入一個函式,直接操作原陣列 沒有返回值

let arr = [1, 3, 5]
let arr1 = arr.forEach(function(element, index, arr) {
  arr[index] = element+1
})
console.log(arr); // [2, 4, 6]
console.log(arr1); // undefined

3. 箭頭函式

箭頭函式和普通函式的區別?箭頭函式可以當做建構函式 new 嗎?

箭頭函式是普通函式的簡寫,可以更優雅的定義一個函式,和普通函式相比,有以下幾點差異:

  • 函式體內的this, 指向定義時所在的物件,而不是使用時所在的物件
  • 不可以使用arguments物件,該物件在函式體內不存在。如果要用,可以用rest引數代替
  • 不可以使用yield命令,因此箭頭函式不能用作Generator函式
  • 不可以使用new命令,因為它沒有自己的this和prototype屬性

4. this

this指向

  • 物件呼叫,this指向該物件(誰呼叫this就指向誰)

    var obj = { 
        name:'小鹿', 
        age: '21', 
        print: function(){ 
            console.log(this) 
            console.log(this.name + ':' + this.age) 
        } 
    }// 通過物件的方式呼叫函式 obj.print(); // this 指向 obj
    
  • 直接呼叫的函式,this指向的是全域性window物件

    function print(){ 
        console.log(this); 
    }// 全域性呼叫函式 
    print(); // this 指向 window
    
  • 通過new的方式,this永遠指向新建立的物件

    function Person(name, age){ 
        this.name = name; 
        this.age = age; 
        console.log(this); 
    }
    var xiaolu = new Person('小鹿',22); // this = > xaiolu
    
  • 箭頭函式中的this

    由於箭頭函式沒有單獨的 this 值。箭頭函式的 this 與宣告所在的上下文相同。也就是說呼叫箭頭函式的時候,不會隱式的呼叫 this 引數,而是從定義時的函式繼承上下文。

    const obj = { 
        a:()=>{ 
            console.log(this); 
        } 
    }
    // 物件呼叫箭頭函式 
    obj.a(); // window
    

如何改變this指向

我們可以通過呼叫函式的call、apply、bind來改變this的指向

var obj ={
    name:'zhangsan',
    age:18
}
function print() {
    console.log(this); //列印this的指向
    console.log(arguments); // 列印傳遞的引數
}
// 通過call 改變this指向
print.call(obj,1,2,3);

// 通過 apply 改變this 指向
print.apply(obj,[1,2,3]);

// 通過 bind 改變this的指向
let fn = print.bind(obj, 1,2,3);
fn();

再說一說這三者的共同點和不同點

共同點:

  • 功能角度:三者都能改變this指向,且第一個傳遞的引數都是this指向的物件
  • 傳參角度:三者都採用的後續傳參的方式

不同點:

  • 傳參方面:call的傳參是單個傳遞的,而apply後續傳遞的引數是陣列形式(傳單個值會報錯),而bind沒有規定,傳遞值和陣列都可以
  • 執行方面:call和apply函式的執行是直接執行的,而bind函式會返回一個函式,然後我們想要呼叫的時候才會執行。

主要應用場景:

  1. call 經常用於繼承
  2. apply 經常跟陣列有關係,比如藉助於數學物件實現陣列最大值最小值
  3. bind 不呼叫函式,但是還想改變this指向,比如改變定時器內部的this指向

由於箭頭函式沒有自己的this指標,通過call()或者apply()方法呼叫一個函式時,只能傳遞引數(不能繫結this), 他們的第一個引數會被忽略

5. new

建立物件的幾種方式?

  • 字面量
  • new
  • Object.create()

字面量

var obj={
    name:'lxy'
}
  • 程式碼量更少,更易讀
  • 物件字面量執行速度更快。它們可以在解析的時候被優化,他不會像 new 一個物件一樣,解析器需要順著作用域鏈從當前作用域開始查詢,如果在當前作用域找到了名為Object() 的函式就執行,如果沒找到,就繼續順著作用域鏈往上找,直到找到全域性Object() 建構函式為止
  • Object()建構函式可以接收引數,通過這個引數可以把物件例項的建立過程委託給另一個內建建構函式,並返回另外一個物件例項,而這往往不是你想要的

new

問:new 內部發生了什麼過程?可不可以手寫實現一個 new 操作符?

對於new關鍵字,我們第一想到的就是在面向物件中new 一個例項物件,但是在JS中new和Java中的new的機制不一樣

一般Java中,宣告一個建構函式,通過new 類名() 來建立一個例項物件,而這個建構函式是一種特殊的函式。但是在JS中,只要new一個函式,就可以new一個物件,函式和建構函式沒有任何的區別

對於new建立物件:

var arr = new Array();

new 的過程包括一下四個階段:

  • 建立一個新物件。
  • 這個新物件的_proro_ 屬性指向原函式的prototype屬性。(即繼承原函式的原型)
  • 將這個新物件繫結到此函式的this上
  • 返回新物件,如果這個函式沒有返回其他物件

Object.create(null)

對於Object.create() 方式建立物件:

Object.create(proto,[propertiesObject]);
  • proto: 新建立物件的原型物件。

  • propertiesObject: (可選) 可為建立的新物件設定屬性和值。

  • 一般用於繼承:

    var People= function(name) {
        this.name = name;
    }
    People.peototype.sayName= function() {
        console.log(this.name);
    }
    function Person(name, age) {
        this.age = age;
        People.call(this, name); // 使用call, 實現了People屬性的繼承
    };
    // 使用Object.create()方法,實現People原型方法的繼承,並且修改了constructor 指向
    Person.prototype = Object.create(People.peototype, {
        constructor:{
            configurable:true,
            enumerable: true,
            value:Person,
            writable:true
        }
    });
    Person.prototype.sayAge = function() {
        console.log(this.age);
    }
    var p1 = new Person('person1',25);
    
    p1.sayName() //'person1'
    p1.sayAge(); // 25
    

三者建立物件的區別

  • new 和字面量建立的物件的原型指向Object.prototype, 會繼承object的屬性和方法。
  • 而通過Object.create(null) 建立的物件,其原型指向null, null作為原型鏈的頂端,沒有也不會繼承任何屬性和方法。

6. 閉包

什麼是閉包?

閉包就是能夠訪問其他函式內部變數的函式

閉包的作用

  • 訪問其他函式內部變數
  • 保護變數不被記憶體回收機制回收
  • 避免全域性變數被汙染,方便呼叫上下文的區域性變數加強封裝性

閉包的缺點

閉包長期佔用記憶體,記憶體消耗很大,可能導致記憶體洩漏

如何避免閉包引起的記憶體洩漏

  • 在退出函式前,將不使用的區域性變數全部刪除。可以使變數賦值為null;

7. 記憶體洩漏,垃圾回收機制

什麼是記憶體洩漏?

不再用到的記憶體,沒有及時釋放,就叫做記憶體洩漏。

為什麼會導致記憶體洩漏?

**記憶體洩漏是指我們已經無法再通過js程式碼來引用到某個物件,但垃圾回收器卻認為這個物件還在被引用,因此在回收的時候不會釋放它。**導致了分配的這塊記憶體永遠也無法被釋放出來。如果這樣的情況越來越多,會導致記憶體不夠用而系統崩潰

垃圾回收機制

問:怎麼解決記憶體洩漏?說一說JS垃圾回收機制的執行原理?

需要我們手動管理好記憶體,但是對於JS有自動垃圾回收機制,自行對記憶體進行管理

兩種垃圾回收策略

垃圾回收器主要的功能就是每隔一段時間,就去週期性的執行收集不再繼續用到的記憶體,然後將其釋放掉

標記清除法

它的實現原理就是通過判斷一個變數是否再執行環境中被引用,來進行標記刪除

引用計數法

引用計數的最基本的含義就是跟蹤記錄每個值被引用的次數。

  • 缺陷:兩個物件的互相迴圈引用,在函式執行完成的時候,兩個物件相互的引用計數並未歸0,而是依然佔據記憶體,無法回收,當函式執行多次時,記憶體佔用就會變多,導致大量的記憶體得不到回收。

8. 原型鏈

原型:

每個JS物件都有_proto_ 屬性,這個屬性指向了原型

原型鏈:

原型鏈就是多個物件通過_proto_ 的方式連線了起來形成一條鏈

總結:

  • 所有的例項的_proto_ 都指向該建構函式的原型物件(prototype)。
  • 所有的函式(包括建構函式) 是Function() 的例項,所以所有函式的_proto_ 都指向Function() 的原型物件
  • 所有的原型物件(包括Function 的原型物件) 都是Object 的例項,所以_proto_都指向Object(建構函式) 的原型物件。 而Object 建構函式的 _proto_ 指向null
  • Function 建構函式本身就是Function的例項,所以_proto_ 指向Function的原型物件

9. 物件繼承方法

繼承

繼承的核心思想就是,能夠繼承父類方法的同時,保證自己的私有屬性和方法。

四個最常用的繼承方式

原型繼承

  • 核心思想:將父類的例項作為子類的原型
  • 優點:方法複用,由於方法定義在父類的原型上,複用了父類建構函式原型上的方法
  • 缺點:
    • 建立的子類例項不能傳參
    • 子類例項共享了父類建構函式的引用屬性

組合繼承

  • 核心思想:通過呼叫父類建構函式,繼承父類的屬性並保留傳參的優點;然後通過將父類例項作為子類原型,實現函式複用
  • 優點:
    • 可傳參:子類例項建立可以傳遞引數
    • 方法複用:同時所有的子類可以複用父類引用型別的共享
  • 缺點:
    • 組合繼承呼叫了兩次父類的建構函式,造成了不必要的消耗

寄生組合繼承

  • 核心思想:組合繼承+原型繼承結合兩者的優點
  • 優點:完美!
  • 缺點:無!
// 父類 
function Father(name){ 
    this.name = name;
    this.colors = ["red","blue","green"]; 
}
// 方法定義在原型物件上(共享) 
Father.prototype.sayName = function(){ 
    alert(this.name); 
};
function Son(name,age){ 
    Father.call(this,name); // 核心 
    this.age = age; 
}
Son.prototype = Object.create(Father.prototype); // 核心: 
Son.prototype.constructor = Son; // 修復子類的 constructor 的指向

ES6的 extend 繼承

ES6 的extend 繼承其實就是寄生組合式繼承的語法糖。

  • 核心思想:

    • extends: 內部相當於設定了Son.prototype = Object.create(Father.prototype);
    • super() :內部相當於呼叫了Father.call(this)
  • 小結:

    • 子類只要繼承父類,可以不寫constructor, 一旦寫了,則在constructor 中的第一句話必須是super.
    • 把父類當作普通方法執行,給方法傳遞引數,讓方法中的this 是子類的例項
    class Son extends Father { // Son.prototype.__proto__ = Father.prototype
        constructor(y) { 
            super(200); // super(200) => Father.call(this,200) 
            this.y = y } }
    

10. 深淺拷貝

什麼是深淺拷貝?

深淺拷貝是隻針對Object和Array這樣的引用資料型別的

  • 淺拷貝:只進行一層關係的拷貝,如果屬性是基本型別,直接拷貝基本型別的值,如果屬性值是記憶體地址,就拷貝這個地址,新舊物件公用一塊記憶體
  • 深拷貝:進行無限層次的拷貝,會創造一個一摸一樣的物件,不共享記憶體,修改物件不會互相影響

為什麼要進行深淺拷貝?

let arr1 = arr2 = [1,2,3]
let obj1 = obj2 = {a:1, b:2, c:3}
arr1[0] = 2
obj1.a = 2
console.log(arr2[0]) // 2
console.log(obj2.a) // 2

從上面的程式碼可以看出:同一個Array或者Object賦值給兩個不同變數時,變數指向的是同一個記憶體地址,改變其中一個變數的屬性值,另一個也會改變。如果我們想要的是兩個初始值相等但互不影響的變數,就要使用到拷貝。

深淺拷貝的使用

淺拷貝:

  • 擴充套件運算子(ES6新語法)

    let a = {c: 1}
    let b = {...a}
    a.c = 2
    console.log(b.c) // 1
    
  • Object.assign(target, source)

    將source的值淺拷貝到target目標物件上

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

深拷貝:

  • JSON.stringify()

    let obj = {
        name: 'lxy',
        city: {
            city1: '北京',
            city2: '上海'
        }
    }
    // 淺拷貝
    let obj1 = {...obj}
    // 深拷貝
    let obj2 = JSON.stringify(obj)
    // 改變源物件的引用型別值
    obj.city.city1 = '杭州'
    console.log(obj1.city.city1) // 杭州
    console.log(JSON.parse(obj2).city.city1) // 北京
    

深淺拷貝的手動實現?

淺拷貝:

迴圈遍歷物件,將物件的屬性值拷貝到另一個物件中,返回該物件。

function shallowClone(o) {
    const onj = {};
    for(let i in o) {
        obj[i] = o[i]
    }
    return obj;
}

深拷貝:(簡單實現)

對於深拷貝來說,就是在淺拷貝的基礎上加上遞迴

var a1 = {
    b: {
        c: {
            d: 1
        }
    }
}
function deepClone(obj) {
    var target = {}
    for(var i in obj) {
        if(obj.hasOwnProperty(i)) {
            if(typeof obj[i] === 'object') {
                target[i] = deepClone(obj[i])
            } else {
                target[i] = obj[i]
            }
        }
    }
    return target
}

11. js事件迴圈機制

JavaScript是一門單執行緒非阻塞的指令碼語言。

  • 單執行緒:程式碼執行時,都只有一個主執行緒來處理所有的任務,
  • 非阻塞:是指進行非同步任務時,主執行緒會掛起這個任務,然後在非同步任務返回結果的時候再根據一定規則去執行相應的回撥。JavaScript引擎是通過 event loop (事件迴圈) 實現非阻塞的

主執行緒從任務佇列讀取事件,這個過程是迴圈不斷地,所以整個執行機制又稱為Event Loop(事件迴圈)

執行上下文

執行上下文是一個抽象的概念,可以理解為是程式碼執行的一個環境。分為全域性執行上下文,函式(區域性)執行上下文,Eval執行上下文

  • 全域性執行上下文: this指向的是window,<script></script>標籤中的程式碼
  • 函式執行上下文:每個函式呼叫的時候,會建立一個新的函式執行上下文
  • Eval執行上下文:不常用

執行棧

  • “棧”,一種資料結構。具有“先進後出”的特點。
  • 程式碼執行的時候,遇到一個執行上下文就將其依次壓入執行棧中。
  • 先執行位於棧頂的執行上下文中的程式碼,當棧頂的執行上下文程式碼執行完畢就會出棧,繼續執行下一個位於棧頂的執行上下文。
function foo() {
  console.log('a');
  bar();
  console.log('b');
}
function bar() {
  console.log('c')
}
foo()

程式碼解釋:

  • 初始化狀態,執行棧為空
  • foo(), foo函式執行,foo進入執行棧,console.log(‘a’),列印a
  • bar(), 執行函式bar,bar 進入執行棧,開始執行bar函式,console.log(‘c’) , 列印c
  • bar函式執行完畢,出棧,繼續執行foo函式
  • console.log(‘b’) , 列印b, foo函式執行完畢。出棧。

巨集任務

巨集任務一般包括:

  • 整體的script標籤內的程式碼
  • setTimeout
  • setInterval
  • setImmediate(Node)
  • I/o

微任務

微任務一般包括:

  • Promise
  • process.nextTick(Node)–所有非同步任務之前觸發(nextTick 佇列會比 Promie 佇列先執行。)
  • MutationObserver

迴圈機制的執行

  • 首先,事件迴圈機制是從<script>標籤內的程式碼開始的(巨集任務)
  • 在程式碼執行的過程中,如果遇到巨集任務,如setTimeout, 就會將當前任務分發到巨集任務佇列中(佇列,資料結構,先進先出)
  • 如果遇到微任務,如Promise, 在建立Promise例項物件時,程式碼順序執行,如果遇到.then 任務,該任務就會被分配到微任務佇列中
  • script標籤內的程式碼執行完畢後,此時該巨集任務執行完畢,然後去微任務佇列執行所有的微任務
  • 微任務執行完畢,第一輪的訊息迴圈執行完畢,頁面進行一次渲染
  • 然後開始第二輪的訊息迴圈,從巨集任務佇列中取出任務執行
  • 如果兩個任務佇列沒有任務可執行了,此時所有的任務執行完畢

案例

console.log('1');
setTimeout(() => {
  console.log('2')
}, 1000);
new Promise((resolve, reject) => {
  console.log('3');
  resolve();
  console.log('4');
}).then(() => {
  console.log('5');
});
console.log('6')
// 列印順序:1->3->4->6->5->2

程式碼解釋:

  • 初始化狀態,執行棧為空。
  • 首先執行 <script> 標籤內的同步程式碼,此時全域性的程式碼進入執行棧中,同步順序執行程式碼,輸出 1。
  • 執行過程中遇到非同步程式碼 setTimeout (巨集任務),將其分配到巨集任務非同步佇列中。
  • 同步程式碼繼續執行,遇到一個 promise 非同步程式碼(微任務)。但是建構函式中的程式碼為同步程式碼,依次輸出3、4,則 then 之後的任務加入到微任務佇列中去。
  • 最後執行同步程式碼,輸出 6。該巨集任務執行完畢
  • 然後執行微任務佇列裡面的微任務
  • 微任務佇列中只有一個微任務,所以輸出 5。
  • 此時頁面要進行一次頁面渲染,渲染完成之後,進行下一次迴圈。
  • 在巨集任務佇列中取出一個巨集任務,也就是之前的 setTimeout ,最後輸出 2。
  • 此時任務佇列為空,執行棧中為空,整個程式執行完畢。

12. Generator

Generator 是什麼

  • generator函式是es6提供的一種非同步程式設計的解決方案,可以理解成generator函式是一個狀態機,封裝了多個內部狀態
  • 執行generator函式會返回一個迭代器物件,也就是說,generator函式除了是狀態機還是一個迭代器物件生成函式
  • 返回迭代器物件,可以依次遍歷generator函式內部的每一個狀態

建立Generator函式

function* helloWorldGenerator() {
    yield 'hello'
    yield 'world'
    return 'ending'
}
var hw = helloWorldGenerator()
console.log(hw.next()); //  {value: "hello", done: false}
console.log(hw.next()); // {value: "world", done: false}
console.log(hw.next()); // {value: "ending", done: true}
console.log(hw.next()); // {value: undefined, done: true}
  • 每次遇到yield,函式暫停執行,下一次再從該位置繼續向後執行,具備位置記憶的功能
  • yield表示式只能用在Generator函式裡面
  • yield表示式如果用在另一個表示式之中,必須放在圓括號內
  • yield表示式用作函式引數或放在賦值表示式的右邊

13. Promise

為什麼會有Promise, Promise的誕生解決了哪些問題?

  • 由於JS的執行是單執行緒的, 所以當執行耗時的任務時,就會造成UI渲染的阻塞。當前的解決方法是使用回撥函式來解決這個問題,當任務執行完畢,會呼叫回撥方法
  • 回撥函式存在以下缺點
    • 不能捕捉異常(錯誤處理困難) ——回撥函式的程式碼和開始任務程式碼不在同一事件迴圈中
    • 回撥地獄問題——promise鏈式呼叫
    • 處理並行任務棘手(請求之間互不依賴) ——promise.all

Promise用法

function fn() {
    return new Promise((resolve,reject)=>{
        // 成功時呼叫resolve(資料)
        // 失敗時呼叫reject(錯誤)
    })
}
fn().then(success,fail).then(success1,fail1)
  • 通過內建的Promise建構函式可以建立一個Promise物件,建構函式中傳入兩個函式引數:resolve, reject。 兩個引數的作用是,回撥成功呼叫resolve, 回撥失敗呼叫reject
  • 呼叫Promise物件內建的方法 then, 傳入兩個函式,一個是成功回撥的函式,一個失敗回撥的函式。當在promise內部呼叫resolve函式時,之後就會回撥then方法裡的第一個函式。當呼叫了reject方法時,就會呼叫then方法的第二個函式。
  • promise 相當於是一個承諾,當承諾兌現的時候(呼叫了resolve函式),就會呼叫then中的第一個回撥函式,在回撥函式中做處理。當承諾出現未知的錯誤或異常的時候(呼叫了reject函式), 就會呼叫then方法的第二個回撥函式,提示開發者出現錯誤

Promise 的狀態

其實Promise物件用作非同步任務的一個佔位符,代表暫時還沒有獲得但在未來獲得的值。

Promise共有三種狀態,完成狀態和拒絕狀態都是由等待狀態轉變的。一旦Promise進入了拒絕或完成狀態,它的狀態就不能切換了。

  • 等待狀態(pending)
  • 完成狀態(resolve)
  • 拒絕狀態(reject): 顯示拒絕(直接呼叫reject) 和隱式拒絕(丟擲異常)

Promise 鏈式呼叫

Promise 可以實現鏈式呼叫,每次then之後返回的都是一個promise物件,可以緊接著使用then繼續處理接下來的任務,這樣就實現了鏈式呼叫。如果在then中使用了return, 那麼return的值也會被Promise.resolve() 包裝

Promise.all用法

Promise.all([promise1,promise2]).then(success,fail)

promise1和promise2都成功才會呼叫success

Promise.race用法

Promise.race([promise1,promise2]).then(success,fail)

promise1和promise2只要有一個成功的就會呼叫success

14. async/await

問:async 及 await 和 Generator 以及 Promise 什麼區別?它們的優點和缺點分別是什麼?await 原理是什麼?

其實 ES7 中的 async 及 await 就是 Generator 以及 Promise 的語法糖,內部的實現原理還是原來的,只不過是在寫法上有所改變,這些實現一些非同步任務寫起來更像是執行同步任務。

執行順序

舉個例子:

async function async1() {
    console.log('async1 start')
    await async2()
    console.log('async1 end')
}
async function async2() {
    console.log('async2')
}
async1()
console.log('script start')
// async1 start
// async2
// script start
// async1 end

特點

問:async 做一件什麼事情?

帶async關鍵字的函式,使得你的函式的返回值必定是promise物件。

  • 如果async關鍵字函式返回的不是promise,就自動用Promise.resolve()包裝

  • 如果 async關鍵字函式函式顯示地返回promise, 那就以你返回的promise為準

    async function fn1() {
        return 123
    }
    function fn2() {
        return 123
    }
    console.log(fn1()) // Promise {<resolved>:123}
    console.log(fn2()) // 123
    

問:await在等什麼?

await 等的是右側表示式的結果。

  • 右側如果是函式,那麼函式的return的值就是表示式的結果。

  • 右側如果是一個’123’ 或者什麼值,那表示式的結果就是’123’

問:await 等到之後,做了一件什麼事情

  • 如果不是promise, await 會阻塞後面的程式碼,先執行async外面的同步程式碼,同步程式碼執行完,再回到async內部,把這個非promise的東西,作為await表示式的結果
  • 如果它等到的是一個promise物件, await也會暫停async後面的程式碼,先執行async外面的同步程式碼,等著Promise物件fulfilled,然後把resolve的引數作為await表示式的運算結果

15. 模組化

為什麼要使用模組化?

模組化解決了命名衝突問題,可以提高程式碼的複用率,提高程式碼的可維護性。

模組化的好處:

  • 可以解決命名衝突(減少名稱空間汙染)
  • 更好的分離,按需載入
  • 更高的複用性
  • 高可維護性

使用模組化的幾種方式?

1. 函式:

最起初,實現模組化的方式是使用函式進行封裝。將實現不同功能的程式碼封裝到不同的函式中。通常一個檔案就是一個模組,有自己的作用域,只向外暴露特定的變數和函式。

function a() {
    // 功能1
}
function b() {
    // 功能2
}

缺點:容易發生命名衝突或者資料的不安全性

2. 立即執行函式:

立即執行函式中的匿名函式中有獨立的詞法作用域,避免了外界訪問此作用域的變數。通過函式作用域解決了命名衝突、汙染全域性作用域的問題

// module.js 檔案
(function(window) {
    let name = 'JavaScript'
    function foo() {
        console.log(`name:${name}`)
    }
    // 暴露介面
    window.myModule = { foo }
})(window)
<script type="text/javascript" src="module.js"></script>
<script type="text/javascript">
    myModule.name = 'java' // 無法訪問
    myModule.foo() // name:JavaScript
</script>

缺點:不能直接訪問到內部的變數

3. CommonJS 規範:

CommonJS的規範主要用在Node.js中,為模組提供了四個介面:module、exports、require、global, CommonJS用同步的方式載入模組(伺服器端),在瀏覽器端使用的是非同步載入模組。

暴露模組:

  • module.exports = {}
  • exports.xxx = ‘xxx’
// lib.js
var counter = 3;
function incCounter() {
    counter++;
}
// 對外暴露介面
module.exports = {
    counter: counter,
    incCounter: incCounter
}

引入模組:

主要通過require的方式引入, 但是require是Node的語法,在瀏覽器中無法識別

// 載入外部模組
var mod = require('./lib');
console.log(mod.counter); // 3
mod.incCounter();
// 原始型別的值被快取,所以就沒有被改變(commonJS 不會隨著執行而去模組隨時呼叫)
console.log(mod.counter) // 3

其它:

// 核心模組直接匯入
const path = require('path');
// 路徑模組
const m = require('./m.js');
// 自定義模組
const lodash = require('lodash')
  • 核心模組, 直接跳過路徑分析和檔案定位;
  • 路徑模組, 直接得出相對路徑就好了;
  • 自定義模組,先在當前目錄的node_modules裡找到這個模組,如果沒有,它會往上一級目錄查詢查詢上一級的node_modules, 依次往上,直到根目錄下都沒有,就丟擲錯誤。

特點:

  • CommonJS模組的載入機制是,輸入的是被輸出的值的拷貝。也就是說,一旦輸出一個值,模組內部的變化就影響不到這個值。
  • 所有程式碼都執行在模組作用域,不會汙染全域性作用域
  • 模組可以多次載入,但是隻會在第一次載入時執行一次,然後執行結果就被快取了,以後再載入,就直接讀取快取結果。要想讓模組再次執行,必須清除快取。
  • 模組載入的順序,按照其在程式碼中出現的順序

AMD和CMD

上面有CommonJS規範了,為什麼還出AMD規範,因為CommmonJS是同步載入程式碼的,在瀏覽器中會發生堵塞問題,造成頁面的無響應。所以瀏覽器不太適合使用CommonJS來載入。

CommonJS 規範對瀏覽器端和伺服器端的不同之處

  • 伺服器端所有的模組都存放在本地硬碟中,可以同步載入完成,等待的時間就是硬碟的讀取時間。
  • 瀏覽器,所有的模組都放在伺服器端,等待的時間取決於網速的快慢,可能要等很長時間,瀏覽器處於"假死"狀態

AMD:

AMD(Asynchronous Module Definition), 即"非同步模組定義"。 它主要採用非同步方式載入模組,模組的載入不影響它後邊語句的執行。所載入的模組,都會定義在回撥函式中,載入完成,再執行回撥函式

(1)使用方式

defined 是一個javascript庫中的一個方法,使用之前需要安裝一個庫。

npm i requirejs

使用語法如下:

define(id, dependencies, factory)
  • id: 一個字串,表示模組的名稱
  • dependencies: 一個數組,是我們當前定義的模組要依賴於哪些模組,陣列中的每一項表示的是要依賴模組的相對路徑
  • factory: 工廠方法,具體模組內容

(2)匯出模組

將add.js中的一個函式匯出語法如下:

define(function() {
    var add = function(a,b) {
        return a+b;
    }
    return {
        add: add
    }
})

(3) 引入模組

匯入上述匯出的模組

var requirejs = require("requirejs"); // 引入 requirejs 模組
requirejs(['add'], funtion(math) {
    console.log(add.add(1,2))
})

CMD:

CMD(Common Module Definition), 主要是seajs的規範

AMD和CMD最大的區別是對依賴模組的執行時機處理不同,注意不是載入的時機或者方式不同,二者皆為非同步載入模組

(1) 使用方式

// 所有模組都通過define 來定義
define(function(require, exports, module) {
    // 通過require 引入依賴
    var $ = require('jquery');
    var Spinning = require('./spinning')
    // 通過exports 對外提供介面
    exports.doSomething = ...
    // 或者通過module.exports 提供整個介面
    module.exports = ...
})
define(function (require, exports, module) {
    console.log('我比m1 要早載入。。。')
    var m1 = require('m1'); // 用到時才載入
    var add = function (a, b) {
        return a+b;
    }
    var print = function() {
        console.log(m1.name)
    }
    module.exports = {
        add: add,
        print: print
    }
})

AMD和CMD的區別

  • AMD依賴前置, js很方便的就知道要載入的是哪個模組了,因為已經在define的dependencies引數中就定義好了,會立即載入它
  • CMD是就近依賴,需要使用把模組變為字串解析一遍才知道依賴了那些模組。只有在用到某個模組的時候再去require

ES6 Moudle

ES6實現的模組就非常簡單,用於瀏覽器和伺服器端。import命令會被Javascript引擎靜態分析,在編譯時就引入模組程式碼。

(1) export匯出模組

  • 命名式匯出
  • 預設匯出
// 命名式匯出
// 方式一
const a = 1;
export { a };
// 方式二
export const a = 1;
// 方式三(as 重新命名匯出)
const a = 1;
export {a as A}

// 預設匯出
const a = 1;
export default a;
// 等價於
export {a as default}

(2) import 匯入模組

  • 命名式匯入
  • 預設匯入
// 預設匯入
import { a } from './module';
// 重新命名
import { a as A} from './module';
// 只想要執行被載入的模組
import './module';
// 整體載入
import * as module from './module'
// default介面和具名介面
import module, { a } from './module'

ES6和CommonJS的區別

  • CommonJS 模組輸出的是一個值的拷貝,ES6模組輸出的是值的引用
    • 所謂值的拷貝,原始型別的值被快取,不隨模組內部的改變而改變。
    • ES6模組是動態引用,不快取值, 模組內外是繫結的,而且是隻讀引用,不能修改值。ES6的js引擎對指令碼靜態分析的時候,遇到載入命令模組import, 就會生成一個只讀引用, 當真正用到模組裡面的值的時候,就會去模組內部去取。
  • CommonJS 模組是執行時載入, ES6模組是編譯時載入輸出介面
    • 執行時載入:CommonJS模組就是物件;是先載入整個模組,生成一個物件,然後再從這個物件上面讀取方法,這種載入稱為執行時載入
    • 編譯時載入:ES6模組不是物件,而是通過export命令顯式指定輸出的程式碼。import時採用靜態命令的形式,即在import指定載入某個輸出值,而不是載入整個模組,這種載入稱為編譯時載入。
  • CommonJS匯入的模組路徑可以是一個表示式,因為它使用的是require() 方法;而ES6 Modules只能是字串
  • CommonJS this 指向當前模組, ES6 Modules this 指向undefined

16. 防抖節流

認識防抖節流

JavaScript是事件驅動的,大量的操作會觸發事件,加入到事件佇列中處理。而對於某些頻繁的事件處理會造成效能的損耗,可以通過防抖和節流來限制事件頻繁發生

為什麼需要防抖節流

為什麼需要防抖?

舉個例子:

比如想要搜尋一個MacBook:

  • 當輸入m時,為了更好的使用者體驗,通常會出現對應的聯想內容,這些聯想內容通常是儲存在伺服器的,所以需要一次網路請求;
  • 當繼續輸入ma時,再次傳送網路請求;
  • 那麼macbook一共需要傳送7次網路請求;
  • 這大大損耗我們整個系統的效能,無論是前端的事件處理,還是對於伺服器的壓力;

但是我們需要這麼多次的網路請求嗎?

  • 不需要,正確的做法應該是在合適的情況下再發送網路請求;
  • 比如如果使用者快速的輸入一個macbook,那麼只是傳送一次網路請求;
  • 比如如果使用者是輸入一個m想了一會兒,這個時候m確實應該傳送一次網路請求;
  • 也就是我們應該監聽使用者在某個時間,比如500ms內,沒有再次觸發時間時,再發送網路請求;

這就是防抖的操作:

  • 只有在某個時間內,沒有再次觸發某個函式時,才真正的呼叫這個函式;

  • 當事件觸發時,相應的函式並不會立即觸發,而是會等待一定的時間;

  • 當事件密集觸發時,函式的觸發會被頻繁的推遲;

  • 只有等待了一段時間也沒有事件觸發,才會真正的執行響應函式;

防抖的應用場景:

  • 輸入框中頻繁的輸入內容,搜尋或者提交資訊
  • 頻繁的點選按鈕,觸發某個事件
  • 監聽瀏覽器滾動事件,完成某些特定操作
  • 使用者縮放瀏覽器的resize事件

總之,密集的事件觸發,我們只希望觸發比較靠後發生的事件,就可以使用防抖函式;

為什麼需要節流?

節流:在某個時間內(比如500ms),某個函式只能被觸發一次;

節流的應用場景:

  • 監聽頁面的滾動事件
  • 滑鼠移動事件
  • 使用者頻繁點選按鈕操作

總之,依然是密集的事件觸發,但是這次密集事件觸發的過程不會等到最後一次才進行函式呼叫,而是會按照一定的頻率進行呼叫