JavaScript手寫程式碼無敵祕籍

草榴社群發表於2019-03-29

轉自JavaScript手寫程式碼無敵祕籍

轉自面試題錦(大廠面試前夕的掙扎)

【前端面試】同學,你會手寫程式碼嗎?


手寫路徑導航

1. 實現一個new操作符

來源:「你不知道的javascript」 英文版

new操作符做了這些事:

  • 它建立了一個全新的物件。
  • 它會被執行[[Prototype]](也就是__proto__)連結。
  • 它使this指向新建立的物件。。
  • 通過new建立的每個物件將最終被[[Prototype]]連結到這個函式的prototype物件上。
  • 如果函式沒有返回物件型別Object(包含Functoin, Array, Date, RegExg, Error),那麼new表示式中的函式呼叫將返回該物件引用。
    function New(func) {
        var res = {};
        if (func.prototype !== null) {
            res.__proto__ = func.prototype;
        }
        var ret = func.apply(res, Array.prototype.slice.call(arguments, 1));
        if ((typeof ret === "object" || typeof ret === "function") && ret !== null) {
            return ret;
        }
        return res;
    }
    var obj = New(A, 1, 2);
    // equals to
    var obj = new A(1, 2);
複製程式碼

2. 實現一個JSON.stringify

JSON.stringify(value[, replacer [, space]])

  • Boolean | Number| String 型別會自動轉換成對應的原始值。
  • undefined、任意函式以及symbol,會被忽略(出現在非陣列物件的屬性值中時),或者被轉換成 null(出現在陣列中時)。
  • 不可列舉的屬性會被忽略
  • 如果一個物件的屬性值通過某種間接的方式指回該物件本身,即迴圈引用,屬性也會被忽略。
    function jsonStringify(obj) {
        lettype = typeof obj;
        if (type !== "object" || type === null) {
            if (/string|undefined|function/.test(type)) {
                obj = '"' + obj + '"';
            }
            return String(obj);
        } else {
            let json = []
            arr = (obj && obj.constructor === Array);
            for (let k in obj) {
                let v = obj[k];
                lettype = typeof v;
                if (/string|undefined|function/.test(type)) {
                    v = '"' + v + '"';
                } elseif (type === "object") {
                    v = jsonStringify(v);
                }
                json.push((arr ? "" : '"' + k + '":') + String(v));
            }
            return (arr ? "[" : "{") + String(json) + (arr ? "]" : "}")
        }
    }
    jsonStringify({x : 5}) // "{"x":5}"
    jsonStringify([1, "false", false]) // "[1,"false",false]"
    jsonStringify({b: undefined}) // "{"b":"undefined"}"
複製程式碼

3. 實現一個JSON.parse

JSON.parse(text[, reviver])

用來解析JSON字串,構造由字串描述的JavaScript值或物件。提供可選的reviver函式用以在返回之前對所得到的物件執行變換(操作)。

3.1 第一種:直接呼叫 eval

    function jsonParse(opt) {
        returneval('(' + opt + ')');
    }
    jsonParse(jsonStringify({x : 5}))
    // Object { x: 5}
    jsonParse(jsonStringify([1, "false", false]))
    // [1, "false", falsr]
    jsonParse(jsonStringify({b: undefined}))
    // Object { b: "undefined"}
複製程式碼

避免在不必要的情況下使用 eval,eval() 是一個危險的函式, 他執行的程式碼擁有著執行者的權利。如果你用 eval()執行的字串程式碼被惡意方(不懷好意的人)操控修改,您最終可能會在您的網頁/擴充套件程式的許可權下,在使用者計算機上執行惡意程式碼。

它會執行JS程式碼,有XSS漏洞。

