1. 程式人生 > >前端技術總結

前端技術總結

文章主要是用來記錄一下一些常見問題,權當是複習了。

1 請用原生JS實現物件的深度拷貝。

其實這個題目還是挺基礎的,主要考慮當objt是物件object或者陣列時遞迴呼叫即可,當然這裡還有一個小問題需要注意的就是對於純物件的考量,這裡直接用typeof parent[i] ==='object'可能會存在一定的問題。
具體程式碼如下:

function extendsDeep(parent,child){
    var i,
        toStr = Object.prototype.toString,
        astr = "[object Array]";
    child = child || {};

    for
(i in parent){ if(parent.hasOwnProperty(i)){ if(typeof parent[i] === 'object'){ child[i] =(toStr.call(parent[i]) === astr) ? [] : {}; extendsDeep(parent[i],child[i]); }else{ child[i] = parent[i]; } } } }

2 請用原生程式碼實現簡易事件系統。

這裡主要涉及瀏覽器的事件系統,包括事件冒泡,事件捕獲,目標事件,以及對不同瀏覽器的事件相容性問題處理,最後就是this的指向處理。

//add event
funciton addEvent(elem,type,fn){
    if(elem.addEventListener){
        elem.addEventListener(type,fn,false);//標準瀏覽器預設事件為冒泡
    }else if(elem.attachEvent){
        elem.attachEvent('on'+type,function
(){
fn.call(elem); }); }else{ elem['on'+type] = fn; } } //remove event function removeEvent(elem,type,fn){ if(elem.removeEventListener){ elem.removeEventListener(type,fn,false); }else if{ elem.detachEvent('on'+type,fn); } }

3 請用原生JS程式碼實現事件代理

addEvent: function(elem,selector,type,fn){
        //判斷是否為代理
        if(arguments.length == 3){
            var fn = type, 
                type = selector;
                selector = "";//代理置空
        }

        if(elem.addEventListener){
            elem.addEventListener(type,function(event){
                //是否存在代理
                if(selector != ""){
                    var event = event || window.event,
                        target = event.target;
                        if("." + target.className == selector){
                            fn.apply(elem,arguments);
                        }
                }else{
                    fn.apply(elem,arguments);
                }
            },false);
        }else if(elem.attachEvent){
            elem.attachEvent("on" + type,function(event){
                //是否存在代理
                if(selector != ""){
                    var event = event || window.event,
                        target = event.target || event.srcElement;
                    if("." + target.className == selector){
                        fn.apply(elem,arguments);
                    }
                }else{
                    fn.apply(elem,arguments);
                }
            });
        }
    }

4 請用原生JS實現物件繼承

物件繼承的方式有許多種,主要包括預設模式,借用構造方法模式,借用與設定模式,共享原型模式,臨時構造方法模式;
當然這裡可能只要你能答出2到3種就已經差不多了;

1 預設模式

其實預設模式應該是最簡單的,需要繼承父類的方法,直接簡單粗暴的new一個父類的例項出來就搞定了。應該由於js中prototype機制的關係,new出來的例項中會有一個內部屬性__proto__ 指向prototype物件,那麼將子類的prototype物件直接賦值為new出來的例項,一切就搞定了。

function extend(Chid,Parent){
    Child.prototype = new Parent(); 
}

當然他的缺點也很明顯:
1 子類同時繼承了兩個物件的屬性,即新增到this的屬性與原型鏈上的屬性;
2 預設模式並不支援將引數傳遞到子建構函式中,而子建構函式又將引數傳遞到父建構函式中;
考慮以下情況:

function Parent(name){
    this.name = name || 'parent';
}
Parent.prototype.say = function(){
    console.log(this.name);
}

function Child(name){
}

extend(Child,Parent);

var child = new Child('child');
//由於這裡'child'引數並不會傳遞到Parent構造方方法中
//雖然子構造方法可以將引數傳遞到父構造方方法,
//但是如果那樣的話,在每次需要一個新的子物件時都必須重新執行這種繼承機制,
//而且這種機制效率低下,原因在於最終會反覆的重新建立父物件;
s.say();    // parent   

2 借用構造方法模式

function Child(a,b,c,d){
    Parent.appliy(this,arguments);
}

