前端工程師面試題(js)

極客教程發表於2017-09-19

image.png

正值金九銀十的招聘旺季,我把我珍藏整理多年的前端面試題分享給大家,分三部分。這是第二部分,js相關的很有用的基礎知識。

1. 如何實現一個LazyMan?

1.1 題目

實現一個LazyMan,可以按照以下方式呼叫:
LazyMan(“Hank”)輸出:
Hi! This is Hank!

LazyMan(“Hank”).sleep(10).eat(“dinner”)輸出
Hi! This is Hank!
//等待10秒..
Wake up after 10
Eat dinner~

LazyMan(“Hank”).eat(“dinner”).eat(“supper”)輸出
Hi This is Hank!
Eat dinner~
Eat supper~

LazyMan(“Hank”).sleepFirst(5).eat(“supper”)輸出
//等待5秒
Wake up after 5
Hi This is Hank!
Eat supper

以此類推。
複製程式碼

這是典型的JavaScript流程控制,問題的關鍵是如何實現任務的順序執行。在Express有一個類似的東西叫中介軟體,這個中介軟體和我們這裡的吃飯、睡覺等任務很類似,每一箇中介軟體執行完成後會呼叫next()函式,這個函式用來呼叫下一個中介軟體。

對於這個問題,我們也可以利用相似的思路來解決,首先建立一個任務佇列,然後利用next()函式來控制任務的順序執行:

1.2 佇列實現

function _LazyMan(name){
  this.tasks=[];
  var self=this;
  var fn=(function(n){
    var name=n;
    return function(){
      console.log("Hi! this is "+name+"!");
      self.next();
    }
  })(name);
  this.tasks.push(fn);
  setTimeout(function(){
    self.next();
  },0);  // 在下一個事件迴圈啟動任務
}
/* 事件排程函式 */
_LazyMan.prototype.next=function(){
  var fn=this.tasks.shift();
  fn && fn();
}
_LazyMan.prototype.eat=function(name){
  var self=this;
  var fn=(function(name){
    return function(){
      console.log("Eat "+name+" ~");
      self.next()
    }
  })(name);
  this.tasks.push(fn);
  return this; // 實現鏈式呼叫
}
_LazyMan.prototype.sleep=function(time){
  var self=this;
  var fn=(function(time){
    return function(){
      setTimeout(function(){
        console.log("Wake up after "+time+" s!");
        self.next();
      },time*1000);
    }
  })(time);
  this.tasks.push(fn);
  return this;
}
_LazyMan.prototype.sleepFirst=function(time){
  var self=this;
  var fn=(function(time){
    return function(){
      setTimeout(function(){
        console.log("Wake up after "+time+" s!");
      },time*1000);
    }
  })(time);
  this.tasks.unshift(fn);
  return this;
}
/* 封裝 */
function LazyMan(name){
  return new _LazyMan(name);
}

複製程式碼

1.3 promise實現

lazyman裡邊含有鏈式呼叫,那麼每一個子任務 return this;這個程式支援任務優先順序,那麼就需要兩個貫穿全場的Promise物件:第一,普通順序promise;第二,插入順序promise,同時插入順序是阻塞普通順序的,程式碼如下:

function _LazyMan(name){
  this.orderPromise=this.newPromise(); // 定義順序promise物件
  this.insertPromise=this.newPromise(); // 定義插入promise物件
  this.order(function(resolve){
    console.log(name);
    resolve();
  })
}

_LazyMan.prototype={
  /*例項化promise物件工廠*/
  newPromise:function(){
    return new Promise(function(resolve,reject){
      resolve();
    })
  },
  order:function(fn){
    var self=this;
    this.orderPromise=this.orderPromise.then(function(){
      return new Promise(function(resolve,reject){
        //如果有insertPromise,阻塞orderPromise.
        self.fir?self.insertPromise.then(function(){
          fn(resolve)
        }):fn(resolve)
      })
    })
  },
  insert:function(fn){
    var self=this;
    this.fir=true;
    this.insertPromise=this.insertPromise.then(function(){
      return new Promise(function(resolve,reject){
        fn(resolve);
        self.fir=false;
      })
    })
  },
  sleepFirst:function(time){
    this.insert(function(resolve){
      setTimeout(function(){
        console.log('wait '+time+' s,other logic');
        resolve();
      },time*1000)
    })
    return this;
  },
  eat:function(something){
    this.order(function(resolve){
      console.log(something+' ~~');
      resolve();
    })
    return this;
  },
  sleep:function(time){
    this.order(function(resolve){
      setTimeout(function(){
        console.log('sleep '+time+' s');
      },time*1000);
    })
    return this;
  }
}

