前端技術總結
文章主要是用來記錄一下一些常見問題,權當是複習了。
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模版繫結渲染方案