如果你只想記這個方法,就得對引數json做校驗。

    var rx_one = /^[\],:{}\s]*$/;
    var rx_two = /\\(?:["\\\/bfnrt]|u[0-9a-fA-F]{4})/g;
    var rx_three = /"[^"\\\n\r]*"|true|false|null|-?\d+(?:\.\d*)?(?:[eE][+\-]?\d+)?/g;
    var rx_four = /(?:^|:|,)(?:\s*\[)+/g;
    if (
        rx_one.test(
            json
                .replace(rx_two, "@")
                .replace(rx_three, "]")
                .replace(rx_four, "")
        )
    ) {
        var obj = eval("(" +json + ")");
    }
複製程式碼

3.2 第二種:Function

來源 神奇的eval()與new Function()

核心:Functioneval有相同的字串引數特性。

var func = new Function(arg1, arg2, ..., functionBody);

在轉換JSON的實際應用中,只需要這麼做。

    var jsonStr = '{ "age": 20, "name": "jack" }'
    var json = (new Function('return ' + jsonStr))();
複製程式碼

evalFunction 都有著動態編譯js程式碼的作用,但是在實際的程式設計中並不推薦使用。

這裡是面向面試程式設計,寫這兩種就夠了。至於第三,第四種,涉及到繁瑣的遞迴和狀態機相關原理,具體可以看:

《JSON.parse 三種實現方式》

4. 實現一個callapply

call語法:

fun.call(thisArg, arg1, arg2, ...),呼叫一個函式, 其具有一個指定的this值和分別地提供的引數(引數的列表)。

apply語法:

func.apply(thisArg, [argsArray]),呼叫一個函式,以及作為一個陣列(或類似陣列物件)提供的引數。

4.1 Function.call按套路實現

call核心:

  • 將函式設為物件的屬性
  • 執行&刪除這個函式
  • 指定this到函式並傳入給定引數執行函式
  • 如果不傳入引數,預設指向為 window

為啥說是套路實現呢?因為真實面試中,面試官很喜歡讓你逐步地往深考慮,這時候你可以反套路他,先寫個簡單版的:

4.1.1 簡單版

    var foo = {
        value: 1,
        bar: function() {
            console.log(this.value)
        }
    }
    foo.bar() // 1
複製程式碼

4.1.2 完善版

當面試官有進一步的發問,或者此時你可以假裝思考一下。然後寫出以下版本:

    Function.prototype.call2 = function(content = window) {
        content.fn = this;
        let args = [...arguments].slice(1);
        let result = content.fn(...args);
        delect content.fn;
        return result;
    }
    var foo = {
        value: 1
    }
    function bar(name, age) {
        console.log(name)
        console.log(age)
        console.log(this.value);
    }
    bar.call2(foo, 'black', '18') // black 18 1
複製程式碼

4.2 Function.apply的模擬實現

apply()的實現和call()類似,只是引數形式不同。直接貼程式碼吧:

    Function.prototype.apply2 = function(context = window) {
        context.fn = this
        let result;
        // 判斷是否有第二個引數
        if(arguments[1]) {
            result = context.fn(...arguments[1])
        } else {
            result = context.fn()
        }
        delete context.fn()
        return result
    }
複製程式碼

5. 實現一個Function.bind()

bind()方法:

會建立一個新函式。當這個新函式被呼叫時,bind() 的第一個引數將作為它執行時的 this,之後的一序列引數將會在傳遞的實參前傳入作為它的引數。(來自於 MDN )

此外,bind實現需要考慮例項化後對原型鏈的影響。

    Function.prototype.bind2 = function(content) {
        if(typeof this != "function") {
            throw Error("not a function")
        }
        // 若沒問引數型別則從這開始寫
        let fn = this;
        let args = [...arguments].slice(1);
        
        let resFn = function() {
            return fn.apply(this instanceof resFn ? this : content,args.concat(...arguments) )
        }
        functiontmp() {}
        tmp.prototype = this.prototype;
        resFn.prototype = new tmp();
        
        return resFn;
    }
複製程式碼

6. 實現一個繼承

寄生組合式繼承

一般只建議寫這種,因為其它方式的繼承會在一次例項中呼叫兩次父類的建構函式或有其它缺點。

核心實現是:用一個 F 空的建構函式去取代執行了 Parent 這個建構函式。

    function Parent(name) {
        this.name = name;
    }
    Parent.prototype.sayName = function() {
        console.log('parent name:', this.name);
    }
    function Child(name, parentName) {
        Parent.call(this, parentName);  
        this.name = name;    
    }
    function create(proto) {
        functionF(){}
        F.prototype = proto;
        return new F();
    }
    Child.prototype = create(Parent.prototype);
    Child.prototype.sayName = function() {
        console.log('child name:', this.name);
    }
    Child.prototype.constructor = Child;
    
    var parent = new Parent('father');
    parent.sayName();    // parent name: father
    
    var child = new Child('son', 'father');
複製程式碼

7. 實現一個JS函式柯里化

JavaScript手寫程式碼無敵祕籍
什麼是柯里化?

在電腦科學中,柯里化(Currying)是把接受多個引數的函式變換成接受一個單一引數(最初函式的第一個引數)的函式,並且返回接受餘下的引數且返回結果的新函式的技術。

函式柯里化的主要作用和特點就是引數複用、提前返回和延遲執行。

7.1 通用版

    functioncurry() {
        var args = Array.prototype.slice.call(arguments);
    	var fn = function() {
    		var newArgs = args.concat(Array.prototype.slice.call(arguments));
            return multi.apply(this, newArgs);
        }
        fn.toString = function() {
            return args.reduce(function(a, b) {
                return a * b;
            })
        }
        return fn;
    }
    function multiFn(a, b, c) {
        return a * b * c;
    }
    
    var multi = curry(multiFn);
    
    multi(2)(3)(4);
    multi(2,3,4);
    multi(2)(3,4);
    multi(2,3)(4);
複製程式碼

7.2 ES6騷寫法

    const curry = (fn, arr = []) => (...args) => (
      arg => arg.length === fn.length
        ? fn(...arg)
        : curry(fn, arg)
    )([...arr, ...args])
    
    let curryTest=curry((a,b,c,d)=>a+b+c+d)
    curryTest(1,2,3)(4) //返回10
    curryTest(1,2)(4)(3) //返回10
    curryTest(1,2)(3,4) //返回10
複製程式碼

8. 手寫一個Promise(中高階必考)

我們來過一遍Promise/A+規範:

  • 三種狀態pending| fulfilled(resolved) | rejected
  • 當處於pending狀態的時候,可以轉移到fulfilled(resolved)或者rejected狀態
  • 當處於fulfilled(resolved)狀態或者rejected狀態的時候,就不可變。
  1. 必須有一個then非同步執行方法,then接受兩個引數且必須返回一個promise:
    // onFulfilled 用來接收promise成功的值
    // onRejected 用來接收promise失敗的原因
    promise1=promise.then(onFulfilled, onRejected);
複製程式碼

8.1 Promise的流程圖分析

JavaScript手寫程式碼無敵祕籍
來回顧下Promise用法:

    var promise = new Promise((resolve,reject) => {
        if (操作成功) {
            resolve(value)
        } else {
            reject(error)
        }
    })
    promise.then(function (value) {
        // success
    },function (value) {
        // failure
    })
複製程式碼

8.2 面試夠用版

來源:實現一個完美符合Promise/A+規範的Promise

    function myPromise(constructor){
        let self=this;
        self.status="pending" //定義狀態改變前的初始狀態
        self.value=undefined;//定義狀態為resolved的時候的狀態
        self.reason=undefined;//定義狀態為rejected的時候的狀態
        function resolve(value){
            //兩個==="pending",保證了狀態的改變是不可逆的
           if(self.status==="pending"){
              self.value=value;
              self.status="resolved";
           }
        }
        function reject(reason){
            //兩個==="pending",保證了狀態的改變是不可逆的
           if(self.status==="pending"){
              self.reason=reason;
              self.status="rejected";
           }
        }
        //捕獲構造異常
        try{
           constructor(resolve,reject);
        }catch(e){
           reject(e);
        }
    }
複製程式碼

同時,需要在myPromise的原型上定義鏈式呼叫的then方法:

    myPromise.prototype.then=function(onFullfilled,onRejected){
       let self=this;
       switch(self.status){
          case"resolved":
            onFullfilled(self.value);
            break;
          case"rejected":
            onRejected(self.reason);
            break;
          default:       
       }
    }
複製程式碼

測試一下:

    var p=new myPromise(function(resolve,reject){resolve(1)});
    p.then(function(x){console.log(x)})
    //輸出1
複製程式碼

8.3 大廠專供版

直接貼出來吧,這個版本還算好理解

    const PENDING = "pending";
    const FULFILLED = "fulfilled";
    const REJECTED = "rejected";
    
    function Promise(excutor) {
        let that = this; // 快取當前promise例項物件
        that.status = PENDING; // 初始狀態
        that.value = undefined; // fulfilled狀態時 返回的資訊
        that.reason = undefined; // rejected狀態時 拒絕的原因
        that.onFulfilledCallbacks = []; // 儲存fulfilled狀態對應的onFulfilled函式
        that.onRejectedCallbacks = []; // 儲存rejected狀態對應的onRejected函式
    
        function resolve(value) { // value成功態時接收的終值
            if(value instanceof Promise) {
                return value.then(resolve, reject);
            }
            // 實踐中要確保 onFulfilled 和 onRejected 方法非同步執行,且應該在 then 方法被呼叫的那一輪事件迴圈之後的新執行棧中執行。
            setTimeout(() => {
                // 呼叫resolve 回撥對應onFulfilled函式
                if (that.status === PENDING) {
                    // 只能由pending狀態 => fulfilled狀態 (避免呼叫多次resolve reject)
                    that.status = FULFILLED;
                    that.value = value;
                    that.onFulfilledCallbacks.forEach(cb => cb(that.value));
                }
            });
        }
        function reject(reason) { // reason失敗態時接收的拒因
            setTimeout(() => {
                // 呼叫reject 回撥對應onRejected函式
                if (that.status === PENDING) {
                    // 只能由pending狀態 => rejected狀態 (避免呼叫多次resolve reject)
                    that.status = REJECTED;
                    that.reason = reason;
                    that.onRejectedCallbacks.forEach(cb => cb(that.reason));
                }
            });
        }
    
        // 捕獲在excutor執行器中丟擲的異常
        // new Promise((resolve, reject) => {
        //     throw new Error('error in excutor')
        // })
        try {
            excutor(resolve, reject);
        } catch (e) {
            reject(e);
        }
    }
    
    Promise.prototype.then = function(onFulfilled, onRejected) {
        const that = this;
        let newPromise;
        // 處理引數預設值 保證引數後續能夠繼續執行
        onFulfilled =
            typeof onFulfilled === "function" ? onFulfilled : value => value;
        onRejected =
            typeof onRejected === "function" ? onRejected : reason => {
                throw reason;
            };
        if (that.status === FULFILLED) { // 成功態
            return newPromise = new Promise((resolve, reject) => {
                setTimeout(() => {
                    try{
                        let x = onFulfilled(that.value);
                        resolvePromise(newPromise, x, resolve, reject); // 新的promise resolve 上一個onFulfilled的返回值
                    } catch(e) {
                        reject(e); // 捕獲前面onFulfilled中丟擲的異常 then(onFulfilled, onRejected);
                    }
                });
            })
        }
    
        if (that.status === REJECTED) { // 失敗態
            return newPromise = new Promise((resolve, reject) => {
                setTimeout(() => {
                    try {
                        let x = onRejected(that.reason);
                        resolvePromise(newPromise, x, resolve, reject);
                    } catch(e) {
                        reject(e);
                    }
                });
            });
        }
    
        if (that.status === PENDING) { // 等待態
            // 當非同步呼叫resolve/rejected時 將onFulfilled/onRejected收集暫存到集合中
            return newPromise = new Promise((resolve, reject) => {
                that.onFulfilledCallbacks.push((value) => {
                    try {
                        let x = onFulfilled(value);
                        resolvePromise(newPromise, x, resolve, reject);
                    } catch(e) {
                        reject(e);
                    }
                });
                that.onRejectedCallbacks.push((reason) => {
                    try {
                        let x = onRejected(reason);
                        resolvePromise(newPromise, x, resolve, reject);
                    } catch(e) {
                        reject(e);
                    }
                });
            });
        }
    };
複製程式碼

emmm,我還是乖乖地寫回進階版吧。

9. 手寫防抖(Debouncing)和節流(Throttling)

scroll 事件本身會觸發頁面的重新渲染,同時 scroll 事件的 handler 又會被高頻度的觸發, 因此事件的 handler 內部不應該有複雜操作,例如 DOM 操作就不應該放在事件處理中。 針對此類高頻度觸發事件問題(例如頁面 scroll ,螢幕 resize,監聽使用者輸入等),有兩種常用的解決方法,防抖和節流。

9.1 防抖(Debouncing)實現

典型例子:限制 滑鼠連擊 觸發。

一個比較好的解釋是:

當一次事件發生後,事件處理器要等一定閾值的時間,如果這段時間過去後 再也沒有 事件發生,就處理最後一次發生的事件。假設還差 0.01 秒就到達指定時間,這時又來了一個事件,那麼之前的等待作廢,需要重新再等待指定時間。

JavaScript手寫程式碼無敵祕籍

    // 防抖動函式
    function debounce(fn,wait=50,immediate) {
        let timer;
        returnfunction() {
            if(immediate) {
                fn.apply(this,arguments)
            }
            if(timer) clearTimeout(timer)
            timer = setTimeout(()=> {
                fn.apply(this,arguments)
            },wait)
        }
    }
複製程式碼

9.2 節流(Throttling)實現

可以理解為事件在一個管道中傳輸,加上這個節流閥以後,事件的流速就會減慢。實際上這個函式的作用就是如此,它可以將一個函式的呼叫頻率限制在一定閾值內,例如 1s,那麼 1s 內這個函式一定不會被呼叫兩次

JavaScript手寫程式碼無敵祕籍
簡單的節流函式:

    function throttle(fn, wait) {
    	let prev = new Date();
    	returnfunction() { 
    	    const args = arguments;
    		const now = new Date();
    		if (now - prev > wait) {
    			fn.apply(this, args);
    			prev = new Date();
    		}
    	}
複製程式碼

9.3 結合實踐

通過第三個引數來切換模式。

    const throttle = function(fn, delay, isDebounce) {
      let timer
      let lastCall = 0
      returnfunction (...args) {
        if (isDebounce) {
          if (timer) clearTimeout(timer)
          timer = setTimeout(() => {
            fn(...args)
          }, delay)
        } else {
          const now = new Date().getTime()
          if (now - lastCall < delay) return
          lastCall = now
          fn(...args)
        }
      }
    }
複製程式碼

10. 手寫一個JS深拷貝

有個最著名的乞丐版實現,在《你不知道的JavaScript(上)》裡也有提及:

JavaScript手寫程式碼無敵祕籍

10.1 乞丐版

     var newObj = JSON.parse( JSON.stringify( someObj ) );
    複製程式碼

### 10.2 面試夠用版

    function deepCopy(obj){
        //判斷是否是簡單資料型別,
        if(typeof obj == "object"){
            //複雜資料型別
            var result = obj.constructor == Array ? [] : {};
            for(let i in obj){
                result[i] = typeof obj[i] == "object" ? deepCopy(obj[i]) : obj[i];
            }
        }else {
            //簡單資料型別 直接 == 賦值
            var result = obj;
        }
        return result;
    }
複製程式碼

關於深拷貝的討論天天有,這裡就貼兩種吧,畢竟我...

JavaScript手寫程式碼無敵祕籍

11.實現一個instanceOf

    function instanceOf(left,right) {
    
        let proto = left.__proto__;
        let prototype = right.prototype
        while(true) {
            if(proto === null) returnfalseif(proto === prototype) returntrue
            proto = proto.__proto__;
        }
    }
複製程式碼

歡迎評論區糾錯完善

css部分

實現三欄佈局

(兩側定寬,中間自適應)

  1. 採用了 absolute,導致父元素脫離了文件流,那所有的子元素也需要脫離文件流。如果頁面複雜,那開發的難度可想而知

  2. 利用浮動 當中間內容高於兩側時,兩側高度不會隨中間內容變高而變高

  3. 彈性盒子佈局

  4. 利用負邊距和浮動,實現起來比較複雜

  5. 利用網格佈局

    .container { display: grid; grid-template-columns: 100px auto 200px; } 複製程式碼

BFC(塊級格式化上下文)

  • BFC 的原理 其實也就是 BFC 的渲染規則(能說出以下四點就夠了)。包括:
  1. BFC 內部的子元素,在垂直方向,邊距會發生重疊。
  2. BFC在頁面中是獨立的容器,外面的元素不會影響裡面的元素,反之亦然。
  3. BFC區域不與旁邊的float box區域重疊。(可以用來清除浮動帶來的影響)。
  4. 計算BFC的高度時,浮動的子元素也參與計算。
  • 如何生成BFC
  • 方法1:overflow: 不為vidible,可以讓屬性是 hidden、auto。【最常用】
  • 方法2:浮動中:float的屬性值不為none。意思是,只要設定了浮動,當前元素就建立了BFC。
  • 方法3:定位中:只要posiiton的值不是 static或者是relative即可,可以是absolute或fixed,也就生成了一個BFC。
  • 方法4:display為inline-block, table-cell, table-caption, flex, inline-flex

flex(面試常問,略)

js部分

call, apply, bind區別? 怎麼實現call,apply方法

js繼承,建構函式,原型鏈,建構函式、原型鏈組合式繼承,寄生式組合繼承,Object.create polyfill;

陣列去重

[...new Set(arr]
複製程式碼

var arr = [1,2,1,2,3,5,4,5,3,4,4,4,4],
    init=[]
var result = arr.sort().reduce((init, current)=>{
    console.log(init,current)
    if(init.length===0 || init[init.length-1]!==current){
        init.push(current);
    }
    return init;
}, []);
console.log(result);//1,2,3,4,5複製程式碼
複製程式碼

防抖節流

var deBounce=function(fn,wait=300){
    let timer
    returnfunction(){
      if(timer){
          clearTimeOut(timer)
      }
      timer=setTimeOut(()=>{
          fn.apply(this,arguments)
      },wait)
    } 
}
var throttle=function(fn,wait=300){
    let prev=+newDate();
    returnfunction(){
        const args=argument,
            now=+newDate();
            if(now>last+wait){
                last=now;
                fn.apply(this,args)
            } 
    }
}
複製程式碼
複製程式碼

實現Promise思路

//0 pending , 1 resolve,2 reject functionPromise(fn) {
        ...
        this._state = 0// 狀態標記
        doResolve(fn, this)
    }
    
    functiondoResolve(fn, self) {
        var done = false// 保證只執行一個監聽try {
            fn(function(value) {
                if (done) return
                done = true
                resolve(self, value)
            },
            function(reason) {
                if (done) return;
                done = true
                reject(self, value)
            })
        } catch(err) {
            if (done) return
            done = true
            reject(self, err)
        }
    }
    
    functionresolve(self, newValue) {
        try {
            self._state = 1;
            ...
        }
        catch(err) {
            reject(self, err)
        }
    }
    
    functionreject(self, newValue) {
        self._state = 2;
        ...
        if (!self._handled) {
            Promise._unhandledRejectionFn(self._value);
        }
    }
複製程式碼
複製程式碼

正則實現千位分隔符

functioncommafy(num) {
        return num && num
            .toString()
            .replace(/(\d)(?=(\d{3})+\.)/g, function($0, $1) {
                return $1 + ",";
            });
    }
 console.log(commafy(1312567.903000))
複製程式碼
複製程式碼

js事件迴圈

javascript是單執行緒語言,任務設計成了兩類,同步任務和非同步任務 同步和非同步任務分別進入不同的執行“場所”,同步進入主執行緒,非同步進入Event Table並註冊函式。當指定的事情完成時,Event Table會將這個函式移入Event Queue。主執行緒內的任務執行完畢為空,回去了Event Queue讀取對應的函式,進入主執行緒。 上述過程會不斷重複,也就是常說的Event Loop(事件迴圈)。 但是,JS非同步還有一個機制,就是遇到巨集任務,先執行巨集任務,將巨集任務放入event queue,然後再執行微任務,將微任務放入eventqueue,但是,這兩個queue不是一個queue。當你往外拿的時候先從微任務裡拿這個回撥函式,然後再從巨集任務的queue拿巨集任務的回撥函式 巨集任務一般包括:整體程式碼script,setTimeout,setInterval。 微任務:Promise,process.nextTick

事件流機制,事件委託 event.targe和event.currentTarget的區別

  1. 事件捕獲,
  2. 處於目標階段,
  3. 事件冒泡階段 event.target返回觸發事件的元素 event.currentTarget返回繫結事件的元素

new的過程以及實現new

functioncreate(){
   //1.建立一個空物件let obj={}
   //2.獲取建構函式let Con=[].shift.call(arguments)
   //3.設定空物件的原型
   obj._proto_=Con.prototype
   //4.繫結this並執行建構函式,給新物件新增屬性和方法let result=Con.apply(obj,arguments)
   //5.確保返回值為物件return result instanceofObject?result:obj
}
複製程式碼
複製程式碼

前端路由的兩種實現原理

  1. Hash模式
  • window物件提供了onhashchange事件來監聽hash值的改變,一旦url中的hash值發生改變,便會觸發該事件。
  1. History 模式
  • popstate監聽歷史棧資訊變化,變化時重新渲染
  • 使用pushState方法實現新增功能
  • 使用replaceState實現替換功能

封裝ajax

/* 封裝ajax函式
 * @param {string}opt.type http連線的方式,包括POST和GET兩種方式
 * @param {string}opt.url 傳送請求的url
 * @param {boolean}opt.async 是否為非同步請求,true為非同步的,false為同步的
 * @param {object}opt.data 傳送的引數,格式為物件型別
 * @param {function}opt.success ajax傳送並接收成功呼叫的回撥函式
 */functionmyAjax(opt){
     opt = opt || {};
     opt.method = opt.method.toUpperCase() || 'POST';
     opt.url = opt.url || '';
     opt.async = opt.async || true;
     opt.data = opt.data || null;
     opt.success = opt.success || function () {}
     let xmlHttp = null;
     if (XMLHttpRequest) {
        xmlHttp = new XMLHttpRequest();
     }else{
         xmlHttp =new ActiveXObject('Microsoft.XMLHTTP')
     }
     let params;
    for (var key in opt.data){
        params.push(key + '=' + opt.data[key]);
    }
    let postData = params.join('&');
    if (opt.method.toUpperCase() === 'POST') {
        xmlHttp.open(opt.method, opt.url, opt.async);
        xmlHttp.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded;charset=utf-8');
        xmlHttp.send(postData);
    }elseif (opt.method.toUpperCase() === 'GET') {
        xmlHttp.open(opt.method, opt.url + '?' + postData, opt.async);
        xmlHttp.send(null);
    } 
     xmlHttp.onreadystatechange= function () {
            if (xmlHttp.readyState == 4 && xmlHttp.status == 200) {
                opt.success(xmlHttp.responseText);//如果是json資料可以在這使用opt.success(JSON.parse( xmlHttp.responseText))
            }
     };
}
複製程式碼
複製程式碼

url拿引數

var url = "http://www.taobao.com/index.php?key0=0&key1=1&key2=2";
functionparseQueryString(url){
    var str = url.split("?")[1],    //通過?得到一個陣列,取?後面的引數
        items = str.split("&");    //分割成陣列var arr,name,value;

    for(var i=0; i<items.length; i++){
        arr = items[i].split("=");    //["key0", "0"]
        name = arr[0];
        value = arr[1];
        this[name] = value;
    }
}

var obj = new parseQueryString(url);
alert(obj.key2)
複製程式碼
複製程式碼

HTTP部分

http協議

HTTP協議(超文字傳輸協議) 主要特點

  1. 簡單快速:客戶向伺服器請求服務時,只需傳送請求方法和路徑。請求方法常用的有GET、HEAD、POST。每種方法規定了客戶與伺服器聯絡的型別不同。由於HTTP協議簡單,使得HTTP伺服器的程式規模小,因而通訊速度很快。
  2. 靈活:HTTP允許傳輸任意型別的資料物件。正在傳輸的型別由Content-Type加以標記。
  3. 無連線:無連線的含義是限制每次連線只處理一個請求。伺服器處理完客戶的請求,並收到客戶的應答後,即斷開連線。採用這種方式可以節省傳輸時間。
  4. 無狀態:HTTP協議是無狀態協議。無狀態是指協議對於事務處理沒有記憶能力。缺少狀態意味著如果後續處理需要前面的資訊,則它必須重傳,這樣可能導致每次連線傳送的資料量增大。另一方面,在伺服器不需要先前資訊時它的應答就較快。
  5. 支援B/S及C/S模式。

HTTP之請求訊息Request

  1. 請求行(request line)、請求頭部(header)、空行和請求資料四個部分組成。
  2. 請求行,用來說明請求型別,要訪問的資源以及所使用的HTTP版本.
  3. 請求頭部,緊接著請求行(即第一行)之後的部分,用來說明伺服器要使用的附加資訊
  4. 空行,請求頭部後面的空行是必須的
  5. 請求資料也叫主體,可以新增任意的其他資料。

HTTP之響應訊息Response

HTTP響應也由四個部分組成,分別是:狀態行、訊息報頭、空行和響應正文。

  1. 狀態行,由HTTP協議版本號, 狀態碼, 狀態訊息 三部分組成。
  2. 訊息報頭,用來說明客戶端要使用的一些附加資訊
  3. 第三部分:空行,訊息報頭後面的空行是必須的
  4. 第四部分:響應正文,伺服器返回給客戶端的文字資訊。

在瀏覽器位址列鍵入URL,按下回車之後會經歷以下流程:

  1. 瀏覽器向 DNS 伺服器請求解析該 URL 中的域名所對應的 IP 地址;
  2. 建立TCP連線;
  3. 瀏覽器發出讀取檔案(URL 中域名後面部分對應的檔案)的HTTP 請求,該請求報文作為 TCP 三次握手的第三個報文的資料傳送給伺服器;
  4. 伺服器對瀏覽器請求作出響應,並把對應的 html 文字傳送給瀏覽器;
  5. 釋放 TCP連線(四次揮手);
  6. 瀏覽器將該 html 文字並顯示內容;

三次握手

SYN (同步序列編號)ACK(確認字元)

  1. 第一次握手:Client將標誌位SYN置為1,隨機產生一個值seq=J,並將該資料包傳送給Server,Client進入SYN_SENT狀態,等待Server確認。
  2. 第二次握手:Server收到資料包後由標誌位SYN=1知道Client請求建立連線,Server將標誌位SYN和ACK都置為1,ack=J+1,隨機產生一個值seq=K,並將該資料包傳送給Client以確認連線請求,Server進入SYN_RCVD狀態。
  3. 第三次握手:Client收到確認後,檢查ack是否為J+1,ACK是否為1,如果正確則將標誌位ACK置為1,ack=K+1,並將該資料包傳送給Server,Server檢查ack是否為K+1,ACK是否為1,如果正確則連線建立成功,Client和Server進入ESTABLISHED狀態,完成三次握手,隨後Client與Server之間可以開始傳輸資料了。

四次揮手

  1. 第一次揮手:Client傳送一個FIN,用來關閉Client到Server的資料傳送,Client進入FIN_WAIT_1狀態。
  2. 第二次揮手:Server收到FIN後,傳送一個ACK給Client,確認序號為收到序號+1(與SYN相同,一個FIN佔用一個序號),Server進入CLOSE_WAIT狀態。
  3. 第三次揮手:Server傳送一個FIN,用來關閉Server到Client的資料傳送,Server進入LAST_ACK狀態。
  4. 第四次揮手:Client收到FIN後,Client進入TIME_WAIT狀態,接著傳送一個ACK給Server,確認序號為收到序號+1,Server進入CLOSED狀態,完成四次揮手。

為什麼建立連線是三次握手,而關閉連線卻是四次揮手呢?

這是因為服務端在LISTEN狀態下,收到建立連線請求的SYN報文後,把ACK和SYN放在一個報文裡傳送給客戶端。而關閉連線時,當收到對方的FIN報文時,僅僅表示對方不再傳送資料了但是還能接收資料,己方也未必全部資料都傳送給對方了,所以己方可以立即close,也可以傳送一些資料給對方後,再傳送FIN報文給對方來表示同意現在關閉連線,因此,己方ACK和FIN一般都會分開傳送。

網頁生成的過程,大致可以分為五步:

  1. html程式碼轉化為dom
  2. css程式碼轉化為cssom
  3. 結合dom和cssom,生成一顆渲染樹
  4. 生成佈局layout,即將所有的渲染樹的節點進行平面合成
  5. 將佈局繪製paint在螢幕上(可以擴充講一下減少瀏覽器渲染的重排和重繪)

瀏覽器快取

當瀏覽器再次訪問一個已經訪問過的資源時,它會這樣做:

  1. 看看是否命中強快取,如果命中,就直接使用快取了。
  2. 如果沒有命中強快取,就發請求到伺服器檢查是否命中協商快取。
  3. 如果命中協商快取,伺服器會返回 304 告訴瀏覽器使用本地快取。
  4. 否則,返回最新的資源。

vue

vue Virtual DOM

其實 VNode 是對真實 DOM 的一種抽象描述,它的核心定義無非就幾個關鍵屬性,標籤名、資料、子節點、鍵值等,其它屬性都是都是用來擴充套件 VNode 的靈活性以及實現一些特殊 feature 的。由於 VNode 只是用來對映到真實 DOM 的渲染,不需要包含操作 DOM 的方法,因此它是非常輕量和簡單的。 Virtual DOM 除了它的資料結構的定義,對映到真實的 DOM 實際上要經歷 VNode 的 create、diff、patch 等過程。那麼在 Vue.js 中,VNode 的 create 是通過之前提到的 createElement 方法建立的,我們接下來分析這部分的實現。

vue的響應式原理

  • Object.defineProperty(obj, prop, descriptor)

  • obj 是要在其上定義屬性的物件;prop 是要定義或修改的屬性的名稱;descriptor 是將被定義或修改的屬性描述符。 比較核心的是 descriptor,它有很多可選鍵值,具體的可以去參閱它的文件。這裡我們最關心的是 get 和 set,get 是一個給屬性提供的 getter 方法,當我們訪問了該屬性的時候會觸發 getter 方法;set 是一個給屬性提供的 setter 方法,當我們對該屬性做修改的時候會觸發 setter 方法。一旦物件擁有了 getter 和 setter,我們可以簡單地把這個物件稱為響應式物件

  • 物件遞迴呼叫

  • 陣列變異方法的解決方法:代理原型/例項方法

  • observe

  • observe 方法的作用就是給非 VNode 的物件型別資料新增一個 Observer,如果已經新增過則直接返回,否則在滿足一定條件下去例項化一個 Observer 物件例項。

  • observe 的功能就是用來監測資料的變化.

  • Observer 是一個類,它的作用是給物件的屬性新增 getter 和 setter,用於依賴收集和派發更新:

  • 依賴收集和派發更新

  • 收集依賴的目的是為了當這些響應式資料發生變化,觸發它們的 setter 的時候,能知道應該通知哪些訂閱者去做相應的邏輯處理,我們把這個過程叫派發更新,其實 Watcher 和 Dep 就是一個非常經典的觀察者設計模式的實現

  • 派發更新就是資料發生變化的時候,觸發 setter 邏輯,把在依賴過程中訂閱的的所有觀察者,也就是 watcher,都觸發它們的 update 過程,這個過程又利用了佇列做了進一步優化,在 nextTick 後執行所有 watcher 的 run,最後執行它們的回撥函式

  • vue編譯Compile的過程主要分以下幾步 parse(生成AST)=> optimize(優化靜態節點) => generate(生成render function)

    // 解析模板字串生成 ASTconst ast = parse(template.trim(), options) //優化語法樹 optimize(ast, options) //生成程式碼const code = generate(ast, options) 複製程式碼

對vuex的理解,單向資料流

前端安全

XSS和CSRF

  • XSS:跨站指令碼攻擊,是一種網站應用程式的安全漏洞攻擊,是程式碼注入的一種。常見方式是將惡意程式碼注入合法程式碼裡隱藏起來,再誘發惡意程式碼,從而進行各種各樣的非法活動。

預防:

使用XSS Filter

  1. 輸入過濾,對使用者提交的資料進行有效性驗證,僅接受指定長度範圍內並符合我們期望格式的的內容提交,阻止或者忽略除此外的其他任何資料。
  2. 輸出轉義,當需要將一個字串輸出到Web網頁時,同時又不確定這個字串中是否包括XSS特殊字元,為了確保輸出內容的完整性和正確性,輸出HTML屬性時可以使用HTML轉義編碼(HTMLEncode)進行處理,輸出到<script>中,可以進行JS編碼。

使用 HttpOnly Cookie 將重要的cookie標記為httponly,這樣的話當瀏覽器向Web伺服器發起請求的時就會帶上cookie欄位,但是在js指令碼中卻不能訪問這個cookie,這樣就避免了XSS攻擊利用JavaScript的document.cookie獲取cookie。

CSRF:跨站請求偽造,也稱 XSRF,是一種挾制使用者在當前已登入的Web應用程式上執行非本意的操作的攻擊方法。與 XSS 相比,XSS利用的是使用者對指定網站的信任,CSRF利用的是網站對使用者網頁瀏覽器的信任。

  1. 預防:使用者操作限制——驗證碼機制
  • 方法:新增驗證碼來識別是不是使用者主動去發起這個請求,由於一定強度的驗證碼機器無法識別,因此危險網站不能偽造一個完整的請求。
  • 優點:簡單粗暴,低成本,可靠,能防範99.99%的攻擊者。
  • 缺點:對使用者不友好。
  1. 請求來源限制——驗證 HTTP Referer 欄位
  • 方法:在HTTP請求頭中有一個欄位叫Referer,它記錄了請求的來源地址。 伺服器需要做的是驗證這個來源地址是否合法,如果是來自一些不受信任的網站,則拒絕響應。
  • 優點:零成本,簡單易實現。
  • 缺點:由於這個方法嚴重依賴瀏覽器自身,因此安全性全看瀏覽器。
  • 額外驗證機制——token的使用
  1. 方法:使用token來代替驗證碼驗證。由於黑客並不能拿到和看到cookie裡的內容,所以無法偽造一個完整的請求。基本思路如下:
  2. 伺服器隨機產生token(比如把cookie hash化生成),存在session中,放在cookie中或者以ajax的形式交給前端。
  • 前端發請求的時候,解析cookie中的token,放到請求url裡或者請求頭中。
  • 伺服器驗證token,由於黑客無法得到或者偽造token,所以能防範csrf

正在整理中的問題

寫過webpack loader嗎

vue怎麼監聽陣列

手寫實現json

實現一個釋出訂閱模式;

手寫快排,時間複雜度,優化

vue裡面的虛擬dom

HTTPS的工作原理

那通過物件.方法呼叫箭頭函式,裡面的this指向什麼

webpack用過嗎?搖樹是什麼,什麼場景下用過?

你遇到過最難的問題是什麼

react 虛擬 dom實現,diff演算法;

手寫快排,怎麼優化;說下sort實現原理;

解釋一個你最近遇到的技術挑戰


沒有套路,只是面試前攢人品,有錯誤請指出,謝謝。

CSS 部分

兩欄佈局

要求:垂直兩欄,左邊固定右邊自適應。

檢視程式碼 <htmllang="en"><metacharset="UTF-8"><metaname="viewport"content="width=device-width, initial-scale=1.0"><metahttp-equiv="X-UA-Compatible"content="ie=edge">

Document<divclass="outer outer1"><divclass="left">1-left<divclass="right">1-right<divclass="outer outer2"><divclass="left">2-left<divclass="right">2-right<divclass="outer outer3"><divclass="left">3-left<divclass="right">3-right<divclass="outer outer4"><divclass="left">4-left<divclass="right">4-right複製程式碼

三欄佈局

要求:垂直三欄佈局,左右兩欄寬度固定,中間自適應

檢視程式碼 <htmllang="en"><metacharset="UTF-8"><metaname="viewport"content="width=device-width, initial-scale=1.0"><metahttp-equiv="X-UA-Compatible"content="ie=edge">

Document<divclass="outer outer1"><divclass="left">1-left<divclass="middle">1-middle<divclass="right">1-right<divclass="outer outer2"><divclass="left">2-left<divclass="middle">2-middle<divclass="right">2-right<divclass="outer outer3"><divclass="left">3-left<divclass="right">3-right<divclass="middle">3-middle複製程式碼

聖盃佈局 和 雙飛翼佈局

和三欄佈局要求相同,不過中間列要寫在前面保證優先渲染。

檢視程式碼 <htmllang="en"><metacharset="UTF-8"><metaname="viewport"content="width=device-width, initial-scale=1.0"><metahttp-equiv="X-UA-Compatible"content="ie=edge">

Document<divclass="outer outer1"><divclass="middle">聖盃-middle<divclass="left">聖盃-left<divclass="right">聖盃-right<divclass="outer outer2"><divclass="middle-wrapper"><divclass="middle">雙飛翼佈局-middle<divclass="left">雙飛翼佈局-left<divclass="right">雙飛翼佈局-right複製程式碼

三角形

實現一個三角形

常見題目,通過 border 實現 檢視程式碼

三角形<styletype="text/css">.box1, .box2, .box3, .box4 { height: 0px; width: 0px; float: left; border-style: solid; margin: 10px; } .box1 { /* 等腰直角 /border-width: 100px; border-color: tomato transparent transparent transparent; } .box2 { / 等邊 /border-width: 100px173px; border-color: transparent tomato transparent transparent; } .box3 { / 等腰 /border-width: 100px80px; border-color: transparent transparent tomato transparent; } .box4 { / 其他 */border-width: 100px90px80px70px; border-color: transparent transparent transparent tomato; } <divclass="box1"><divclass="box2"><divclass="box3"><divclass="box4">複製程式碼

正方形

使用 css 實現一個寬高自適應的正方形

檢視程式碼 <metacharset="utf-8">

<divclass="square1"><divclass="square2"><divclass="square3">複製程式碼

扇形

實現一個 1/4 圓、任意弧度的扇形

有多種實現方法,這裡選幾種簡單方法(我看得懂的)實現。 檢視程式碼 <htmllang="en"><metacharset="UTF-8"><metaname="viewport"content="width=device-width, initial-scale=1.0"><metahttp-equiv="X-UA-Compatible"content="ie=edge">

Document<divstyle="display: flex; justify-content: space-around;"><divclass="sector1"><divclass="sector2"><divclass="sector3"><divclass="sector4"><divclass="sector5">複製程式碼

水平垂直居中

實現子元素的水平垂直居中

檢視程式碼

水平垂直居中<styletype="text/css">.outer { height: 200px; width: 200px; background: tomato; margin: 10px; float: left; position: relative; } .inner { height: 100px; width: 100px; background: black; } /* * 通過 position 和 margin 居中 * 缺點:需要知道 inner 的長寬 /.inner1 { position: absolute; top: 50%; left: 50%; margin-top: -50px; margin-left: -50px; } / * 通過 position 和 margin 居中 (2 /.inner2 { position: absolute; top: 0; right: 0; bottom: 0; left: 0; margin: auto; } / * 通過 flex 進行居中 /.outer3 { display: flex; justify-content: center; align-items: center; } /* * 通過 position 和 transform 居中 */.inner4 { top: 50%; left: 50%; transform: translate(-50%,-50%); position: absolute; } <divclass="outer outer1"><divclass="inner inner1"><divclass="outer outer2"><divclass="inner inner2"><divclass="outer outer3"><divclass="inner inner3"><divclass="outer outer4"><divclass="inner inner4">複製程式碼

清除浮動

要求:清除浮動

可以通過 clear:both 或 BFC 實現 檢視程式碼

清除浮動<styletype="text/css">.outer { width: 200px; background: tomato; margin: 10px; position: relative; } .inner { height: 100px; width: 100px; background: pink; margin: 10px; float: left; } /* 偽元素 /.outer1::after { content: ''; display: block; clear: both; } / 建立 BFC */.outer2 { overflow: hidden; } <divclass="outer outer1"><divclass="inner"><divclass="outer outer2"><divclass="inner">複製程式碼

彈出框

使用 CSS 寫一個彈出框效果

檢視程式碼 <htmllang="en"><metacharset="UTF-8"><metaname="viewport"content="width=device-width, initial-scale=1.0"><metahttp-equiv="X-UA-Compatible"content="ie=edge">

Document<divclass="bg"> 頁面內容 <divclass="dialog"><divclass="content"> 彈出框 複製程式碼

導航欄

要求:一個 div 內部放很多水平 div ,並可以橫向滾動。

檢視程式碼 <htmllang="en"><metacharset="UTF-8"><metaname="viewport"content="width=div, initial-scale=1.0"><metahttp-equiv="X-UA-Compatible"content="ie=edge">

Document<divclass="nav"><divclass="item">item1<divclass="item">item2<divclass="item">item3<divclass="item">item4<divclass="item">item5<divclass="item">item6<divclass="item">item7<divclass="item">item8<divclass="item">item9複製程式碼

CSS 部分完,總結,Flex 無敵。

JavaScript 部分

手寫 bind、call 和 apply

Function.prototype.bind = function(context, ...bindArgs) {
  // func 為呼叫 bind 的原函式const func = this;
  context = context || window;
  
  if (typeof func !== 'function') {
    thrownewTypeError('Bind must be called on a function');
  }
  // bind 返回一個繫結 this 的函式returnfunction(...callArgs) {
    let args = bindArgs.concat(callArgs);
    if (thisinstanceof func) {
      // 意味著是通過 new 呼叫的 而 new 的優先順序高於 bindreturnnew func(...args);
    }
    return func.call(context, ...args);
  }
}

// 通過隱式繫結實現Function.prototype.call = function(context, ...args) {
  context = context || window;
  context.func = this;

  if (typeof context.func !== 'function') {
    thrownewTypeError('call must be called on a function');
  }

  let res = context.func(...args);
  delete context.func;
  return res;
}