//介面封裝。
function LazyMan(name) {
    return new _LazyMan(name);
}
//呼叫測試
LazyMan(‘RoryWu‘).firstTime(1).sleep(2).firstTime(3).eat(‘dinner‘).eat(‘breakfast‘);
// 彈出:
// wait 1 s, other logic
// wait 3 s, other logic
// RoryWu
// sleep 2 s
// dinner~~
// breakfast~~
複製程式碼

2. 用JS程式碼求出頁面上一個元素的最終的background-color,不考慮IE瀏覽器,不考慮元素float情況。

2.1程式碼例項

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <title>Document</title>
  <style>
    .button {
    height: 2em;
    border: 0;
    border-radius: .2em;
    background-color: #34538b;
    color: #fff;
    font-size: 12px;
    font-weight: bold;
}
  </style>
</head>
<body>
  <input type="button" id="button" class="button" value="點選我,顯示背景色" />
  <script>
    document.getElementById("button").onclick = function() {
    var oStyle =window.getComputedStyle(this, null); // null不是必須
    // 如果考慮IE var oStyle = this.currentStyle? this.currentStyle : window.getComputedStyle(this, null);
    alert(oStyle.getPropertyValue("background-color")); //這裡也可以用鍵值獲取,建議用getPropertyValue("background-color")
};
  </script>
</body>
</html>
複製程式碼

3. setTimeout(fn,0)

3.1 題目

for (var i = 0; i < 3; i++) {
	setTimeout(function() {
		console.log(i);
	}, 0);
	console.log(i);
}
複製程式碼

3.2 題解

結果是:0 1 2 3 3 3 很多公司面試都愛出這道題,此題考察的知識點還是蠻多的。 為了防止初學者栽在此問題上,此文稍微分析一下。 都考察了那些知識點呢? 非同步、作用域、閉包,你沒聽錯,是閉包。 我們來簡化此題:

setTimeout(function() {
        console.log(1);
}, 0);
console.log(2);
複製程式碼

先列印2,後列印1。 因為是setTimeout是非同步的。 正確的理解setTimeout的方式(註冊事件): 有兩個引數,第一個引數是函式,第二引數是時間值。 呼叫setTimeout時,把函式引數,放到事件佇列中。等主程式執行完,再呼叫。 沒啥不好理解的。就像我們給按鈕繫結事件一樣:

btn.onclick = function() {
        alert(1);
};
複製程式碼

這麼寫完,會彈出1嗎。不會!!只是繫結事件而已! 必須等我們去觸發事件,比如去點選這個按鈕,才會彈出1。 setTimeout也是這樣的!只是繫結事件,等主程式執行完畢後,再去呼叫。 setTimeout的時間值是怎麼回事呢? 比如:

setTimeout(fn, 2000)
複製程式碼

我們可以理解為2000之後,再放入事件佇列中,如果此時佇列為空,那麼就直接呼叫fn。如果前面還有其他的事件,那就等待。 因此setTimeout是一個約會從來都不準時的童鞋。 繼續看:

setTimeout(function() {
        console.log(i);
}, 0);
var i = 1;
複製程式碼

程式會不會報錯? 不會!而且還會準確得列印1。 為什麼? 因為真正去執行console.log(i)這句程式碼時,var i = 1已經執行完畢了! 所以我們進行dom操作。可以先繫結事件,然後再去寫其他邏輯。

window.onload = function() {
        fn();
}
var fn = function() {
        alert('hello')
};
複製程式碼

這麼寫,完全是可以的。因為非同步!

es5中是沒有塊級作用域的

