前端面試題 回顧與複習(更新中)

李赫feixuan發表於2019-10-07

還沒有完全整理好 希望大家見諒 後面逐步優化

原生DOM操作

  • 如需替換 HTML DOM 中的元素,請使用replaceChild(newnode,oldnode)方法
  • 從父元素中刪除子元素 parent.removeChild(child);
  • insertBefore(newItem,existingItem) 在指定的已有子節點之前插入新的子節點
  • appendChild(newListItem向元素新增新的子節點,作為最後一個子節點 

事件模型

事件傳播指的是發生事件時傳播的過程。一共按順序分為以下三個階段。

äºä»¶æµ  

第一階段捕獲階段:從window物件傳導到目標節點(從上到下)的過程,直到目標的元素,為截獲事件提供機會

第二階段目標階段:在當前目標觸發的過程 ,目標接受事件

第三階段冒泡階段:從目標節點傳導回到windowd物件(從下到上)的過程,在這個階段對事件做出響應。

事件物件:

DOM事件模型中的事件物件常用屬性:

  • type用於獲取事件型別 
  • target獲取事件目標 
  • stopPropagation()阻止事件冒泡 preventDefault()阻止事件預設行為

IE事件模型中的事件物件常用屬性:

  • type用於獲取事件型別 
  • srcElement獲取事件目標 
  • cancelBubble阻止事件冒泡 
  • returnValue阻止事件預設行為

事件委託/代理:

“事件代理”即是把原本需要繫結的事件委託給⽗元素,讓⽗元素擔當事件監聽的職務。事件代理的原理是DOM元素的事件冒泡。

使⽤事件代理的好處是: 

  • 可以提⾼效能 可以⼤量節省記憶體佔⽤ 
  • 減少事件註冊

<ul id="parent">
  <li class="child">one</li>
  <li class="child">two</li>
  <li class="child">three</li>
</ul>複製程式碼

<script type="text/javascript">
  //父元素
  var dom= document.getElementById('parent');

  //父元素繫結事件,代理子元素的點選事件
  dom.onclick= function(event) {
    var event= event || window.event;
    var curTarget= event.target || event.srcElement;

    if (curTarget.tagName.toLowerCase() == 'li') {
      //事件處理
    }
  }
</script>複製程式碼

釋出訂閱模式與觀察者模式

參考連結 https://segmentfault.com/a/1190000019722065

觀察者模式:

觀察者模式:定義了物件間一種一對多的依賴關係,當目標物件 Subject 的狀態發生改變時,所有依賴它的物件 Observer 都會得到通知。

簡單點:女神有男朋友了,朋友圈曬個圖,甜蜜宣言 “老孃成功脫單,希望你們歡喜”。各位潛藏備胎紛紛失戀,只能安慰自己你不是唯一一個。

模式特徵 

1) 一個目標者物件 Subject,擁有方法:新增 / 刪除 / 通知 Observer; 

2) 多個觀察者物件 Observer,擁有方法:接收 Subject 狀態變更通知並處理; 

3) 目標物件 Subject 狀態變更時,通知所有 Observer。

Subject 新增一系列 Observer, Subject 負責維護與這些 Observer 之間的聯絡,“你對我有興趣,我更新就會通知你”。

// 目標者類
class Subject {
    constructor() {
        // 觀察者列表
        this.observers = [];
    }
    // 新增
    add(observer) {
        this.observers.push(observer);
    }
    // 刪除
    remove(observer) {
        let idx = this.observers.findIndex(item => item === observer);
        idx > -1 && this.observers.splice(idx, 1);
    }
    // 通知
    notify() {
        for (let observer of this.observers) {
            observer.update();
        }
    }
}

// 觀察者類
class Observer {
    constructor(name) {
        this.name = name;
    }
    // 目標物件更新時觸發的回撥
    update() {
        console.log(`目標者通知我更新了,我是:${this.name}`);
    }
}

// 例項化目標者
let subject = new Subject();

// 例項化兩個觀察者
let obs1 = new Observer('前端開發者');
let obs2 = new Observer('後端開發者');

// 向目標者新增觀察者
subject.add(obs1);
subject.add(obs2);

// 目標者通知更新
subject.notify();
// 輸出:
// 目標者通知我更新了,我是前端開發者
// 目標者通知我更新了,我是後端開發者複製程式碼

優勢

  1. 目標者與觀察者,功能耦合度降低,專注自身功能邏輯;
  2. 觀察者被動接收更新,時間上解耦,實時接收目標者更新狀態。

不完美

觀察者模式雖然實現了物件間依賴關係的低耦合,但卻不能對事件通知進行細分管控,如 “篩選通知”,“指定主題事件通知” 。

比如上面的例子,僅通知 “前端開發者” ?觀察者物件如何只接收自己需要的更新通知?上例中,兩個觀察者接收目標者狀態變更通知後,都執行了 update(),並無區分。

釋出訂閱模式(Publisher && Subscriber)

釋出訂閱模式:基於一個事件(主題)通道,希望接收通知的物件 Subscriber 通過自定義事件訂閱主題,被啟用事件的物件 Publisher 通過釋出主題事件的方式通知各個訂閱該主題的 Subscriber 物件。

釋出訂閱模式與觀察者模式的不同,“第三者” (事件中心)出現。目標物件並不直接通知觀察者,而是通過事件中心來派發通知。在釋出訂閱模式裡,釋出者,並不會直接通知訂閱者,換句話說,釋出者和訂閱者,彼此互不相識。

// 事件中心
let pubSub = {
    // 快取列表
    list: {},
    /*
     * 新增訂閱者(訂閱函式),將訂閱的型別與回撥函式加入快取列表
     * key: 訊息的型別
     * fn: 訂閱的回撥函式
    */
    subscribe: function (key, fn) {
        if (!this.list[key]) {
            this.list[key] = [];
        }
        this.list[key].push(fn);
    },
    //釋出訊息(釋出函式), 依次通知訂閱者
    publish: function(key, ...arg) {
        for(let fn of this.list[key]) {
            fn.call(this, ...arg);
        }
    },
    // 取消訂閱
    unSubscribe: function (key, fn) {
        let fnList = this.list[key];
        if (!fnList) return false;

        if (!fn) {
            // 不傳入指定取消的訂閱方法,則清空所有key下的訂閱
            fnList && (fnList.length = 0);
        } else {
            fnList.forEach((item, index) => {
                if (item === fn) {
                fnList.splice(index, 1);
            }
        })
        }
    }
}

// 訂閱
pubSub.subscribe('onwork', time => {
    console.log(`上班了:${time}`);
})
pubSub.subscribe('offwork', time => {
    console.log(`下班了:${time}`);
})
pubSub.subscribe('launch', time => {
    console.log(`吃飯了:${time}`);
})

// 釋出
pubSub.publish('offwork', '18:00:00');
pubSub.publish('launch', '12:00:00');

// 輸出
// 下班了:18:00:00
// 吃飯了:12:00:00複製程式碼

觀察者模式 VS 釋出訂閱模式 區別:

前端面試題 回顧與複習(更新中)

類似點

都是定義一個一對多的依賴關係,有關狀態發生變更時執行相應的通知。

區別點

釋出訂閱模式更靈活,是進階版的觀察者模式,指定對應分發。

  1. 觀察者模式維護單一事件對應多個依賴該事件的物件關係;
  2. 釋出訂閱維護多個事件(主題)及依賴各事件(主題)的物件之間的關係;
  3. 觀察者模式是目標物件直接觸發通知(全部通知),觀察物件被迫接收通知。釋出訂閱模式多了箇中間層(事件中心),由其去管理通知廣播(只通知訂閱對應事件的物件);
  4. 觀察者模式物件間依賴關係較強,釋出訂閱模式中物件之間實現真正的解耦

原型和繼承

原型鏈:

每個物件都有一個私有屬性(非標準屬性:__proto__) ,指向它的建構函式的原型物件 (prototype)——「它的建構函式的原型物件」也有一個自己的原型物件,以此類推直到一個物件的原型物件為 nullnull 沒有原型,null 是原型鏈中最後一環。

我們需要牢記兩點:

①__proto__和constructor屬性是物件所獨有的;

② prototype屬性是函式所獨有的,因為函式也是一種物件,所以函式也擁有__proto__和constructor屬性。

__proto__屬性的作用就是當訪問一個物件的屬性時,如果該物件內部不存在這個屬性,那麼就會去它的__proto__屬性所指向的那個物件(父物件)裡找,一直找,直到__proto__屬性的終點null,再往上找就相當於在null上取值,會報錯。通過__proto__屬性將物件連線起來的這條鏈路即我們所謂的原型鏈。

prototype屬性的作用就是讓該函式所例項化的物件們都可以找到公用的屬性和方法,即f1.__proto__ === Foo.prototype。 constructor屬性的含義就是指向該物件的建構函式,所有函式(此時看成物件了)最終的建構函式都指向Function。

function Child(){
    this.name = 'lili'
}
const child = new Child()

console.log(child.__proto__ === Child.prototype) // true
console.log(Child.__proto__ === Function.prototype) // true
console.log(child.__proto__.__proto__.__proto__) // null複製程式碼

1 child.__proto__等於什麼? 

答案:Child.prototype 

2 Child.__proto__等於什麼? 

答案:Function.prototype 

解析:例項的__proto__屬性(原型)等於其建構函式的prototype屬性。例項child的建構函式為Child,而Child的建構函式為Function,結果就一目瞭然了。

繼承的實現方案

ES5組合繼承

//定義一個父類:人
function Person(cai) {
    this.cai = cai;
    this.emotion = ['喜', '怒', '哀', '樂']; //人都有喜怒哀樂
}
//定義原型類方法 將 Person 類中需共享的方法放到 prototype 中,實現複用
Person.prototype.eat = function () {
    console.log('吃' + this.cai);
}

//定義子類:學生,繼承了“人”這個類
function Student(cai, studentID) {
    //先呼叫父類構造器 子類繼承父類的屬性 需要將this指向父類中的cai
    Person.call(this, cai);
    this.studentID = studentID; // studentID是子類自己的屬性
}
Student.prototype = new Person(); //子類繼承父類的方法此時 Student.prototype 中的 constructor 被重寫了,會導致 stu1.constructor === Person
Student.prototype.constructor = Student; //將 Student 原型物件的 constructor 指標重新指向 Student 本身
//建立子類的例項
var stu1 = new Student('西蘭花', 1001);
console.log(stu1.emotion); //['喜', '怒', '哀', '樂']
console.log(stu1.cai); // 西蘭花
stu1.eat(); //吃西蘭花
console.log(stu1.constructor); //Student複製程式碼

結合原型鏈繼承和借用建構函式繼承即組合繼承是javascript最常用的繼承模式,不過,它也有自己的不足:組合繼承無論在什麼情況下,都會呼叫兩次父類建構函式。 一次是在建立子類原型的時候,另一次是在子類建構函式內部.子類最終會包含父類物件的全部例項屬性,但我們不得不在呼叫子類建構函式時重寫這些屬性。

繼承實質的區別:

**ES5:**先創造子類的例項物件this,然後再將父類的方法新增到this上面(Parent.apply(this))。  

**ES6:**先創造父類的例項物件this,(所以必須先呼叫super方法)然後再用子類的建構函式修改this。

ES6類式繼承

Es6 引入了新的關鍵字實現 class ,除了 class 之外,還有 constructorstaticextendssuper

class Person {
    constructor(cai) {
        this.cai = cai;
        this.emotion = ['喜', '怒', '哀', '樂']; //人都有喜怒哀樂
    }
    eat() {
        console.log('吃' + this.cai);
    }
}

class Student extends Person {
    constructor(cai, studentID) {
        // 指向父類的建構函式
        super(cai);
        this.studentID = studentID;
    }
    showStudentID() {
        console.log(this.studentID)
    }
}
var stu1 = new Student('西蘭花', 1001);
console.log(stu1.emotion);
stu1.eat(); //吃西蘭花
stu1.showStudentID() //1001複製程式碼

注:

ES6 裡的 Class 是通過關鍵字 extends 實現繼承 

子類必須在 constructor 方法中呼叫 super 方法 ,super的關鍵字在這裡表示父類的建構函式,用來創造父類的this物件, 而子類是沒有自己的 this 物件的,需要呼叫 super 方法,來繼承父類的 this 物件,然後對其加工 ,故可知只有呼叫了 super 之後才可以使用 this 關鍵字,否則會報錯

效能優化:

alt


1)利⽤多個域名來儲存⽹站資源:

cdn:內容分發⽹絡,基本思路是儘可能避開互聯⽹上有可能影響資料傳輸速度和穩 定性的瓶頸和環節,使內容傳輸的更快、更穩定。

原因:

  • CDN 快取更⽅便
  • 突破瀏覽器併發限制
  • 節約 cookie 頻寬
  • 節約主域名的連線數,優化⻚⾯響應速度
  • 防⽌不必要的安全問題

重繪(Repaint)和迴流(Reflow)是啥?如何優化?

  • 重繪是當節點需要更改外觀而不會影響佈局的,比如改變 color 就叫稱為重繪。
  • 迴流是佈局或者幾何屬性需要改變就稱為迴流。 迴流必定會發生重繪,重繪不一定會引發迴流。迴流所需的成本比重繪高的多,改變父節點裡的子節點很可能會導致父節點的一系列迴流。
  • 優化方案:
  1. 使用 transform 替代 top
  2. 使用 visibility 替換 display: none ,因為前者只會引起重繪,後者會引發迴流(改變了佈局)
  3. 不要把節點的屬性值放在一個迴圈裡當成迴圈裡的變數
  4. 不要使用 table 佈局,可能很小的一個小改動會造成整個 table 的重新佈局
  5. 動畫實現的速度的選擇,動畫速度越快,迴流次數越多,也可以選擇使用requestAnimationFrame
  6. CSS 選擇符從右往左匹配查詢,避免節點層級過多
  7. 將頻繁重繪或者回流的節點設定為圖層,圖層能夠阻止該節點的渲染行為影響別的節點。比如對於 video 標籤來說,瀏覽器會自動將該節點變為圖層。

for in 和for of的區別

for in

1 適合遍歷物件 
2 遍歷的是key
3 遍歷可列舉的所有屬性(包括原型上的)
4 可以正確響應break continue

遍歷物件:

Object.prototype.method = function(){  
}

var myObject={  
    a:1,  
    b:2,  
    c:3
}

for(var key in myObject) {
    console.log(key);
    //a
    //b
    //c
    //method
}

複製程式碼

for in 可以遍歷到myObject的原型方法method,如果不想遍歷原型方法和屬性的話,可以在迴圈內部判斷一下,hasOwnPropery方法可以判斷某屬性是否是該物件的例項屬性

首先輸出的是物件的屬性名,再是物件原型中的屬性和方法, 如果不想讓其輸出原型中的屬性和方法,可以使用hasOwnProperty方法進行過濾
Object.prototype.method = function(){  
}

var myObject={  
    a:1,  
    b:2,  
    c:3
}

for (var key in myObject) {
    if(myObject.hasOwnProperty(key)){
        console.log(key); // a b c
    }
}複製程式碼

遍歷陣列:

Array.prototype.sayHello = function(){
}
Array.prototype.str = 'world';
var myArray = [1,2,10,30,100];
myArray.name='陣列';

for(let index in myArray){
    console.log(index);
    //   0,1,2,3,4,name,str,sayHello
}複製程式碼

注意: for in更適合遍歷物件,不要使用for in遍歷陣列。使用for in 也可以遍歷陣列,但是會存在以下問題:

  1. index索引為字串型數字,不能直接進行幾何運算

  2. 使用for in會遍歷陣列所有的可列舉屬性,包括原型。例如上慄的原型方法method和name屬性

  3. 遍歷順序有可能不是按照實際陣列的內部順序

  4. for in遍歷的是陣列的索引(即鍵名)

for of

適合遍歷擁有迭代器物件的集合 
遍歷的value 
不包括原型上的 
也可以正確響應break continue

遍歷物件:

Object.prototype.method = function(){  
}

var myObject={  
    a:1,  
    b:2,  
    c:3
}

for (var key of myObject) {
    console.log(key); //  TypeError: myObject is not iterable
}複製程式碼

遍歷陣列:

Array.prototype.sayHello = function(){
    console.log("Hello");
}
var myArray = [1,200,3,400,100];

for(let key of myArray){
    console.log(key);
    // 1  200  2  400 400
}複製程式碼

注意:for in遍歷的是陣列的索引(即鍵名),而for of遍歷的是陣列元素值。 所以for in更適合遍歷物件,不要使用for in遍歷陣列。

nodejs模組載入機制


判斷兩個變數是否相等

在js中比較兩個值時,你可能會用相等運算子==或者嚴格相等運算子 === 

==表示抽象相等,兩邊值型別不同的時候,會先做隱式型別轉換,再對值進行比較;
===表示嚴格相等,不會做型別轉換,兩邊的型別不同一定不相等。但是===情況下NaN不等於NaN,+0等於-0

console.log(NaN === NaN) //false
console.log(+0 === -0 ) // true複製程式碼

ES6中的Object.is()接受兩個引數,並且會在二者的值相等時返回true,此時要求二者的資料型別相同並且值也相等。

console.log( Object.is(NaN, NaN)) //true

console.log(Object.is(+0, -0)) //false複製程式碼

原理:

if (!Object.is) {
    Object.is = function(x, y) {
        if (x === y) { // Steps 1-5, 7-10
            // 針對 +0不等於-0
            return x !== 0 || 1 / x === 1 / y;
        } else {
            // 針對 NaN等於NaN
            return x !== x && y !== y;
        }
    };
}複製程式碼

深拷貝與淺拷貝(前端基礎面試高頻面試題)

參考連結 https://juejin.im/post/59ac1c4ef265da248e75892b#heading-2

引子:

Js裡有兩種資料型別,基本資料型別和引用資料型別。深拷貝、淺拷貝一般都是針對引用資料型別的。

基本資料型別主要是:undefined,boolean,number,string,null

var a = 1;
var b = a;
a = 2;
console.log(a); // 2
console.log(b); // 1複製程式碼
對於基本資料型別賦值操作,b 複製了 a 的值,而不是引用。即儲存 b 的值與 a 的值的記憶體空間是完全獨立的。
var arr1 = [1,2,3,4];
var arr2 = arr1;

arr1.push(5);
console.log(arr1); // [1,2,3,4,5]
console.log(arr2); // [1,2,3,4,5]

arr2.push(6);
console.log(arr1); // [1,2,3,4,5,6]
console.log(arr2); // [1,2,3,4,5,6]複製程式碼

然而,對於引用資料型別的賦值操作,arr2 僅僅是複製了 arr1的引用(也可以稱之為指向 arr1 記憶體地址的指標)。簡單來說,就是 arr1 與 arr2 指向了同一個記憶體空間

淺拷貝:

如果屬性是基本型別,拷貝的就是基本型別的值;如果屬性是記憶體地址(引用型別),拷貝的就是記憶體地址 ,因此如果其中一個物件改變了這個地址,就會影響到另一個物件。

function shallowCopy(copyTarget) {
    var obj = {};
    for (var key in copyTarget) {
        obj[key] = copyTarget[key];
    }
    return obj;
}

var json1 = {
    'name': '張三',
    'family': {
        'children': '張三三',
        'wife': '李四'
    }
}

var json2 = shallowCopy(json1);

// before
console.log(json2);

// after
json1.family['father'] = '張一'
console.log(json1);
console.log(json2);複製程式碼

前端面試題 回顧與複習(更新中)

由此可以看出,淺拷貝僅僅拷貝了基本型別的資料,對於引用型別資料,則指向被複制的記憶體地址,若原地址中的物件發生改變,那麼淺複製出來的物件也會相應改變。

深拷貝:

深拷貝可概括為:為引用型別資料成員另闢了一個獨立的記憶體空間,實現真正內容上的拷貝。

function deepCopy(copyTarget) {
    var obj = {};
    for(var key in copyTarget) {
        // 先判斷obj[key]是否為物件
        if(typeof copyTarget[key] === "object"){
            // 遞迴
            obj[key] = deepCopy(copyTarget[key]);
        } else {
            // 如果不是物件,直接賦值即可
            obj[key] = copyTarget[key];
        }
    }
    return obj;
}

var json1 = {
    'name': '張三',
    'family': {
        'children': '張三三','wife': '李四'
    }
}

var json2 = deepCopy(json1);

// before
console.log(json2);

// after
json1.family['father'] = '張一'
console.log(json1);
console.log(json2);複製程式碼

前端面試題 回顧與複習(更新中)

擴充

深複製可以用JSON的方式:JSON.parse(JSON.stringify(obj))

但是JSON複製會忽略掉值為undefined以及函式表示式。

var obj = {
    a: 1,
    b: 2,
    c: undefined,
    sum: function() { return a + b; }
};

var obj2 = JSON.parse(JSON.stringify(obj));
console.log(obj2); //Object {a: 1, b: 2}複製程式碼

前端模組化AMD、CMD、CommonJS&ES6

參考連結 juejin.im/post/5aaa37…

CommonJS規範:

CommonJS 是伺服器端模組的規範, Node.js 採⽤了這個規範。 CommonJS 規範載入模 塊是同步的,也就是說,只有載入完成,才能執⾏後⾯的操作。 CommonJS 的規範中通過對 module.exports 或 exports 的屬性賦值來達到暴露模組物件的⽬的。CommonJS 是同步載入模組,在瀏覽器中會出現堵塞情況,所以不適⽤

AMD:

AMD 規範則是非同步載入模組,需要定義回撥 define ⽅式,AMD推崇依賴前置,--requireJS 推廣過程中出現的規範。

define(["a", "b", "c", "d", "e", "f"], function(a, b, c, d, e, f) { 
    // 等於在最前面宣告並初始化了要用到的所有模組
    if (false) {
      // 即便沒用到某個模組 b,但 b 還是提前執行了
      b.foo()
    } 
});複製程式碼