Function.prototype.apply = function(context, args) {
  context = context || window;
  context.func = this;

  if (typeof context.func !== 'function') {
    thrownewTypeError('apply must be called on a function');
  }

  let res = context.func(...args);
  delete context.func;
  return res;
}
複製程式碼
複製程式碼

實現一個繼承

// 參考 You Dont Know JavaScript 上卷// 基類functionBase() {
}
// 派生類functionDerived() {
    Base.call(this);
}
// 將派生類的原型的原型鏈掛在基類的原型上Object.setPrototypeOf(Derived.prototype, Base.prototype);
複製程式碼
複製程式碼

實現一個 new

// 手動實現一個 new 關鍵字的功能的函式 _new(fun, args) --> new fun(args)function_new(fun, ...args) {
    if (typeof fun !== 'function') {
        returnnewError('引數必須是一個函式');
    }
    let obj = Object.create(fun.prototype);
    let res = fun.call(obj, ...args);
    if (res !== null && (typeof res === 'object' || typeof res === 'string')) {
        return res;
    }
    return obj;
}
複製程式碼
複製程式碼

實現一個 instanceof

// a instanceof bfunction_instanceof(a, b) {
    while (a) {
        if (a.__proto__ === b.prototype) returntrue;
        a = a.__proto__;
    }
    returnfalse;
}
複製程式碼
複製程式碼