for (var i = 0; i < 3; i++) {}
console.log(i);
複製程式碼

也就說i可以在for迴圈體外訪問到。所以是沒有塊級作用域。 但此問題在es6裡終結了,因為es6,發明了let。 這回我們再來看看原題。 原題使用了for迴圈。迴圈的本質是幹嘛的? 是為了方便我們程式設計師,少寫重複程式碼。 讓我們倒退50年,原題等價於:

var i = 0;
setTimeout(function() {
	console.log(i);
}, 0);
console.log(i);
i++;
setTimeout(function() {
	console.log(i);
}, 0);
console.log(i);
i++;
setTimeout(function() {
	console.log(i);
}, 0);
console.log(i);
i++;
複製程式碼

因為setTimeout是註冊事件。根據前面的討論,可以都放在後面。 原題又等價於如下的寫法:

var i = 0;
console.log(i);
i++;
console.log(i);
i++;
console.log(i);
i++;
setTimeout(function() {
	console.log(i);
}, 0);
setTimeout(function() {
	console.log(i);
}, 0);
setTimeout(function() {
	console.log(i);
}, 0);
複製程式碼

這回你明白了為啥結果是0 1 2 3 3 3了吧。

那個,說它是閉包,又是怎麼回事? 為了很好的說明白這個事情,我們把它放到一個函式中:

var fn = function() {
        for (var i = 0; i < 3; i++) {
                setTimeout(function() {
                        console.log(i);
                }, 0);
                console.log(i);
        }
};
fn();
複製程式碼

上面的函式跟我們常見另一個例子(div繫結事件)有什麼區別:

var fn = function() {
        var divs = document.querySelectorAll('div');
        for (var i = 0; i < 3; i++) {
                divs[i].onclick = function() {
                        alert(i);
                };
        }
};
fn();
複製程式碼

點選每個div都會彈出3。道理是一樣的。因為alert(i)中的i是fn作用越中的,因而這是閉包。 《javascript忍者祕籍》書裡把一個函式能呼叫全域性變數,也稱閉包。 因為作者認為全域性環境也可以想象成一個大的頂級函式。 怎麼保證能彈出0,1, 2呢。 解決之道:以毒攻毒! 再建立個閉包!!

var fn = function() {
        var divs = document.querySelectorAll('div');
        for (var i = 0; i < 3; i++) {
                divs[i].onclick = (function(i) {
                        return function() {
                                alert(i);
                        };
                })(i);
        }
};
fn();
複製程式碼

或者如下的寫法:

var fn = function() {
        var divs = document.querySelectorAll('div');
        for (var i = 0; i < 3; i++) {
                (function(i) {
                        divs[i].onclick = function() {
                                alert(i);
                        };
                })(i);
        }
};
fn();
複製程式碼

因此原題如果也想setTimeout也彈出0,1,2的話,改成如下:

for (var i = 0; i < 3; i++) {
	setTimeout((function(i) {
		return function() {
			console.log(i);
		};
	})(i), 0);
	console.log(i);
}
複製程式碼

想了解更多關於setTimeout(fn,0),可以參考setTimeout(fn,0)

4. 原型與繼承及new過程

4.1 原型和繼承

先看題目: 請用js實現一個類P,包含成員變數a,成員變數b,成員函式sum,sum輸出a與b的和,a,b預設值都為0。實現一個類M,M繼承自P,在P的基礎上增加成員變數c,成員函式sum變成輸出a,b,c的和。 題目分析 Js所有的函式都有一個prototype屬性,這個屬性引用了一個物件,即原型物件,也簡稱原型。這個函式包括建構函式和普通函式,我們講的更多是建構函式的原型,但是也不能否定普通函式也有原型,實現繼承的方法很多,這裡使用原型鏈和構造繼承,即組合繼承的方式。

function P(a,b){
  this.a=a||0;
  this.b=b||0;
  this.sum=function(){
    return this.a+this.b;
  }
}
function M(a,b,c){
  P.call(this,a,b)
  this.c=c;
  this.sum=function(){
    return this.a+this.b+this.c;
  }
}
M.prototype=new P();
var m=new M(2,2,2);
m.sum()  //輸出6
複製程式碼