CMD

CMD: CMD和AMD類似 不同的是AMD是推崇依賴前置,--requireJS 推廣過程中出現的規範。CMD推崇就近依賴。 --sea.js推廣過程中出現的規範。 但是因為在AMD&CMD都是在瀏覽器端使用,採用的是非同步載入,其實CMD還是需要在一開始就請求需要的,只是寫法上更方便了。(採用的是正則匹配來獲得js檔名,所以註釋掉的仍然會請求,並且只可以書寫字串,不可以使用表示式)

/** AMD寫法 **/
define(["a", "b", "c", "d", "e", "f"], function(a, b, c, d, e, f) { 
     // 等於在最前面宣告並初始化了要用到的所有模組
    a.doSomething();
    if (false) {
        // 即便沒用到某個模組 b,但 b 還是提前執行了
        b.doSomething()
    } 
});

/** CMD寫法 **/
define(function(require, exports, module) {
    var a = require('./a'); //在需要時申明
    a.doSomething();
    if (false) {
        var b = require('./b');
        b.doSomething();
    }
});

/** sea.js **/
// 定義模組 math.js
define(function(require, exports, module) {
    var $ = require('jquery.js');
    var add = function(a,b){
        return a+b;
    }
    exports.add = add;
});
// 載入模組
seajs.use(['math.js'], function(math){
    var sum = math.add(1+2);
});複製程式碼

es6

ES6 在語言標準的層面上,實現了模組功能,而且實現得相當簡單,旨在成為瀏覽器和伺服器通用的模組解決方案。其模組功能主要由兩個命令構成:export和import。export命令用於規定模組的對外介面,import命令用於輸入其他模組提供的功能。

注:es6模組就是⼀個獨⽴的⽂件,該⽂件內部的所有變數,外部⽆法獲取。如果你希 望外部能夠讀取模組內部的某個變數,就必須使⽤ export 關鍵字輸出該變數 es6 還可 以匯出類、⽅法,⾃動適⽤嚴格模式

/** 定義模組 math.js **/
var basicNum = 0;
var add = function (a, b) {
    return a + b;
};
export { basicNum, add };

/** 引用模組 **/
import { basicNum, add } from './math';
function test(ele) {
    ele.textContent = add(99 + basicNum);
}

複製程式碼

如上例所示,使用import命令的時候,使用者需要知道所要載入的變數名或函式名。其實ES6還提供了export default命令,為模組指定預設輸出,對應的import語句不需要使用大括號。這也更趨近於ADM的引用寫法。

/** export default **/
//定義輸出
export default { basicNum, add };
//引入
import math from './math';
function test(ele) {
    ele.textContent = math.add(99 + math.basicNum);
}複製程式碼

ES6的模組不是物件,import命令會被 JavaScript 引擎靜態分析,在編譯時就引入模組程式碼,而不是在程式碼執行時載入,所以無法實現條件載入。也正因為這個,使得靜態分析成為可能。

ES6 模組與 CommonJS 模組的差異

1. CommonJS 模組輸出的是一個值的拷貝,ES6 模組輸出的是值的引用。

  • CommonJS 模組輸出的是值的拷貝,也就是說,一旦輸出一個值,模組內部的變化就影響不到這個值。
  • ES6 模組的執行機制與 CommonJS 不一樣。JS 引擎對指令碼靜態分析的時候,遇到模組載入命令import,就會生成一個只讀引用。等到指令碼真正執行時,再根據這個只讀引用,到被載入的那個模組裡面去取值。換句話說,ES6 的import有點像 Unix 系統的“符號連線”,原始值變了,import載入的值也會跟著變。因此,ES6 模組是動態引用,並且不會快取值,模組裡面的變數繫結其所在的模組。
2. CommonJS 模組是執行時載入,ES6 模組是編譯時輸出介面。

  • 執行時載入: CommonJS 模組就是物件;即在輸入時是先載入整個模組,生成一個物件,然後再從這個物件上面讀取方法,這種載入稱為“執行時載入”。

  • 編譯時載入: ES6 模組不是物件,而是通過 export 命令顯式指定輸出的程式碼,import時採用靜態命令的形式。即在import時可以指定載入某個輸出值,而不是載入整個模組,這種載入稱為“編譯時載入”。

CommonJS 載入的是一個物件(即module.exports屬性),該物件只有在指令碼執行完才會生成。而 ES6 模組不是物件,它的對外介面只是一種靜態定義,在程式碼靜態解析階段就會生成。


事件環(個人認為是比較高階的面試題)

參考連結 juejin.im/post/5b35cd…

瀏覽器中 Event Loop:

瀏覽器中, js引擎執行緒會迴圈從 任務佇列 中讀取事件並且執行, 這種執行機制稱作 Event Loop (事件迴圈).

每個瀏覽器環境,至多有一個event loop。 一個event loop可以有1個或多個task queue(任務佇列) 先執行同步的程式碼,然後js會跑去訊息佇列中執行非同步的程式碼,非同步完成後,再輪到回撥函式,然後是去下個事件迴圈中執行setTimeout 它從script(整體程式碼)開始第一次迴圈。之後全域性上下文進入函式呼叫棧。直到呼叫棧清空(只剩全域性),然後執行所有的micro-task(微任務)。當所有可執行的micro-task(微任務)執行完畢之後。迴圈再次從macro-task 巨集任務開始,找到其中一個任務佇列執行完畢,然後再執行所有的micro-task,這樣一直迴圈下去。  

從規範上來講,setTimeout有一個4ms的最短時間,也就是說不管你設定多少,反正最少都要間隔4ms才執行裡面的回撥。而Promise的非同步沒有這個問題。Promise所在的那個非同步佇列優先順序要高一些 Promise是非同步的,是指他的then()和catch()方法,Promise本身還是同步的 Promise的任務會在當前事件迴圈末尾中執行,而setTimeout中的任務是在下一次事件迴圈執行