手寫 jsonp 的實現

// foo 函式將會被呼叫 傳入後臺返回的資料functionfoo(data) {
    console.log('通過jsonp獲取後臺資料:', data);
    document.getElementById('data').innerHTML = data;
}
/**
 * 通過手動建立一個 script 標籤傳送一個 get 請求
 * 並利用瀏覽器對 <script> 不進行跨域限制的特性繞過跨域問題
 */
(functionjsonp() {
    let head = document.getElementsByTagName('head')[0]; // 獲取head元素 把js放裡面let js = document.createElement('script');
    js.src = 'http://domain:port/testJSONP?a=1&b=2&callback=foo'; // 設定請求地址
    head.appendChild(js); // 這一步會傳送請求
})();

// 後臺程式碼// 因為是通過 script 標籤呼叫的 後臺返回的相當於一個 js 檔案// 根據前端傳入的 callback 的函式名直接呼叫該函式// 返回的是 'foo(3)'functiontestJSONP(callback, a, b) {
  return`${callback} + (${a + b})`;
}
複製程式碼
複製程式碼

ajax 的實現

感覺這個有點無聊了…… 檢視程式碼 // Asynchronous Javascript And XMLfunctionajax(options) { // 選項var method = options.method || 'GET', params = options.params, data = options.data, url = options.url + (params ? '?' + Object.keys(params).map(key => key + '=' + params[key]).join('&') : ''), async = options.async === false ? false : true, success = options.success, headers = options.headers;

  var request;
  if (window.XMLHttpRequest) {
    request = new XMLHttpRequest();
  } else {
    request = new ActiveXObject('Microsoft.XMLHTTP');
  }

  request.onstatechange = function() {
    /**
    readyState:
      0: 請求未初始化
      1: 伺服器連線已建立
      2: 請求已接收
      3: 請求處理中
      4: 請求已完成,且響應已就緒

    status: HTTP 狀態碼
    **/if (request.readyState === 4 && request.status === 200) {
      success && success(request.responseText);
    }
  }

  request.open(method, url, async);
  if (headers) {
    Object.keys(headers).forEach(key => request.setRequestHeader(key, headers[key]));
  }
  method === 'GET' ? request.send() : request.send(request.data);
}
// e.g.
ajax({
  method: 'GET',
  url: '...',
  success: function(res) {
    console.log('success', res);
  },
  async: true,
  params: {
    p: 'test',
    t: 666
  },
  headers: {
    'Content-Type': 'application/json'
  }
})
複製程式碼
複製程式碼