接下來,我們深入js繼承,看看js實現繼承幾種方式的特點。

4.2 js繼承的實現方式

既然要實現繼承,那麼首先我們得有一個父類,程式碼如下:

// 定義一個動物類
function Animal (name,eye,skin) {
  // 屬性
  this.name = name || 'Animal';
  this.eye=eye;
  this.skin=skin;
  // 例項方法
  this.sleep = function(){
    console.log(this.name + '正在睡覺!');
  }
}
// 原型方法
Animal.prototype.eat = function(food) {
  console.log(this.name + '正在吃:' + food);
};
複製程式碼

下面給大家列出幾種繼承方式的實現

4.3 原型鏈繼承

實現父類程式碼在(4.2 js繼承的實現方式中) 核心: 將父類的例項作為子類的原型

function Cat(){ 
}
Cat.prototype = new Animal();
Cat.prototype.name = 'cat';

//&emsp;Test Code
var cat = new Cat();
console.log(cat.name);
console.log(cat.eat('fish'));
console.log(cat.sleep());
console.log(cat instanceof Animal); //true 
console.log(cat instanceof Cat); //true
複製程式碼

特點:

  1. 非常純粹的繼承關係,例項是子類的例項,也是父類的例項
  2. 父類新增原型方法/原型屬性,子類都能訪問到
  3. 簡單,易於實現

缺點:

  1. 要想為子類新增屬性和方法,必須要在new Animal()這樣的語句之後執行,不能放到構造器中
  2. 無法實現多繼承
  3. 來自原型物件的引用屬性是所有例項共享的(詳細請看附錄程式碼)
  4. 建立子類例項時,無法向父類建構函式傳參(即無法像這樣var cat=new Cat(hair,eye,skin)傳參給父類)

推薦指數:★★(3、4兩大致命缺陷)

4.4 構造繼承

**核心:**使用父類的建構函式來增強子類例項,等於是複製父類的例項屬性給子類(沒用到原型)

function Cat(name){
  Animal.call(this);
  this.name = name || 'Tom';
}

// Test Code
var cat = new Cat();
console.log(cat.name);
console.log(cat.sleep());
console.log(cat instanceof Animal); // false
console.log(cat instanceof Cat); // true
複製程式碼

特點:

  1. 解決了1中,子類例項共享父類引用屬性的問題
  2. 建立子類例項時,可以向父類傳遞引數(可通過Animal.call(this,name,eye,skin)或者Animal.apply(this,[name,eye,skin])實現)
  3. 可以實現多繼承(call多個父類物件)

缺點:

  1. 例項並不是父類的例項,只是子類的例項
  2. 只能繼承父類的例項屬性和方法,不能繼承原型屬性/方法
  3. 無法實現函式複用,每個子類都有父類例項函式的副本,影響效能

推薦指數:★★(缺點3)

4.5 例項繼承

**核心:**為父類例項新增新特性,作為子類例項返回

function Cat(name){
  var instance = new Animal();
  instance.name = name || 'Tom';
  return instance;
}

// Test Code
var cat = new Cat();
console.log(cat.name);
console.log(cat.sleep());
console.log(cat instanceof Animal); // true
console.log(cat instanceof Cat); // false
複製程式碼

特點:

  1. 不限制呼叫方式,不管是new 子類()還是子類(),返回的物件具有相同的效果

缺點:

  1. 例項是父類的例項,不是子類的例項
  2. 不支援多繼承

推薦指數:★★

4.6 拷貝繼承

function Cat(name){
  var animal = new Animal();
  for(var p in animal){
    Cat.prototype[p] = animal[p];
  }
  Cat.prototype.name = name || 'Tom';
}

// Test Code
var cat = new Cat();
console.log(cat.name);
console.log(cat.sleep());
console.log(cat instanceof Animal); // false
console.log(cat instanceof Cat); // true
複製程式碼

特點:

  1. 支援多繼承

缺點:

  1. 效率較低,記憶體佔用高(因為要拷貝父類的屬性)
  2. 無法獲取父類不可列舉的方法(不可列舉方法,不能使用for in 訪問到)