總結:

  • 我們的同步任務在主執行緒上執行會形成一個執行棧
  • 如果碰到非同步任務,比如setTimeout、onClick等等的一些操作,我們會將他的執行結果放入佇列,此期間主執行緒不阻塞
  • 等到主執行緒中的所有同步任務執行完畢,就會通過event loop在佇列裡面從頭開始取,在執行棧中執行
  • event loop永遠不會斷
  • 以上的這一整個流程就是Event Loop(事件迴圈機制)

  • 補充:

    巨集任務:js同步執行的程式碼塊,setTimeout、setInterval、XMLHttprequest等。

    微任務:promise、process.nextTick(node環境)等。
    執行棧中執行的任務都是巨集任務,當巨集任務遇到Promise的時候會建立微任務,當Promise狀態fullfill的時候塞入微任務佇列。在一次巨集任務完成後,會檢查微任務佇列有沒有需要執行的任務,有的話按順序執行微任務佇列中所有的任務。之後再開始執行下一次巨集任務。具體步驟:
    1. 執行主程式碼塊
    2. 若遇到Promise,把then之後的內容放進微任務佇列
    3. 一次巨集任務執行完成,檢查微任務佇列有無任務
    4. 有的話執行所有微任務
    5. 執行完畢後,開始下一次巨集任務。

    setTimeout(function(){
        console.log(4)
    },0);
    new Promise(function(resolve){
        console.log(1)
        for( var i=0 ; i<10000 ; i++ ){
            i===9999 && resolve()
        }
        console.log(2)
    }).then(function(){
        console.log(5)
    });
    console.log(3);
    //依次輸出 1  2  3  5  4
    複製程式碼
    setTimeout(() => {
        console.log(1);
    }, 0);
    
    new Promise((resolve) => {
        console.log(2);
        resolve();
    }).then(() => {
        console.log(3);
    });
    
    console.log(4);
    // 輸出最後的結果 2  4  3  1複製程式碼

    分析:

  • setTimeout丟給瀏覽器的非同步執行緒處理,因為時間是0,馬上放入訊息佇列
  • new Promise裡面的console.log(2)加入執行棧,並執行,然後退出
  • 直接resolve,then後面的內容加入微任務佇列
  • console.log(4)加入執行棧,執行完成後退出
  • 檢查微任務佇列,發現有任務,執行console.log(3)
  • 發現訊息佇列有任務,執行下一次巨集任務console.log(1)

  • node環境

    node環境中的事件機制要比瀏覽器複雜很多,node的事件輪詢有階段的概念。每個階段切換的時候執行,process.nextTick之類的所有微任務。

    前端面試題 回顧與複習(更新中)

    timer階段

    執行所有的時間已經到達的計時事件

    peding callbacks階段

    這個階段將執行所有上一次poll階段沒有執行的I/O操作callback,一般是報錯。

    idle.prepare

    可以忽略

    poll階段

    這個階段特別複雜

    1. 阻塞等到所有I/O操作,執行所有的callback.
    2. 所有I/O回撥執行完,檢查是否有到時的timer,有的話回到timer階段
    3. 沒有timer的話,進入check階段.

    check階段

    執行setImmediate

    close callbacks階段

    執行所有close回撥事件,例如socket斷開。


    函式柯里化

    在一個函式中,首先填充幾個引數,然後再返回一個新的函式的技術,稱為函式的柯里化。通常可用於在不侵入函式的前提下,為函式 預置通用引數,供多次重複呼叫。

    const add = function add(x) {
    	return function (y) {
    		return x + y
    	}
    }
    
    const add1 = add(1)
    
    add1(2) === 3
    add1(20) === 21複製程式碼

    HTTP相關

    從輸入URL到頁面載入發生了什麼?

    • 1、瀏覽器的位址列輸入URL並按下回車。 
    • 2、瀏覽器查詢當前URL是否存在快取,並比較快取是否過期。 
    • 3、DNS解析URL對應的IP。 
    • 4、根據IP建立TCP連線(三次握手)。 
    • 5、HTTP發起請求。 
    • 6、伺服器處理請求,瀏覽器接收HTTP響應。 
    • 7、渲染頁面,構建DOM樹。 
    • 8、關閉TCP連線(四次揮手)。

    AST 

    抽象語法樹 (Abstract Syntax Tree),是將程式碼逐字母解析成 樹狀物件 的形式。這是語言之間的轉換、程式碼語法檢查,程式碼風格檢查,程式碼格式化,程式碼高亮,程式碼錯誤提示,程式碼自動補全等等的基礎。例如: 

    function square(n){
    	return n * n
    }複製程式碼

    通過解析轉化成的AST如下圖:

    前端面試題 回顧與複習(更新中)

    babel編譯原理: 

    babylon 將 ES6/ES7 程式碼解析成 AST 

    babel-traverse 對 AST 進行遍歷轉譯,得到新的 AST 

    新 AST 通過 babel-generator 轉換成 ES5

    關於vuex

    其實在學習vuex的時候最好要模擬資料體驗下最好

    模擬資料:

    const fs = require('fs')
    // Koa 為一個class
    const Koa = require('koa')
    const path = require('path')
    // koa 路由中介軟體
    const Router = require('koa-router');
    const app = new Koa()
    // 例項化路由
    const router = new Router();
    // 處理post請求,把 koa2 上下文的表單資料解析到
    const bodyParser = require('koa-bodyparser');
    app.use(bodyParser())
    
    router.get('/', async (ctx, next) => {
        ctx.response.body = 'hello koa !'
    })
    // 加字首
    //router.prefix('/api');
    router.get('/api/news', async (ctx, next) => {
        ctx.response.body = {
            result: "這是一條新聞!!!!"
        }
    })
    
    
    app
        .use(router.routes())
        .use(router.allowedMethods());
    app.listen(4000)複製程式碼

    store/index.js

    import Vue from 'vue'
    import 'es6-promise/auto' //引用依賴
    import Vuex from 'vuex'
    import http from '@/http/index.js'
    Vue.use(Vuex) //使用vuex外掛
    
    //state
    export const state = {
        news: ''
    }
    
    //action
    // Action 函式接受一個與 store 例項具有相同方法和屬性的 context 物件,因此你可以呼叫 context.commit 提交一個 mutation,或者通過 context.state 和 context.getters 來獲取 state 和 getters。
    export const actions = {
        //非同步操作都在這裡
        async getNews(context, grade) {
            //所有在此的方法第一個引數都是context
            let newsResult = await http.get('/api/news', {})
            context.commit('SET_NEWS', newsResult)
        }
    }
    
    //mutations
    export const mutations = {
        SET_NEWS(state, data) {
            state.news = data.data.result
        }
    }
    export default new Vuex.Store({
        state,
        mutations,
        actions
    })複製程式碼

    呼叫

    <template>
        <div>{{news}}</div>
    </template>
    
    <script>
        /* 輔助函式 */
        import {
            mapState,
            mapGetters,
            mapMutations,
            mapActions
        } from 'vuex'
    
        export default {
            data() {
                return {
                }
            },
            computed: {
                ...mapState(['news'])
            },
            methods: {
                ...mapActions(['getNews'])
            },
            mounted() {
                this.getNews()
                //或者
                //this.$store.dispatch('getNews')
    
            }
        }
    </script>複製程式碼

    參考連結 https://www.yuque.com/testdog/dev/zuphei

    vue路由傳參

    通過Vue傳遞引數可以分為兩種方式: params引數 query引數

    params引數傳遞方式分兩種:(1)通過router-link進行跳轉 (2)通過程式設計導航進行路由跳轉

    params傳值 


    <div
      v-for="item in list"
      @click="getDescribe(item)"
    >
      {{item}}
    </div>複製程式碼
    data() {
        return {
            list: ['101','102','103']
        }
    }複製程式碼

    對應路的由配置如下:

    {
        path: '/two/:id',
        name: 'two',
        component: () => import('@/components/two.vue'),
        meta: {
            title: 'two'
        }
    }複製程式碼

    push 後面可以是物件,也可以是字串:

    物件:

    getDescribe(item) {
        this.$router.push({
            path: `/two/${item}`
        })
    }複製程式碼

    字串:

    getDescribe(item) {
        this.$router.push(`/two/${item}`)
    }複製程式碼

    命名的路由:

    getDescribe(item) {
        this.$router.push({ name: 'two', params: { id: `${item}` }})
    }複製程式碼

    注意: 需要注意的是使用params必須和name屬性一起使用,否則要跳轉的目標路由頁面無法通過params獲取到傳遞過來的引數。params: 相當於 post 請求,請求引數不會體現在位址列中。 這種方法會出現如下問題: 如果子頁面點選【重新整理】按鈕時,剛才傳過來的引數並不會同步。因為引數沒有同步到網頁位址列中。但是如果要讓他體現在位址列中,可以在配置路由時,寫在path屬性中。 如下可以解決重新整理的問題:

    {
        path: '/two/:id',
        name: 'two',
        component: () => import('@/components/two.vue'),
        meta: {
            title: 'two'
        }
    }複製程式碼

    讀取引數:

    console.log(this.$route.params)複製程式碼


    query傳值。 類似get傳值

    query: 相當於 get 請求,請求引數會體現在位址列中。

    this.$router.push({
        path: `/two`,
        query: {
            id:item
        }
    })複製程式碼

    http://localhost:8888/#/two?id=101

    console.log(this.$route.query)複製程式碼

    this.$router 和this.$route有何區別?

    上面提到的程式設計式導航中用this.$router.push()來改變路由,用this.$route.params來得到引數值

    1.$router為VueRouter例項,想要導航到不同URL,則使用$router.push方法。

    2.$route為當前router跳轉物件,裡面可以獲取name、path、query、params等


    vue中watch&&computed 

    computed特性 

    1.是計算值, 

    2.應用:就是簡化tempalte裡面{{}}計算和處理props或$emit的傳值 

    3.具有快取性,頁面重新渲染值不變化,計算屬性會立即返回之前的計算結果,而不必再次執行函式  

    注意:computed的值在getter執行後是會快取的,只有在它依賴的屬性值改變之後,下一次獲取computed的值時才會重新呼叫對應的getter來計算

    watch特性 

    1.是觀察的動作, 

    2.應用:監聽props,$emit或本元件的值執行非同步操作 

    3.無快取性,頁面重新渲染時值不變化也會執行

    注意:watch在每次監聽的值變化時,都會執行回撥。其實從這一點來看,都是在依賴的值變化之後,去執行回撥

    總結:如果一個值依賴多個屬性(多對一),用computed肯定是更加方便的。如果一個值變化後會引起一系列操作,或者一個值變化會引起一系列值的變化(一對多),用watch更加方便一些。

    watch的回撥裡面會傳入監聽屬性的新舊值,通過這兩個值可以做一些特定的操作。computed通常就是簡單的計算。 watch和computed並沒有哪個更底層,watch內部呼叫的是vm.$watch,它們的共同之處就是每個定義的屬性都單獨建立了一個Watcher物件

    Vue.js 提供了一個方法 watch,它用於觀察Vue例項上的資料變動。

    <template>
      <div>
        <input
          type="text"
          v-model="age"
        >
      </div>
    </template>
    
    <script>
        export default {
            data() {
                return {
                    age:18
                }
            },
            watch: {
                age: (newAge,oldAge) => {
                    console.log('新值為:'+newAge+',舊值為:'+oldAge);
                }
            },
            methods: {
            },
            components: {
            },
            computed: {
            },
            mounted() {        
            }
        }
    複製程式碼

    如何理解Virtual DOM

    一、vdom是什麼? 

    vdom是虛擬DOM(Virtual DOM)的簡稱,指的是用JS模擬的DOM結構,將DOM變化的對比放在JS層來做。換而言之,vdom就是JS物件。

    如下真實DOM

    <ul id="list">
        <li class="item">Item1</li>
        <li class="item">Item2</li>
    </ul>複製程式碼

    對映成虛擬DOM就是這樣:

    {
        tag: "ul",
        attrs: {
            id:&emsp;"list"
        },
        children: [
            {
                tag: "li",
                attrs: { className: "item" },
                children: ["Item1"]
            }, {
                tag: "li",
                attrs: { className: "item" },
                children: ["Item2"]
            }
        ]
    } 複製程式碼

    二、為什麼要用vdom?

    採用JS物件模擬的方法,將DOM的比對操作放在JS層,減少瀏覽器不必要的重繪,提高效率。

    當然有人說虛擬DOM並不比真實的DOM快,其實也是有道理的。當一個table中的每一條資料都改變時,顯然真實的DOM操作更快,因為虛擬DOM還存在js中diff演算法的比對過程。所以,效能優勢僅僅適用於大量資料的渲染並且改變的資料只是一小部分的情況。

    虛擬DOM更加優秀的地方在於: 

    1、它開啟了函式式的UI程式設計的大門,即UI = f(data)這種構建UI的方式。  

    2、可以將JS物件渲染到瀏覽器DOM以外的環境中,也就是支援了跨平臺開發,比如ReactNative。

    三、diff演算法


    vue 元件之間的通訊

    1、父元件向子元件通訊

    使用props,父元件可以使用props向子元件傳遞資料。

    father.vue

    <template>
      <div>
        <Child :name="msg"></Child>
      </div>
    </template>
    
    <script>
        import Child from '@/components/child.vue'
        export default {
            components: {
                Child
            },
            data() {
                return {
                    msg: 'feixuan'
                }
            },
            computed: {
    
            },
            mounted() {
    
            },
            methods: {
            }
        }
    </script>複製程式碼

    child.vue

    <template>
      <div>
        {{name}}
      </div>
    </template>
    
    <script>
        export default {
            props:{
                name:{
                    type:String,
                    default:''
                }
            },
            data() {
                return {
                }
            },
            components: {
    
            },
            computed: {
    
            },
            mounted() {
            },
            methods: {
            }
        }
    </script>複製程式碼

    2、子元件向父元件通訊

    方法一: 使用vue事件 父元件向子元件傳遞事件方法,子元件通過$emit觸發事件,回撥給父元件。

    father.vue

    <template>
      <div>
        <Child @msgFunc="msgEvent"></Child>
        {{message}}
      </div>
    </template>
    
    <script>
        import Child from '@/components/child.vue'
        export default {
            components: {
                Child
            },
            data() {
                return {
                    message: ''
                }
            },
            methods: {
                msgEvent(msg) {
                    console.log(msg)
                    this.message = msg
                }
            },
            computed: {
    
            },
            mounted() {
    
            }
            
        }
    </script>複製程式碼

    child.vue

    <template>
      <div>
        <button @click="handleClick">點我</button>
      </div>
    </template>
    
    <script>
        export default {
            data() {
                return {
                }
            },
            methods: {
                handleClick() {
                    this.$emit('msgFunc','我是來自子元件的訊息');
                }
            },
            components: {
    
            },
            computed: {
    
            },
            mounted() {
            }
            
        }
    </script>複製程式碼

    3、非父子元件通訊

    對於兩個元件不是父子關係,那麼又該如何實現通訊呢?在專案規模不大的情況下,完全可以使用中央事件匯流排 EventBus 的方式。如果你的專案規模是大中型的,那麼可以使用vuex狀態管理

    EventBus 通過新建一個 Vue 事件 bus 物件,然後通過 bus.$emit 觸發事件,bus.$on 監聽觸發的事件。

    Vue.js非同步更新DOM策略及nextTick

    在使用vue.js的時候,有時候因為一些特定的業務場景,不得不去操作DOM,比如這樣:

    <template>
      <div>
        <div ref="test">{{test}}</div>
        <button @click="handleClick">點選</button>
      </div>
    </template>
    
    <script>
        export default {
            data() {
                return {
                    test: 'begin'
                }
            },
            methods: {
                handleClick () {
                    this.test = 'end';
                    console.log(this.$refs.test.innerText);//列印“begin”
                }
            },
            components: {
            },
            computed: {
            },
            mounted() {
            }
        }
    </script>
    複製程式碼

    列印的結果是begin,為什麼我們明明已經將test設定成了“end”,獲取真實DOM節點的innerText卻沒有得到我們預期中的“end”,而是得到之前的值“begin”呢?

    原因

    Vue.js原始碼的Watch實現。當某個響應式資料發生變化的時候,它的setter函式會通知閉包中的Dep,Dep則會呼叫它管理的所有Watch物件。觸發Watch物件的update實現。我們來看一下update的實現。

    update () {
        /* istanbul ignore else */
        if (this.lazy) {
            this.dirty = true
        } else if (this.sync) {
            /*同步則執行run直接渲染檢視*/
            this.run()
        } else {
            /*非同步推送到觀察者佇列中,下一個tick時呼叫。*/
            queueWatcher(this)
        }
    }複製程式碼

    Vue的雙向資料繫結

    Vue是一個MVVM框架,資料繫結簡單來說,就是當資料發生變化時,相應的檢視會進行更新,當檢視更新時,資料也會跟著變化。

    實現資料繫結的方式大致有以下幾種:

    - 1、釋出者-訂閱者模式(backbone.js)
    - 2、髒值檢查(angular.js)
    - 3、資料劫持(vue.js)複製程式碼

    Vue.js則是通過資料劫持以及結合釋出者-訂閱者來實現的,資料劫持是利用ES5的Object.defineProperty(obj, key, val)來劫持各個屬性的的setter以及getter,在資料變動時釋出訊息給訂閱者,從而觸發相應的回撥來更新檢視。


    八、十、十六進位制轉換

    二進位制 → 十進位制

    方法:二進位制數從低位到高位(即從右往左)計算,第0位的權值是2的0次方,第1位的權值是2的1次方,第2位的權值是2的2次方,依次遞增下去,把最後的結果相加的值就是十進位制的值了。

    例:將二進位制的(101011)B轉換為十進位制的步驟如下:
    
    1. 第0位 1 x 2^0 = 1;
    
    2. 第1位 1 x 2^1 = 2;
    
    3. 第2位 0 x 2^2 = 0;
    
    4. 第3位 1 x 2^3 = 8;
    
    5. 第4位 0 x 2^4 = 0;
    
    6. 第5位 1 x 2^5 = 32;
    
    7. 讀數,把結果值相加,1+2+0+8+0+32=43,即(101011)B=(43)D。
    複製程式碼

    八進位制 → 十進位制

    方法:八進位制數從低位到高位(即從右往左)計算,第0位的權值是8的0次方,第1位的權值是8的1次方,第2位的權值是8的2次方,依次遞增下去,把最後的結果相加的值就是十進位制的值了。   八進位制就是逢8進1,八進位制數採用 0~7這八數來表達一個數。

    例:將八進位制的(53)O轉換為十進位制的步驟如下:
    
    1. 第0位 3 x 8^0 = 3;
    
    2. 第1位 5 x 8^1 = 40;
    
    3. 讀數,把結果值相加,3+40=43,即(53)O=(43)D。複製程式碼

    十進位制 → 二進位制

    方法:除2取餘法,即每次將整數部分除以2,餘數為該位權上的數,而商繼續除以2,餘數又為上一個位權上的數,這個步驟一直持續下去,直到商為0為止,最後讀數時候,從最後一個餘數讀起,一直到最前面的一個餘數。 

    例:將十進位制的(43)D轉換為二進位制的步驟如下:
    
    1. 將商43除以2,商21餘數為1;
    
    2. 將商21除以2,商10餘數為1;
    
    3. 將商10除以2,商5餘數為0;
    
    4. 將商5除以2,商2餘數為1;
    
    5. 將商2除以2,商1餘數為0; 
    
    6. 將商1除以2,商0餘數為1; 
    
    7. 讀數,因為最後一位是經過多次除以2才得到的,因此它是最高位,讀數字從最後的餘數向前讀,101011,即(43)D=(101011)B。複製程式碼


    函式節流跟防抖

    函式防抖(debounce)

    防抖函式 debounce 指的是某個函式在某段時間內,無論觸發了多少次回撥,都只執行最後一次。假如我們設定了一個等待時間 3 秒的函式,在這 3 秒內如果遇到函式呼叫請求就重新計時 3 秒,直至新的 3 秒內沒有函式呼叫請求,此時執行函式,不然就以此類推重新計時。

    應用場景:

    使用者需要不斷的輸入值,比如填寫input表單驗證或者在搜尋框要輸入一些內容。 

    使用者不斷地進行tab切換頁面操作時。

    原理及實現 :

    實現原理就是利用定時器,函式第一次執行時設定一個定時器,之後呼叫時發現已經設定過定時器就清空之前的定時器,並重新設定一個新的定時器,如果存在沒有被清空的定時器,當定時器計時結束後觸發函式執行。

    function  debounce(fn,delay) {
        let timer;
        return function () {
            var context = this; //儲存當前this指向當前物件
            var args = arguments;  //獲取隱式傳入的引數
            clearTimeout(timer);
            timer = setTimeout(function () {
                fn.apply(context, args);
            }, delay);
        }
    }複製程式碼

    函式節流(throttle)

    規定在一個單位時間內,只能觸發一次函式。如果這個單位時間內觸發多次函式,只有一次生效,
    即每隔一段時間後執行一次,也就是降低頻率,將高頻操作優化成低頻操作
    複製程式碼

    應用場景

    滾動上拉載入,滾動下拉重新整理。 oversize,mousemove ,mousehover。 還有拖拽事件等等。

    function  throttle(fn,delay) {
        let timer = null;
        return function () {
            var context = this; //儲存當前this指向當前物件
            var args = arguments;  //獲取隱式傳入的引數
            if(timer == null){
                timer = setTimeout(() => {
                    fn.apply(context,args)
                }, delay);
            }
        }
    }複製程式碼

    總結 :

    • 防抖 是將多次執⾏變為最後⼀次執⾏
    • 節流 是將多次執⾏變成每隔⼀段時間執⾏



    斐波那契數列

    斐波那契數列也叫黃金分割數列,也叫兔子數列 

    原理:假定一對大兔子每月能生一對小兔子,且每對新生的小兔子經過一個月可以長成一對大兔子,如果不發生死亡,且每次均生下一雌一雄,問一年後共有多少對兔子?

    月份兔子情況總數
    第0個月a(小兔子)1
    第1個月a(具備繁殖能力)1
    第2個月b(生啦生啦)+ a(他父母)2
    第3個月b(2月份出生的具備繁殖能力,正躍躍欲試) + b2(他父母又生二胎了) +a(他父母)3
    第4個月c(2月份的兔子b喜當爹)+b(二月份出生的兔子) + b2(二胎具備繁殖能力,準備生娃) +a(他父母)+d(a生三胎)5

    1、1 、2、3、5、8、13、21、34、55、89…… 

    所以規律就是 fn(n)=fn(n-1)+fn(n-2)

    迭代 方式

    /*
    *i 月份
    */
    function fn(i){
        var a=[];
        /*0個月什麼都不存在*/
        a[0]=0;
        a[1]=1;
        for(var j = 2; j<= i;j++){
            a[j]=a[j-1] + a[j-2];
        }
        return a[i]
    }複製程式碼

    遞迴方式

    /*
    * i 月份
    */
    function fn(i){
        if(i < 2){return i === 0 ? 0 : 1;}
        return fn(i-1)+fn(i-2)
    }複製程式碼

    總結: 針對這個例子來說,這裡的遞迴會進行太多次的呼叫(比迭代多),所以簡潔的背後犧牲的是效能


    vue-router實現原理

    這個是阿里的一道面試題

    核心原理: 更新檢視但不重新請求頁面。

    vue-router實現單頁面路由跳轉,提供了三種方式:hash模式、history模式、abstract模式,根據mode引數來決定採用哪一種方式。

    路由模式

    vue-router 提供了三種執行模式: 

    hash: 使用 URL hash 值來作路由。預設模式。 

    history: 依賴 HTML5 History API 和伺服器配置。檢視 HTML5 History 模式。 

    abstract: 支援所有 JavaScript 執行環境,如 Node.js 伺服器端

    Hash模式:

    hash即瀏覽器url中#後面的內容,包含#。hash是URL中的錨點,代表的是網頁中的一個位置,單單改變#後的部分,瀏覽器只會載入相應位置的內容,不會重新載入頁面。 也就是說 即#是用來指導瀏覽器動作的,對伺服器端完全無用,HTTP請求中,不包含#。 每一次改變#後的部分,都會在瀏覽器的訪問歷史中增加一個記錄,使用”後退”按鈕,就可以回到上一個位置。 所以說Hash模式通過錨點值的改變,根據不同的值,渲染指定DOM位置的不同資料。

    History模式:

    HTML5 History API提供了一種功能,能讓開發人員在不重新整理整個頁面的情況下修改站點的URL,就是利用 history.pushState API 來完成 URL 跳轉而無須重新載入頁面; 由於hash模式會在url中自帶#,如果不想要很醜的 hash,我們可以用路由的 history 模式,只需要在配置路由規則時,加入"mode: 'history'",這種模式充分利用 history.pushState API 來完成 URL 跳轉而無須重新載入頁面。 有時,history模式下也會出問題: eg: hash模式下:xxx.com/#/id=5 請求地址為 xxx.com,沒有問題。 history模式下:xxx.com/id=5 請求地址為 xxx.com/id=5,如果後端沒有對應的路由處理,就會返回404錯誤; 為了應對這種情況,需要後臺配置支援: 在服務端增加一個覆蓋所有情況的候選資源:如果 URL 匹配不到任何靜態資源,則應該返回同一個 index.html 頁面,這個頁面就是你 app 依賴的頁面。

    abstract模式:

    abstract模式是使用一個不依賴於瀏覽器的瀏覽歷史虛擬管理後端。 根據平臺差異可以看出,在 Weex 環境中只支援使用 abstract 模式。 不過,vue-router 自身會對環境做校驗,如果發現沒有瀏覽器的 API,vue-router 會自動強制進入 abstract 模式,所以 在使用 vue-router 時只要不寫 mode 配置即可,預設會在瀏覽器環境中使用 hash 模式,在移動端原生環境中使用 abstract 模式。 (當然,你也可以明確指定在所有情況下都使用 abstract 模式)。


    觀察者模式

    非同步程式設計

    async 及 await

    async 是讓方法變成非同步。 await 是等待非同步方法執行完成。

    注意:await 必須在 async 方法中才可以使用因為await 訪問本身就會造成程式停止堵塞,所以必須在非同步方法中才可以使用

    使用await語法的時候,後面的函式需要返回Promise物件

    async test1() {
        let result = await this.$http.get('/api/job',{})
        console.log(result)
    }複製程式碼

    十大經典排序演算法

    氣泡排序:

    兩兩元素進行比較,較大者放置後頭,類似水中的氣泡較大者浮在水的上端 

    參考:juejin.im/post/5d20ab…

    function bubble(arr) {
      for(var i = 0; i < arr.length; i++) {
        for(var j = 0; j < arr.length - 1- i; j++) {
          if(arr[j] > arr[j + 1]) {
            var max = arr[j];
            arr[j] = arr[j+1];//交換資料
            arr[j+1] = max;//交換資料
          }
        }
      }
      console.log(arr)
    }
    
    bubble([1,4,6,2,7,2])// [1, 2, 2, 4, 6, 7]複製程式碼

    快速排序:

    取一個元素為基準,把序列分成兩部分,小於基準的放到它的左面,大於等於的放到它的右面,然後在把左面和右面的子序列再進行上述的拆分,直到子序列不可再分割(小於2個元素),最終達到整個序列有序 

    paste image


    function quickSort(arr) {
      if(arr.length < 2) {
        return arr;
      } else {
        const pivot = arr[0]; // 基準值
        const pivotArr = []; // 一樣大的放中間
        const lowArr= []; // 小的放左邊
        const hightArr = []; // 大的放右邊
        arr.forEach(current => {
          if(current === pivot) pivotArr.push(current);
          else if(current > pivot) hightArr.push(current);
          else lowArr.push(current);
        })
        return quickSort(lowArr).concat(pivotArr).concat(quickSort(hightArr));
      }
    }
    
    console.log(quickSort([4,6,2,3,1,5,7,8])) // [1, 2, 3, 4, 5, 6, 7, 8]複製程式碼

    簡單選擇排序:

    首先在未排序序列中找到最小(大)元素,存放到排序序列的起始位置。 再從剩餘未排序元素中繼續尋找最小(大)元素,然後放到已排序序列的末尾。 重複第二步,直到所有元素均排序完畢。

    const selectionSort = array => {
      const len = array.length;
      let minIndex, temp;
      for (let i = 0; i < len - 1; i++) {
        minIndex = i;
        for (let j = i + 1; j < len; j++) {
          if (array[j] < array[minIndex]) {
            // 尋找最小的數
            minIndex = j; // 將最小數的索引儲存
          }
        }
        temp = array[i];
        array[i] = array[minIndex];
        array[minIndex] = temp;
        console.log('array: ', array);
      }
      return array;
    };
    selectionSort([6,4,3,8])//[3, 4, 6, 8]複製程式碼


    async和defer

    非同步載入js有三種 : defer 、 async 、 動態建立script標籤 、 按需非同步載入js

    async : 並行載入指令碼檔案,下載完畢立即解釋執行程式碼,不會按照頁面上的script順序執行。 defer : 並行下載js,會按照頁面上的script標籤的順序執行,然後在文件解析完成之後執行指令碼

    defer 與 async 的相同點是採用並行下載,在下載過程中不會產生阻塞。區別在於執行時機,async 是載入完成後自動執行,而 defer 需要等待頁面完成後執行。

    解析:

    <script src="script.js"></script>

    沒有 defer 或 async,瀏覽器會立即載入並執行指定的指令碼,“立即”指的是在渲染該 script 標籤之下的文件元素之前,也就是說不等待後續載入的文件元素,讀到就載入並執行。

    <script async src="script.js"></script>

    有 async,載入和渲染後續文件元素的過程將和 script.js 的載入與執行並行進行(非同步)。

    <script defer src="myscript.js"></script>

    有 defer,載入後續文件元素的過程將和 script.js 的載入並行進行(非同步),但是 script.js 的執行要在所有元素解析完成之後,DOMContentLoaded 事件觸發之前完成。

    Load 事件觸發代表頁面中的 DOM,CSS,JS,圖片已經全部載入完畢。DOMContentLoaded 事件觸發代表初始的 HTML 被完全載入和解析,不需要等待 CSS,JS,圖片載入。

    call、apply、bind方法的使用

    call 和 apply 是為了動態改變 this 而出現的,當一個 object 沒有某個方法,但是其他的有,我們可以藉助 call 或 apply 用其它物件的方法來操作。

    知乎簡單易懂答案:
    貓吃魚狗吃肉,奧特曼打怪獸
    有一天,狗想吃魚了
    貓.吃魚.call(狗,魚)
    狗就吃到魚了
    貓成精了,想打怪獸
    奧特曼.打怪獸.call(貓,小怪獸)
    
    obj.call(thisObj, arg1, arg2, ...)
    obj.apply(thisObj, [arg1, arg2, ...])
    
    兩者作用一致,都是把obj(即this)繫結到thisObj,這時候thisObj具備了obj的屬性和方法。
    或者說thisObj繼承了obj的屬性和方法。唯一區別是apply接受的是陣列引數,call接受的是連續引數。複製程式碼

    例子:

    var obj1 = {
      value: 1
    }
    
    function say() {
      console.log(this.value)
    }
    
    say() // 輸出undefined
    say.call(obj1) // 輸出1複製程式碼

    注意兩點 : call 改變了 this 的指向,此時的 this 指到了 obj1 say 函式執行了

    bind方法的特殊性:

    之所以把bind方法單獨放出來是因為bind方法和前面兩者還是有不小的區別的,雖然都是動態改變this的值,舉個例子

    var obj = {
        x: 81,
    };
    
    var foo = {
        getX: function() {
            return this.x;
        }
    }
    console.log(foo.getX.bind(obj)());  //81
    console.log(foo.getX.call(obj));    //81
    console.log(foo.getX.apply(obj));   //81複製程式碼

    有沒有注意到使用bind方法時候後面還要多加上一對括號,因為使用bind只是返回了對應函式並沒有立即執行,而call和apply方法是立即執行的

    bind與call和apply又有區別,一個函式被call的時候,會直接去呼叫,但是bind是會返回一個函式,當這個函式執行的時候,bind()的第一個引數將作為它執行時的this。

    手動實現bind方法

     參考連結 segmentfault.com/a/119000001…

    bind方法的全稱是Function.prototype.bind()  

    我們先來看看bind方法的定義:

    bind方法會建立一個新函式。當這個新函式被呼叫時,bind的第一個引數將作為它執行時的this(該引數不能被重寫), 之後的一序列引數將會在傳遞的實參前傳入作為它的引數。
    新函式也能使用new操作符建立物件:這種行為就像把原函式當成構造器,提供的this值被忽略。

    做了什麼:

    1. 返回了一個新函式
    2. 將this(傳入的引數)關鍵字繫結到該函式 引數合併,
    3. 將bind函式的引數與原來的函式引數合併作為引數傳給建立的新的函式
    4. 返回該函式

    初步思路

    1. 因為bind方法不是立即執行函式,需要返回一個待執行的函式,這裡可以利用閉包:return function(){}
    2. 作用域繫結:可以使用applycall方法來實現;
    3. 引數傳遞:由於引數的不確定性,需要用apply傳遞陣列;
    Function.prototype.mybind = function(context){
        //只能對函式執行bind方法
        if (typeof this !== "function") {
            throw new TypeError(this + ' must be a function');
        }
        let self = this;
        /**
         * 由於引數的不確定性,我們用 arguments 來處理
         * 這裡的 arguments 只是一個類陣列物件,可以用陣列的 slice 方法轉化成標準格式陣列
         * 除了作用域物件 self 以外,後面的所有引數都需要作為陣列進行引數傳遞
         * 獲取除第一個以外的引數,第一個引數是上下文,其它才是真正的引數。
         */
        var args = Array.prototype.slice.call(arguments, 1);
        // 利用一個空函式作為中轉
        let tempFn = function() {};
        var resultFn = function () {
            // 將新函式執行時的引數 arguments 全部陣列化,然後與繫結時傳參 arg 合併
            var innerArgs = Array.prototype.slice.call(arguments);
            // 如果 返回函式被當做建構函式後,生成的物件是 tempFn 的例項,此時應該將 this 的指向指向建立的例項。
            // 如果是new操作符例項出來物件,則為this。
            // 如果是直接執行方法,則為context
            if (this instanceof tempFn) {
                return self.apply(this, args.concat(innerArgs));
            } else {
                return self.apply(context, args.concat(innerArgs))
            }
        }
        // 中轉原型鏈
        // 讓返回函式擁有bind函式的原型。
        // 修改返回函式的 prototype 為繫結函式的 prototype,例項就可以繼承繫結函式的原型中的值
        tempFn.prototype = self.prototype;
        resultFn.prototype = new tempFn();
        return resultFn;
    
    }
    
    var testFn = function(obj, arg) {
        console.log('作用域物件屬性值:' + this.value);
        console.log('繫結函式時引數物件屬性值:' + obj.value);
        console.log('呼叫新函式引數值:' + arg);
    }
    var testObj = {
        value: 1
    };
    var newFn = testFn.mybind(testObj, {value: 2});
    newFn('hello world');
    
    // 作用域物件屬性值:1
    // 繫結函式時引數物件屬性值:2
    // 呼叫新函式引數值:hello world複製程式碼


    請說出以下列印結果

    let a = {a: 10};
    let b = {b: 10};
    let obj = {
        a: 10
    };
    obj[b] = 20;
    obj["c"] = 20;
    console.log(obj[a]);
    console.log(obj)
    // 20
    //{a: 10, [object Object]: 20, c: 20}複製程式碼
    這道題目主要考對JS資料型別的熟練度以及對ES6中屬性名錶達式的理解。在上題中obj[b] = 20的賦值操作後,obj其實已經變成了{a: 10, [object Object]: 20},這是因為如果屬性名錶達式是一個物件的話,那麼預設情況下會自動將物件轉為字串[object Object],最後一步獲取obj[a]時,a本身也是一個物件,所以會被轉換為獲取obj['[object Object]']也就是上一步賦值的20


    Vue 的生命週期

    beforeCreate:

    vue例項的掛載元素el和資料物件data都為undefined,還未初始化

    created:

    例項建立完成 vue例項的資料物件data有了,el還沒有。並已經完成以下配置:資料觀測(data observer),屬性和方法的運算, watch/event 事件回撥
    此時可以呼叫methods中定義的方法,修改data的資料,並且可觸發響應式變化、computed值重新計算,watch到變更等

    還未掛載到DOM,不能訪問到$el屬性,$ref屬性內容為空陣列

    beforeMount: 

    vue 例項的$el 和 data 都初始化了,但還是掛載之前為虛擬的 dom 節點,data.message 還未替換

    mounted:

    例項掛載到DOM上,此時可以通過DOM API獲取到DOM節點,$ref屬性可以訪問,data.message 成功渲染

    beforeUpdate:  

    這裡的更新物件是模板,即需要虛擬 DOM 重新渲染和打補丁,beforeUpdate發生在以上兩個流程之前,此時新的虛擬DOM已經生成

    如果發生變更的資料在模板中並沒有使用(包括直接和間接,間接:比如某個依賴該資料的計算屬性在模板中使用了),則不會觸發更新流程!!!

    updated: 

    由於資料更改導致的虛擬 DOM 重新渲染和打補丁,在這之後會呼叫該鉤子。

    當這個鉤子被呼叫時,元件 DOM 已經更新,可以執行依賴於 DOM 的操作

    beforeDestroy: 

    例項銷燬之前呼叫。在這一步,例項仍然完全可用,this仍能獲取到例項

    一般在這一步中進行:銷燬定時器、解綁全域性事件、銷燬外掛物件等操作

    destroyed:

    Vue 例項銷燬後呼叫。呼叫後,Vue 例項指示的所有東西都會解繫結,所有的事件監聽器會被移除,所有的子例項也會被銷燬


    閉包

    參考 https://segmentfault.com/a/1190000000652891

    閉包就是能夠讀取其他函式內部變數的函式。由於在Javascript語言中,只有函式內部的子函式才能讀取區域性變數,因此可以把閉包簡單理解成“定義在一個函式內部的函式”。

    要理解閉包,首先必須理解Javascript特殊的變數作用域。

    變數的作用域無非就是兩種:全域性變數和區域性變數。

    Javascript語言的特殊之處,就在於函式內部可以直接讀取全域性變數。

      var n = 999;
      function f() {
         console.log(n)
       }
     f(); // 999     
    
    複製程式碼

    另一方面,在函式外部自然無法讀取函式內的區域性變數。

    function f() {
       var n= 999;
    }
    console(n); //Uncaught ReferenceError: n is not defined
    
    複製程式碼

    這裡有一個地方需要注意,函式內部宣告變數的時候,一定要使用var命令。如果不用的話,你實際上宣告瞭一個全域性變數!

    function f() {
        n = 999;
    }
    f();
    console.log(n); // 999複製程式碼

    出於種種原因,我們有時候需要得到函式內的區域性變數。但是,前面已經說過了,正常情況下,這是辦不到的,只有通過變通方法才能實現。

    那就是在函式的內部,再定義一個函式。

    function f1() {
        var n = 999;
        function f2(){
             console.log(n); // 999
        }
    }複製程式碼

    在上面的程式碼中,函式f2就被包括在函式f1內部,這時f1內部的所有區域性變數,對f2都是可見的。但是反過來就不行,f2內部的區域性變數,對f1 就是不可見的。這就是Javascript語言特有的“鏈式作用域”結構(chain scope),

    子物件會一級一級地向上尋找所有父物件的變數。所以,父物件的所有變數,對子物件都是可見的,反之則不成立。

    既然f2可以讀取f1中的區域性變數,那麼只要把f2作為返回值,我們不就可以在f1外部讀取它的內部變數了嗎!

    function f1() {
       n = 999;
         function f2(){
              console.log(n);
         }
       return f2;
    }
    var result=f1();
    result();// 999複製程式碼

    閉包可以用在許多地方。它的最大用處有兩個,一個是前面提到的可以讀取函式內部的變數,另一個就是讓這些變數的值始終保持在記憶體中

    function f1() {
        var n = 999;
        nAdd = function() {
            n += 1
         }
         function f2() {
           console.log(n);
         }
         return f2;
    
     }
     var result = f1();
     result(); // 999
     nAdd();
     result(); // 1000複製程式碼

    在這段程式碼中,result實際上就是閉包f2函式。它一共執行了兩次,第一次的值是999,第二次的值是1000。這證明了,函式f1中的區域性變數n一直儲存在記憶體中,並沒有在f1呼叫後被自動清除。

    為什麼會這樣呢?原因就在於f1是f2的父函式,而f2被賦給了一個全域性變數,這導致f2始終在記憶體中,而f2的存在依賴於f1,因此f1也始終在記憶體中,不會在呼叫結束後,被垃圾回收機制(garbage collection)回收。

    這段程式碼中另一個值得注意的地方,就是“nAdd=function(){n+=1}”這一行,首先在nAdd前面沒有使用var關鍵字,因此 nAdd是一個全域性變數,而不是區域性變數。其次,nAdd的值是一個匿名函式(anonymous function),而這個

    匿名函式本身也是一個閉包,所以nAdd相當於是一個setter,可以在函式外部對函式內部的區域性變數進行操作。

    總結

    特性: 

    1.函式巢狀函式 

    2.函式內部可以引用外部的引數和變數 

    3.引數和變數不會被垃圾回收機制回收

    閉包的缺點就是常駐記憶體,會增大記憶體使用量,使用不當很容易造成記憶體洩露。

    為什麼要使用閉包:

    為了設計私有方法和變數,避免全域性變數汙染 希望一個變數長期駐紮在記憶體中

    陣列去重

    利用陣列的 indexOf 屬性

    let arr = [5,7,8,8]
    console.log(arr.indexOf(5))//0
    console.log(arr.indexOf(8))//2
    console.log(arr.indexOf(9))//-1
    
    function unique(origin) {
        var result = [];
        for (var i = 0; i < origin.length; i++) {
            var item = origin[i];
            if (result.indexOf(item) === -1) {
                result.push(item);
            }
        }
        return result;
    }
    console.log(unique(arr))//[5, 7, 8]複製程式碼

    陣列的 filter 屬性和 indexOf 屬性

    let arr = [5,7,8,8]
    function unique(origin) {
      var result = origin.filter(function(item, index, array) {
        // 獲取元素在源陣列的位置,只返回那些索引等於當前元素索引的值。
        return array.indexOf(item) === index;
      });
      return result;
    }
    console.log(unique(arr))//[5, 7, 8]複製程式碼

    利用 ES6 Set

    ES6 提供了新的資料結構 Set,它類似於陣列,但是成員的值都是唯一的,沒有重複的值。向 Set 加入值的時候,不會發生型別轉變,所以 5 和 ‘5’ 是兩個不同的值。Set 內部判斷兩個值是否相同,用的是類似於 “===”的演算法,但是區別是,在 set 內部認為 NaN 等於 NaN 

    let arr = [5,7,8,8]
    function unique(origin) {
      return Array.from(new Set(origin));
    }
    console.log(unique(arr))//[5, 7, 8]複製程式碼


    盒子模型

    首先盒子模型是由content、padding、border、margin 4部分組成

    Box-Model

    分類: 標準盒子模型 和 ie盒子模型

    區別:

    • 在 標準盒子模型中,width 和 height 指的是內容區域的寬度和高度。增加內邊距、邊框和外邊距不會影響內容區域的尺寸,但是會增加元素框的總尺寸。
    • IE盒子模型中,width 和 height 指的是內容區域+邊框+內邊距的寬度和高度。

    但是目前已經統一去起來了

    <!DOCTYPE html>複製程式碼

    CSS3提供了可以切換盒子模型模式的屬性:box-sizing,它有2個值,分別是content-box和border-box

    content-box:

    讓元素維持W3C的標準盒模型。元素的寬度/高度由border + padding + content的寬度/高度決定,設定width/height屬性指的是content部分的寬/高 

    border-box:

    讓元素維持IE傳統盒模型(IE6以下版本和IE6~7的怪異模式)。設定width/height屬性指的是border + padding + content 


    在瀏覽器輸入 URL 回車之後發生了什麼

    參考 ruoduan.top/2019/04/11/…

    當我們在web瀏覽器的位址列中輸入: www.baidu.com,然後回車,到底發生了什麼

    1.對www.baidu.com這個網址進行DNS域名解析,得到對應的IP地址   

    2.根據這個IP,找到對應的伺服器,發起TCP的三次握手   

    3.建立TCP連線後發起HTTP請求   

    4.伺服器響應HTTP請求,瀏覽器得到html程式碼   

    5.瀏覽器解析html程式碼,並請求html程式碼中的資源(如js、css圖片等)(先得到html程式碼,才能去找這些資源)   

    6.瀏覽器對頁面進行渲染呈現給使用者

    三個主要過程:

    • DNS 解析
    • TCP 連線
    • HTTP 請求/響應

    最後一步對前端很重要 瀏覽器是如何對頁面進行渲染的?

    a: 解析html檔案構成 DOM樹, 

    b: 解析CSS檔案構成渲染樹, 

    c: 邊解析,邊渲染 ,  

    d: JS 單執行緒執行,JS有可能修改DOM結構,意味著JS執行完成前,後續所有資源的下載是沒有必要的,所以JS是單執行緒,會阻塞後續資源下載

    new操作符具體做了什麼

    function Animal(name) {
        this.name = name;
    }
    Animal.prototype.run = function() {
        console.log(this.name + 'can run...');
    }
    
    var cat = new Animal('cat');
    cat.run() //catcan run...
    console.log(cat.__proto__ === Animal.prototype); // true複製程式碼

    new共經歷了四個過程。

    var fn = function () { };
    var fnObj = new fn();複製程式碼

    1、建立了一個空物件

    var obj = new object();複製程式碼

    2、設定原型鏈

    obj._proto_ = fn.prototype;複製程式碼

    3、讓fn的this指向obj,並執行fn的函式體

    var result = fn.call(obj);複製程式碼

    4、判斷fn的返回值型別,如果是值型別,返回obj。如果是引用型別,就返回這個引用型別的物件。

    if (typeof(result) == "object"){  
        fnObj = result;  
    } else {  
        fnObj = obj;
    }  複製程式碼


    Symbol,Map和Set

    參考 blog.csdn.net/xzz2222/art…

    為啥需要Symbol:

    一個新規則的提出,必然是因為有需求,熟悉ES5的人都知道,ES5裡面物件的屬性名都是字串,如果你需要使用一個別人提供的物件,你對這個物件有哪些屬性也不是很清楚,但又想為這個物件新增一些屬性,那麼你新增的屬性名就很可能和原來的屬性名傳送衝突,顯然我們是不希望這種情況發生的。所以,我們需要確保每個屬性名都是獨一無二的,這樣就可以防止屬性名的衝突了。因此,ES6裡就引入了Symbol,用它來產生一個獨一無二的值。

    Symbol是什麼:

    Symbol實際上是ES6引入的一種原始資料型別,除了Symbol,JavaScript還有其他5種資料型別,分別是Undefined、Null、Boolean、String、Number,這5種資料型別都是ES5中就有的。

    怎麼生成一個Symbol型別的值:

    既然我們已經知道了Symbol是一種原始的資料型別,那麼怎麼生成這種資料型別的值呢?Symbol值是通過Symbol函式生成的,如下:

    let s = Symbol();
    console.log(s);  // Symbol()
    typeof s;  // "symbol"複製程式碼

    上面程式碼中,s就是一個Symbol型別的值,它是獨一無二的。

    Symbol函式前不能用new

    Symbol函式不是一個建構函式,前面不能用new操作符。所以Symbol型別的值也不是一個物件,不能新增任何屬性,它只是一個類似於字元型的資料型別。如果強行在Symbol函式前加上new操作符,會報錯,如下:

    let s = new Symbol();// Uncaught TypeError: Symbol is not a constructor(…)複製程式碼

    Symbol函式的引數

    1.字串作為引數

    用上面的方法生成的Symbol值不好進行區分,Symbol函式還可以接受一個字串引數,來對產生的Symbol值進行描述,方便我們區分不同的Symbol值。

    let s1 = Symbol('s1');
    let s2 = Symbol('s2');
    console.log(s1);  // Symbol(s1)
    console.log(s2);  // Symbol(s2)
    s1 === s2;  //  false
    let s3 = Symbol('s2');
    s2 === s3;  //  false
    複製程式碼

    從上面程式碼可以看出:

    1. 給Symbol函式加了引數之後,控制檯輸出的時候可以區分到底是哪一個值;
    2. Symbol函式的引數只是對當前Symbol值的描述,因此相同引數的Symbol函式返回值是不相等的;

    2.物件作為引數

    如果Symbol函式的引數是一個物件,就會呼叫該物件的toString方法,將其轉化為一個字串,然後才生成一個Symbol值。所以,說到底,Symbol函式的引數只能是字串。

    set資料集合:類似於陣列 

    set:是一個集合,類似於陣列,與陣列的主要區別是沒有重複的元素。主要的作用可以進行去重。 

    重點:一個屬性,四個方法 

    1、size屬性:返回set陣列的長度,類似於lenght 

    2、四個方法:add,delete,clear,has 

    let set =new Set([1,2,3,4,1]);
    console.log(set.size); //4
    //新增元素
    set.add(7);
    console.log(set); // {1, 2, 3, 4, 7}
    //刪除陣列中的某個元素
    set.delete(3);
    console.log(set); // {1, 2, 4, 7}
    //檢測陣列中是否含有某個元素,返回值為布林
    console.log(set.has(2)); //true
    //清空陣列
    set.clear();
    console.log(set) //{}複製程式碼

    map資料集合:類似於物件 

    let obj={a:1};
    const map = new Map([
        ['name','java'],
        ['feel','今天天氣賊好'],
        ['jieguo','適合看帥哥'],
        [obj,'是!']
    ]);
    console.log(map);//{"name" => "java", "feel" => "今天天氣賊好", "jieguo" => "適合看帥哥", {…} => "是!"}複製程式碼

    Map與Object的區別 

     1、Map的size屬性可以直接獲取鍵值對個數 

     2、兩者都是對應的keys-value鍵值對, 但是Object的keys只能是字串或者 Symbols, Map的key可以是任意值,比如陣列、物件等  


    總結:Map 物件儲存鍵值對。一個物件的鍵只能是字串或者 Symbols,但一個 Map 的鍵可以是任意值。 Set 物件允許你儲存任何型別的唯一值,Set物件是值的集合,Set中的元素只會出現一次 Symbol 是一種特殊的、不可變的資料型別,可以作為物件屬性的識別符號使用(Symbol([description]) )

    JavaScript中的類

    類的宣告:

    使用class關鍵來宣告類

    class Person {
      // 相當於Person建構函式
      constructor (name) {
        this.name = name
      }
      // 相當於Person.prototype.sayName
      sayName () {
        console.log(this.name)
      }
    }
    const person = new Person('AAA')
    person.sayName()                      // AAA
    console.log(person instanceof Person) // true
    console.log(person instanceof Object) // true 複製程式碼

    地將類匯出為 ES2015 模組的一部分

    預設語法

    export default class Person {
     // The body of class
    } 複製程式碼

    或者命名匯出

    export class Person  {
      // The body of class
    }複製程式碼

    分析:

    1)  constructor():我們可以看到constructor()方法相當於我們上面寫到的Person建構函式,在constructor()方法中我們定義了一個name的自有屬性。所謂自有屬性,就是類例項的屬性,其不會出現在原型上,且只能在類的建構函式或方法中被建立

    2)sayName():sayName()方法就相當於我們上面寫到的Person.prototype.sayName。有一個特別需要注意的地方就是:與函式有所不同,類屬性不可被賦予新值,例如:Person.prototype就是這樣一個只讀的類屬性。

    靜態成員 static

    顧名思義是靜態方法的意思,類相當於例項的原型, 所有在類中定義的方法, 都會被例項繼承。 如果在一個方法前, 加上static關鍵字, 就表示該方法不會被例項繼承, 而是直接通過類來呼叫, 這就稱為“ 靜態方法”。靜態方法呼叫直接在類上進行,而在類的例項上不可被呼叫。靜態方法通常用於建立 實用/工具 函式。在ES6中,類語法簡化了建立靜態成員的過程,在方法或者訪問器屬性名前面使用正式的靜態註釋static即可。 

    注意:靜態成員只能在類中訪問,不能在例項中訪問

        class Person {
              constructor (name) {
                this.name = name
              }
              sayName () {
                console.log(this.name)
              }
              static create (name) {
                return new Person(name)
              }
            }
            const person = Person.create('AAA')
            person.sayName() // AAA複製程式碼

    繼承extends

           class Person {
                constructor(name) {
                    this.name = name;
                }
                speak() {
                    console.log(this.name)
                }
            }
    
            class Child extends Person {
                constructor(name) {
                    super(name); // 相當於  Person.call(this)
                    this.name = name;
                }
                
            }
            let c1 = new Child("aaa")
            c1.speak(); // aaa複製程式碼

    es5的寫法

            function Person(name){
               this.name = name
            }
    
            Person.prototype.speak = function(){
               console.log(this.name);
            }
    
            function Child(name){
              Person.call(this);
              this.name = name;
            }
            Child.prototype = Object.create(Person.prototype);
            Child.prototype.constructor = Child;//因為上一步造成constructor為Animal
    
            //或者可以把上邊的兩行改成下面這樣的寫法
            Child.prototype = Object.create(Person.prototype, {
              constructor: Child,
            });複製程式碼

    注意

    super在類的繼承中,有兩個用法 

    • 作為函式使用,如上邊的例子中的super() 
    • super作為物件時,在普通方法中,指向父類的原型物件;在靜態方法中,指向父類。

    作為函式使用:

           class Child extends Person {
                constructor(name) {
                    super(name);
                    this.name = name;
                }
                
            }複製程式碼

    作為物件使用:

            class Person {
                constructor(name) {
                    this.name = name;
                }
                speak() {
                    console.log(this.name)
                }
            }
            Person.prototype.name ="bbbb"
            class Child extends Person {
                constructor(name) {
                    super();
                    this.name = name;
                    console.log(super.name) // bbb
                }
                
            }
            let c1 = new Child("aaa")
            c1.speak(); // aaa複製程式碼

    在靜態方法中指向父類

            class Person {
                constructor(name) {
                    this.name = name;
                }
                static speak() {
                    console.log(111) 
                }
            }
            class Child extends Person {
                constructor() {
                    super();
                }
                static say(name) {
                    super.speak()
                }
                
            }
           
           let c1 = new Child();
           Child.say(111); // 111複製程式碼

    Vue 陣列處理

    <template>
      <div>
        {{list[2]}}
      </div>
    </template>
    <script>
        export default {
            data() {
                return {
                    list: [1,3,5]
                }
            },
            methods: {
            },
            components: {
            },
            computed: {
            },
            mounted() {
                this.list[2] = 8 // 沒有效果
                this.$set(this.list, 2, 8); //有效果
            }
        }
    </script>複製程式碼

    對於set這個方法的解釋: 

    this.$set(陣列或者物件,修改的下標或者物件屬性名,修改的值)

    GET,POST,PUT,Delete

    GET請求會向資料庫獲取資訊,只是用來查詢資料,不會修改,增加資料。使用URL傳遞引數,對所傳送的數量有限制,一般在2000字元


    POST向伺服器傳送資料,會改變資料的種類等資源,就像insert操作一樣,會建立新的內容,大小一般沒有限制,POST安全性高,POST不會被快取


    PUT請求就像資料庫的update操作一樣,用來修改資料內容,不會增加資料種類


    Delete用來刪除操作

    GET和POST的區別:

    1.GET使用URL或Cookie傳參,而POST將資料放在BODY中,這個是因為HTTP協議用法的約定。並非它們的本身區別。
    2.GET方式提交的資料有長度限制,則POST的資料則可以非常大,這個是因為它們使用的作業系統和瀏覽器設定的不同引起的區別。也不是GET和POST本身的區別。
    3.POST比GET安全,因為資料在位址列上不可見,這個說法沒毛病,但依然不是GET和POST本身的區別。

    4.GET和POST最大的區別主要是GET請求是冪等性的,POST請求不是。(冪等性:對同一URL的多個請求應該返回同樣的結果。)因為get請求是冪等的,在網路不好的隧道中會嘗試重試。如果用get請求增資料,會有重複操作的風險,而這種重複操作可能會導致副作用


    vue router的鉤子函式

    1、全域性的鉤子

    beforeEach(to,from,next)(全域性前置守衛)  頁面載入之前  

    • to:即將要進入的目標
    • from:當前導航正要離開的路由
    • next:function函式,必須呼叫
    應用場景:
    進行頁面的跳轉,如判斷登入頁面是否需要進行攔截

    afterEach(to,from,next)(全域性後置守衛) 頁面載入之後,有兩個引數:to/from ,全域性後置鉤子在所有路由跳轉結束的時候呼叫這些鉤子不會接受 next 函式也不會改變導航本身

    vue router.beforeResolve(全域性解析守衛)


    注:beforeEach和afterEach都是vue-router例項物件的屬性,每次跳轉前,beforeEach和afterEach都會執行。

    2、組建內的導航鉤子: 

    beforeRouteEnter、beforeRouteUpdate、beforeRouteLeave,直接在路由元件內部進行定義的  

    beforeRouteEnter(to, from, next) :

    在渲染該元件的對應路由被確認前呼叫,用法和引數與beforeEach類似,next需要被主動呼叫
    注意:

    • 此時元件例項還未被建立,不能訪問this
    • 可以通過傳一個回撥給 next來訪問元件例項。在導航被確認的時候執行回撥,並且把元件例項作為回撥方法的引數
    beforeRouteEnter (to, from, next) {
      // 這裡還無法訪問到元件例項,this === undefined
      next( vm => {
        // 通過 `vm` 訪問元件例項
      })
    }複製程式碼
    • 可以在這個守衛中請求服務端獲取資料,當成功獲取並能進入路由時,呼叫next並在回撥中通過 vm訪問元件例項進行賦值等操作
    • beforeRouteEnter觸發在導航確認、元件例項建立之前:beforeCreate之前;而next中函式的呼叫在mounted之後:為了確保能對元件例項的完整訪問

    beforeRouteUpdate (to, from, next) :

    在當前路由改變,並且該元件被複用時呼叫,可以通過this訪問例項, next需要被主動呼叫,不能傳回撥

    • 對於一個帶有動態引數的路徑 /foo/:id,在 /foo/1 和 /foo/2 之間跳轉的時候,元件例項會被複用,該守衛會被呼叫
    • 當前路由query變更時,該守衛會被呼叫

    beforeRouteLeave (to, from, next) :

    航離開該元件的對應路由時呼叫,可以訪問元件例項 thisnext需要被主動呼叫,不能傳回撥 用途:清除當前元件中的定時器,避免佔用記憶體;當頁面中有未關閉的視窗, 或未儲存的內容時, 阻止頁面跳轉;儲存相關內容到Vuex中或Session中 

    3、路由內的導航鉤子

    主要用於寫某個指定路由跳轉時需要執行的邏輯

    beforeEnter

    有三個引數:to/from/next

    {
        path:'/',
        name:'Login',
        component:Login,
        beforeEnter:(to,from,next)=>{
            consloe.log("即將進入Login");
            next();
        }
    }複製程式碼



    相關文章