reduce 的實現

functionreduce(arr, callback, initial) {
    let i = 0;
    let acc = initial === undefined ? arr[i++] : initial;
    for (; i < arr.length; i++) {
        acc = callback(acc, arr[i], i, arr);
    }
    return acc;
}
複製程式碼
複製程式碼

實現 generator 的自動執行器

要求是 yield 後面只能是 PromiseThunk 函式,詳見 es6.ruanyifeng.com/#docs/gener…

functionrun(gen) {
  let g = gen();

  functionnext(data) {
    let result = g.next(data);
    if (result.done) return result.value;
    if (result.value instanceofPromise) {
      result.value.then(data => next(data));
    } else {
      result.value(next);
    }
  }

  return next();
}

// ======== e.g. ==========functionfunc(data, cb) {
  console.log(data);
  cb();
}

function *gen() {
  let a = yieldPromise.resolve(1);
  console.log(a);
  let b = yieldPromise.resolve(2);
  console.log(b);
  yield func.bind(null, a + b);
}
run(gen);
/** 
output:
1
2
3
**/複製程式碼
複製程式碼

節流

老生常談了,感覺沒必要寫太複雜

/**
 * 節流函式 限制函式在指定時間段只能被呼叫一次
 * 用法 比如防止使用者連續執行一個耗時操作 對操作按鈕點選函式進行節流處理
 */functionthrottle(func, wait) {
  let timer = null;
  returnfunction(...args) {
    if (!timer) {
      func(...args);
      timer = setTimeout(() => {
        timer = null;
      }, wait);
    }
  }
}
複製程式碼
複製程式碼