推薦指數:★(缺點1)

4.7 組合繼承

**核心:**通過呼叫父類構造,繼承父類的屬性並保留傳參的優點,然後通過將父類例項作為子類原型,實現函式複用

function Cat(name){
  Animal.call(this);
  this.name = name || 'Tom';
}
Cat.prototype = new Animal();

// Test Code
var cat = new Cat();
console.log(cat.name);
console.log(cat.sleep());
console.log(cat instanceof Animal); // true
console.log(cat instanceof Cat); // true
複製程式碼

特點:

  1. 彌補了方式2的缺陷,可以繼承例項屬性/方法,也可以繼承原型屬性/方法
  2. 既是子類的例項,也是父類的例項
  3. 不存在引用屬性共享問題
  4. 可傳參
  5. 函式可複用

缺點:

  1. 呼叫了兩次父類建構函式,生成了兩份例項(子類例項將子類原型上的那份遮蔽了)

推薦指數:★★★★(僅僅多消耗了一點記憶體)

4.8 寄生組合繼承

**核心:**通過寄生方式,砍掉父類的例項屬性,這樣,在呼叫兩次父類的構造的時候,就不會初始化兩次例項方法/屬性,避免的組合繼承的缺點

function Cat(name){
  Animal.call(this);
  this.name = name || 'Tom';
}
(function(){
  // 建立一個沒有例項方法的類
  var Super = function(){};
  Super.prototype = Animal.prototype;
  //將例項作為子類的原型
  Cat.prototype = new Super();
})();

// Test Code
var cat = new Cat();
console.log(cat.name);
console.log(cat.sleep());
console.log(cat instanceof Animal); // true
console.log(cat instanceof Cat); //true
複製程式碼

特點:

  1. 堪稱完美

缺點:

  1. 實現較為複雜

推薦指數:★★★★(實現複雜,扣掉一顆星)

4.9 附錄程式碼

示例:

function Animal (name) {
  // 屬性
  this.name = name || 'Animal';
  // 例項方法
  this.sleep = function(){
    console.log(this.name + '正在睡覺!');
  }
  //例項引用屬性
  this.features = [];
}
function Cat(name){
}
Cat.prototype = new Animal();

var tom = new Cat('Tom');
var kissy = new Cat('Kissy');

console.log(tom.name); // "Animal"
console.log(kissy.name); // "Animal"
console.log(tom.features); // []
console.log(kissy.features); // []

tom.name = 'Tom-New Name';
tom.features.push('eat');

//針對父類例項值型別成員的更改,不影響
console.log(tom.name); // "Tom-New Name"
console.log(kissy.name); // "Animal"
//針對父類例項引用型別成員的更改,會通過影響其他子類例項
console.log(tom.features); // ['eat']
console.log(kissy.features); // ['eat']

原因分析:

關鍵點:屬性查詢過程

執行tom.features.push,首先找tom物件的例項屬性(找不到),
那麼去原型物件中找,也就是Animal的例項。發現有,那麼就直接在這個物件的
features屬性中插入值。
在console.log(kissy.features); 的時候。同上,kissy例項上沒有,那麼去原型上找。
剛好原型上有,就直接返回,但是注意,這個原型物件中features屬性值已經變化了。
複製程式碼

其實這裡主要是理解js值型別和引用型別

4.10 new關鍵字

假設已經定義了父類Base物件 我們執行如下程式碼

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

這樣程式碼的結果是什麼,我們在Javascript引擎中看到的物件模型是:

interview01.png
new操作符具體幹了什麼呢?其實很簡單,就幹了三件事情。

var obj  = {};
obj.__proto__ = Base.prototype; 
Base.call(obj);  
複製程式碼
  • 第一行,我們建立了一個空物件obj
  • 第二行,我們將這個空物件的__proto__成員指向了Base函式物件prototype成員物件
  • 第三行,我們將Base函式物件的this指標替換成obj,然後再呼叫Base函式 注意:new的過程會執行建構函式Base() 再對空物件進行構造

5. js題(2)