當然這種模式的缺點也很明顯:
只能繼承this中的屬性,而在prototype中的屬性則不能繼承;
PS:需要至於與上面預設模式的區別,1中的預設模式是繼承的this中屬性是對父級屬性的引用,而2中借用構造方法則是對父類中this屬性的拷貝,畢竟都已經在子類的構造方法中單獨運行了。所以如果修改通過1繼承而來的子類屬性,父類的是屬性會一樣被修改,而2則不會;

3 借用與設定模式

其實就是將上面的2種模式綜合一起。如下:

function Child(a,b,c,d){
    Parent.apply(this,arguments);
}

Child.prototype = new Parent();

既可以解決上面1中子構造方法的傳參問題,也可以解決2中的prototype屬性沒法繼承問題;
但是新的問題是,這裡面呼叫了2次父類的構造方法;

4 共享原型模式

所謂共享,就是將prototype進行復用;

funciton extend(Child,Parent){
    Child.prototype = Parent.prototype;
}

當然它的問題也很明顯,子類與父類的耦合太重,如果修改了子類的prototype,那麼父類也很一樣受到影響;

5 臨時構造方法模式

都說大音希聲,其實到最後最終的解決方案,也很簡單。
從上面來看,共享原型的方式會存在耦合,那麼我們解掉耦合不就over了麼?
解決方案是,用一個臨時的方法 F 的prototype來儲存父類的prototype物件,而子類的prototype物件則選擇指向臨時方法F 的例項,那麼這樣如果你修改了子類的prototype物件,那麼受到印象的也只是F的例項而已,不會影響到父類方法的prototype;
所以解決方案如下:

function extend(Child,Parent){
    var F = function(){}
    F.prototype = Parent.prototype();
    Child.prototype = new F();
}

6 終極解決方案

在5的基礎中進一步優化,重置constructor的指向與優化如下:

var extend = (function(){
    var F = function(){};//單例,只需要申明一次即可
    return function(Child,Parent){
        F.prototype = Parent.prototype;
        Child.prototype = new F();
        Child.prototype.constructor = Child;
        Child.super = Parent;
    }
}());

5 關於json與jsonp的區別

json是一種資料互動的資料格式,jsonp為一種資料互動的跨域手段。
1 由於跨域由於同源策略的存在導致ajax在埠,域名,協議不同的情況下,無法完成請求;
2 但是發現頁面在載入資源的時候img,script等,並不存在些等情況,唯一不同的是,載入資源是屬於GET請求;
3 那麼jsonp請應運而生了,我們可以動態的建立節點,然後動態載入資源,後端只需要把相應的資料以json格式返回回來即可,前端自行進行渲染;
具體實現如下:
服務端有js檔案jsonp.js
返回資料格式如下:

//這裡的showName 就是前端傳遞過來的callback引數值
showName({
    "code": "CA1998",
    "price": 1780,
    "tickets": 5
});

前端呼叫檔案


    var showName = function(data){
        console.log(data);
    }

    var script = document.createElement("script");
    script.type = "text/javascript";
    script.src = "http://www.xx.com/jsonp.js?callback=showName";//通過此處將要回調的方法名傳遞給後端
    document.getElementsByTagName('head')[0].appendChild(script);

ps: ajax與jsonp都可以用來作非同步操作。但ajax和jsonp其實本質上是不同的東西。ajax的核心是通過XmlHttpRequest獲取非本頁內容,而jsonp的核心則是動態新增

6 水平垂直居中問題

垂直居中分為定寬和不定寬;

/*已知高度和寬度的水平垂直居中*/
#body-div{
    position:relative;
}
#body-div-container{
    width:100px;
    height:100px;
    position:absolute;
    top:50%;
    left:50%;
    margin:-50px 0 0 -50px;
}
/*未知高度和寬度的水平垂直居中*/
#body-div{
    position:relative;
}
##body-div-container{
    position:absolute;
    margin:auto;
    top:0;
    right:0;
    bottom:0;
    left:0;
}

7 閉包

關於閉包,確實涉及的東西太多太多,那麼最重要的兩個問題:
1 閉包是怎麼形成的
2 閉包的作用與優缺點
關於第一個閉包是怎麼形成的,這裡又涉及到詞法作用域作用域鏈執行上下文等概念。

7.1 詞法作用域是什麼呢?

詞法作用域就是定義在詞法階段的作用域。換句話說,詞法作用域是由你在寫程式碼時將變數和塊作用域寫在哪裡來決定的,因此當詞法分析器處理程式碼會保持作用域不變(大部分情況是這樣的)。
考慮以下程式碼:

function foo(a){
    var b = a * 2;
    function bar(c){
         console.log(a,b,c);
    }
    bar(b*3);
}
foo(2);//2,4,12

這裡寫圖片描述

三級巢狀的詞法作用域,具體如下:
(1) 包含著整個全域性作用域,其中只有一個識別符號:foo。
(2) 包含著foo所建立的作用域,其中有三個識別符號: a、bar、b。
(3) 包含著bar所建立的作用域,其中只有一個識別符號:c 。

無論函式在哪裡被呼叫,也無論它如何被呼叫,它的詞法作用域都只由函式被宣告時所處於的位置決定。

當函式可以記住並訪問所在詞法作用域時,就產生了閉包,即使函式是在當前詞法作用域之外執行;

function foo(){
    var a = 2;
    function bar(){
        console.log(a);
    }
    return bar;
}

var baz = foo();
baz();//2 ---朋友,這就是閉包的效果

我們可以看到 bar這個函式在 定時時的詞法作用域以外的地方被呼叫。閉包使得函式可以繼續訪問定義時的詞法作用域。
那麼就很容易會發現,上面bar被呼叫時,需要用到變數a。那麼js引擎會被a所在詞法作用域壓縮到bar方法的作用域鏈中,即使foo已經呼叫完成,記憶體中也不會釋放對變數a的引用。需要注意的是,這裡壓縮到作用域鏈中的詞法作用域中只保有bar所引用的變數,沒有引用的變數是不會被記憶體保留下來的。

8 效能優化

關於效能優化,真的能說的太多了。
常見情況:
1) 請求合併
2) CSS Sprite,啟用base64
3) 延遲載入
4) CSS放在頁面上面,JS資源放在頁面最下面
5) 非同步請求
6) CDN加速
7) 反向代理
8) 啟用gizp壓縮
9) 合理設定快取
10) 合理選擇圖片格式(包括:jpg,png,gif,svg,webp,iconfont等)
11) 減少cookie,使用離線儲存
12) 頁面靜態化
13) DNS預載入
14) 域名拆分

9 瀏覽器快取機制

這裡寫圖片描述

10 VUE雙向繫結實現原理

1 Vue雙向繫結原理

這裡寫圖片描述

核心程式碼劫持監聽物件資料, 需要考慮的是Dep物件被封裝在閉包,屬性呼叫get方法,會將對應的watcher新增到Dep.subs中,屬性呼叫set方法是,呼叫dep.notify方法,會呼叫watcher中的update方法:

function observe(data) {
    if (!data || typeof data !== 'object') {
        return;
    }
    // 取出所有屬性遍歷
    Object.keys(data).forEach(function(key) {
        defineReactive(data, key, data[key]);
    });
};
function defineReactive(data, key, val) {
    var dep = new Dep();
    observe(val); // 監聽子屬性

    Object.defineProperty(data, key, {
        // ... 省略
        set: function(newVal) {
            if (val === newVal) return;
            console.log('哈哈哈,監聽到值變化了 ', val, ' --> ', newVal);
            val = newVal;
            dep.notify(); // 通知所有訂閱者
        }
    });
}

function Dep() {
    this.subs = [];
}
Dep.prototype = {
    addSub: function(sub) {
        this.subs.push(sub);
    },
    notify: function() {
        this.subs.forEach(function(sub) {
            sub.update();
        });
    }
};
// Watcher.js
function Watcher(vm, exp, cb) {
    this.cb = cb;
    this.vm = vm;
    this.exp = exp;
    // 此處為了觸發屬性的getter,從而在dep新增自己,結合Observer更易理解
    this.value = this.get(); 
}
Watcher.prototype = {
    update: function() {
        this.run();    // 屬性值變化收到通知
    },
    run: function() {
        var value = this.get(); // 取到最新值
        var oldVal = this.value;
        if (value !== oldVal) {
            this.value = value;
            this.cb.call(this.vm, value, oldVal); // 執行Compile中繫結的回撥,更新檢視
        }
    },
    get: function() {
        Dep.target = this;    // 將當前訂閱者指向自己
        var value = this.vm[exp];    // 觸發getter,新增自己到屬性訂閱器中
        Dep.target = null;    // 新增完畢,重置
        return value;
    }
};

2 Vue模版繫結渲染方案

這裡寫圖片描述

11 XSS與CSRF請求

12 http請求

13 html快取

14 變數提升問題

15 ES6