防抖

/**
 * 函式呼叫後不會被立即執行 之後連續 wait 時間段沒有呼叫才會執行
 * 用法 如處理使用者輸入
 */functiondebounce(func, wait) {
  let timer = null;
  
  returnfunction(...args) {
    if (timer) clearTimeout(timer); // 如果在定時器未執行期間又被呼叫 該定時器將被清除 並重新等待 wait 秒
    timer = setTimeout(() => {
      func(...args);
    }, wait);
  }
}
複製程式碼
複製程式碼

手寫 Promise

簡單實現,基本功能都有了。

const PENDING = 1;
const FULFILLED = 2;
const REJECTED = 3;

functionMyPromise(executor) {
    let self = this;
    this.resolveQueue = [];
    this.rejectQueue = [];
    this.state = PENDING;
    this.val = undefined;
    functionresolve(val) {
        if (self.state === PENDING) {
            setTimeout(() => {
                self.state = FULFILLED;
                self.val = val;
                self.resolveQueue.forEach(cb => cb(val));
            });
        }
    }
    functionreject(err) {
        if (self.state === PENDING) {
            setTimeout(() => {
                self.state = REJECTED;
                self.val = err;
                self.rejectQueue.forEach(cb => cb(err));
            });
        }
    }
    try {
        // 回撥是非同步執行 函式是同步執行
        executor(resolve, reject);
    } catch(err) {
        reject(err);
    }
}