5.1 如何實現懶載入(跟預載入的區別)

5.2 同源,跨域

推薦閱讀 瀏覽器同源政策及其規避方法

5.3 js有幾種型別值,畫記憶體圖

棧:原始資料型別(Undefined,Null,Boolean,Number、String) 
堆:引用資料型別(物件、陣列和函式)

兩種型別的區別是:儲存位置不同;
原始資料型別直接儲存在棧(stack)中的簡單資料段,佔據空間小、大小固定,屬於被頻繁使用資料,所以放入棧中儲存;
引用資料型別儲存在堆(heap)中的物件,佔據空間大、大小不固定,如果儲存在棧中,將會影響程式執行的效能;引用資料型別在棧中儲存了指標,該指標指向堆中該實體的起始地址。當直譯器尋找引用值時,會首先檢索其
在棧中的地址,取得地址後從堆中獲得實體
複製程式碼

interview09.png

5.4 通用的事件偵聽器函式

 // event(事件)工具集,來源:github.com/markyun
    markyun.Event = {
        // 頁面載入完成後
        readyEvent : function(fn) {
            if (fn==null) {
                fn=document;
            }
            var oldonload = window.onload;
            if (typeof window.onload != 'function') {
                window.onload = fn;
            } else {
                window.onload = function() {
                    oldonload();
                    fn();
                };
            }
        },
        // 視能力分別使用dom0||dom2||IE方式 來繫結事件
        // 引數: 操作的元素,事件名稱 ,事件處理程式
        addEvent : function(element, type, handler) {
            if (element.addEventListener) {
                //事件型別、需要執行的函式、是否捕捉
                element.addEventListener(type, handler, false);
            } else if (element.attachEvent) {
                element.attachEvent('on' + type, function() {
                    handler.call(element);
                });
            } else {
                element['on' + type] = handler;
            }
        },
        // 移除事件
        removeEvent : function(element, type, handler) {
            if (element.removeEventListener) {
                element.removeEventListener(type, handler, false);
            } else if (element.datachEvent) {
                element.detachEvent('on' + type, handler);
            } else {
                element['on' + type] = null;
            }
        },
        // 阻止事件 (主要是事件冒泡,因為IE不支援事件捕獲)
        stopPropagation : function(ev) {
            if (ev.stopPropagation) {
                ev.stopPropagation();
            } else {
                ev.cancelBubble = true;
            }
        },
        // 取消事件的預設行為
        preventDefault : function(event) {
            if (event.preventDefault) {
                event.preventDefault();
            } else {
                event.returnValue = false;
            }
        },
        // 獲取事件目標
        getTarget : function(event) {
            return event.target || event.srcElement;
        },
        // 獲取event物件的引用,取到事件的所有資訊,確保隨時能使用event;
        getEvent : function(e) {
            var ev = e || window.event;
            if (!ev) {
                var c = this.getEvent.caller;
                while (c) {
                    ev = c.arguments[0];
                    if (ev && Event == ev.constructor) {
                        break;
                    }
                    c = c.caller;
                }
            }
            return ev;
        }
    };

複製程式碼

5.5 ["1", "2", "3"].map(parseInt) 答案

 [1, NaN, NaN] 因為 parseInt 需要兩個引數 (val, radix),
 其中 radix 表示解析時用的基數。
 map 傳了 3 個 (element, index, array),對應的 radix 不合法導致解析失敗。
複製程式碼

5.6 宣告提升

5.7 那些操作會造成記憶體洩漏?

記憶體洩漏指任何物件在您不再擁有或需要它之後仍然存在。
垃圾回收器定期掃描物件,並計算引用了每個物件的其他物件的數量。如果一個物件的引用數量為 0(沒有其他物件引用過該物件),或對該物件的惟一引用是迴圈的,那麼該物件的記憶體即可回收。

setTimeout 的第一個引數使用字串而非函式的話,會引發記憶體洩漏。
閉包、控制檯日誌、迴圈(在兩個物件彼此引用且彼此保留時,就會產生一個迴圈)
複製程式碼

5.8 Polyfill