MyPromise.prototype.then = function(onResolve, onReject) {
    let self = this;
    // 不傳值的話預設是一個返回原值的函式
    onResolve = typeof onResolve === 'function' ? onResolve : (v => v); 
    onReject = typeof onReject === 'function' ? onReject : (e => { throw e });
    if (self.state === FULFILLED) {
        returnnew MyPromise(function(resolve, reject) {
            setTimeout(() => {
                try {
                    let x = onResolve(self.val);
                    if (x instanceof MyPromise) {
                        x.then(resolve);
                    } else {
                        resolve(x);
                    }
                } catch(e) {
                    reject(e);
                }
            });
        });
    }

    if (self.state === REJECTED) {
        returnnew MyPromise(function(resolve, reject) {
            setTimeout(() => {
                try {
                    let x = onReject(self.val);
                    if (x instanceof MyPromise) {
                        x.then(resolve);
                    } else {
                        resolve(x);
                    }
                } catch(e) {
                    reject(e);
                }
            });
        });
    }
    
    if (self.state === PENDING) {
        returnnew MyPromise(function(resolve, reject) {
            self.resolveQueue.push((val) => {
                try {
                    let x = onResolve(val);
                    if (x instanceof MyPromise) {
                        x.then(resolve);
                    } else {
                        resolve(x);
                    }
                } catch(e) {
                    reject(e);
                }
            });
            self.rejectQueue.push((val) => {
                try {
                    let x = onReject(val);
                    if (x instanceof MyPromise) {
                        x.then(resolve);
                    } else {
                        resolve(x);
                    }
                } catch(e) {
                    reject(e);
                }
            });
        });
    }
}

MyPromise.prototype.catch = function(onReject) {
    returnthis.then(null, onReject);
}

MyPromise.all = function(promises) {
    returnnew MyPromise(function(resolve, reject) {
        let cnt = 0;
        let result = [];
        for (let i = 0; i < promises.length; i++) {
            promises[i].then(res => {
                result[i] = res;
                if (++cnt === promises.length) resolve(result);
            }, err => {
                reject(err);
            })
        }
    });
}

MyPromise.race = function(promises) {
    returnnew MyPromise(function(resolve, reject) {
        for (let i = 0; i < promises.length; i++) {
            promises[i].then(resolve, reject);
        }
    });
}

MyPromise.resolve = function(val) {
    returnnew MyPromise(function(resolve, reject) {
        resolve(val);
    });
}

MyPromise.reject = function(err) {
    returnnew MyPromise(function(resolve, reject) {
        reject(err);
    })
}
複製程式碼
複製程式碼

實現一個路由 - Hash

實現原理就是監聽 url 的雜湊值變化了

<!DOCTYPE html><html><head><title>hash 路由</title></head><body><header><ahref="#home">首頁</a><ahref="#center">個人中心頁</a><ahref="#help">幫助頁</a></header><sectionid="content"></section><script>window.addEventListener('hashchange', (e) => {
      let content = document.getElementById('content');
      content.innerText = location.hash;
    })
  </script></body></html>複製程式碼
複製程式碼

路由實現 - history

<!DOCTYPE html><html><head><title>history 路由</title></head><body><header><aonclick="changeRoute(this)"data-path="home">首頁</a><aonclick="changeRoute(this)"data-path="center">個人中心頁</a><aonclick="changeRoute(this)"data-path="help">幫助頁</a></header><sectionid="content"></section><script>functionchangeRoute(route) {
      let path = route.dataset.path;
      /**
       * window.history.pushState(state, title, url)
       * state:一個與新增的記錄相關聯的狀態物件,主要用於popstate事件。該事件觸發時,該物件會傳入回撥函式。
       *        也就是說,瀏覽器會將這個物件序列化以後保留在本地,重新載入這個頁面的時候,可以拿到這個物件。
       *        如果不需要這個物件,此處可以填 null。
       * title:新頁面的標題。但是,現在所有瀏覽器都忽視這個引數,所以這裡可以填空字串。
       * url:新的網址,必須與當前頁面處在同一個域。瀏覽器的位址列將顯示這個網址。
       */
      changePage(path);
      history.pushState({ content: path }, null, path);
    }
    /**
     * 呼叫 history.pushState() 或者 history.replaceState() 不會觸發 popstate 事件。
     * 點選後退、前進按鈕、或者在 js 中呼叫 history.back()、history.forward()、history.go() 方法會觸發
     */window.addEventListener('popstate', (e) => {
      let content = e.state && e.state.content;
      changePage(content);
    });

    functionchangePage(pageContent) {
      let content = document.getElementById('content');
      content.innerText = pageContent;
    }
  </script></body></html>複製程式碼
複製程式碼

還有一些稍複雜的可以寫,有時間再補。

相關文章