polyfill 是“在舊版瀏覽器上覆制標準 API 的 JavaScript 補充”,可以動態地載入 JavaScript 程式碼或庫,在不支援這些標準 API 的瀏覽器中模擬它們。
例如,geolocation(地理位置)polyfill 可以在 navigator 物件上新增全域性的 geolocation 物件,還能新增 getCurrentPosition 函式以及“座標”回撥物件,
所有這些都是 W3C 地理位置 API 定義的物件和函式。因為 polyfill 模擬標準 API,所以能夠以一種面向所有瀏覽器未來的方式針對這些 API 進行開發,
一旦對這些 API 的支援變成絕對大多數,則可以方便地去掉 polyfill,無需做任何額外工作。
複製程式碼

做的專案中,有沒有用過或自己實現一些 polyfill 方案(相容性處理方案)?

比如: html5shiv、Geolocation、Placeholder 
複製程式碼

5.9 關於arguments

1. 定義 由於JavaScript允許函式有不定數目的引數,所以我們需要一種機制,可以在函式體內部讀取所有引數。這就是arguments物件的由來。

arguments物件包含了函式執行時的所有引數,arguments[0]就是第一個引數,arguments[1]就是第二個引數,以此類推。這個物件只有在函式體內部,才可以使用。

var f = function(one) {
  console.log(arguments[0]);
  console.log(arguments[1]);
  console.log(arguments[2]);
}

f(1, 2, 3)
// 1
// 2
// 3
複製程式碼

arguments物件除了可以讀取引數,還可以為引數賦值(嚴格模式不允許這種用法)

var f = function(a, b) {
  arguments[0] = 3;
  arguments[1] = 2;
  return a + b;
}

f(1, 1)
// 5
複製程式碼

可以通過arguments物件的length屬性,判斷函式呼叫時到底帶幾個引數。

function f() {
  return arguments.length;
}

f(1, 2, 3) // 3
f(1) // 1
f() // 0
複製程式碼

2. 與陣列的關係 需要注意的是,雖然arguments很像陣列,但它是一個物件。陣列專有的方法(比如slice和forEach),不能在arguments物件上直接使用。

但是,可以通過apply方法,把arguments作為引數傳進去,這樣就可以讓arguments使用陣列方法了。

// 用於apply方法
myfunction.apply(obj, arguments).

// 使用與另一個陣列合並
Array.prototype.concat.apply([1,2,3], arguments)
複製程式碼

要讓arguments物件使用陣列方法,真正的解決方法是將arguments轉為真正的陣列。下面是兩種常用的轉換方法:slice方法和逐一填入新陣列。

var args = Array.prototype.slice.call(arguments);

// or

var args = [];
for (var i = 0; i < arguments.length; i++) {
  args.push(arguments[i]);
}
複製程式碼

3. callee屬性 arguments物件帶有一個callee屬性,返回它所對應的原函式。

var f = function(one) {
  console.log(arguments.callee === f);
}

f() // true
複製程式碼

可以通過arguments.callee,達到呼叫函式自身的目的。這個屬性在嚴格模式裡面是禁用的,因此不建議使用。

4. 題目sum(2)(3)

// 寫一個 function 讓下面兩行程式碼輸出的結果都為 5
console.log(sum(2, 3));
console.log(sum(2)(3));
複製程式碼

說實話,第一眼看到的時候心裡是有點虛的(因為第一次看到它)。sum(2)(3),這種形式的程式碼確實少見。但是第一反應就是鏈式呼叫。 鏈式呼叫我們熟悉啊,特別是 jQuery 裡面,我們常常能看到連著寫的程式碼。實現原理就是在方法結束時 return 合適的元素物件。

$('#id').parent().siblings('selector').css({
    color: 'red'
});
複製程式碼

這道題考什麼呢?認真分析了一下,應該有鏈式呼叫,toString,柯里化,陣列操作等相關內容。大概這些可以滿足需求吧? 如何寫程式碼,腦海中大體上有構思了,但是當時手上僅有筆和紙,思路連不上來啊。還好面前放著一臺桌上型電腦(嘿嘿嘿,機器上寫完再抄回紙上)

我的實現大概是這樣的。

var sum = (function() {
    var list = [];

    var add = function() {
        // 拼接陣列
        var args = Array.prototype.slice.call(arguments);
        list = list.concat(args);
        return add;
    }
    // 覆蓋 toString 方法
    add.toString = function() {
        // 計算總和
        var sum = list.reduce(function(pre, next) {
            return pre + next;
        });
        // 清除記錄
        list.length = 0;
        return sum;
    }

    return add;
})();

sum(2, 3);
// 5
sum(2)(3);
// 5
複製程式碼

這個方法比較複雜,下面介紹個簡便的。

var add = function add() {
    var cache;
    if (arguments.length === 1) {
        cache = arguments[0];
        return function ( number ) {return cache + number;};
    }
    else return arguments[0] + arguments[1];
};
複製程式碼

6. js淺複製和深複製

6.1 js淺複製

簡單的淺複製實現:

var obj={ a:1,arr:[2,3] };
var shallowObj=shallowCopy(obj);

function shallowCopy(src){
  var dst = {};
  for (var prop in src){
    if(src.hasOwnProperty(prop)){
      dst[prop]=src[prop];
    }
  }

  return dst;
}
複製程式碼

因為淺複製只會將物件的各個屬性進行依次複製,並不會進行遞迴複製,而 JavaScript 儲存物件都是存地址的,所以淺複製會導致 obj.arr 和 shadowObj.arr 指向同一塊記憶體地址,大概的示意圖如下。

interview10
導致的結果就是:

shadowObj.arr[1] = 5;
obj.arr[1]   // = 5
複製程式碼

6.2 js深複製

而深複製則不同,它不僅將原物件的各個屬性逐個複製出去,而且將原物件各個屬性所包含的物件也依次採用深複製的方法遞迴複製到新物件上。這就不會存在上面 obj 和 shadowObj 的 arr 屬性指向同一個物件的問題。

var obj = { a:1, arr: [1,2] };
var obj2 = deepCopy(obj);
複製程式碼

結果如下面的示意圖所示:

interview11

需要注意的是,如果物件比較大,層級也比較多,深複製會帶來效能上的問題。在遇到需要採用深複製的場景時,可以考慮有沒有其他替代的方案。在實際的應用場景中,也是淺複製更為常用。

程式碼遞迴實現如下:

function deepCopy(o,c){
  var c = c || {}
  for(var i in o){
    if(typeof o[i] == 'object'){
      if(o[i].constructor===Array){
        c[i]=[]
      }else{
        c[i]={}
      }
      deepCopy(o[i],c[i])
    }else{
      c[i]=o[i]
    }
  }
  return c
}
複製程式碼

7. jquery題

7.1 jquery與jquery UI

  • jQuery是一個js庫,主要提供的功能是選擇器,屬性修改和事件繫結等等。
  • jQuery UI則是在jQuery的基礎上,利用jQuery的擴充套件性,設計的外掛。 提供了一些常用的介面元素,諸如對話方塊、拖動行為、改變大小行為等等

7.2 jquery擴充套件

jquery 中如何將陣列轉化為json字串,然後再轉化回來?

   $.fn.stringifyArray = function(array) {
        return JSON.stringify(array)
    }

    $.fn.parseArray = function(array) {
        return JSON.parse(array)
    }

    然後呼叫:
    $("").stringifyArray(array)
複製程式碼

7.3 jquery的優化方法

針對 jQuery 的優化方法?

*基於Class的選擇性的效能相對於Id選擇器開銷很大,因為需遍歷所有DOM元素。

*頻繁操作的DOM,先快取起來再操作。用Jquery的鏈式呼叫更好。
 比如:var str=$("a").attr("href");

*for (var i = size; i < arr.length; i++) {}
 for 迴圈每一次迴圈都查詢了陣列 (arr) 的.length 屬性,在開始迴圈的時候設定一個變數來儲存這個數字,可以讓迴圈跑得更快:
 for (var i = size, length = arr.length; i < length; i++) {}
複製程式碼

線上電子書閱讀前端常見面試題彙總

轉載請註明

極客教程-極客教程

相關文章